Wed, 08 Jan 2014 13:43:39 +0200
- corrected relationships between documents: opening a main file with the same name as another document is to overload it and editing the document is to invalidate its cache so that it gets rendered properly in other documents possibly referencing it.
/* * LDForge: LDraw parts authoring CAD * Copyright (C) 2013, 2014 Santeri Piippo * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include <QNetworkAccessManager> #include <QNetworkRequest> #include <QNetworkReply> #include <QDir> #include <QProgressBar> #include <QPushButton> #include "download.h" #include "ui_downloadfrom.h" #include "types.h" #include "gui.h" #include "document.h" #include "gldraw.h" #include "configDialog.h" #include "moc_download.cpp" cfg (String, net_downloadpath, ""); cfg (Bool, net_guesspaths, true); cfg (Bool, net_autoclose, true); const QString g_unofficialLibraryURL ("http://ldraw.org/library/unofficial/"); // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloader::staticBegin() { QString path = getDownloadPath(); if (path == "" || QDir (path).exists() == false) { critical (PartDownloader::tr ("You need to specify a valid path for " "downloaded files in the configuration to download paths.")); (new ConfigDialog (ConfigDialog::DownloadTab, null))->exec(); return; } PartDownloader* dlg = new PartDownloader; dlg->exec(); } // ============================================================================= // ----------------------------------------------------------------------------- QString PartDownloader::getDownloadPath() { QString path = net_downloadpath; #if DIRSLASH_CHAR != '/' path.replace (DIRSLASH, "/"); #endif return path; } // ============================================================================= // ----------------------------------------------------------------------------- PartDownloader::PartDownloader (QWidget* parent) : QDialog (parent) { setInterface (new Ui_DownloadFrom); getInterface()->setupUi (this); getInterface()->fname->setFocus(); getInterface()->progress->horizontalHeader()->setResizeMode (PartLabelColumn, QHeaderView::Stretch); setDownloadButton (new QPushButton (tr ("Download"))); getInterface()->buttonBox->addButton (getDownloadButton(), QDialogButtonBox::ActionRole); getButton (Abort)->setEnabled (false); connect (getInterface()->source, SIGNAL (currentIndexChanged (int)), this, SLOT (sourceChanged (int))); connect (getInterface()->buttonBox, SIGNAL (clicked (QAbstractButton*)), this, SLOT (buttonClicked (QAbstractButton*))); } // ============================================================================= // ----------------------------------------------------------------------------- PartDownloader::~PartDownloader() { delete getInterface(); } // ============================================================================= // ----------------------------------------------------------------------------- QString PartDownloader::getURL() const { const Source src = getSource(); QString dest; switch (src) { case PartsTracker: dest = getInterface()->fname->text(); modifyDestination (dest); return g_unofficialLibraryURL + dest; case CustomURL: return getInterface()->fname->text(); } // Shouldn't happen return ""; } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloader::modifyDestination (QString& dest) const { dest = dest.simplified(); // If the user doesn't want us to guess, stop right here. if (net_guesspaths == false) return; // Ensure .dat extension if (dest.right (4) != ".dat") { // Remove the existing extension, if any. It may be we're here over a // typo in the .dat extension. const int dotpos = dest.lastIndexOf ("."); if (dotpos != -1 && dotpos >= dest.length() - 4) dest.chop (dest.length() - dotpos); dest += ".dat"; } // If the part starts with s\ or s/, then use parts/s/. Same goes with // 48\ and p/48/. if (dest.left (2) == "s\\" || dest.left (2) == "s/") { dest.remove (0, 2); dest.prepend ("parts/s/"); } elif (dest.left (3) == "48\\" || dest.left (3) == "48/") { dest.remove (0, 3); dest.prepend ("p/48/"); } /* Try determine where to put this part. We have four directories: parts/, parts/s/, p/, and p/48/. If we haven't already specified either parts/ or p/, we need to add it automatically. Part files are numbers wit a possible u prefix for parts with unknown number which can be followed by any of: - c** (composites) - d** (formed stickers) - p** (patterns) - a lowercase alphabetic letter for variants Subfiles (usually) have an s** prefix, in which case we use parts/s/. Note that the regex starts with a '^' so it won't catch already fully given part file names. */ QString partRegex = "^u?[0-9]+(c[0-9][0-9]+)*(d[0-9][0-9]+)*[a-z]?(p[0-9a-z][0-9a-z]+)*"; QString subpartRegex = partRegex + "s[0-9][0-9]+"; partRegex += "\\.dat$"; subpartRegex += "\\.dat$"; if (QRegExp (subpartRegex).exactMatch (dest)) dest.prepend ("parts/s/"); elif (QRegExp (partRegex).exactMatch (dest)) dest.prepend ("parts/"); elif (dest.left (6) != "parts/" && dest.left (2) != "p/") dest.prepend ("p/"); } // ============================================================================= // ----------------------------------------------------------------------------- PartDownloader::Source PartDownloader::getSource() const { return (Source) getInterface()->source->currentIndex(); } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloader::sourceChanged (int i) { if (i == CustomURL) getInterface()->fileNameLabel->setText (tr ("URL:")); else getInterface()->fileNameLabel->setText (tr ("File name:")); } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloader::buttonClicked (QAbstractButton* btn) { if (btn == getButton (Close)) { reject(); } elif (btn == getButton (Abort)) { setAborted (true); for (PartDownloadRequest* req : getRequests()) req->abort(); } elif (btn == getButton (Download)) { QString dest = getInterface()->fname->text(); setPrimaryFile (null); setAborted (false); if (getSource() == CustomURL) dest = basename (getURL()); modifyDestination (dest); if (QFile::exists (PartDownloader::getDownloadPath() + DIRSLASH + dest)) { const QString overwritemsg = fmt (tr ("%1 already exists in download directory. Overwrite?"), dest); if (!confirm (tr ("Overwrite?"), overwritemsg)) return; } getDownloadButton()->setEnabled (false); getInterface()->progress->setEnabled (true); getInterface()->fname->setEnabled (false); getInterface()->source->setEnabled (false); downloadFile (dest, getURL(), true); getButton (Close)->setEnabled (false); getButton (Abort)->setEnabled (true); getButton (Download)->setEnabled (false); } } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloader::downloadFile (QString dest, QString url, bool primary) { const int row = getInterface()->progress->rowCount(); // Don't download files repeadetly. if (getFilesToDownload().indexOf (dest) != -1) return; modifyDestination (dest); log ("DOWNLOAD: %1 -> %2\n", url, PartDownloader::getDownloadPath() + DIRSLASH + dest); PartDownloadRequest* req = new PartDownloadRequest (url, dest, primary, this); pushToFilesToDownload (dest); pushToRequests (req); getInterface()->progress->insertRow (row); req->setTableRow (row); req->updateToTable(); } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloader::checkIfFinished() { bool failed = isAborted(); // If there is some download still working, we're not finished. for (PartDownloadRequest* req : getRequests()) { if (!req->isFinished()) return; if (req->getState() == PartDownloadRequest::EFailed) failed = true; } for (PartDownloadRequest* req : getRequests()) delete req; clearRequests(); // Update everything now if (getPrimaryFile()) { LDDocument::setCurrent (getPrimaryFile()); reloadAllSubfiles(); g_win->doFullRefresh(); g_win->R()->resetAngles(); } if (net_autoclose && !failed) { // Close automatically if desired. accept(); } else { // Allow the prompt be closed now. getButton (Abort)->setEnabled (false); getButton (Close)->setEnabled (true); } } // ============================================================================= // ----------------------------------------------------------------------------- QPushButton* PartDownloader::getButton (PartDownloader::Button i) { switch (i) { case Download: return getDownloadButton(); case Abort: return qobject_cast<QPushButton*> (getInterface()->buttonBox->button (QDialogButtonBox::Abort)); case Close: return qobject_cast<QPushButton*> (getInterface()->buttonBox->button (QDialogButtonBox::Close)); } return null; } // ============================================================================= // ----------------------------------------------------------------------------- PartDownloadRequest::PartDownloadRequest (QString url, QString dest, bool primary, PartDownloader* parent) : QObject (parent), m_State (ERequesting), m_Prompt (parent), m_URL (url), m_Destinaton (dest), m_FilePath (PartDownloader::getDownloadPath() + DIRSLASH + dest), m_NAM (new QNetworkAccessManager), m_FirstUpdate (true), m_Primary (primary), m_FilePointer (null) { // Make sure that we have a valid destination. QString dirpath = dirname (getFilePath()); QDir dir (dirpath); if (!dir.exists()) { log ("Creating %1...\n", dirpath); if (!dir.mkpath (dirpath)) critical (fmt (tr ("Couldn't create the directory %1!"), dirpath)); } setReply (getNAM()->get (QNetworkRequest (QUrl (url)))); connect (getReply(), SIGNAL (finished()), this, SLOT (downloadFinished())); connect (getReply(), SIGNAL (readyRead()), this, SLOT (readyRead())); connect (getReply(), SIGNAL (downloadProgress (qint64, qint64)), this, SLOT (downloadProgress (qint64, qint64))); } // ============================================================================= // ----------------------------------------------------------------------------- PartDownloadRequest::~PartDownloadRequest() {} // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloadRequest::updateToTable() { const int labelcol = PartDownloader::PartLabelColumn, progcol = PartDownloader::ProgressColumn; QTableWidget* table = getPrompt()->getInterface()->progress; QProgressBar* prog; switch (getState()) { case ERequesting: case EDownloading: { prog = qobject_cast<QProgressBar*> (table->cellWidget (getTableRow(), progcol)); if (!prog) { prog = new QProgressBar; table->setCellWidget (getTableRow(), progcol, prog); } prog->setRange (0, getBytesTotal()); prog->setValue (getBytesRead()); } break; case EFinished: case EFailed: { const QString text = (getState() == EFinished) ? "<b><span style=\"color: #080\">FINISHED</span></b>" : "<b><span style=\"color: #800\">FAILED</span></b>"; QLabel* lb = new QLabel (text); lb->setAlignment (Qt::AlignCenter); table->setCellWidget (getTableRow(), progcol, lb); } break; } QLabel* lb = qobject_cast<QLabel*> (table->cellWidget (getTableRow(), labelcol)); if (isFirstUpdate()) { lb = new QLabel (fmt ("<b>%1</b>", getDestinaton()), table); table->setCellWidget (getTableRow(), labelcol, lb); } // Make sure that the cell is big enough to contain the label if (table->columnWidth (labelcol) < lb->width()) table->setColumnWidth (labelcol, lb->width()); setFirstUpdate (true); } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloadRequest::downloadFinished() { if (getReply()->error() != QNetworkReply::NoError) { if (isPrimary() && !getPrompt()->isAborted()) critical (getReply()->errorString()); setState (EFailed); } elif (getState() != EFailed) setState (EFinished); setBytesRead (getBytesTotal()); updateToTable(); if (getFilePointer()) { getFilePointer()->close(); delete getFilePointer(); setFilePointer (null); if (getState() == EFailed) QFile::remove (getFilePath()); } if (getState() != EFinished) { getPrompt()->checkIfFinished(); return; } // Try to load this file now. LDDocument* f = openDocument (getFilePath(), false); if (!f) return; f->setImplicit (!isPrimary()); // Iterate through this file and check for errors. If there's any that stems // from unknown file references, try resolve that by downloading the reference. // This is why downloading a part may end up downloading multiple files, as // it resolves dependencies. for (LDObject* obj : f->getObjects()) { LDError* err = dynamic_cast<LDError*> (obj); if (!err || err->getFileReferenced().isEmpty()) continue; QString dest = err->getFileReferenced(); getPrompt()->modifyDestination (dest); getPrompt()->downloadFile (dest, g_unofficialLibraryURL + dest, false); } if (isPrimary()) { addRecentFile (getFilePath()); getPrompt()->setPrimaryFile (f); } getPrompt()->checkIfFinished(); } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloadRequest::downloadProgress (int64 recv, int64 total) { setBytesRead (recv); setBytesTotal (total); setState (EDownloading); updateToTable(); } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloadRequest::readyRead() { if (getState() == EFailed) return; if (getFilePointer() == null) { replaceInFilePath ("\\", "/"); // We have already asked the user whether we can overwrite so we're good // to go here. setFilePointer (new QFile (getFilePath().toLocal8Bit())); if (!getFilePointer()->open (QIODevice::WriteOnly)) { critical (fmt (tr ("Couldn't open %1 for writing: %2"), getFilePath(), strerror (errno))); setState (EFailed); getReply()->abort(); updateToTable(); getPrompt()->checkIfFinished(); return; } } getFilePointer()->write (getReply()->readAll()); } // ============================================================================= // ----------------------------------------------------------------------------- bool PartDownloadRequest::isFinished() const { return getState() == EFinished || getState() == EFailed; } // ============================================================================= // ----------------------------------------------------------------------------- void PartDownloadRequest::abort() { getReply()->abort(); } // ============================================================================= // ----------------------------------------------------------------------------- DEFINE_ACTION (DownloadFrom, 0) { PartDownloader::staticBegin(); }