|
1 /* |
|
2 * LDForge: LDraw parts authoring CAD |
|
3 * Copyright (C) 2013 - 2015 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 <QNetworkAccessManager> |
|
20 #include <QNetworkRequest> |
|
21 #include <QNetworkReply> |
|
22 #include <QDir> |
|
23 #include <QProgressBar> |
|
24 #include <QPushButton> |
|
25 #include <QFileDialog> |
|
26 #include <QMessageBox> |
|
27 #include "partDownloader.h" |
|
28 #include "ui_downloadfrom.h" |
|
29 #include "basics.h" |
|
30 #include "mainWindow.h" |
|
31 #include "ldDocument.h" |
|
32 #include "glRenderer.h" |
|
33 |
|
34 CFGENTRY (String, DownloadFilePath, "") |
|
35 CFGENTRY (Bool, GuessDownloadPaths, true) |
|
36 CFGENTRY (Bool, AutoCloseDownloadDialog, true) |
|
37 |
|
38 const QString g_unofficialLibraryURL ("http://ldraw.org/library/unofficial/"); |
|
39 |
|
40 // ============================================================================= |
|
41 // |
|
42 void PartDownloader::staticBegin() |
|
43 { |
|
44 PartDownloader dlg; |
|
45 |
|
46 if (not dlg.checkValidPath()) |
|
47 return; |
|
48 |
|
49 dlg.exec(); |
|
50 } |
|
51 |
|
52 // ============================================================================= |
|
53 // |
|
54 QString PartDownloader::getDownloadPath() |
|
55 { |
|
56 QString path = cfg::DownloadFilePath; |
|
57 |
|
58 if (DIRSLASH[0] != '/') |
|
59 path.replace (DIRSLASH, "/"); |
|
60 |
|
61 return path; |
|
62 } |
|
63 |
|
64 // ============================================================================= |
|
65 // |
|
66 PartDownloader::PartDownloader (QWidget* parent) : |
|
67 QDialog (parent), |
|
68 m_source (Source (0)) |
|
69 { |
|
70 setForm (new Ui_DownloadFrom); |
|
71 form()->setupUi (this); |
|
72 form()->fname->setFocus(); |
|
73 |
|
74 #ifdef USE_QT5 |
|
75 form()->progress->horizontalHeader()->setSectionResizeMode (QHeaderView::Stretch); |
|
76 #else |
|
77 form()->progress->horizontalHeader()->setResizeMode (PartLabelColumn, QHeaderView::Stretch); |
|
78 #endif |
|
79 |
|
80 setDownloadButton (new QPushButton (tr ("Download"))); |
|
81 form()->buttonBox->addButton (downloadButton(), QDialogButtonBox::ActionRole); |
|
82 getButton (Abort)->setEnabled (false); |
|
83 |
|
84 connect (form()->source, SIGNAL (currentIndexChanged (int)), |
|
85 this, SLOT (sourceChanged (int))); |
|
86 connect (form()->buttonBox, SIGNAL (clicked (QAbstractButton*)), |
|
87 this, SLOT (buttonClicked (QAbstractButton*))); |
|
88 } |
|
89 |
|
90 // ============================================================================= |
|
91 // |
|
92 PartDownloader::~PartDownloader() |
|
93 { |
|
94 delete form(); |
|
95 } |
|
96 |
|
97 // ============================================================================= |
|
98 // |
|
99 bool PartDownloader::checkValidPath() |
|
100 { |
|
101 QString path = getDownloadPath(); |
|
102 |
|
103 if (path.isEmpty() or not QDir (path).exists()) |
|
104 { |
|
105 QMessageBox::information(this, "Notice", "Please input a path for files to download."); |
|
106 path = QFileDialog::getExistingDirectory (this, "Path for downloaded files:"); |
|
107 |
|
108 if (path.isEmpty()) |
|
109 return false; |
|
110 |
|
111 cfg::DownloadFilePath = path; |
|
112 } |
|
113 |
|
114 return true; |
|
115 } |
|
116 |
|
117 // ============================================================================= |
|
118 // |
|
119 QString PartDownloader::getURL() |
|
120 { |
|
121 const Source src = getSource(); |
|
122 QString dest; |
|
123 |
|
124 switch (src) |
|
125 { |
|
126 case PartsTracker: |
|
127 dest = form()->fname->text(); |
|
128 modifyDestination (dest); |
|
129 form()->fname->setText (dest); |
|
130 return g_unofficialLibraryURL + dest; |
|
131 |
|
132 case CustomURL: |
|
133 return form()->fname->text(); |
|
134 } |
|
135 |
|
136 // Shouldn't happen |
|
137 return ""; |
|
138 } |
|
139 |
|
140 // ============================================================================= |
|
141 // |
|
142 void PartDownloader::modifyDestination (QString& dest) const |
|
143 { |
|
144 dest = dest.simplified(); |
|
145 |
|
146 // If the user doesn't want us to guess, stop right here. |
|
147 if (not cfg::GuessDownloadPaths) |
|
148 return; |
|
149 |
|
150 // Ensure .dat extension |
|
151 if (dest.right (4) != ".dat") |
|
152 { |
|
153 // Remove the existing extension, if any. It may be we're here over a |
|
154 // typo in the .dat extension. |
|
155 const int dotpos = dest.lastIndexOf ("."); |
|
156 |
|
157 if ((dotpos != -1) and (dotpos >= dest.length() - 4)) |
|
158 dest.chop (dest.length() - dotpos); |
|
159 |
|
160 dest += ".dat"; |
|
161 } |
|
162 |
|
163 // If the part starts with s\ or s/, then use parts/s/. Same goes with |
|
164 // 48\ and p/48/. |
|
165 if (Eq (dest.left (2), "s\\", "s/")) |
|
166 { |
|
167 dest.remove (0, 2); |
|
168 dest.prepend ("parts/s/"); |
|
169 } |
|
170 elif (Eq (dest.left (3), "48\\", "48/")) |
|
171 { |
|
172 dest.remove (0, 3); |
|
173 dest.prepend ("p/48/"); |
|
174 } |
|
175 |
|
176 /* Try determine where to put this part. We have four directories: |
|
177 parts/, parts/s/, p/, and p/48/. If we haven't already specified |
|
178 either parts/ or p/, we need to add it automatically. Part files |
|
179 are numbers wit a possible u prefix for parts with unknown number |
|
180 which can be followed by any of: |
|
181 - c** (composites) |
|
182 - d** (formed stickers) |
|
183 - p** (patterns) |
|
184 - a lowercase alphabetic letter for variants |
|
185 |
|
186 Subfiles (usually) have an s** prefix, in which case we use parts/s/. |
|
187 Note that the regex starts with a '^' so it won't catch already fully |
|
188 given part file names. */ |
|
189 QString partRegex = "^u?[0-9]+(c[0-9][0-9]+)*(d[0-9][0-9]+)*[a-z]?(p[0-9a-z][0-9a-z]+)*"; |
|
190 QString subpartRegex = partRegex + "s[0-9][0-9]+"; |
|
191 |
|
192 partRegex += "\\.dat$"; |
|
193 subpartRegex += "\\.dat$"; |
|
194 |
|
195 if (QRegExp (subpartRegex).exactMatch (dest)) |
|
196 dest.prepend ("parts/s/"); |
|
197 elif (QRegExp (partRegex).exactMatch (dest)) |
|
198 dest.prepend ("parts/"); |
|
199 elif (not dest.startsWith ("parts/") and not dest.startsWith ("p/")) |
|
200 dest.prepend ("p/"); |
|
201 } |
|
202 |
|
203 // ============================================================================= |
|
204 // |
|
205 PartDownloader::Source PartDownloader::getSource() const |
|
206 { |
|
207 return m_source; |
|
208 } |
|
209 |
|
210 // ============================================================================= |
|
211 // |
|
212 void PartDownloader::setSource (Source src) |
|
213 { |
|
214 m_source = src; |
|
215 form()->source->setCurrentIndex (int (src)); |
|
216 } |
|
217 |
|
218 // ============================================================================= |
|
219 // |
|
220 void PartDownloader::sourceChanged (int i) |
|
221 { |
|
222 if (i == CustomURL) |
|
223 form()->fileNameLabel->setText (tr ("URL:")); |
|
224 else |
|
225 form()->fileNameLabel->setText (tr ("File name:")); |
|
226 |
|
227 m_source = Source (i); |
|
228 } |
|
229 |
|
230 // ============================================================================= |
|
231 // |
|
232 void PartDownloader::buttonClicked (QAbstractButton* btn) |
|
233 { |
|
234 if (btn == getButton (Close)) |
|
235 { |
|
236 reject(); |
|
237 } |
|
238 elif (btn == getButton (Abort)) |
|
239 { |
|
240 setAborted (true); |
|
241 |
|
242 for (PartDownloadRequest* req : requests()) |
|
243 req->abort(); |
|
244 } |
|
245 elif (btn == getButton (Download)) |
|
246 { |
|
247 QString dest = form()->fname->text(); |
|
248 setPrimaryFile (LDDocumentPtr()); |
|
249 setAborted (false); |
|
250 |
|
251 if (getSource() == CustomURL) |
|
252 dest = Basename (getURL()); |
|
253 |
|
254 modifyDestination (dest); |
|
255 |
|
256 if (QFile::exists (PartDownloader::getDownloadPath() + DIRSLASH + dest)) |
|
257 { |
|
258 const QString overwritemsg = format (tr ("%1 already exists in download directory. Overwrite?"), dest); |
|
259 if (not Confirm (tr ("Overwrite?"), overwritemsg)) |
|
260 return; |
|
261 } |
|
262 |
|
263 downloadFile (dest, getURL(), true); |
|
264 } |
|
265 } |
|
266 |
|
267 // ============================================================================= |
|
268 // |
|
269 void PartDownloader::downloadFile (QString dest, QString url, bool primary) |
|
270 { |
|
271 const int row = form()->progress->rowCount(); |
|
272 |
|
273 // Don't download files repeadetly. |
|
274 if (filesToDownload().indexOf (dest) != -1) |
|
275 return; |
|
276 |
|
277 print ("Downloading %1 from %2\n", dest, url); |
|
278 modifyDestination (dest); |
|
279 PartDownloadRequest* req = new PartDownloadRequest (url, dest, primary, this); |
|
280 m_filesToDownload << dest; |
|
281 m_requests << req; |
|
282 form()->progress->insertRow (row); |
|
283 req->setTableRow (row); |
|
284 req->updateToTable(); |
|
285 downloadButton()->setEnabled (false); |
|
286 form()->progress->setEnabled (true); |
|
287 form()->fname->setEnabled (false); |
|
288 form()->source->setEnabled (false); |
|
289 getButton (Close)->setEnabled (false); |
|
290 getButton (Abort)->setEnabled (true); |
|
291 getButton (Download)->setEnabled (false); |
|
292 } |
|
293 |
|
294 // ============================================================================= |
|
295 // |
|
296 void PartDownloader::downloadFromPartsTracker (QString file) |
|
297 { |
|
298 modifyDestination (file); |
|
299 downloadFile (file, g_unofficialLibraryURL + file, false); |
|
300 } |
|
301 |
|
302 // ============================================================================= |
|
303 // |
|
304 void PartDownloader::checkIfFinished() |
|
305 { |
|
306 bool failed = isAborted(); |
|
307 |
|
308 // If there is some download still working, we're not finished. |
|
309 for (PartDownloadRequest* req : requests()) |
|
310 { |
|
311 if (not req->isFinished()) |
|
312 return; |
|
313 |
|
314 if (req->state() == PartDownloadRequest::State::Failed) |
|
315 failed = true; |
|
316 } |
|
317 |
|
318 for (PartDownloadRequest* req : requests()) |
|
319 delete req; |
|
320 |
|
321 m_requests.clear(); |
|
322 |
|
323 // Update everything now |
|
324 if (primaryFile() != null) |
|
325 { |
|
326 LDDocument::setCurrent (primaryFile()); |
|
327 g_win->doFullRefresh(); |
|
328 g_win->R()->resetAngles(); |
|
329 } |
|
330 |
|
331 for (LDDocumentPtr f : m_files) |
|
332 f->reloadAllSubfiles(); |
|
333 |
|
334 if (cfg::AutoCloseDownloadDialog and not failed) |
|
335 { |
|
336 // Close automatically if desired. |
|
337 accept(); |
|
338 } |
|
339 else |
|
340 { |
|
341 // Allow the prompt be closed now. |
|
342 getButton (Abort)->setEnabled (false); |
|
343 getButton (Close)->setEnabled (true); |
|
344 } |
|
345 } |
|
346 |
|
347 // ============================================================================= |
|
348 // |
|
349 QPushButton* PartDownloader::getButton (PartDownloader::Button i) |
|
350 { |
|
351 switch (i) |
|
352 { |
|
353 case Download: |
|
354 return downloadButton(); |
|
355 |
|
356 case Abort: |
|
357 return qobject_cast<QPushButton*> (form()->buttonBox->button (QDialogButtonBox::Abort)); |
|
358 |
|
359 case Close: |
|
360 return qobject_cast<QPushButton*> (form()->buttonBox->button (QDialogButtonBox::Close)); |
|
361 } |
|
362 |
|
363 return null; |
|
364 } |
|
365 |
|
366 // ============================================================================= |
|
367 // |
|
368 PartDownloadRequest::PartDownloadRequest (QString url, QString dest, bool primary, PartDownloader* parent) : |
|
369 QObject (parent), |
|
370 m_state (State::Requesting), |
|
371 m_prompt (parent), |
|
372 m_url (url), |
|
373 m_destinaton (dest), |
|
374 m_filePath (PartDownloader::getDownloadPath() + DIRSLASH + dest), |
|
375 m_networkManager (new QNetworkAccessManager), |
|
376 m_isFirstUpdate (true), |
|
377 m_isPrimary (primary), |
|
378 m_filePointer (null) |
|
379 { |
|
380 // Make sure that we have a valid destination. |
|
381 QString dirpath = Dirname (filePath()); |
|
382 |
|
383 QDir dir (dirpath); |
|
384 |
|
385 if (not dir.exists()) |
|
386 { |
|
387 print ("Creating %1...\n", dirpath); |
|
388 |
|
389 if (not dir.mkpath (dirpath)) |
|
390 Critical (format (tr ("Couldn't create the directory %1!"), dirpath)); |
|
391 } |
|
392 |
|
393 setNetworkReply (networkManager()->get (QNetworkRequest (QUrl (url)))); |
|
394 connect (networkReply(), SIGNAL (finished()), this, SLOT (downloadFinished())); |
|
395 connect (networkReply(), SIGNAL (readyRead()), this, SLOT (readyRead())); |
|
396 connect (networkReply(), SIGNAL (downloadProgress (qint64, qint64)), |
|
397 this, SLOT (downloadProgress (qint64, qint64))); |
|
398 } |
|
399 |
|
400 // ============================================================================= |
|
401 // |
|
402 PartDownloadRequest::~PartDownloadRequest() {} |
|
403 |
|
404 // ============================================================================= |
|
405 // |
|
406 void PartDownloadRequest::updateToTable() |
|
407 { |
|
408 int const labelcol = PartDownloader::PartLabelColumn; |
|
409 int const progcol = PartDownloader::ProgressColumn; |
|
410 QTableWidget* table = prompt()->form()->progress; |
|
411 QProgressBar* prog; |
|
412 |
|
413 switch (state()) |
|
414 { |
|
415 case State::Requesting: |
|
416 case State::Downloading: |
|
417 { |
|
418 prog = qobject_cast<QProgressBar*> (table->cellWidget (tableRow(), progcol)); |
|
419 |
|
420 if (not prog) |
|
421 { |
|
422 prog = new QProgressBar; |
|
423 table->setCellWidget (tableRow(), progcol, prog); |
|
424 } |
|
425 |
|
426 prog->setRange (0, numBytesTotal()); |
|
427 prog->setValue (numBytesRead()); |
|
428 } break; |
|
429 |
|
430 case State::Finished: |
|
431 case State::Failed: |
|
432 { |
|
433 const QString text = (state() == State::Finished) |
|
434 ? "<b><span style=\"color: #080\">FINISHED</span></b>" |
|
435 : "<b><span style=\"color: #800\">FAILED</span></b>"; |
|
436 |
|
437 QLabel* lb = new QLabel (text); |
|
438 lb->setAlignment (Qt::AlignCenter); |
|
439 table->setCellWidget (tableRow(), progcol, lb); |
|
440 } break; |
|
441 } |
|
442 |
|
443 QLabel* lb = qobject_cast<QLabel*> (table->cellWidget (tableRow(), labelcol)); |
|
444 |
|
445 if (isFirstUpdate()) |
|
446 { |
|
447 lb = new QLabel (format ("<b>%1</b>", destinaton()), table); |
|
448 table->setCellWidget (tableRow(), labelcol, lb); |
|
449 } |
|
450 |
|
451 // Make sure that the cell is big enough to contain the label |
|
452 if (table->columnWidth (labelcol) < lb->width()) |
|
453 table->setColumnWidth (labelcol, lb->width()); |
|
454 |
|
455 setFirstUpdate (true); |
|
456 } |
|
457 |
|
458 // ============================================================================= |
|
459 // |
|
460 void PartDownloadRequest::downloadFinished() |
|
461 { |
|
462 if (networkReply()->error() != QNetworkReply::NoError) |
|
463 { |
|
464 if (isPrimary() and not prompt()->isAborted()) |
|
465 Critical (networkReply()->errorString()); |
|
466 |
|
467 print ("Unable to download %1: %2\n", m_destinaton, networkReply()->errorString()); |
|
468 setState (State::Failed); |
|
469 } |
|
470 elif (state() != State::Failed) |
|
471 { |
|
472 setState (State::Finished); |
|
473 } |
|
474 |
|
475 setNumBytesRead (numBytesTotal()); |
|
476 updateToTable(); |
|
477 |
|
478 if (filePointer()) |
|
479 { |
|
480 filePointer()->close(); |
|
481 delete filePointer(); |
|
482 setFilePointer (null); |
|
483 |
|
484 if (state() == State::Failed) |
|
485 QFile::remove (filePath()); |
|
486 } |
|
487 |
|
488 if (state() != State::Finished) |
|
489 { |
|
490 prompt()->checkIfFinished(); |
|
491 return; |
|
492 } |
|
493 |
|
494 // Try to load this file now. |
|
495 LDDocumentPtr f = OpenDocument (filePath(), false, not isPrimary()); |
|
496 |
|
497 if (f == null) |
|
498 return; |
|
499 |
|
500 // Iterate through this file and check for errors. If there's any that stems |
|
501 // from unknown file references, try resolve that by downloading the reference. |
|
502 // This is why downloading a part may end up downloading multiple files, as |
|
503 // it resolves dependencies. |
|
504 for (LDObjectPtr obj : f->objects()) |
|
505 { |
|
506 LDErrorPtr err = obj.dynamicCast<LDError>(); |
|
507 |
|
508 if ((err == null) or (err->fileReferenced().isEmpty())) |
|
509 continue; |
|
510 |
|
511 QString dest = err->fileReferenced(); |
|
512 prompt()->downloadFromPartsTracker (dest); |
|
513 } |
|
514 |
|
515 prompt()->addFile (f); |
|
516 |
|
517 if (isPrimary()) |
|
518 { |
|
519 AddRecentFile (filePath()); |
|
520 prompt()->setPrimaryFile (f); |
|
521 } |
|
522 |
|
523 prompt()->checkIfFinished(); |
|
524 } |
|
525 |
|
526 // ============================================================================= |
|
527 // |
|
528 void PartDownloader::addFile (LDDocumentPtr f) |
|
529 { |
|
530 m_files << f; |
|
531 } |
|
532 |
|
533 // ============================================================================= |
|
534 // |
|
535 void PartDownloadRequest::downloadProgress (int64 recv, int64 total) |
|
536 { |
|
537 setNumBytesRead (recv); |
|
538 setNumBytesTotal (total); |
|
539 setState (State::Downloading); |
|
540 updateToTable(); |
|
541 } |
|
542 |
|
543 // ============================================================================= |
|
544 // |
|
545 void PartDownloadRequest::readyRead() |
|
546 { |
|
547 if (state() == State::Failed) |
|
548 return; |
|
549 |
|
550 if (filePointer() == null) |
|
551 { |
|
552 m_filePath.replace ("\\", "/"); |
|
553 |
|
554 // We have already asked the user whether we can overwrite so we're good |
|
555 // to go here. |
|
556 setFilePointer (new QFile (filePath().toLocal8Bit())); |
|
557 |
|
558 if (not filePointer()->open (QIODevice::WriteOnly)) |
|
559 { |
|
560 Critical (format (tr ("Couldn't open %1 for writing: %2"), filePath(), strerror (errno))); |
|
561 setState (State::Failed); |
|
562 networkReply()->abort(); |
|
563 updateToTable(); |
|
564 prompt()->checkIfFinished(); |
|
565 return; |
|
566 } |
|
567 } |
|
568 |
|
569 filePointer()->write (networkReply()->readAll()); |
|
570 } |
|
571 |
|
572 // ============================================================================= |
|
573 // |
|
574 bool PartDownloadRequest::isFinished() const |
|
575 { |
|
576 return Eq (state(), State::Finished, State::Failed); |
|
577 } |
|
578 |
|
579 // ============================================================================= |
|
580 // |
|
581 void PartDownloadRequest::abort() |
|
582 { |
|
583 networkReply()->abort(); |
|
584 } |
|
585 |
|
586 // ============================================================================= |
|
587 // |
|
588 void MainWindow::actionDownloadFrom() |
|
589 { |
|
590 PartDownloader::staticBegin(); |
|
591 } |