diff --git a/src/lay/LogViewerDialog.ui b/src/lay/LogViewerDialog.ui index eeb86327f..889342ade 100644 --- a/src/lay/LogViewerDialog.ui +++ b/src/lay/LogViewerDialog.ui @@ -1,117 +1,213 @@ - + + LogViewerDialog - - + + 0 0 - 578 - 579 + 516 + 287 - + Log Viewer - - + + 9 - + + 9 + + + 9 + + + 9 + + 6 - - - - Clear - - - - - - - Separator - - - - - - - Copy - - - - - - - Qt::Horizontal - - - - 101 - 22 - - - - - - - - Verbosity - - - - - + + - + Silent - + Information - + Details - + Verbose - + Noisy - - - - QListView::Adjust - - - true + + + + Separator - - - + + + + Copy + + + + + + + Clear + + + + + + + Verbosity + + + + + + + + 16 + 16 + + + + false + + + QListView::Adjust + + + true + + + false + + + + + + Qt::Horizontal - - QDialogButtonBox::Close + + + 101 + 0 + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + :/warn_16.png + + + + + + + There are errors or warnings + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + - + + verbosity_cbx + clear_pb + separator_pb + copy_pb + log_view + + + + buttonBox @@ -119,11 +215,11 @@ LogViewerDialog accept() - + 248 254 - + 157 274 @@ -135,11 +231,11 @@ LogViewerDialog reject() - + 316 260 - + 286 274 diff --git a/src/lay/SaltGrainPropertiesDialog.ui b/src/lay/SaltGrainPropertiesDialog.ui new file mode 100644 index 000000000..1f8cc352f --- /dev/null +++ b/src/lay/SaltGrainPropertiesDialog.ui @@ -0,0 +1,837 @@ + + + SaltGrainPropertiesDialog + + + + 0 + 0 + 693 + 538 + + + + Package Properties + + + + + + + 0 + 1 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16777215 + 80 + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + + + + 0 + 0 + + + + <a href="%1">Open link</a> + + + true + + + + + + + + 75 + true + + + + Author contact + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 75 + true + + + + License + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + 75 + true + + + + Description + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + Qt::Vertical + + + + 20 + 32 + + + + + + + + + 75 + true + + + + Title + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 75 + true + + + + Depends on + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + 75 + true + + + + Version + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 0 + 0 + + + + + true + + + + (enter an URL to provide a documentation link) + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + Qt::NoFocus + + + ... + + + + :/warn.png:/warn.png + + + true + + + + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + Qt::NoFocus + + + ... + + + + :/warn.png:/warn.png + + + true + + + + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Delete dependency + + + ... + + + + :/clear.png:/clear.png + + + true + + + + + + + Add new dependency + + + ... + + + + :/add.png:/add.png + + + true + + + + + + + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + true + + + false + + + true + + + + Name + + + + + Version + + + + + URL + + + + + + + + Qt::NoFocus + + + ... + + + + :/warn.png:/warn.png + + + true + + + + + + + + + + + 75 + true + + + + Images + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + true + + + + (use numeric versions like "1.5" or "2.1.3") + + + + + + + + 75 + true + + + + Author + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 1 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 2 + + + 6 + + + + + Reset Screenshot + + + ... + + + + :/clear_edit.png:/clear_edit.png + + + true + + + + + + + Icon +(64x64) + + + + + + + Select an Icon Image + + + ... + + + + :/salt_icon.png:/salt_icon.png + + + + 64 + 64 + + + + + + + + Select a Screenshot Image + + + ... + + + + :/add.png:/add.png + + + + 64 + 64 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 30 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Reset Icon + + + ... + + + + :/clear_edit.png:/clear_edit.png + + + true + + + + + + + Showcase image +(max 1024x1024) + + + + + + + + + + + 75 + true + + + + Documentation + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + true + + + + (license information like "GPLv3" or "MIT") + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + Qt::NoFocus + + + ... + + + + :/warn.png:/warn.png + + + true + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + lay::AlertLogButton + QToolButton +
layLogViewerDialog.h
+
+
+ + version + title + author + author_contact + license + doc + doc_url + icon_config_button + icon_delete_button + screenshot_config_button + screenshot_delete_button + dependencies + add_dependency + remove_dependency + + + + + + + buttonBox + accepted() + SaltGrainPropertiesDialog + accept() + + + 546 + 425 + + + 561 + 435 + + + + + buttonBox + rejected() + SaltGrainPropertiesDialog + reject() + + + 638 + 418 + + + 649 + 433 + + + + +
diff --git a/src/lay/SaltGrainTemplateSelectionDialog.ui b/src/lay/SaltGrainTemplateSelectionDialog.ui new file mode 100644 index 000000000..65406eb88 --- /dev/null +++ b/src/lay/SaltGrainTemplateSelectionDialog.ui @@ -0,0 +1,237 @@ + + + SaltGrainTemplateSelectionDialog + + + + 0 + 0 + 401 + 499 + + + + Create Package + + + + + + + 75 + true + + + + Template + + + + + + + true + + + + 64 + 64 + + + + false + + + + + + + + true + + + + (Pick a template with which to initialize your new package) + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 75 + true + + + + Package Name + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::NoFocus + + + ... + + + + :/warn.png:/warn.png + + + true + + + + + + + + + + + 50 + true + false + + + + (Choose a simple name composed of letters, digits and underscores. Use the notation "group/package" to create a package inside a group) + + + true + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + :/add.png:/add.png + + + New + + + New package + + + + + + :/clear.png:/clear.png + + + Delete + + + Delete package + + + + + + :/import.png:/import.png + + + Import + + + Import package + + + + + + lay::AlertLogButton + QToolButton +
layLogViewerDialog.h
+
+
+ + + + + + buttonBox + accepted() + SaltGrainTemplateSelectionDialog + accept() + + + 273 + 431 + + + 276 + 448 + + + + + buttonBox + rejected() + SaltGrainTemplateSelectionDialog + reject() + + + 351 + 426 + + + 363 + 445 + + + + +
diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui new file mode 100644 index 000000000..77651bd95 --- /dev/null +++ b/src/lay/SaltManagerDialog.ui @@ -0,0 +1,798 @@ + + + SaltManagerDialog + + + + 0 + 0 + 692 + 440 + + + + Salt Package Manager + + + + + + 1 + + + + Browse Installed Packages + + + + + + Qt::Horizontal + + + + + 4 + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::NoFocus + + + Create New Package + + + ... + + + + :/add.png:/add.png + + + true + + + + + + + Qt::NoFocus + + + Uninstall Package + + + ... + + + + :/clear.png:/clear.png + + + true + + + + + + + Qt::Horizontal + + + + 50 + 0 + + + + + + + + + + + :/find.png + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 64 + 64 + + + + false + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 343 + 207 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + :/salt.png + + + + + + + + 0 + 0 + + + + + 9 + 50 + false + + + + <h1>Salt Package Manager</h1> + + + + + + + + 1 + 0 + + + + + 0 + 0 + + + + <html><body><h4>No packages are installed currently.</h4><p>Use<br/> +<table> + <tr><td>The "Install New Packages" tab to install a package<br/>from an external repository</td></tr> + <tr></tr> + <tr><td>The <a href=":add"><img src=":/add.png"></a> button to create a new package</td></tr> +</table> +</body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + + + + + + + + + + + 1 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 4 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Details + + + + + + + Edit Package Details + + + Edit + + + + :/edit.png:/edit.png + + + true + + + + + + + true + + + + + + + + + + + + Install or Update Packages + + + + + + + 0 + 1 + + + + Qt::Horizontal + + + + + 4 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Mark or unmark for installation + + + Mark + + + + :/marked_16.png:/marked_16.png + + + true + + + + + + + Qt::Horizontal + + + + 126 + 20 + + + + + + + + + + + :/find.png + + + + + + + + 0 + 0 + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 64 + 64 + + + + false + + + + + + + + + + + + 1 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 4 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Details + + + + + + + false + + + ... + + + + :/empty_16.png:/empty_16.png + + + true + + + + + + + + + + true + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Apply + + + false + + + false + + + + + + + Set in code + + + + + + + Qt::Horizontal + + + + 563 + 20 + + + + + + + + + + + + + + + QDialogButtonBox::Close + + + + + + + + :/add.png:/add.png + + + New + + + New package + + + + + + :/clear.png:/clear.png + + + Delete + + + Delete package + + + + + Unmark all + + + + + Show marked only + + + + + Show all + + + + + + lay::DecoratedLineEdit + QLineEdit +
layWidgets.h
+
+ + lay::SaltGrainDetailsTextWidget + QTextBrowser +
laySaltGrainDetailsTextWidget.h
+
+
+ + mode_tab + search_installed_edit + salt_view + details_text + edit_button + search_new_edit + mark_button + salt_mine_view + details_new_text + toolButton + apply_button + scrollArea + + + + + + + buttonBox + clicked(QAbstractButton*) + SaltManagerDialog + accept() + + + 635 + 421 + + + 653 + 432 + + + + +
diff --git a/src/lay/SaltManagerInstallConfirmationDialog.ui b/src/lay/SaltManagerInstallConfirmationDialog.ui new file mode 100644 index 000000000..d8dcf20e4 --- /dev/null +++ b/src/lay/SaltManagerInstallConfirmationDialog.ui @@ -0,0 +1,165 @@ + + + SaltManagerInstallConfirmationDialog + + + + 0 + 0 + 495 + 478 + + + + Ready for Installation + + + + + + The following packages are now ready for installation or update: + + + true + + + + + + + Qt::NoFocus + + + false + + + true + + + + Package + + + + + Action + + + + + Version + + + + + Download link + + + + + + + + Press "Ok" to install or update these packages or "Cancel" to abort. + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 6 + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + :/add.png:/add.png + + + New + + + New package + + + + + + :/clear.png:/clear.png + + + Delete + + + Delete package + + + + + + :/import.png:/import.png + + + Import + + + Import package + + + + + + + + + buttonBox + accepted() + SaltManagerInstallConfirmationDialog + accept() + + + 273 + 431 + + + 276 + 448 + + + + + buttonBox + rejected() + SaltManagerInstallConfirmationDialog + reject() + + + 351 + 426 + + + 363 + 445 + + + + + diff --git a/src/lay/gsiDeclLayMainWindow.cc b/src/lay/gsiDeclLayMainWindow.cc index a64fad18d..77c8eb45e 100644 --- a/src/lay/gsiDeclLayMainWindow.cc +++ b/src/lay/gsiDeclLayMainWindow.cc @@ -539,6 +539,10 @@ Class decl_MainWindow (QT_EXTERNAL_BASE (QMainWindow) "MainWind "@brief 'cm_technologies' action (bound to a menu)" "\nThis method has been added in version 0.22." ) + + gsi::method ("cm_packages", &lay::MainWindow::cm_packages, + "@brief 'cm_packages' action (bound to a menu)" + "\nThis method has been added in version 0.25." + ) + gsi::method ("cm_open_too", &lay::MainWindow::cm_open_too, "@brief 'cm_open_too' action (bound to a menu)" ) + diff --git a/src/lay/images/empty_16.png b/src/lay/images/empty_16.png new file mode 100644 index 000000000..10b1f1be6 Binary files /dev/null and b/src/lay/images/empty_16.png differ diff --git a/src/lay/images/error_16.png b/src/lay/images/error_16.png new file mode 100644 index 000000000..c9dad1310 Binary files /dev/null and b/src/lay/images/error_16.png differ diff --git a/src/lay/images/info_16.png b/src/lay/images/info_16.png new file mode 100644 index 000000000..d1fa36644 Binary files /dev/null and b/src/lay/images/info_16.png differ diff --git a/src/lay/images/marked_16.png b/src/lay/images/marked_16.png new file mode 100644 index 000000000..777a625a7 Binary files /dev/null and b/src/lay/images/marked_16.png differ diff --git a/src/lay/images/marked_24.png b/src/lay/images/marked_24.png new file mode 100644 index 000000000..cf50321a8 Binary files /dev/null and b/src/lay/images/marked_24.png differ diff --git a/src/lay/images/marked_64.png b/src/lay/images/marked_64.png new file mode 100644 index 000000000..02c23d603 Binary files /dev/null and b/src/lay/images/marked_64.png differ diff --git a/src/lay/images/salt.png b/src/lay/images/salt.png new file mode 100644 index 000000000..181409a28 Binary files /dev/null and b/src/lay/images/salt.png differ diff --git a/src/lay/images/salt_icon.png b/src/lay/images/salt_icon.png new file mode 100644 index 000000000..ec11c7ac0 Binary files /dev/null and b/src/lay/images/salt_icon.png differ diff --git a/src/lay/images/warn.png b/src/lay/images/warn.png new file mode 100644 index 000000000..adea92d4f Binary files /dev/null and b/src/lay/images/warn.png differ diff --git a/src/lay/images/warn_16.png b/src/lay/images/warn_16.png new file mode 100644 index 000000000..357216719 Binary files /dev/null and b/src/lay/images/warn_16.png differ diff --git a/src/lay/lay.pro b/src/lay/lay.pro index 7e99a9574..d6b1a048a 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -47,7 +47,15 @@ HEADERS = \ layCommon.h \ layConfig.h \ layMacroController.h \ - layTechnologyController.h + layTechnologyController.h \ + laySalt.h \ + laySaltGrain.h \ + laySaltGrains.h \ + laySaltManagerDialog.h \ + laySaltGrainDetailsTextWidget.h \ + laySaltGrainPropertiesDialog.h \ + laySaltDownloadManager.h \ + laySaltModel.h FORMS = \ ClipDialog.ui \ @@ -92,7 +100,11 @@ FORMS = \ XORToolDialog.ui \ TechLoadOptionsEditorPage.ui \ TechSaveOptionsEditorPage.ui \ - MainConfigPage7.ui + MainConfigPage7.ui \ + SaltManagerDialog.ui \ + SaltGrainPropertiesDialog.ui \ + SaltGrainTemplateSelectionDialog.ui \ + SaltManagerInstallConfirmationDialog.ui SOURCES = \ gsiDeclLayApplication.cc \ @@ -136,13 +148,22 @@ SOURCES = \ layTextProgress.cc \ layVersion.cc \ layMacroController.cc \ - layTechnologyController.cc + layTechnologyController.cc \ + laySalt.cc \ + laySaltGrain.cc \ + laySaltGrains.cc \ + laySaltManagerDialog.cc \ + laySaltGrainDetailsTextWidget.cc \ + laySaltGrainPropertiesDialog.cc \ + laySaltDownloadManager.cc \ + laySaltModel.cc RESOURCES = layBuildInMacros.qrc \ layHelpResources.qrc \ layLayoutStatistics.qrc \ layMacroTemplates.qrc \ layResources.qrc \ + laySaltTemplates.qrc INCLUDEPATH += ../tl ../gsi ../db ../rdb ../laybasic ../ant ../img ../edt DEPENDPATH += ../tl ../gsi ../db ../rdb ../laybasic ../ant ../img ../edt diff --git a/src/lay/layLogViewerDialog.cc b/src/lay/layLogViewerDialog.cc index f6be30d4a..b1a5a8172 100644 --- a/src/lay/layLogViewerDialog.cc +++ b/src/lay/layLogViewerDialog.cc @@ -27,6 +27,7 @@ #include #include #include +#include #include @@ -97,25 +98,30 @@ LogReceiver::begin () // ----------------------------------------------------------------- // LogFile implementation -LogFile::LogFile (size_t max_entries) - : m_error_receiver (this, 0, &LogFile::error), - m_warn_receiver (this, 0, &LogFile::warn), - m_log_receiver (this, 10, &LogFile::info), - m_info_receiver (this, 0, &LogFile::info), +LogFile::LogFile (size_t max_entries, bool register_global) + : m_error_receiver (this, 0, &LogFile::add_error), + m_warn_receiver (this, 0, &LogFile::add_warn), + m_log_receiver (this, 10, &LogFile::add_info), + m_info_receiver (this, 0, &LogFile::add_info), m_max_entries (max_entries), m_generation_id (0), - m_last_generation_id (0) + m_last_generation_id (0), + m_has_errors (false), + m_has_warnings (false), + m_last_attn (false) { connect (&m_timer, SIGNAL (timeout ()), this, SLOT (timeout ())); - m_timer.setSingleShot (true); + m_timer.setSingleShot (false); m_timer.setInterval (100); m_timer.start (); - tl::info.add (&m_info_receiver, false); - tl::log.add (&m_log_receiver, false); - tl::error.add (&m_error_receiver, false); - tl::warn.add (&m_warn_receiver, false); + if (register_global) { + tl::info.add (&m_info_receiver, false); + tl::log.add (&m_log_receiver, false); + tl::error.add (&m_error_receiver, false); + tl::warn.add (&m_warn_receiver, false); + } } void @@ -123,8 +129,23 @@ LogFile::clear () { QMutexLocker locker (&m_lock); - m_messages.clear (); - ++m_generation_id; + if (!m_messages.empty ()) { + m_messages.clear (); + m_has_errors = m_has_warnings = false; + ++m_generation_id; + } +} + +bool +LogFile::has_errors () const +{ + return m_has_errors; +} + +bool +LogFile::has_warnings () const +{ + return m_has_warnings; } void @@ -159,9 +180,13 @@ void LogFile::timeout () { bool changed = false; + bool attn = false, last_attn = false; m_lock.lock (); if (m_generation_id != m_last_generation_id) { + attn = m_has_errors || m_has_warnings; + last_attn = m_last_attn; + m_last_attn = attn; m_last_generation_id = m_generation_id; changed = true; } @@ -169,9 +194,10 @@ LogFile::timeout () if (changed) { emit layoutChanged (); + if (last_attn != attn) { + emit attention_changed (attn); + } } - - m_timer.start (); } void @@ -183,6 +209,12 @@ LogFile::add (LogFileEntry::mode_type mode, const std::string &msg, bool continu m_messages.pop_front (); } + if (mode == LogFileEntry::Warning || mode == LogFileEntry::WarningContinued) { + m_has_warnings = true; + } else if (mode == LogFileEntry::Error || mode == LogFileEntry::ErrorContinued) { + m_has_errors = true; + } + m_messages.push_back (LogFileEntry (mode, msg, continued)); ++m_generation_id; @@ -201,18 +233,25 @@ LogFile::data(const QModelIndex &index, int role) const { QMutexLocker locker (&m_lock); - if (role == Qt::DisplayRole) { + if (role == Qt::DecorationRole) { if (index.row () < int (m_messages.size ()) && index.row () >= 0) { LogFileEntry::mode_type mode = m_messages [index.row ()].mode (); - std::string message = m_messages [index.row ()].text (); if (mode == LogFileEntry::Error) { - return QVariant (tl::to_qstring (tl::to_string (QObject::tr ("ERROR: ")) + message)); + return QIcon (QString::fromUtf8 (":/error_16.png")); } else if (mode == LogFileEntry::Warning) { - return QVariant (tl::to_qstring (tl::to_string (QObject::tr ("Warning: ")) + message)); + return QIcon (QString::fromUtf8 (":/warn_16.png")); + } else if (mode == LogFileEntry::Info) { + return QIcon (QString::fromUtf8 (":/info_16.png")); } else { - return QVariant (tl::to_qstring (message)); + return QIcon (QString::fromUtf8 (":/empty_16.png")); } + } + + } else if (role == Qt::DisplayRole) { + + if (index.row () < int (m_messages.size ()) && index.row () >= 0) { + return QVariant (tl::to_qstring (m_messages [index.row ()].text ())); } } else if (role == Qt::FontRole) { @@ -251,21 +290,36 @@ LogFile::data(const QModelIndex &index, int role) const // ----------------------------------------------------------------- // LogViewerDialog implementation -LogViewerDialog::LogViewerDialog (QWidget *parent) +LogViewerDialog::LogViewerDialog (QWidget *parent, bool register_global, bool interactive) : QDialog (parent), - m_file (50000) // TODO: make this variable .. + m_file (50000, register_global) // TODO: make this variable .. { setupUi (this); + // For non-global log views, the verbosity selector does not make sense + if (!register_global) { + verbosity_cbx->hide (); + verbosity_label->hide (); + } else { + verbosity_cbx->setCurrentIndex (std::min (4, tl::verbosity () / 10)); + connect (verbosity_cbx, SIGNAL (currentIndexChanged (int)), this, SLOT (verbosity_changed (int))); + } + + if (!interactive) { + clear_pb->hide (); + separator_pb->hide (); + copy_pb->hide (); + } else { + connect (clear_pb, SIGNAL (clicked ()), &m_file, SLOT (clear ())); + connect (separator_pb, SIGNAL (clicked ()), &m_file, SLOT (separator ())); + connect (copy_pb, SIGNAL (clicked ()), &m_file, SLOT (copy ())); + } + + attn_frame->hide (); log_view->setModel (&m_file); - verbosity_cbx->setCurrentIndex (std::min (4, tl::verbosity () / 10)); - connect (&m_file, SIGNAL (layoutChanged ()), log_view, SLOT (scrollToBottom ())); - connect (verbosity_cbx, SIGNAL (currentIndexChanged (int)), this, SLOT (verbosity_changed (int))); - connect (clear_pb, SIGNAL (clicked ()), &m_file, SLOT (clear ())); - connect (separator_pb, SIGNAL (clicked ()), &m_file, SLOT (separator ())); - connect (copy_pb, SIGNAL (clicked ()), &m_file, SLOT (copy ())); + connect (&m_file, SIGNAL (attention_changed (bool)), attn_frame, SLOT (setVisible (bool))); } void @@ -274,5 +328,56 @@ LogViewerDialog::verbosity_changed (int index) tl::verbosity (index * 10 + 1); } +// ----------------------------------------------------------------- +// AlertLog implementation + +AlertLogButton::AlertLogButton (QWidget *parent) + : QToolButton (parent) +{ + mp_logger = new LogViewerDialog (this, false, false); + hide (); + connect (&mp_logger->file (), SIGNAL (attention_changed (bool)), this, SLOT (attention_changed (bool))); + connect (this, SIGNAL (clicked ()), mp_logger, SLOT (exec ())); +} + +void +AlertLogButton::attention_changed (bool attn) +{ + setVisible (attn); + + // as a special service, enlarge and color any surrounding frame red - + // this feature allows putting the alert button together with other entry fields into a frame and + // make this frame highlighted on error or warning. + QFrame *frame = dynamic_cast (parent ()); + if (frame) { + + if (frame->layout ()) { + int l = 0, t = 0, r = 0, b = 0; + frame->layout ()->getContentsMargins (&l, &t, &r, &b); + if (attn) { + l += 3; t += 3; r += 2; b += 2; + } else { + l -= 3; t -= 3; r -= 2; b -= 2; + } + frame->layout ()->setContentsMargins (l, t, r, b); + } + + if (attn) { + + frame->setAutoFillBackground (true); + QPalette palette = frame->palette (); + palette.setColor (QPalette::Window, QColor (255, 160, 160)); + frame->setPalette (palette); + + } else { + + frame->setAutoFillBackground (false); + frame->setPalette (QPalette ()); + + } + + } +} + } diff --git a/src/lay/layLogViewerDialog.h b/src/lay/layLogViewerDialog.h index e2d132146..c31eca626 100644 --- a/src/lay/layLogViewerDialog.h +++ b/src/lay/layLogViewerDialog.h @@ -26,11 +26,13 @@ #include "ui_LogViewerDialog.h" #include "tlLog.h" +#include "layCommon.h" #include #include #include #include +#include #include #include @@ -40,6 +42,9 @@ namespace lay class LogFile; +/** + * @brief A helper class describing one log entry + */ class LogFileEntry { public: @@ -70,7 +75,10 @@ private: bool m_continued; }; -class LogReceiver +/** + * @brief The log receiver abstraction that connects a channel with the LogFile object + */ +class LAY_PUBLIC LogReceiver : public tl::Channel { public: @@ -91,40 +99,110 @@ private: QMutex m_lock; }; -class LogFile +/** + * @brief A log collection ("log file") + * + * The log collector collects warnings, errors and info messages + * and presents this collection as a QAbstractListModel view + * viewing inside a QTreeWidget or the LogViewerDialog. + * + * The log collector can either be used standalone or as a + * global receiver that will collect the global log + * messages. + */ +class LAY_PUBLIC LogFile : public QAbstractListModel { Q_OBJECT public: - LogFile (size_t max_entries); - - void error (const std::string &msg, bool continued) - { - add (continued ? LogFileEntry::ErrorContinued : LogFileEntry::Error, msg, continued); - } - - void info (const std::string &msg, bool continued) - { - add (continued ? LogFileEntry::InfoContinued : LogFileEntry::Info, msg, continued); - } - - void warn (const std::string &msg, bool continued) - { - add (continued ? LogFileEntry::WarningContinued : LogFileEntry::Warning, msg, continued); - } + /** + * @brief Constructs a log file receiver + * If "register_global" is true, the receiver will register itself as a global log receiver. + * Otherwise it's a private one that can be used with the "error", "warn" and "info" channels + * provided by the respective methods. + */ + LogFile (size_t max_entries, bool register_global = true); + /** + * @brief Implementation of the QAbstractItemModel interface + */ int rowCount(const QModelIndex &parent) const; + /** + * @brief Implementation of the QAbstractItemModel interface + */ QVariant data(const QModelIndex &index, int role) const; -private slots: - void timeout (); + /** + * @brief Gets a value indicating whether errors are present + */ + bool has_errors () const; + + /** + * @brief Gets a value indicating whether warnings are present + */ + bool has_warnings () const; + +public slots: + /** + * @brief Clears the log + */ void clear (); + + /** + * @brief Adds a separator + */ void separator (); + + /** + * @brief copies the contents to the clipboard + */ void copy (); -public: + /** + * @brief Gets the error channel + */ + tl::Channel &error () + { + return m_error_receiver; + } + + /** + * @brief Gets the warning channel + */ + tl::Channel &warn () + { + return m_warn_receiver; + } + + /** + * @brief Gets the info channel + */ + tl::Channel &info () + { + return m_info_receiver; + } + + /** + * @brief Gets the log channel + */ + tl::Channel &log () + { + return m_log_receiver; + } + +private slots: + void timeout (); + +signals: + /** + * @brief This signal is emitted if the log's attention state has changed + * Attention state is "true" if either errors or warnings are present. + */ + void attention_changed (bool f); + +private: QTimer m_timer; mutable QMutex m_lock; LogReceiver m_error_receiver; @@ -135,18 +213,63 @@ public: size_t m_max_entries; size_t m_generation_id; size_t m_last_generation_id; + bool m_has_errors, m_has_warnings; + bool m_last_attn; + /** + * @brief Adds an error + */ + void add_error (const std::string &msg, bool continued) + { + add (continued ? LogFileEntry::ErrorContinued : LogFileEntry::Error, msg, continued); + } + + /** + * @brief Adds a info message + */ + void add_info (const std::string &msg, bool continued) + { + add (continued ? LogFileEntry::InfoContinued : LogFileEntry::Info, msg, continued); + } + + /** + * @brief Adds a warning + */ + void add_warn (const std::string &msg, bool continued) + { + add (continued ? LogFileEntry::WarningContinued : LogFileEntry::Warning, msg, continued); + } + + /** + * @brief Adds anything + */ void add (LogFileEntry::mode_type mode, const std::string &msg, bool continued); }; -class LogViewerDialog +/** + * @brief A dialog presenting the log file + */ +class LAY_PUBLIC LogViewerDialog : public QDialog, public Ui::LogViewerDialog { Q_OBJECT public: - LogViewerDialog (QWidget *parent); + /** + * @brief The constructor + * If "register_global" is true, the log is registered globally + * and will receiver global log messages. + */ + LogViewerDialog (QWidget *parent, bool register_global = true, bool interactive = true); + + /** + * @brief Gets the log file object + */ + LogFile &file () + { + return m_file; + } public slots: void verbosity_changed (int l); @@ -155,7 +278,93 @@ private: LogFile m_file; }; +/** + * @brief A tool button that collects logs and makes itself visible once attention is required + */ +class LAY_PUBLIC AlertLogButton + : public QToolButton +{ +Q_OBJECT + +public: + /** + * @brief Constructor + */ + AlertLogButton (QWidget *parent); + + /** + * @brief Gets the error channel + */ + tl::Channel &error () const + { + return mp_logger->file ().error (); + } + + /** + * @brief Gets the warn channel + */ + tl::Channel &warn () const + { + return mp_logger->file ().warn (); + } + + /** + * @brief Gets the info channel + */ + tl::Channel &info () const + { + return mp_logger->file ().info (); + } + + /** + * @brief Gets the log channel + */ + tl::Channel &log () const + { + return mp_logger->file ().log (); + } + + /** + * @brief Gets the error status of the log + */ + bool has_errors () const + { + return mp_logger->file ().has_errors (); + } + + /** + * @brief Gets the warning status of the log + */ + bool has_warnings () const + { + return mp_logger->file ().has_warnings (); + } + + /** + * @brief Gets the attention status of the log + * (either warnings or errors are present) + */ + bool needs_attention () const + { + return has_errors () || has_warnings (); + } + +public slots: + /** + * @brief Clears the log (and makes the button invisible) + */ + void clear () + { + mp_logger->file ().clear (); + } + +private slots: + void attention_changed (bool); + +private: + LogViewerDialog *mp_logger; +}; + } #endif - diff --git a/src/lay/layMainWindow.cc b/src/lay/layMainWindow.cc index d0c98f402..465821e34 100644 --- a/src/lay/layMainWindow.cc +++ b/src/lay/layMainWindow.cc @@ -85,8 +85,8 @@ #include "layLogViewerDialog.h" #include "layLayerToolbox.h" #include "laySettingsForm.h" -#include "laySettingsForm.h" #include "layTechnologyController.h" +#include "laySaltManagerDialog.h" #include "layTipDialog.h" #include "laySelectCellViewForm.h" #include "layLayoutPropertiesForm.h" @@ -928,6 +928,7 @@ MainWindow::init_menu () }; MenuLayoutEntry tools_menu [] = { + MenuLayoutEntry ("packages", tl::to_string (QObject::tr ("Manage Packages")), SLOT (cm_packages ())), MenuLayoutEntry ("technologies", tl::to_string (QObject::tr ("Manage Technologies")), SLOT (cm_technologies ())), MenuLayoutEntry::separator ("verification_group"), MenuLayoutEntry ("drc", tl::to_string (QObject::tr ("DRC")), drc_menu), @@ -4509,6 +4510,13 @@ MainWindow::show_progress_bar (bool show) } } +void +MainWindow::cm_packages () +{ + lay::SaltManagerDialog dialog (this); + dialog.exec (); +} + void MainWindow::cm_technologies () { diff --git a/src/lay/layMainWindow.h b/src/lay/layMainWindow.h index 55bb7adb0..715e47f40 100644 --- a/src/lay/layMainWindow.h +++ b/src/lay/layMainWindow.h @@ -720,6 +720,7 @@ public slots: void cm_macro_editor (); void cm_new_drc_script (); void cm_edit_drc_scripts (); + void cm_packages (); void cm_technologies (); void cm_open_too (); void cm_open_new_view (); diff --git a/src/lay/layResources.qrc b/src/lay/layResources.qrc index 3c2e3bdd4..52e7bc522 100644 --- a/src/lay/layResources.qrc +++ b/src/lay/layResources.qrc @@ -113,8 +113,18 @@ images/upup.png images/waived.png images/yellow_flag.png + images/salt.png + images/salt_icon.png + images/warn.png + images/warn_16.png + images/empty_16.png + images/error_16.png + images/info_16.png + images/marked_24.png + images/marked_64.png + images/marked_16.png - + syntax/ruby.xml syntax/python.xml diff --git a/src/lay/laySalt.cc b/src/lay/laySalt.cc new file mode 100644 index 000000000..4b1b2174d --- /dev/null +++ b/src/lay/laySalt.cc @@ -0,0 +1,427 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "laySalt.h" +#include "tlString.h" +#include "tlFileUtils.h" +#include "tlLog.h" +#include "tlInternational.h" +#include "tlWebDAV.h" + +#include +#include +#include + +namespace lay +{ + +Salt::Salt () +{ + // .. nothing yet .. +} + +Salt::Salt (const Salt &other) + : QObject () +{ + operator= (other); +} + +Salt &Salt::operator= (const Salt &other) +{ + if (this != &other) { + m_root = other.m_root; + invalidate (); + } + return *this; +} + +Salt::flat_iterator +Salt::begin_flat () +{ + validate (); + return mp_flat_grains.begin (); +} + +Salt::flat_iterator +Salt::end_flat () +{ + validate (); + return mp_flat_grains.end (); +} + +SaltGrain * +Salt::grain_by_name (const std::string &name) +{ + validate (); + std::map::const_iterator g = m_grains_by_name.find (name); + if (g != m_grains_by_name.end ()) { + return g->second; + } else { + return 0; + } +} + +void +Salt::add_location (const std::string &path) +{ + tl_assert (! path.empty ()); + + if (path[0] != ':') { + // do nothing if the collection is already there + QFileInfo fi (tl::to_qstring (path)); + for (lay::SaltGrains::collection_iterator g = m_root.begin_collections (); g != m_root.end_collections (); ++g) { + if (QFileInfo (tl::to_qstring (g->path ())) == fi) { + return; + } + } + } + + lay::SaltGrains gg = lay::SaltGrains::from_path (path); + m_root.add_collection (gg); + invalidate (); +} + +void +Salt::remove_location (const std::string &path) +{ + QFileInfo fi (tl::to_qstring (path)); + for (lay::SaltGrains::collection_iterator g = m_root.begin_collections (); g != m_root.end_collections (); ++g) { + if (QFileInfo (tl::to_qstring (g->path ())) == fi) { + m_root.remove_collection (g, false); + invalidate (); + return; + } + } +} + +void +Salt::refresh () +{ + lay::SaltGrains new_root; + for (lay::SaltGrains::collection_iterator g = m_root.begin_collections (); g != m_root.end_collections (); ++g) { + new_root.add_collection (lay::SaltGrains::from_path (g->path ())); + } + if (new_root != m_root) { + m_root = new_root; + invalidate (); + } +} + +void +Salt::add_collection_to_flat (SaltGrains &gg) +{ + for (lay::SaltGrains::grain_iterator g = gg.begin_grains (); g != gg.end_grains (); ++g) { + // TODO: get rid of the const cast - would require a non-const grain iterator + mp_flat_grains.push_back (const_cast (g.operator-> ())); + } + for (lay::SaltGrains::collection_iterator g = gg.begin_collections (); g != gg.end_collections (); ++g) { + // TODO: get rid of the const cast - would require a non-const grain collection iterator + add_collection_to_flat (const_cast (*g)); + } +} + +namespace { + +struct NameCompare +{ + bool operator () (lay::SaltGrain *a, lay::SaltGrain *b) const + { + // TODO: UTF-8 support? + return a->name () < b->name (); + } +}; + +} + +void +Salt::validate () +{ + if (mp_flat_grains.empty ()) { + + add_collection_to_flat (m_root); + + m_grains_by_name.clear (); + for (std::vector::const_iterator i = mp_flat_grains.begin (); i != mp_flat_grains.end (); ++i) { + m_grains_by_name.insert (std::make_pair ((*i)->name (), *i)); + } + + // NOTE: we intentionally sort after the name list has been built - this way + // the first entry will win in the name to grain map. + std::sort (mp_flat_grains.begin (), mp_flat_grains.end (), NameCompare ()); + + } +} + +void +Salt::invalidate () +{ + mp_flat_grains.clear (); + emit collections_changed (); +} + + +static +bool remove_from_collection (SaltGrains &collection, const std::string &name) +{ + bool res = false; + + for (SaltGrains::grain_iterator g = collection.begin_grains (); g != collection.end_grains (); ++g) { + if (g->name () == name) { + SaltGrains::grain_iterator gnext = g; + ++gnext; + collection.remove_grain (g, true); + res = true; + g = gnext; + } + } + + for (SaltGrains::collection_iterator gg = collection.begin_collections (); gg != collection.end_collections (); ++gg) { + // TODO: remove this const_cast + if (remove_from_collection (const_cast (*gg), name)) { + res = true; + } + } + + return res; +} + +bool +Salt::remove_grain (const SaltGrain &grain) +{ + tl::info << QObject::tr ("Removing package '%1' ..").arg (tl::to_qstring (grain.name ())); + if (remove_from_collection (m_root, grain.name ())) { + + tl::info << QObject::tr ("Package '%1' removed.").arg (tl::to_qstring (grain.name ())); + + // NOTE: this is a bit brute force .. we could as well try to insert the new grain into the existing structure + refresh (); + + invalidate (); + return true; + + } else { + tl::warn << QObject::tr ("Failed to remove package '%1'.").arg (tl::to_qstring (grain.name ())); + return false; + } +} + +namespace +{ + +/** + * @brief A helper class required because directory traversal is not supported by QResource directly + * This class supports resource file trees and extraction of a tree from the latter + */ +class ResourceDir + : public QResource +{ +public: + using QResource::isFile; + + /** + * @brief Constructor + * Creates a resource representing a resource tree. + */ + ResourceDir (const QString &path) + : QResource (path) + { + // .. nothing yet .. + } + + /** + * @brief Writes the resource tree to the target directory + * Returns false on error - see log in this case. + */ + bool copy_to (const QDir &target) + { + if (isDir ()) { + + QStringList templ_dir = children (); + for (QStringList::const_iterator t = templ_dir.begin (); t != templ_dir.end (); ++t) { + + ResourceDir child_res (fileName () + QString::fromUtf8 ("/") + *t); + if (child_res.isFile ()) { + + QFile file (target.absoluteFilePath (*t)); + if (! file.open (QIODevice::WriteOnly)) { + tl::error << QObject::tr ("Unable to open target file for writing: %1").arg (target.absoluteFilePath (*t)); + return false; + } + + QByteArray data; + if (child_res.isCompressed ()) { + data = qUncompress ((const unsigned char *)child_res.data (), (int)child_res.size ()); + } else { + data = QByteArray ((const char *)child_res.data (), (int)child_res.size ()); + } + + file.write (data); + + file.close (); + + } else { + + QFileInfo child_dir (target.absoluteFilePath (*t)); + if (! child_dir.exists ()) { + if (! target.mkdir (*t)) { + tl::error << QObject::tr ("Unable to create target directory: %1").arg (child_dir.path ()); + return false; + } + } else if (! child_dir.isDir ()) { + tl::error << QObject::tr ("Unable to create target directory (is a file already): %1").arg (child_dir.path ()); + return false; + } + + if (! child_res.copy_to (QDir (target.absoluteFilePath (*t)))) { + return false; + } + + } + + } + + } + + return true; + } +}; + +} + +bool +Salt::create_grain (const SaltGrain &templ, SaltGrain &target) +{ + tl_assert (!m_root.is_empty ()); + + const SaltGrains *coll = m_root.begin_collections ().operator-> (); + + if (target.name ().empty ()) { + target.set_name (templ.name ()); + } + + if (target.path ().empty ()) { + lay::SaltGrain *g = grain_by_name (target.name ()); + if (g) { + target.set_path (g->path ()); + } + } + + std::string path = target.path (); + if (! path.empty ()) { + coll = 0; + for (SaltGrains::collection_iterator gg = m_root.begin_collections (); gg != m_root.end_collections (); ++gg) { + if (tl::is_parent_path (tl::to_qstring (gg->path ()), tl::to_qstring (path))) { + coll = gg.operator-> (); + break; + } + } + tl_assert (coll != 0); + } + + tl::info << QObject::tr ("Installing package '%1' ..").arg (tl::to_qstring (target.name ())); + + QDir target_dir (tl::to_qstring (coll->path ())); + + try { + + // change down to the desired target location and create the directory structure while doing so + std::vector name_parts = tl::split (target.name (), "/"); + for (std::vector::const_iterator n = name_parts.begin (); n != name_parts.end (); ++n) { + + QFileInfo subdir (target_dir.filePath (tl::to_qstring (*n))); + if (subdir.exists () && ! subdir.isDir ()) { + throw tl::Exception (tl::to_string (tr ("Unable to create target directory '%1' for installing package - is already a file").arg (subdir.path ()))); + } else if (! subdir.exists ()) { + if (! target_dir.mkdir (tl::to_qstring (*n))) { + throw tl::Exception (tl::to_string (tr ("Unable to create target directory '%1' for installing package").arg (subdir.path ()))); + } + if (! target_dir.cd (tl::to_qstring (*n))) { + throw tl::Exception (tl::to_string (tr ("Unable to change to target directory '%1' for installing package").arg (subdir.path ()))); + } + } else { + if (! target_dir.cd (tl::to_qstring (*n))) { + throw tl::Exception (tl::to_string (tr ("Unable to change to target directory '%1' for installing package").arg (subdir.path ()))); + } + } + + } + + } catch (tl::Exception &ex) { + tl::error << ex.msg (); + return false; + } + + bool res = true; + + std::string target_name = target.name (); + target = templ; + target.set_path (tl::to_string (target_dir.absolutePath ())); + target.set_name (target_name); + + if (! templ.path ().empty ()) { + + if (templ.path ()[0] != ':') { + + // if the template represents an actual folder, use the files from there + tl::info << QObject::tr ("Copying package from '%1' to '%2' ..").arg (tl::to_qstring (templ.path ())).arg (tl::to_qstring (target.path ())); + res = tl::cp_dir_recursive (templ.path (), target.path ()); + + } else { + + // if the template represents a resource path, use the files from there + tl::info << QObject::tr ("Installing package from resource '%1' to '%2' ..").arg (tl::to_qstring (templ.path ())).arg (tl::to_qstring (target.path ())); + res = ResourceDir (tl::to_qstring (templ.path ())).copy_to (QDir (tl::to_qstring (target.path ()))); + + } + + } else if (! templ.url ().empty ()) { + + // otherwise download from the URL + tl::info << QObject::tr ("Downloading package from '%1' to '%2' ..").arg (tl::to_qstring (templ.url ())).arg (tl::to_qstring (target.path ())); + res = tl::WebDAVObject::download (templ.url (), target.path ()); + + target.set_url (templ.url ()); + + } + + if (res) { + + tl::info << QObject::tr ("Package '%1' installed").arg (tl::to_qstring (target.name ())); + target.set_installed_time (QDateTime::currentDateTime ()); + target.save (); + + // NOTE: this is a bit brute force .. we could as well try to insert the new grain into the existing structure + refresh (); + + } else { + + tl::warn << QObject::tr ("Failed to install package '%1' - removing files ..").arg (tl::to_qstring (target.name ())); + if (! tl::rm_dir_recursive (target.path ())) { + tl::warn << QObject::tr ("Failed to remove files").arg (tl::to_qstring (target.name ())); + } + + } + + return res; +} + +} diff --git a/src/lay/laySalt.h b/src/lay/laySalt.h new file mode 100644 index 000000000..d238476ae --- /dev/null +++ b/src/lay/laySalt.h @@ -0,0 +1,206 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_laySalt +#define HDR_laySalt + +#include "layCommon.h" +#include "laySaltGrain.h" +#include "laySaltGrains.h" + +#include + +#include + +namespace lay +{ + +/** + * @brief The global salt (package manager) object + * This object can be configured to represent a couple of locations. + * It will provide a collection of grains for these locations. + */ +class LAY_PUBLIC Salt + : public QObject +{ +Q_OBJECT + +public: + typedef SaltGrains::collection_iterator iterator; + typedef std::vector::const_iterator flat_iterator; + + /** + * @brief Default constructor + */ + Salt (); + + /** + * @brief Copy constructor + */ + Salt (const Salt &other); + + /** + * @brief assignment + */ + Salt &operator= (const Salt &other); + + /** + * @brief Adds the given location to the ones the package manager uses + * Adding a location will scan the folder and make the contents available + * as a new collection. + */ + void add_location (const std::string &path); + + /** + * @brief Removes a given location + * This will remove the collection from the package locations. + */ + void remove_location (const std::string &path); + + /** + * @brief Refreshes the collections + * This method rescans all registered locations. + */ + void refresh (); + + /** + * @brief Iterates the collections (begin) + */ + iterator begin () const + { + return m_root.begin_collections (); + } + + /** + * @brief Iterates the collections (end) + */ + iterator end () const + { + return m_root.end_collections (); + } + + /** + * @brief Returns a value indicating whether the collection is empty + */ + bool is_empty () const + { + return m_root.is_empty (); + } + + /** + * @brief A flat iterator of (sorted) grains (begin) + */ + flat_iterator begin_flat (); + + /** + * @brief A flat iterator of (sorted) grains (end) + */ + flat_iterator end_flat (); + + /** + * @brief Gets the grain with the given name + */ + SaltGrain *grain_by_name (const std::string &name); + + /** + * @brief Gets the grain with the given name (const version) + */ + const SaltGrain *grain_by_name (const std::string &name) const + { + return const_cast (this)->grain_by_name (name); + } + + /** + * @brief Loads the salt from a "salt mine" file + */ + void load (const std::string &p) + { + m_root.load (p); + } + + /** + * @brief Loads the salt from a "salt mine" stream + */ + void load (tl::InputStream &s) + { + m_root.load (s); + } + + /** + * @brief Saves the salt to a "salt mine" file + * This feature is provided for debugging purposes mainly. + */ + void save (const std::string &p) + { + m_root.save (p); + } + + /** + * @brief Removes a grain from the salt + * + * This operation will remove the grain with the given name from the salt and delete all files and directories related to it. + * If multiple grains with the same name exist, they will all be removed. + * + * Returns true, if the package could be removed successfully. + */ + bool remove_grain (const SaltGrain &grain); + + /** + * @brief Creates a new grain from a template + * + * This method will create a folder for a grain with the given path and download or copy + * all files related to this grain. It will copy the download URL from the template into the + * new grain, so updates will come from the original location. + * + * If the target's name is not set, it will be taken from the template. + * If the target's path is not set and a grain with the given name already exists in + * the package, the path is taken from that grain. + * If no target path is set and no grain with this name exists yet, a new path will + * be constructed using the first location in the salt. + * + * The target grain will be updated with the installation information. If the target grain + * contains an installation path prior to the installation, this path will be used for the + * installation of the grain files. + * + * Returns true, if the package could be created successfully. + */ + bool create_grain (const SaltGrain &templ, SaltGrain &target); + +signals: + /** + * @brief A signal triggered when one of the collections changed + */ + void collections_changed (); + +private: + SaltGrains m_root; + std::vector mp_flat_grains; + std::map m_grains_by_name; + + void validate (); + void invalidate (); + void add_collection_to_flat (lay::SaltGrains &gg); +}; + +} + +#endif diff --git a/src/lay/laySaltDownloadManager.cc b/src/lay/laySaltDownloadManager.cc new file mode 100644 index 000000000..08956c7f6 --- /dev/null +++ b/src/lay/laySaltDownloadManager.cc @@ -0,0 +1,243 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "laySaltDownloadManager.h" +#include "laySalt.h" +#include "tlFileUtils.h" +#include "tlWebDAV.h" + +#include "ui_SaltManagerInstallConfirmationDialog.h" + +#include +#include + +namespace lay +{ + +// ---------------------------------------------------------------------------------- + +class ConfirmationDialog + : public QDialog, private Ui::SaltManagerInstallConfirmationDialog +{ +public: + ConfirmationDialog (QWidget *parent) + : QDialog (parent) + { + Ui::SaltManagerInstallConfirmationDialog::setupUi (this); + } + + void add_info (const std::string &name, bool update, const std::string &version, const std::string &url) + { + QTreeWidgetItem *item = new QTreeWidgetItem (list); + + item->setFlags (item->flags () & ~Qt::ItemIsSelectable); + + item->setText (0, tl::to_qstring (name)); + item->setText (1, update ? tr ("UPDATE") : tr ("INSTALL")); + item->setText (2, tl::to_qstring (version)); + item->setText (3, tl::to_qstring (url)); + + for (int column = 0; column < list->colorCount (); ++column) { + item->setData (column, Qt::ForegroundRole, update ? Qt::blue : Qt::black); + } + } +}; + +// ---------------------------------------------------------------------------------- + +SaltDownloadManager::SaltDownloadManager () +{ + // .. nothing yet .. +} + +void +SaltDownloadManager::register_download (const std::string &name, const std::string &url, const std::string &version) +{ + m_registry.insert (std::make_pair (name, Descriptor (url, version))); +} + +void +SaltDownloadManager::compute_dependencies (const lay::Salt &salt, const lay::Salt &salt_mine) +{ + tl::AbsoluteProgress progress (tl::to_string (QObject::tr ("Computing package dependencies .."))); + + std::map registry; + + // remove those registered entries which don't need to be updated + + registry = m_registry; + for (std::map::const_iterator p = registry.begin (); p != registry.end (); ++p) { + const SaltGrain *g = salt.grain_by_name (p->first); + if (g && SaltGrain::compare_versions (p->second.version, g->version ()) == 0 && p->second.url == g->url ()) { + m_registry.erase (p->first); + } + } + + // add further entries as derived from the dependencies + + while (needs_iteration ()) { + + fetch_missing (salt_mine, progress); + + registry = m_registry; + for (std::map::const_iterator p = registry.begin (); p != registry.end (); ++p) { + + for (std::vector::const_iterator d = p->second.grain.dependencies ().begin (); d != p->second.grain.dependencies ().end (); ++d) { + + std::map::iterator r = m_registry.find (d->name); + if (r != m_registry.end ()) { + + if (SaltGrain::compare_versions (r->second.version, d->version) < 0) { + + // Grain is present, but too old -> update version and reload in the next iteration + r->second.downloaded = false; + r->second.version = d->version; + r->second.url = d->url; + r->second.downloaded = false; + + } + + } else { + + const SaltGrain *g = salt.grain_by_name (d->name); + if (g) { + + // Grain is installed already, but too old -> register for update + if (SaltGrain::compare_versions (g->version (), d->version) < 0) { + register_download (d->name, d->url, d->version); + } + + } else { + register_download (d->name, d->url, d->version); + } + + } + + } + + } + + } +} + +bool +SaltDownloadManager::needs_iteration () +{ + for (std::map::const_iterator p = m_registry.begin (); p != m_registry.end (); ++p) { + if (! p->second.downloaded) { + return true; + } + } + return false; +} + +void +SaltDownloadManager::fetch_missing (const lay::Salt &salt_mine, tl::AbsoluteProgress &progress) +{ + for (std::map::iterator p = m_registry.begin (); p != m_registry.end (); ++p) { + + if (! p->second.downloaded) { + + ++progress; + + // If no URL is given, utilize the salt mine to fetch it + if (p->second.url.empty ()) { + const lay::SaltGrain *g = salt_mine.grain_by_name (p->first); + if (SaltGrain::compare_versions (g->version (), p->second.version) < 0) { + throw tl::Exception (tl::to_string (QObject::tr ("Package '%1': package in repository is too old (%2) to satisfy requirements (%3)").arg (tl::to_qstring (p->first)).arg (tl::to_qstring (g->version ())).arg (tl::to_qstring (p->second.version)))); + } + p->second.version = g->version (); + p->second.url = g->url (); + } + + try { + p->second.grain = SaltGrain::from_url (p->second.url); + } catch (tl::Exception &ex) { + throw tl::Exception (tl::to_string (QObject::tr ("Error fetching spec file for package '%1': %2").arg (tl::to_qstring (p->first)).arg (tl::to_qstring (ex.msg ())))); + } + + p->second.downloaded = true; + + } + + } +} + +bool +SaltDownloadManager::show_confirmation_dialog (QWidget *parent, const lay::Salt &salt) +{ + // Stop with a warning if there is nothing to do + if (m_registry.empty()) { + QMessageBox::warning (parent, tr ("Nothing to do"), tr ("No packages need update or are marked for installation")); + return false; + } + + lay::ConfirmationDialog dialog (parent); + + // First the packages to update + for (std::map::const_iterator p = m_registry.begin (); p != m_registry.end (); ++p) { + const lay::SaltGrain *g = salt.grain_by_name (p->first); + if (g) { + // \342\206\222 is UTF-8 "right arrow" + dialog.add_info (p->first, true, g->version () + " \342\206\222 " + p->second.version, p->second.url); + } + } + + // Then the packages to install + for (std::map::const_iterator p = m_registry.begin (); p != m_registry.end (); ++p) { + const lay::SaltGrain *g = salt.grain_by_name (p->first); + if (!g) { + dialog.add_info (p->first, false, p->second.version, p->second.url); + } + } + + return dialog.exec (); +} + +bool +SaltDownloadManager::execute (lay::Salt &salt) +{ + bool result = true; + + tl::RelativeProgress progress (tl::to_string (QObject::tr ("Downloading packages")), m_registry.size (), 1); + + for (std::map::const_iterator p = m_registry.begin (); p != m_registry.end (); ++p) { + + lay::SaltGrain target; + target.set_name (p->first); + lay::SaltGrain *g = salt.grain_by_name (p->first); + if (g) { + target.set_path (g->path ()); + } + + if (! salt.create_grain (p->second.grain, target)) { + result = false; + } + + ++progress; + + } + + return result; +} + +} diff --git a/src/lay/laySaltDownloadManager.h b/src/lay/laySaltDownloadManager.h new file mode 100644 index 000000000..89fd74620 --- /dev/null +++ b/src/lay/laySaltDownloadManager.h @@ -0,0 +1,117 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_laySaltDownloadManager +#define HDR_laySaltDownloadManager + +#include "layCommon.h" +#include "laySaltGrain.h" +#include "tlProgress.h" + +#include +#include +#include + +namespace lay +{ + +class Salt; + +/** + * @brief The download manager + * + * This class is responsible for handling the downloads for + * grains. The basic sequence is: + * + "register_download" (multiple times) to register the packages intended for download + * + "compute_dependencies" to determine all related packages + * + (optional) "show_confirmation_dialog" + * + "execute" to actually execute the downloads + */ +class LAY_PUBLIC SaltDownloadManager + : public QObject +{ +Q_OBJECT + +public: + /** + * @brief Default constructor + */ + SaltDownloadManager (); + + /** + * @brief Registers an URL (with version) for download in the given target directory + * + * The target directory can be empty. In this case, the downloader will pick an approriate one. + */ + void register_download (const std::string &name, const std::string &url, const std::string &version); + + /** + * @brief Computes the dependencies after all required packages have been registered + * + * This method will compute the dependencies. Packages not present in the list of + * packages ("salt" argument), will be scheduled for download too. Dependency packages + * are looked up in "salt_mine" if no download URL is given. + */ + void compute_dependencies (const lay::Salt &salt, const Salt &salt_mine); + + /** + * @brief Presents a dialog showing the packages scheduled for download + * + * This method requires all dependencies to be computed. It will return false + * if the dialog is not confirmed. + * + * "salt" needs to be the currently installed packages so the dialog can + * indicate which packages will be updated. + */ + bool show_confirmation_dialog (QWidget *parent, const lay::Salt &salt); + + /** + * @brief Actually execute the downloads + * + * This method will return false if anything goes wrong. + * Failed packages will be removed entirely after they have been listed in + * an error dialog. + */ + bool execute (lay::Salt &salt); + +private: + struct Descriptor + { + Descriptor (const std::string &_url, const std::string &_version) + : url (_url), version (_version), downloaded (false) + { } + + std::string url; + std::string version; + bool downloaded; + lay::SaltGrain grain; + }; + + std::map m_registry; + + bool needs_iteration (); + void fetch_missing (const lay::Salt &salt_mine, tl::AbsoluteProgress &progress); +}; + +} + +#endif diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc new file mode 100644 index 000000000..63c7f5689 --- /dev/null +++ b/src/lay/laySaltGrain.cc @@ -0,0 +1,422 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "laySaltGrain.h" +#include "tlString.h" +#include "tlXMLParser.h" +#include "tlHttpStream.h" + +#include +#include +#include +#include + +namespace lay +{ + +static const std::string grain_filename = "grain.xml"; + +SaltGrain::SaltGrain () +{ + // .. nothing yet .. +} + +bool +SaltGrain::operator== (const SaltGrain &other) const +{ + return m_name == other.m_name && + m_path == other.m_path && + m_version == other.m_version && + m_url == other.m_url && + m_title == other.m_title && + m_doc == other.m_doc && + m_doc_url == other.m_doc_url && + m_icon == other.m_icon && + m_screenshot == other.m_screenshot && + m_dependencies == other.m_dependencies && + m_author == other.m_author && + m_author_contact == other.m_author_contact && + m_license == other.m_license && + m_authored_time == other.m_authored_time && + m_installed_time == other.m_installed_time; +} + +void +SaltGrain::set_name (const std::string &n) +{ + m_name = n; +} + +void +SaltGrain::set_version (const std::string &v) +{ + m_version = v; +} + +void +SaltGrain::set_path (const std::string &p) +{ + m_path = p; +} + +void +SaltGrain::set_url (const std::string &u) +{ + m_url = u; +} + +void +SaltGrain::set_title (const std::string &t) +{ + m_title = t; +} + +void +SaltGrain::set_doc (const std::string &t) +{ + m_doc = t; +} + +void +SaltGrain::set_doc_url (const std::string &u) +{ + m_doc_url = u; +} + +void +SaltGrain::set_author (const std::string &a) +{ + m_author = a; +} + +void +SaltGrain::set_author_contact (const std::string &a) +{ + m_author_contact = a; +} + +void +SaltGrain::set_license (const std::string &l) +{ + m_license = l; +} + +void +SaltGrain::set_authored_time (const QDateTime &t) +{ + m_authored_time = t; +} + +void +SaltGrain::set_installed_time (const QDateTime &t) +{ + m_installed_time = t; +} + +void +SaltGrain::set_screenshot (const QImage &i) +{ + m_screenshot = i; +} + +void +SaltGrain::set_icon (const QImage &i) +{ + m_icon = i; +} + +int +SaltGrain::compare_versions (const std::string &v1, const std::string &v2) +{ + tl::Extractor ex1 (v1.c_str ()); + tl::Extractor ex2 (v2.c_str ()); + + while (true) { + + if (ex1.at_end () && ex2.at_end ()) { + return 0; + } + + int n1 = 0, n2 = 0; + if (! ex1.at_end ()) { + ex1.try_read (n1); + } + if (! ex2.at_end ()) { + ex2.try_read (n2); + } + + if (n1 != n2) { + return n1 < n2 ? -1 : 1; + } + + while (! ex1.at_end ()) { + char c = *ex1; + ++ex1; + if (c == '.') { + break; + } + } + + while (! ex2.at_end ()) { + char c = *ex2; + ++ex2; + if (c == '.') { + break; + } + } + + } +} + +std::string +SaltGrain::spec_url (const std::string &url) +{ + std::string res = url; + if (! res.empty()) { + if (res [res.size () - 1] != '/') { + res += "/"; + } + res += grain_filename; + } + return res; +} + +bool +SaltGrain::valid_name (const std::string &n) +{ + std::string res; + + tl::Extractor ex (n); + + std::string s; + if (! ex.try_read_word (s, "_")) { + return false; + } + res += s; + + while (! ex.at_end ()) { + if (! ex.test ("/")) { + return false; + } + if (! ex.try_read_word (s, "_")) { + return false; + } + res += "/"; + res += s; + } + + // this captures the cases where the extractor skips blanks + // TODO: the extractor should have a "non-blank-skipping" mode + return res == n; +} + +bool +SaltGrain::valid_version (const std::string &v) +{ + tl::Extractor ex (v.c_str ()); + + while (! ex.at_end ()) { + int n = 0; + if (! ex.try_read (n)) { + return false; + } + if (! ex.at_end ()) { + if (*ex != '.') { + return false; + } else { + ++ex; + } + } + } + + return true; +} + +struct TimeConverter +{ + std::string to_string (const QDateTime &time) const + { + if (time.isNull ()) { + return std::string (); + } else { + return tl::to_string (time.toString (Qt::ISODate)); + } + } + + void from_string (const std::string &time, QDateTime &res) const + { + if (time.empty ()) { + res = QDateTime (); + } else { + res = QDateTime::fromString (tl::to_qstring (time), Qt::ISODate); + } + } +}; + +struct ImageConverter +{ + std::string to_string (const QImage &image) const + { + if (image.isNull ()) { + return std::string (); + } else { + QBuffer buffer; + buffer.open (QIODevice::WriteOnly); + image.save (&buffer, "PNG"); + buffer.close (); + return buffer.buffer ().toBase64 ().constData (); + } + } + + void from_string (const std::string &image, QImage &res) const + { + if (image.empty ()) { + res = QImage (); + } else { + res = QImage::fromData (QByteArray::fromBase64 (QByteArray (image.c_str (), image.size ()))); + } + } +}; + +static tl::XMLElementList s_xml_elements = + tl::make_member (&SaltGrain::name, &SaltGrain::set_name, "name") + + tl::make_member (&SaltGrain::version, &SaltGrain::set_version, "version") + + tl::make_member (&SaltGrain::title, &SaltGrain::set_title, "title") + + tl::make_member (&SaltGrain::doc, &SaltGrain::set_doc, "doc") + + tl::make_member (&SaltGrain::doc_url, &SaltGrain::set_doc_url, "doc-url") + + tl::make_member (&SaltGrain::url, &SaltGrain::set_url, "url") + + tl::make_member (&SaltGrain::license, &SaltGrain::set_license, "license") + + tl::make_member (&SaltGrain::author, &SaltGrain::set_author, "author") + + tl::make_member (&SaltGrain::author_contact, &SaltGrain::set_author_contact, "author-contact") + + tl::make_member (&SaltGrain::authored_time, &SaltGrain::set_authored_time, "authored-time", TimeConverter ()) + + tl::make_member (&SaltGrain::installed_time, &SaltGrain::set_installed_time, "installed-time", TimeConverter ()) + + tl::make_member (&SaltGrain::icon, &SaltGrain::set_icon, "icon", ImageConverter ()) + + tl::make_member (&SaltGrain::screenshot, &SaltGrain::set_screenshot, "screenshot", ImageConverter ()) + + tl::make_element (&SaltGrain::begin_dependencies, &SaltGrain::end_dependencies, &SaltGrain::add_dependency, "depends", + tl::make_member (&SaltGrain::Dependency::name, "name") + + tl::make_member (&SaltGrain::Dependency::url, "url") + + tl::make_member (&SaltGrain::Dependency::version, "version") + ); + +static tl::XMLStruct s_xml_struct ("salt-grain", s_xml_elements); + +tl::XMLElementList & +SaltGrain::xml_struct () +{ + return s_xml_elements; +} + +bool +SaltGrain::is_readonly () const +{ + return !QFileInfo (tl::to_qstring (path ())).isWritable (); +} + +void +SaltGrain::load (const std::string &p) +{ + tl_assert (!p.empty ()); + + if (p[0] != ':') { + + tl::XMLFileSource source (p); + s_xml_struct.parse (source, *this); + + } else { + + QResource res (tl::to_qstring (p)); + QByteArray data; + if (res.isCompressed ()) { + data = qUncompress ((const unsigned char *)res.data (), (int)res.size ()); + } else { + data = QByteArray ((const char *)res.data (), (int)res.size ()); + } + + std::string str_data (data.constData (), data.size ()); + tl::XMLStringSource source (str_data); + s_xml_struct.parse (source, *this); + + } +} + +void +SaltGrain::load (tl::InputStream &p) +{ + tl::XMLStreamSource source (p); + s_xml_struct.parse (source, *this); +} + +void +SaltGrain::save () const +{ + save (tl::to_string (QDir (tl::to_qstring (path ())).filePath (tl::to_qstring (grain_filename)))); +} + +void +SaltGrain::save (const std::string &p) const +{ + tl::OutputStream os (p, tl::OutputStream::OM_Plain); + s_xml_struct.write (os, *this); +} + +SaltGrain +SaltGrain::from_path (const std::string &path) +{ + QDir dir (tl::to_qstring (path)); + + SaltGrain g; + g.load (tl::to_string (dir.filePath (tl::to_qstring (grain_filename)))); + g.set_path (tl::to_string (dir.absolutePath ())); + return g; +} + +SaltGrain +SaltGrain::from_url (const std::string &url) +{ + if (url.empty ()) { + throw tl::Exception (tl::to_string (QObject::tr ("No download link available"))); + } + + tl::InputHttpStream http (SaltGrain::spec_url (url)); + tl::InputStream stream (http); + + SaltGrain g; + g.load (stream); + g.set_url (url); + return g; +} + +bool +SaltGrain::is_grain (const std::string &path) +{ + tl_assert (! path.empty ()); + + if (path[0] != ':') { + QDir dir (tl::to_qstring (path)); + QString gf = dir.filePath (tl::to_qstring (grain_filename)); + return QFileInfo (gf).exists (); + } else { + return QResource (tl::to_qstring (path + "/" + grain_filename)).isValid (); + } +} + +} diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h new file mode 100644 index 000000000..df30e7f49 --- /dev/null +++ b/src/lay/laySaltGrain.h @@ -0,0 +1,420 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_laySaltGrain +#define HDR_laySaltGrain + +#include "layCommon.h" +#include "tlObject.h" +#include "tlStream.h" +#include "tlXMLParser.h" + +#include +#include + +namespace lay +{ + +/** + * @brief This class represents on grain of salt + * "One grain of salt" is one package. + */ +class LAY_PUBLIC SaltGrain + : public tl::Object +{ +public: + /** + * @brief A descriptor for one dependency + * A dependency can be specified either through a name (see name property) + * or a download URL. If download URL are specified, they have precedence + * over names. + * The version is the minimum required version. If empty, any version is + * allowed to resolve this dependency. + */ + struct Dependency + { + std::string name; + std::string url; + std::string version; + + bool operator== (const Dependency &other) const + { + return name == other.name && url == other.url && version == other.version; + } + }; + + /** + * @brief Constructor + */ + SaltGrain (); + + /** + * @brief Equality + */ + bool operator== (const SaltGrain &other) const; + + /** + * @brief Inequality + */ + bool operator!= (const SaltGrain &other) const + { + return !operator== (other); + } + + /** + * @brief Gets the name of the grain + * + * The name is either a plain name (a word) or a path into a collection. + * Name paths are formed using the "/" separator. "mypackage" is a plain name, + * while "mycollection/mypackage" is a package within a collection. Collections + * can be used to group packages. Names are case sensitive in general, but + * names differing only in case should be avoided. + */ + const std::string &name () const + { + return m_name; + } + + /** + * @brief Sets the name of the grain + */ + void set_name (const std::string &p); + + /** + * @brief Gets the title of the grain + * + * The title is a brief description that is shown in the title of the + * package manager. + */ + const std::string &title () const + { + return m_title; + } + + /** + * @brief Sets the title of the grain + */ + void set_title (const std::string &t); + + /** + * @brief Gets the documentation text of the grain + * + * The documentation text is a brief description. + */ + const std::string &doc () const + { + return m_doc; + } + + /** + * @brief Sets the documentation text of the grain + */ + void set_doc (const std::string &t); + + /** + * @brief Gets the documentation URL of the grain + * + * The documentation URL provides a detailed documentation. + */ + const std::string &doc_url () const + { + return m_doc_url; + } + + /** + * @brief Sets the documentation URL of the grain + */ + void set_doc_url (const std::string &u); + + /** + * @brief Gets the version of the grain + * + * A version string is of the form "x.y..." where x, y and other version + * components are integer numbers. + */ + const std::string &version () const + { + return m_version; + } + + /** + * @brief Sets the version of the grain + */ + void set_version (const std::string &v); + + /** + * @brief Gets the author of the grain + */ + const std::string &author () const + { + return m_author; + } + + /** + * @brief Sets the author of the grain + */ + void set_author (const std::string &a); + + /** + * @brief Gets the author's contact + */ + const std::string &author_contact () const + { + return m_author_contact; + } + + /** + * @brief Sets the author's contact + */ + void set_author_contact (const std::string &a); + + /** + * @brief Gets the license of the grain + */ + const std::string &license () const + { + return m_license; + } + + /** + * @brief Sets the license of the grain + */ + void set_license (const std::string &l); + + /** + * @brief Gets the release date and/or time of the grain + */ + const QDateTime &authored_time () const + { + return m_authored_time; + } + + /** + * @brief Sets the release date and/or time + */ + void set_authored_time (const QDateTime &t); + + /** + * @brief Gets the installation date and/or time of the grain + */ + const QDateTime &installed_time () const + { + return m_installed_time; + } + + /** + * @brief Sets the installation date and/or time + */ + void set_installed_time (const QDateTime &t); + + /** + * @brief Gets the icon image for the grain. + * The preferred image size is 64x64 pixels. + * The image may be null image. In this case, a default image is used. + */ + const QImage &icon () const + { + return m_icon; + } + + /** + * @brief Sets icon image + */ + void set_icon (const QImage &i); + + /** + * @brief Gets a screenshot image for documentation. + * The image may be null image. In this case, no screenshot is shown. + */ + const QImage &screenshot () const + { + return m_screenshot; + } + + /** + * @brief Sets screenshot image + */ + void set_screenshot (const QImage &i); + + /** + * @brief Gets the absolute file path of the installed grain + * This is the file path to the grain folder. + */ + const std::string &path () const + { + return m_path; + } + + /** + * @brief Sets the absolute file path of the installed grain + */ + void set_path (const std::string &p); + + /** + * @brief Gets the download URL + * The download URL is the place from which the grain was installed originally. + */ + const std::string &url () const + { + return m_url; + } + + /** + * @brief Sets the download URL + */ + void set_url (const std::string &u); + + /** + * @brief Gets the dependencies of the grain + * Grains this grain depends on are installed automatically when the grain + * is installed. + */ + const std::vector &dependencies () const + { + return m_dependencies; + } + + /** + * @brief Gets the dependencies of the grain (non-const) + */ + std::vector &dependencies () + { + return m_dependencies; + } + + /** + * @brief Dependency iterator (begin) + */ + std::vector::const_iterator begin_dependencies () const + { + return m_dependencies.begin (); + } + + /** + * @brief Dependency iterator (end) + */ + std::vector::const_iterator end_dependencies () const + { + return m_dependencies.end (); + } + + /** + * @brief Adds a dependency + */ + void add_dependency (const Dependency &dep) + { + m_dependencies.push_back (dep); + } + + /** + * @brief Returns true, if the collection is read-only + */ + bool is_readonly () const; + + /** + * @brief Loads the data from a given file + * This method will *not* set the path. + */ + void load (const std::string &file_path); + + /** + * @brief Loads the data from a given stream + */ + void load (tl::InputStream &stream); + + /** + * @brief Saves the data to the path inside the grain folder given by the "path" property + */ + void save () const; + + /** + * @brief Saves the data to the given file + */ + void save (const std::string &file_path) const; + + /** + * @brief Gets the XML structure representing a grain + */ + static tl::XMLElementList &xml_struct (); + + /** + * @brief Compares two version strings + * Returns -1 if v1 < v2, 0 if v1 == v2 and 1 if v1 > v2. + * Malformed versions are read gracefully. Letters and non-digits are skipped. + * Missing numbers are read as 0. Hence "1.0 == 1" for example. + */ + static int compare_versions (const std::string &v1, const std::string &v2); + + /** + * @brief Gets a value indicating whether the given version string is a valid version + */ + static bool valid_version (const std::string &v); + + /** + * @brief Checks whether the given string is a valid name + */ + static bool valid_name (const std::string &n); + + /** + * @brief Detects a grain from the given directory + * This method will return a grain constructed from the given directory. + * The data is read from "path/grain.xml". This method will throw an + * exception if an error occurs during reading. + */ + static SaltGrain from_path (const std::string &path); + + /** + * @brief Loads the grain from the given URL + * This method will return a grain constructed from the downloaded data. + * The data is read from "URL/grain.xml". This method will throw an + * exception if an error occurs during reading. + */ + static SaltGrain from_url (const std::string &url); + + /** + * @brief Forms the spec file download URL from a given download URL + */ + static std::string spec_url (const std::string &url); + + /** + * @brief Returns a value indicating whether the given path represents is a grain + */ + static bool is_grain (const std::string &path); + +private: + std::string m_name; + std::string m_version; + std::string m_path; + std::string m_url; + std::string m_title; + std::string m_doc, m_doc_url; + std::string m_author; + std::string m_author_contact; + std::string m_license; + QDateTime m_authored_time, m_installed_time; + QImage m_icon, m_screenshot; + std::vector m_dependencies; +}; + +} + +#endif diff --git a/src/lay/laySaltGrainDetailsTextWidget.cc b/src/lay/laySaltGrainDetailsTextWidget.cc new file mode 100644 index 000000000..310a18a6e --- /dev/null +++ b/src/lay/laySaltGrainDetailsTextWidget.cc @@ -0,0 +1,247 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "laySaltGrainDetailsTextWidget.h" +#include "laySaltGrain.h" +#include "tlString.h" + +#include +#include +#include +#include + +namespace lay +{ + +SaltGrainDetailsTextWidget::SaltGrainDetailsTextWidget (QWidget *w) + : QTextBrowser (w), mp_grain (0) +{ + // .. nothing yet .. +} + +void SaltGrainDetailsTextWidget::set_grain (SaltGrain *g) +{ + mp_grain = g; + setHtml (details_text ()); +} + +QVariant +SaltGrainDetailsTextWidget::loadResource (int type, const QUrl &url) +{ + if (url.path () == QString::fromUtf8 ("/icon")) { + + int icon_dim = 64; + + if (!mp_grain || mp_grain->icon ().isNull ()) { + + return QImage (":/salt_icon.png"); + + } else { + + QImage img = mp_grain->icon (); + if (img.width () != icon_dim || img.height () != icon_dim) { + + img = img.scaled (QSize (icon_dim, icon_dim), Qt::KeepAspectRatio, Qt::SmoothTransformation); + + QImage final_img (icon_dim, icon_dim, QImage::Format_ARGB32); + final_img.fill (QColor (0, 0, 0, 0)); + QPainter painter (&final_img); + painter.drawImage ((icon_dim - img.width ()) / 2, (icon_dim - img.height ()) / 2, img); + + return final_img; + + } else { + return img; + } + + } + + } else if (url.path () == QString::fromUtf8 ("/screenshot")) { + + QImage s = mp_grain->screenshot ().convertToFormat (QImage::Format_ARGB32_Premultiplied); + + QImage smask (s.size (), QImage::Format_ARGB32_Premultiplied); + smask.fill (QColor (0, 0, 0, 0)); + { + int border = 0; + int radius = 6; + + QPainter painter (&smask); + + painter.setRenderHint (QPainter::Antialiasing); + + painter.setCompositionMode (QPainter::CompositionMode_Source); + for (int b = border; b > 0; --b) { + QPen pen (QColor (255, 255, 255, ((border - b + 1) * 255) / border)); + pen.setWidth (b * 2 + 1); + painter.setBrush (Qt::NoBrush); + painter.setPen (pen); + painter.drawRoundedRect (QRectF (border, border, s.width () - 2 * border, s.height () - 2 * border), radius, radius, Qt::AbsoluteSize); + } + + painter.setPen (Qt::white); + painter.setBrush (Qt::white); + painter.drawRoundedRect (QRectF (border, border, s.width () - 2 * border, s.height () - 2 * border), radius, radius, Qt::AbsoluteSize); + } + + { + QPainter painter (&s); + painter.setCompositionMode (QPainter::CompositionMode_DestinationIn); + painter.drawImage (0, 0, smask); + } + + return s; + + } else { + return QTextBrowser::loadResource (type, url); + } +} + +QString +SaltGrainDetailsTextWidget::details_text () +{ + SaltGrain *g = mp_grain; + if (! g) { + return QString (); + } + + QBuffer buffer; + buffer.open (QIODevice::WriteOnly); + QTextStream stream (&buffer); + stream.setCodec ("UTF-8"); + + stream << ""; + + stream << ""; + stream << ""; + + stream << "
"; + stream << "

"; + stream << tl::to_qstring (tl::escaped_to_html (g->name ())) << " " << tl::to_qstring (tl::escaped_to_html (g->version ())); + stream << "

"; + if (! g->title ().empty()) { + stream << "

" << tl::to_qstring (tl::escaped_to_html (g->title ())) << "

"; + } + + if (g->version ().empty ()) { + stream << "

"; + stream << QObject::tr ("This package does not have a version. " + "Use the <version> element of the specification file or edit the package properties to provide a version."); + stream << "

"; + } + + if (g->title ().empty ()) { + stream << "

"; + stream << QObject::tr ("This package does not have a title. " + "Use the <title> element of the specification file or edit the package properties to provide a title."); + stream << "

"; + } + + stream << "


"; + if (! g->doc ().empty ()) { + stream << tl::to_qstring (tl::escaped_to_html (g->doc ())); + } else { + stream << ""; + stream << QObject::tr ("This package does not have a description. " + "Use the <doc> element of the specification file or edit the package properties to provide a description."); + stream << ""; + } + stream << "

"; + + stream << "

"; + if (! g->author ().empty ()) { + stream << "" << QObject::tr ("Author") << ": " << tl::to_qstring (tl::escaped_to_html (g->author ())) << " "; + if (! g->author_contact ().empty ()) { + stream << "(" << tl::to_qstring (tl::escaped_to_html (g->author_contact ())) << ")"; + } + if (!g->authored_time ().isNull ()) { + stream << "
"; + stream << "" << QObject::tr ("Released") << ": " << g->authored_time ().date ().toString (Qt::ISODate); + } + } else { + stream << ""; + stream << QObject::tr ("This package does not have a author information. " + "Use the <author>, <authored-time> and <author-contact> elements of the specification file or edit the package properties to provide authoring information."); + stream << ""; + } + stream << "

"; + + stream << "

"; + if (! g->license ().empty ()) { + stream << "" << QObject::tr ("License") << ": " << tl::to_qstring (tl::escaped_to_html (g->license ())) << " "; + } else { + stream << ""; + stream << QObject::tr ("This package does not have license information. " + "Use the <license> elements of the specification file or edit the package properties to provide license information."); + stream << ""; + } + stream << "

"; + + stream << "

"; + if (! g->doc_url ().empty ()) { + stream << "" << QObject::tr ("Documentation link") << ": doc_url ()) << "\">" << tl::to_qstring (tl::escaped_to_html (g->doc_url ())) << ""; + } else { + stream << ""; + stream << QObject::tr ("This package does not have a documentation link. " + "Use the <doc-url> element of the specification file or edit the package properties to provide a link."); + stream << ""; + } + stream << "

"; + + if (! g->screenshot ().isNull ()) { + stream << "
"; + stream << "

" << QObject::tr ("Screenshot") << "

"; + } + + stream << "
"; + stream << "

" << QObject::tr ("Installation") << "

"; + + stream << "

" << QObject::tr ("Installation path: ") << "" << tl::to_qstring (tl::escaped_to_html (g->path ())) << "

"; + if (! g->url ().empty ()) { + stream << "

" << QObject::tr ("Download URL: ") << "" << tl::to_qstring (tl::escaped_to_html (g->url ())) << "

"; + } + if (! g->installed_time ().isNull ()) { + stream << "

" << QObject::tr ("Installed: ") << "" << g->installed_time ().toString () << "

"; + } + if (! g->dependencies ().empty ()) { + stream << "

" << QObject::tr ("Depends on: ") << "
"; + for (std::vector::const_iterator d = g->dependencies ().begin (); d != g->dependencies ().end (); ++d) { + stream << "    " << tl::to_qstring (tl::escaped_to_html (d->name)) << " "; + stream << tl::to_qstring (tl::escaped_to_html (d->version)); + if (! d->url.empty ()) { + stream << " - "; + stream << "[" << tl::to_qstring (tl::escaped_to_html (d->url)) << "]
"; + } + } + stream << "

"; + } + + stream << "
"; + + stream << ""; + + stream.flush (); + + return QString::fromUtf8 (buffer.buffer()); +} + +} diff --git a/src/lay/laySaltGrainDetailsTextWidget.h b/src/lay/laySaltGrainDetailsTextWidget.h new file mode 100644 index 000000000..bb25a0e48 --- /dev/null +++ b/src/lay/laySaltGrainDetailsTextWidget.h @@ -0,0 +1,61 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_laySaltGrainDetailsTextWidget +#define HDR_laySaltGrainDetailsTextWidget + +#include + +namespace lay +{ + +class SaltGrain; + +/** + * @brief A specialisation of QTextBrowser that displays the details of the salt grain + */ +class SaltGrainDetailsTextWidget + : public QTextBrowser +{ +public: + /** + * @brief Constructor + */ + SaltGrainDetailsTextWidget (QWidget *w); + + /** + * @brief Sets the grain whose details are to be shown + */ + void set_grain (SaltGrain *g); + +protected: + virtual QVariant loadResource (int type, const QUrl &url); + +private: + lay::SaltGrain *mp_grain; + + QString details_text (); +}; + +} + +#endif diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc new file mode 100644 index 000000000..d78ee94bf --- /dev/null +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -0,0 +1,632 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "laySaltGrainPropertiesDialog.h" +#include "laySalt.h" +#include "tlString.h" +#include "tlExceptions.h" +#include "tlHttpStream.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace lay +{ + +// ---------------------------------------------------------------------------------------------------- + +/** + * @brief A delegate for editing a field of the dependency list + */ +class SaltGrainEditDelegate + : public QItemDelegate +{ +public: + SaltGrainEditDelegate (QWidget *parent) + : QItemDelegate (parent) + { + // .. nothing yet .. + } + + QWidget *createEditor (QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const + { + QLineEdit *editor = new QLineEdit (parent); + editor->setFrame (false); + editor->setTextMargins (2, 0, 2, 0); + return editor; + } + + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex & /*index*/) const + { + editor->setGeometry(option.rect); + } + + void setEditorData (QWidget *widget, const QModelIndex &index) const + { + QLineEdit *editor = dynamic_cast (widget); + if (editor) { + editor->setText (index.model ()->data (index, Qt::UserRole).toString ()); + } + } + + void setModelData (QWidget *widget, QAbstractItemModel *model, const QModelIndex &index) const + { + QLineEdit *editor = dynamic_cast (widget); + if (editor) { + model->setData (index, QVariant (editor->text ()), Qt::UserRole); + } + } + + QSize sizeHint (const QStyleOptionViewItem &option, const QModelIndex & /*index*/) const + { + QSize sz = option.fontMetrics.size (Qt::TextSingleLine, QString::fromUtf8 ("M")); + sz += QSize (0, 8); + return sz; + } +}; + +/** + * @brief A delegate for editing a field of the dependency list + */ +class SaltGrainNameEditDelegate + : public SaltGrainEditDelegate +{ +public: + SaltGrainNameEditDelegate (QWidget *parent, Salt *salt) + : SaltGrainEditDelegate (parent), mp_completer (0) + { + QStringList names; + for (lay::Salt::flat_iterator i = salt->begin_flat (); i != salt->end_flat (); ++i) { + names << tl::to_qstring ((*i)->name ()); + } + mp_completer = new QCompleter (names, this); + } + + QWidget *createEditor (QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const + { + QWidget *editor = SaltGrainEditDelegate::createEditor (parent, option, index); + QLineEdit *line_edit = dynamic_cast (editor); + if (line_edit) { + line_edit->setCompleter (mp_completer); + } + return editor; + } + +public: + QCompleter *mp_completer; +}; + +// ---------------------------------------------------------------------------------------------------- +// SaltGrainPropertiesDialog implementation + +SaltGrainPropertiesDialog::SaltGrainPropertiesDialog (QWidget *parent) + : QDialog (parent), mp_salt (0), m_update_enabled (true) +{ + Ui::SaltGrainPropertiesDialog::setupUi (this); + + m_title = windowTitle (); + m_open_label = open_label->text (); + + connect (icon_delete_button, SIGNAL (clicked ()), this, SLOT (reset_icon ())); + connect (icon_config_button, SIGNAL (clicked ()), this, SLOT (set_icon ())); + connect (screenshot_delete_button, SIGNAL (clicked ()), this, SLOT (reset_screenshot ())); + connect (screenshot_config_button, SIGNAL (clicked ()), this, SLOT (set_screenshot ())); + connect (doc_url, SIGNAL (textChanged (const QString &)), this, SLOT (url_changed (const QString &))); + connect (add_dependency, SIGNAL (clicked ()), this, SLOT (add_dependency_clicked ())); + connect (remove_dependency, SIGNAL (clicked ()), this, SLOT (remove_dependency_clicked ())); + connect (dependencies, SIGNAL (itemChanged (QTreeWidgetItem *, int)), this, SLOT (dependency_changed (QTreeWidgetItem *, int))); + + dependencies->setItemDelegateForColumn (1, new SaltGrainEditDelegate (dependencies)); + dependencies->setItemDelegateForColumn (2, new SaltGrainEditDelegate (dependencies)); + + url_changed (QString ()); +} + +void +SaltGrainPropertiesDialog::update_controls () +{ + setWindowTitle (m_title + tl::to_qstring (" - " + m_grain.name ())); + license_alert->clear (); + version_alert->clear (); + doc_url_alert->clear (); + dependencies_alert->clear (); + + version->setText (tl::to_qstring (m_grain.version ())); + title->setText (tl::to_qstring (m_grain.title ())); + author->setText (tl::to_qstring (m_grain.author ())); + author_contact->setText (tl::to_qstring (m_grain.author_contact ())); + doc->setText (tl::to_qstring (m_grain.doc ())); + doc_url->setText (tl::to_qstring (m_grain.doc_url ())); + license->setText (tl::to_qstring (m_grain.license ())); + + dependencies->clear (); + for (std::vector::const_iterator d = m_grain.dependencies ().begin (); d != m_grain.dependencies ().end (); ++d) { + + QTreeWidgetItem *item = new QTreeWidgetItem (dependencies); + item->setFlags (item->flags () | Qt::ItemIsEditable); + + item->setData (0, Qt::UserRole, tl::to_qstring (d->name)); + dependency_changed (item, 0); + item->setData (1, Qt::UserRole, tl::to_qstring (d->version)); + dependency_changed (item, 1); + item->setData (2, Qt::UserRole, tl::to_qstring (d->url)); + dependency_changed (item, 2); + + dependencies->addTopLevelItem (item); + + } + + update_icon (); + update_screenshot (); +} + +void +SaltGrainPropertiesDialog::update_icon () +{ + if (m_grain.icon ().isNull ()) { + icon_config_button->setIcon (QIcon (":/salt_icon.png")); + } else { + QImage img = m_grain.icon (); + if (img.width () == icon_config_button->iconSize ().width ()) { + icon_config_button->setIcon (QIcon (QPixmap::fromImage (img))); + } else { + icon_config_button->setIcon (QIcon (QPixmap::fromImage (img.scaled (icon_config_button->iconSize (), Qt::KeepAspectRatio, Qt::SmoothTransformation)))); + } + } +} + +void +SaltGrainPropertiesDialog::update_screenshot () +{ + if (m_grain.screenshot ().isNull ()) { + screenshot_config_button->setIcon (QIcon (":/add.png")); + } else { + QImage img = m_grain.screenshot (); + if (img.width () == screenshot_config_button->iconSize ().width ()) { + screenshot_config_button->setIcon (QIcon (QPixmap::fromImage (img))); + } else { + screenshot_config_button->setIcon (QIcon (QPixmap::fromImage (img.scaled (screenshot_config_button->iconSize (), Qt::KeepAspectRatio, Qt::SmoothTransformation)))); + } + } +} + +void +SaltGrainPropertiesDialog::update_data () +{ + m_grain.set_version (tl::to_string (version->text ())); + m_grain.set_title (tl::to_string (title->text ())); + m_grain.set_author (tl::to_string (author->text ())); + m_grain.set_author_contact (tl::to_string (author_contact->text ())); + m_grain.set_doc (tl::to_string (doc->toPlainText ())); + m_grain.set_doc_url (tl::to_string (doc_url->text ())); + m_grain.set_license (tl::to_string (license->text ())); + + m_grain.dependencies ().clear (); + for (int i = 0; i < dependencies->topLevelItemCount (); ++i) { + + QTreeWidgetItem *item = dependencies->topLevelItem (i); + QString name = item->data (0, Qt::UserRole).toString ().simplified (); + QString version = item->data (1, Qt::UserRole).toString ().simplified (); + QString url = item->data (2, Qt::UserRole).toString ().simplified (); + + if (! name.isEmpty ()) { + lay::SaltGrain::Dependency dep = lay::SaltGrain::Dependency (); + dep.name = tl::to_string (name); + dep.version = tl::to_string (version); + dep.url = tl::to_string (url); + m_grain.dependencies ().push_back (dep); + } + + } +} + +void +SaltGrainPropertiesDialog::dependency_changed (QTreeWidgetItem *item, int column) +{ + if (! m_update_enabled) { + return; + } + m_update_enabled = false; + + std::string name = tl::to_string (item->data (0, Qt::UserRole).toString ().simplified ()); + SaltGrain *g = mp_salt->grain_by_name (name); + + if (column == 0 && mp_salt) { + + item->setData (0, Qt::EditRole, tl::to_qstring (name)); + + // set URL and version for known grains + if (name == m_grain.name ()) { + + item->setData (1, Qt::UserRole, QString ()); + item->setData (2, Qt::UserRole, QString ()); + // placeholder texts: + item->setData (1, Qt::EditRole, QString ()); + item->setData (2, Qt::EditRole, tr ("(must not depend on itself)")); + + } else { + + if (g) { + item->setData (1, Qt::UserRole, tl::to_qstring (g->version ())); + item->setData (2, Qt::UserRole, QString ()); + // placeholder texts: + item->setData (1, Qt::EditRole, tl::to_qstring (g->version ())); + if (! g->url ().empty ()) { + item->setData (2, Qt::EditRole, tl::to_qstring ("(" + g->url () + ")")); + } else { + item->setData (2, Qt::EditRole, tr ("(from repository)")); + } + } else { + item->setData (1, Qt::UserRole, QString ()); + item->setData (2, Qt::UserRole, QString ()); + // placeholder texts: + item->setData (1, Qt::EditRole, QString ()); + item->setData (2, Qt::EditRole, tr ("(from repository)")); + } + + } + + } else if (column == 1) { + + QString text = item->data (column, Qt::UserRole).toString (); + if (! text.isEmpty ()) { + item->setData (1, Qt::EditRole, text); + } else if (g) { + item->setData (1, Qt::EditRole, tl::to_qstring (g->version ())); + } + + } else if (column == 2) { + + QString text = item->data (column, Qt::UserRole).toString (); + if (! text.isEmpty ()) { + item->setData (2, Qt::EditRole, text); + } else if (g) { + if (! g->url ().empty ()) { + item->setData (2, Qt::EditRole, tl::to_qstring ("(" + g->url () + ")")); + } else { + item->setData (2, Qt::EditRole, tr ("(from repository)")); + } + } + + } + + m_update_enabled = true; +} + +void +SaltGrainPropertiesDialog::url_changed (const QString &url) +{ + // inserts the URL into the label + open_label->setText (m_open_label.arg (url)); + open_label->setEnabled (! url.isEmpty ()); +} + +void +SaltGrainPropertiesDialog::set_icon () +{ +BEGIN_PROTECTED + + const int max_dim = 256; + + QString fileName = QFileDialog::getOpenFileName (this, tr ("Pick Icon Image File"), m_image_dir, tr ("Images (*.png *.jpg);;All Files (*)")); + if (! fileName.isNull ()) { + + bool ok = true; + QImage img = QImage (fileName); + if (img.width () > max_dim || img.height () > max_dim) { + if (QMessageBox::warning (this, tr ("Image Too Big"), + tr ("Icon image too big - must be %1x%2 pixels max, but is %3x%4.\n\nScale image?").arg (max_dim).arg (max_dim).arg (img.width ()).arg (img.height ()), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { + ok = false; + } else { + img = img.scaled (max_dim, max_dim, Qt::KeepAspectRatio); + } + } + + if (ok) { + m_grain.set_icon (img); + m_image_dir = QFileInfo (fileName).path (); + update_icon (); + } + + } + +END_PROTECTED +} + +void +SaltGrainPropertiesDialog::reset_icon () +{ + m_grain.set_icon (QImage ()); + update_icon (); +} + +void +SaltGrainPropertiesDialog::set_screenshot () +{ +BEGIN_PROTECTED + + const int max_dim = 1024; + + QString fileName = QFileDialog::getOpenFileName (this, tr ("Pick Showcase Image File"), m_image_dir, tr ("Images (*.png *.jpg);;All Files (*)")); + if (! fileName.isNull ()) { + + bool ok = true; + QImage img = QImage (fileName); + if (img.width () > max_dim || img.height () > max_dim) { + if (QMessageBox::warning (this, tr ("Image Too Big"), + tr ("Showcase image too big - must be %1x%2 pixels max, but is %3x%4.\n\nScale image?").arg (max_dim).arg (max_dim).arg (img.width ()).arg (img.height ()), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { + ok = false; + } else { + img = img.scaled (max_dim, max_dim, Qt::KeepAspectRatio); + } + } + + if (ok) { + m_grain.set_screenshot (img); + m_image_dir = QFileInfo (fileName).path (); + update_screenshot (); + } + + } + +END_PROTECTED +} + +void +SaltGrainPropertiesDialog::reset_screenshot () +{ + m_grain.set_screenshot (QImage ()); + update_screenshot (); +} + +void +SaltGrainPropertiesDialog::add_dependency_clicked () +{ + QTreeWidgetItem *item = new QTreeWidgetItem (dependencies); + item->setFlags (item->flags () | Qt::ItemIsEditable); + dependencies->addTopLevelItem (item); + dependencies->setCurrentItem (dependencies->topLevelItem (dependencies->topLevelItemCount () - 1)); +} + +void +SaltGrainPropertiesDialog::remove_dependency_clicked () +{ + int index = dependencies->indexOfTopLevelItem (dependencies->currentItem ()); + if (index >= 0 && index < dependencies->topLevelItemCount ()) { + delete dependencies->topLevelItem (index); + } +} + +namespace +{ + +class DependencyGraph +{ +public: + DependencyGraph (Salt *salt) + { + for (lay::Salt::flat_iterator i = salt->begin_flat (); i != salt->end_flat (); ++i) { + m_name_to_grain.insert (std::make_pair ((*i)->name (), *i)); + } + } + + bool is_valid_name (const std::string &name) const + { + return m_name_to_grain.find (name) != m_name_to_grain.end (); + } + + const lay::SaltGrain *grain_for_name (const std::string &name) const + { + std::map ::const_iterator n = m_name_to_grain.find (name); + if (n != m_name_to_grain.end ()) { + return n->second; + } else { + return 0; + } + } + + void check_circular (const lay::SaltGrain *current, const lay::SaltGrain *new_dep) + { + std::vector path; + path.push_back (current); + check_circular_follow (new_dep, path); + } + +private: + std::map m_name_to_grain; + + void check_circular_follow (const lay::SaltGrain *current, std::vector &path) + { + if (! current) { + return; + } + + path.push_back (current); + + for (std::vector ::const_iterator p = path.begin (); p != path.end () - 1; ++p) { + if (*p == current) { + circular_reference_error (path); + } + } + + for (std::vector::const_iterator d = current->dependencies ().begin (); d != current->dependencies ().end (); ++d) { + check_circular_follow (grain_for_name (d->name), path); + } + + path.pop_back (); + } + + void circular_reference_error (std::vector &path) + { + std::string msg = tl::to_string (QObject::tr ("The following path forms a circular dependency: ")); + for (std::vector ::const_iterator p = path.begin (); p != path.end (); ++p) { + if (p != path.begin ()) { + msg += "->"; + } + msg += (*p)->name (); + } + throw tl::Exception (msg); + } +}; + +} + +void +SaltGrainPropertiesDialog::accept () +{ + update_data (); + + // Perform some checks + + // license + license_alert->clear (); + if (m_grain.license ().empty ()) { + license_alert->warn () << tr ("License field is empty. Please consider specifying a license model.") << tl::endl + << tr ("A license model tells users whether and how to use the source code of the package."); + } + + // version + version_alert->clear (); + if (m_grain.version ().empty ()) { + version_alert->warn () << tr ("Version field is empty. Please consider specifying a version number.") << tl::endl + << tr ("Versions help the system to apply upgrades if required."); + } else if (! SaltGrain::valid_version (m_grain.version ())) { + version_alert->error () << tr ("'%1' is not a valid version string. A version string needs to be numeric (like '1.2.3' or '4.5'').").arg (tl::to_qstring (m_grain.version ())); + } + + // doc URL + doc_url_alert->clear (); + if (! m_grain.doc_url ().empty ()) { + tl::InputHttpStream stream (m_grain.doc_url ()); + try { + char b; + stream.read (&b, 1); + } catch (tl::Exception &ex) { + doc_url_alert->error () << tr ("Attempt to read documentation URL failed. Error details follow.") << tl::endl + << tr ("URL: ") << m_grain.doc_url () << tl::endl + << tr ("Message: ") << ex.msg (); + } + } + + // dependencies + dependencies_alert->clear (); + DependencyGraph dep (mp_salt); + std::set dep_seen; + for (std::vector::const_iterator d = m_grain.dependencies ().begin (); d != m_grain.dependencies ().end (); ++d) { + + if (! SaltGrain::valid_name (d->name)) { + dependencies_alert->error () << tr ("'%1' is not a valid package name").arg (tl::to_qstring (d->name)) << tl::endl + << tr ("Valid package names are words (letters, digits, underscores).") << tl::endl + << tr ("Package groups can be specified in the form 'group/package'."); + continue; + } + + if (dep_seen.find (d->name) != dep_seen.end ()) { + dependencies_alert->error () << tr ("Duplicate dependency '%1'").arg (tl::to_qstring (d->name)) << tl::endl + << tr ("A package cannot be dependent on the same package twice. Remove on entry."); + continue; + } + dep_seen.insert (d->name); + + if (dep.is_valid_name (d->name)) { + try { + dep.check_circular (dep.grain_for_name (m_grain.name ()), dep.grain_for_name (d->name)); + } catch (tl::Exception &ex) { + dependencies_alert->error () << ex.msg () << tl::endl + << tr ("Circular dependency means, a package is eventually depending on itself."); + } + } + + if (d->version.empty ()) { + dependencies_alert->warn () << tr ("No version specified for dependency '%1'").arg (tl::to_qstring (d->name)) << tl::endl + << tr ("Please consider giving a version here. Versions help deciding whether a package needs to be updated.") << tl::endl + << tr ("If the dependency package has a version itself, the version is automatically set to it's current version."); + } + + if (!d->url.empty ()) { + SaltGrain gdep; + try { + gdep = SaltGrain::from_url (d->url); + if (gdep.name () != d->name) { + dependencies_alert->error () << tr ("Package name obtained from download URL is not the expected name.") << tl::endl + << tr ("Downloaded name: ") << gdep.name () << tl::endl + << tr ("Expected name: ") << d->name; + } + } catch (tl::Exception &ex) { + dependencies_alert->error () << tr ("Attempt to test-download package from URL failed. Error details follow.") << tl::endl + << tr ("URL: ") << d->url << tl::endl + << tr ("Message: ") << ex.msg (); + } + } + + } + + if (!license_alert->needs_attention () && + !doc_url_alert->needs_attention () && + !dependencies_alert->needs_attention () && + !version_alert->needs_attention ()) { + QDialog::accept (); + } else { + if (QMessageBox::warning (this, tr ("Issues Encountered"), + tr ("Some issues have been found when inspecting the package details.\nThe respective fields are marked with warning icons.\n\nIgnore these issues and commit the package details?"), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) { + QDialog::accept (); + } + } +} + +bool +SaltGrainPropertiesDialog::exec_dialog (lay::SaltGrain *grain, lay::Salt *salt) +{ + m_grain = *grain; + mp_salt = salt; + + dependencies->setItemDelegateForColumn (0, new SaltGrainNameEditDelegate (dependencies, mp_salt)); + + update_controls (); + + bool res = exec (); + if (res && *grain != m_grain) { + *grain = m_grain; + // save modified grain + grain->save (); + } + + delete dependencies->itemDelegateForColumn (0); + dependencies->setItemDelegateForColumn (0, 0); + + mp_salt = 0; + return res; +} + +} diff --git a/src/lay/laySaltGrainPropertiesDialog.h b/src/lay/laySaltGrainPropertiesDialog.h new file mode 100644 index 000000000..cb2b62285 --- /dev/null +++ b/src/lay/laySaltGrainPropertiesDialog.h @@ -0,0 +1,96 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_laySaltGrainPropertiesDialog +#define HDR_laySaltGrainPropertiesDialog + +#include "laySaltGrain.h" + +#include + +#include "ui_SaltGrainPropertiesDialog.h" + +namespace lay +{ + +class Salt; + +/** + * @brief The dialog for managing the Salt ("Packages") + */ +class SaltGrainPropertiesDialog + : public QDialog, private Ui::SaltGrainPropertiesDialog +{ +Q_OBJECT + +public: + /** + * @brief Constructor + */ + SaltGrainPropertiesDialog (QWidget *parent); + + /** + * @brief Executes the dialog for the given grain + * If the dialog is committed with "Ok", the new data is written into + * the grain provided and "true" is returned. Otherwise, "false" is + * returned and the object remains unchanged. + */ + bool exec_dialog (lay::SaltGrain *grain, lay::Salt *salt); + + /** + * @brief Gets the current package index + */ + lay::Salt *salt () + { + return mp_salt; + } + +private slots: + void reset_icon (); + void set_icon (); + void reset_screenshot (); + void set_screenshot (); + void url_changed (const QString &url); + void add_dependency_clicked (); + void remove_dependency_clicked (); + void dependency_changed (QTreeWidgetItem *item, int column); + +protected: + void accept (); + +private: + lay::SaltGrain m_grain; + lay::Salt *mp_salt; + QString m_title; + QString m_open_label; + QString m_image_dir; + bool m_update_enabled; + + void update_controls (); + void update_data (); + void update_icon (); + void update_screenshot (); +}; + +} + +#endif diff --git a/src/lay/laySaltGrains.cc b/src/lay/laySaltGrains.cc new file mode 100644 index 000000000..221861340 --- /dev/null +++ b/src/lay/laySaltGrains.cc @@ -0,0 +1,264 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "laySaltGrains.h" +#include "tlString.h" +#include "tlFileUtils.h" + +#include +#include +#include + +namespace lay +{ + +SaltGrains::SaltGrains () +{ + // .. nothing yet .. +} + +bool +SaltGrains::operator== (const SaltGrains &other) const +{ + return m_name == other.m_name && + m_path == other.m_path && + m_title == other.m_title && + m_collections == other.m_collections && + m_grains == other.m_grains; +} + +void +SaltGrains::set_name (const std::string &n) +{ + m_name = n; +} + +void +SaltGrains::set_title (const std::string &t) +{ + m_title = t; +} + +void +SaltGrains::set_path (const std::string &p) +{ + m_path = p; +} + +void +SaltGrains::add_collection (const SaltGrains &collection) +{ + m_collections.push_back (collection); +} + +bool +SaltGrains::remove_collection (collection_iterator iter, bool with_files) +{ + // NOTE: this is kind of inefficient, but in order to maintain the const iterator semantics this approach is required + for (collections_type::iterator i = m_collections.begin (); i != m_collections.end (); ++i) { + if (i == iter) { + if (with_files && !tl::rm_dir_recursive (tl::to_qstring (i->path ()))) { + return false; + } + m_collections.erase (i); + return true; + } + } + + return false; +} + +void +SaltGrains::add_grain (const SaltGrain &grain) +{ + m_grains.push_back (grain); +} + +bool +SaltGrains::remove_grain (grain_iterator iter, bool with_files) +{ + // NOTE: this is kind of inefficient, but in order to maintain the const iterator semantics this approach is required + for (grains_type::iterator i = m_grains.begin (); i != m_grains.end (); ++i) { + if (i == iter) { + if (with_files && !tl::rm_dir_recursive (tl::to_qstring (i->path ()))) { + return false; + } + m_grains.erase (i); + return true; + } + } + + return false; +} + +bool +SaltGrains::is_empty () const +{ + if (! m_grains.empty ()) { + return false; + } + for (collections_type::const_iterator i = m_collections.begin (); i != m_collections.end (); ++i) { + if (!i->is_empty ()) { + return false; + } + } + return true; +} + +bool +SaltGrains::is_readonly () const +{ + return QFileInfo (tl::to_qstring (path ())).isWritable (); +} + +namespace +{ + +/** + * @brief A helper class required because directory traversal is not supported by QResource directly + */ +class OpenResource + : public QResource +{ +public: + using QResource::isDir; + using QResource::isFile; + using QResource::children; + + OpenResource (const QString &path) + : QResource (path) + { + // .. nothing yet .. + } +}; + +} + +SaltGrains +SaltGrains::from_path (const std::string &path, const std::string &prefix) +{ + tl_assert (! path.empty ()); + + SaltGrains grains; + grains.set_path (path); + + if (path[0] != ':') { + + QDir dir (tl::to_qstring (path)); + QStringList entries = dir.entryList (QDir::NoDotAndDotDot | QDir::Dirs, QDir::Name); + for (QStringList::const_iterator e = entries.begin (); e != entries.end (); ++e) { + + std::string new_prefix = prefix; + if (! new_prefix.empty ()) { + new_prefix += "/"; + } + new_prefix += tl::to_string (*e); + + std::string epath = tl::to_string (dir.absoluteFilePath (*e)); + if (SaltGrain::is_grain (epath)) { + try { + SaltGrain g (SaltGrain::from_path (epath)); + g.set_name (new_prefix); + grains.add_grain (g); + } catch (...) { + // ignore errors (TODO: what to do here?) + } + } else if (QFileInfo (tl::to_qstring (epath)).isDir ()) { + SaltGrains c = SaltGrains::from_path (epath, new_prefix); + c.set_name (new_prefix); + if (! c.is_empty ()) { + grains.add_collection (c); + } + } + + } + + } else { + + OpenResource resource (tl::to_qstring (path)); + if (resource.isDir ()) { + + QStringList templ_dir = resource.children (); + for (QStringList::const_iterator t = templ_dir.begin (); t != templ_dir.end (); ++t) { + + std::string new_prefix = prefix; + if (! new_prefix.empty ()) { + new_prefix += "/"; + } + new_prefix += tl::to_string (*t); + + std::string epath = path + "/" + tl::to_string (*t); + + if (SaltGrain::is_grain (epath)) { + try { + SaltGrain g (SaltGrain::from_path (epath)); + g.set_name (new_prefix); + grains.add_grain (g); + } catch (...) { + // ignore errors (TODO: what to do here?) + } + } else if (OpenResource (tl::to_qstring (epath)).isDir ()) { + SaltGrains c = SaltGrains::from_path (epath, new_prefix); + c.set_name (new_prefix); + if (! c.is_empty ()) { + grains.add_collection (c); + } + } + + } + + } + + } + + return grains; +} + +static tl::XMLElementList s_group_struct = + tl::make_member (&SaltGrains::name, &SaltGrains::set_name, "name") + + tl::make_element (&SaltGrains::begin_collections, &SaltGrains::end_collections, &SaltGrains::add_collection, "group", &s_group_struct) + + tl::make_element (&SaltGrains::begin_grains, &SaltGrains::end_grains, &SaltGrains::add_grain, "salt-grain", SaltGrain::xml_struct ()); + +static tl::XMLStruct s_xml_struct ("salt-mine", s_group_struct); + +void +SaltGrains::load (const std::string &p) +{ + tl::XMLFileSource source (p); + s_xml_struct.parse (source, *this); +} + +void +SaltGrains::load (tl::InputStream &p) +{ + tl::XMLStreamSource source (p); + s_xml_struct.parse (source, *this); +} + +void +SaltGrains::save (const std::string &p) const +{ + tl::OutputStream os (p, tl::OutputStream::OM_Plain); + s_xml_struct.write (os, *this); +} + +} diff --git a/src/lay/laySaltGrains.h b/src/lay/laySaltGrains.h new file mode 100644 index 000000000..2dc524a5a --- /dev/null +++ b/src/lay/laySaltGrains.h @@ -0,0 +1,210 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_laySaltGrains +#define HDR_laySaltGrains + +#include "layCommon.h" +#include "laySaltGrain.h" + +#include + +namespace lay +{ + +/** + * @brief A class representing a collection of grains (packages) + * A collection can have child collections and grains (leafs). + */ +class LAY_PUBLIC SaltGrains +{ +public: + typedef std::list collections_type; + typedef collections_type::const_iterator collection_iterator; + typedef std::list grains_type; + typedef grains_type::const_iterator grain_iterator; + + /** + * @brief Constructor: creates an empty collection + */ + SaltGrains (); + + /** + * @brief Equality + */ + bool operator== (const SaltGrains &other) const; + + /** + * @brief Inequality + */ + bool operator!= (const SaltGrains &other) const + { + return !operator== (other); + } + + /** + * @brief Gets the name of the grain collection + * + * The name is either a plain name (a word) or a path into a collection. + * Name paths are formed using the "/" separator. "mycollection" is a plain name, + * while "mycollection/subcollection" is a collection within a collection. + */ + const std::string &name () const + { + return m_name; + } + + /** + * @brief Sets the name of the grain collection + */ + void set_name (const std::string &p); + + /** + * @brief Gets the title of the grain collection + * + * The title is a brief description that is shown in the title of the + * package manager. + */ + const std::string &title () const + { + return m_title; + } + + /** + * @brief Sets the title of the grain collection + */ + void set_title (const std::string &t); + + /** + * @brief Gets the absolute file path of the installed grain + * This is the file path to the grain folder. + */ + const std::string &path () const + { + return m_path; + } + + /** + * @brief Sets the absolute file path of the installed grain + */ + void set_path (const std::string &p); + + /** + * @brief Gets the collections which are members of this collection (begin iterator) + */ + collection_iterator begin_collections () const + { + return m_collections.begin (); + } + + /** + * @brief Gets the collections which are members of this collection (end iterator) + */ + collection_iterator end_collections () const + { + return m_collections.end (); + } + + /** + * @brief Adds a collection to this collection + */ + void add_collection (const SaltGrains &collection); + + /** + * @brief Removes the collection given by the collection iterator + * If "with_files" is true, also the folder and all sub-folders will be removed + * @return true, if the remove was successful. + */ + bool remove_collection (collection_iterator iter, bool with_files = false); + + /** + * @brief Gets the grains (leaf nodes) which are members of this collection (begin iterator) + */ + grain_iterator begin_grains () const + { + return m_grains.begin (); + } + + /** + * @brief Gets the grains (leaf nodes) which are members of this collection (end iterator) + */ + grain_iterator end_grains () const + { + return m_grains.end (); + } + + /** + * @brief Adds a grain to this collection + */ + void add_grain (const SaltGrain &grain); + + /** + * @brief Removes the grain given by the grain iterator + * If "with_files" is true, also the files and the folder will be removed. + * @return true, if the remove was successful. + */ + bool remove_grain (grain_iterator iter, bool with_files = false); + + /** + * @brief Gets a value indicating whether the collection is empty + */ + bool is_empty () const; + + /** + * @brief Returns true, if the collection is read-only + */ + bool is_readonly () const; + + /** + * @brief Loads the grain collection from the given path + */ + void load (const std::string &p); + + /** + * @brief Loads the grain collection from the given input stream + */ + void load (tl::InputStream &p); + + /** + * @brief Saves the grain collection to the given file + */ + void save (const std::string &p) const; + + /** + * @brief Scan grains from a given path + * This will scan the grains found within this path and return a collection containing + * the grains from this path. + * Sub-collections are created from folders which contain grains or sub-collections. + */ + static SaltGrains from_path (const std::string &path, const std::string &pfx = std::string ()); + +private: + std::string m_name; + std::string m_title; + std::string m_path; + collections_type m_collections; + grains_type m_grains; +}; + +} + +#endif diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc new file mode 100644 index 000000000..aa3ecd745 --- /dev/null +++ b/src/lay/laySaltManagerDialog.cc @@ -0,0 +1,643 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "laySaltManagerDialog.h" +#include "laySaltModel.h" +#include "laySaltGrainPropertiesDialog.h" +#include "laySaltDownloadManager.h" +#include "laySalt.h" +#include "ui_SaltGrainTemplateSelectionDialog.h" +#include "tlString.h" +#include "tlExceptions.h" +#include "tlHttpStream.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lay +{ + +// -------------------------------------------------------------------------------------- + +/** + * @brief A tiny dialog to select a template and a name for the grain + */ +class SaltGrainTemplateSelectionDialog + : public QDialog, private Ui::SaltGrainTemplateSelectionDialog +{ +public: + SaltGrainTemplateSelectionDialog (QWidget *parent, lay::Salt *salt) + : QDialog (parent), mp_salt (salt) + { + Ui::SaltGrainTemplateSelectionDialog::setupUi (this); + + m_salt_templates.add_location (":/salt_templates"); + salt_view->setModel (new SaltModel (this, &m_salt_templates)); + salt_view->setItemDelegate (new SaltItemDelegate (this)); + salt_view->setCurrentIndex (salt_view->model ()->index (0, 0, QModelIndex ())); + } + + lay::SaltGrain templ () const + { + SaltModel *model = dynamic_cast (salt_view->model ()); + tl_assert (model != 0); + + SaltGrain *g = model->grain_from_index (salt_view->currentIndex ()); + tl_assert (g != 0); + + return *g; + } + + std::string name () const + { + return tl::to_string (name_edit->text ()); + } + + void accept () + { + name_alert->clear (); + std::string name = tl::to_string (name_edit->text ().simplified ()); + if (name.empty ()) { + name_alert->error () << tr ("Name must not be empty"); + } else if (! SaltGrain::valid_name (name)) { + name_alert->error () << tr ("Name is not valid (must be composed of letters, digits or underscores.\nGroups and names need to be separated with slashes."); + } else { + + // check, if this name does not exist yet + for (Salt::flat_iterator g = mp_salt->begin_flat (); g != mp_salt->end_flat (); ++g) { + if ((*g)->name () == name) { + name_alert->error () << tr ("A package with this name already exists"); + return; + } + } + + QDialog::accept (); + + } + } + +private: + lay::Salt m_salt_templates; + lay::Salt *mp_salt; +}; + +// -------------------------------------------------------------------------------------- +// SaltManager implementation + +// @@@ +lay::Salt salt; +static bool salt_initialized = false; +void make_salt () +{ + if (!salt_initialized) { + salt_initialized = true; + salt.add_location (tl::to_string (QDir::homePath () + QString::fromUtf8("/.klayout/salt"))); + } +} +lay::Salt *get_salt () +{ + salt = lay::Salt (); salt_initialized = false; + make_salt (); + return &salt; +} +// @@@ + +// @@@ +lay::Salt salt_mine; +void make_salt_mine () +{ + salt_mine = lay::Salt (); + salt_mine.load ("/home/matthias/salt.mine"); +} +lay::Salt *get_salt_mine () +{ + make_salt_mine(); + return &salt_mine; +} +// @@@ + +SaltManagerDialog::SaltManagerDialog (QWidget *parent) + : QDialog (parent), + m_current_changed_enabled (true), dm_update_models (this, &SaltManagerDialog::update_models) +{ + Ui::SaltManagerDialog::setupUi (this); + mp_properties_dialog = new lay::SaltGrainPropertiesDialog (this); + + connect (edit_button, SIGNAL (clicked ()), this, SLOT (edit_properties ())); + connect (create_button, SIGNAL (clicked ()), this, SLOT (create_grain ())); + connect (delete_button, SIGNAL (clicked ()), this, SLOT (delete_grain ())); + connect (apply_button, SIGNAL (clicked ()), this, SLOT (apply ())); + + mp_salt = get_salt (); + mp_salt_mine = get_salt_mine (); + + SaltModel *model = new SaltModel (this, mp_salt); + salt_view->setModel (model); + salt_view->setItemDelegate (new SaltItemDelegate (this)); + + SaltModel *mine_model = new SaltModel (this, mp_salt_mine); + salt_mine_view->setModel (mine_model); + salt_mine_view->setItemDelegate (new SaltItemDelegate (this)); + + mode_tab->setCurrentIndex (mp_salt->is_empty () ? 1 : 0); + + connect (mode_tab, SIGNAL (currentChanged (int)), this, SLOT (mode_changed ())); + + connect (mp_salt, SIGNAL (collections_changed ()), this, SLOT (salt_changed ())); + connect (mp_salt_mine, SIGNAL (collections_changed ()), this, SLOT (salt_mine_changed ())); + + update_models (); + + connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); + connect (salt_view, SIGNAL (doubleClicked (const QModelIndex &)), this, SLOT (edit_properties ())); + connect (salt_mine_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (mine_current_changed ()), Qt::QueuedConnection); + connect (salt_mine_view, SIGNAL (doubleClicked (const QModelIndex &)), this, SLOT (mark_clicked ())); + + search_installed_edit->set_clear_button_enabled (true); + search_new_edit->set_clear_button_enabled (true); + connect (search_installed_edit, SIGNAL (textChanged (const QString &)), this, SLOT (search_text_changed (const QString &))); + connect (search_new_edit, SIGNAL (textChanged (const QString &)), this, SLOT (search_text_changed (const QString &))); + + connect (mark_button, SIGNAL (clicked ()), this, SLOT (mark_clicked ())); + + salt_mine_view->addAction (actionUnmarkAll); + QAction *a = new QAction (this); + a->setSeparator (true); + salt_mine_view->addAction (a); + salt_mine_view->addAction (actionShowMarkedOnly); + salt_mine_view->addAction (actionShowAll); + salt_mine_view->setContextMenuPolicy (Qt::ActionsContextMenu); + + connect (actionUnmarkAll, SIGNAL (triggered ()), this, SLOT (unmark_all ())); + connect (actionShowMarkedOnly, SIGNAL (triggered ()), this, SLOT (show_marked_only ())); + connect (actionShowAll, SIGNAL (triggered ()), this, SLOT (show_all ())); +} + +void +SaltManagerDialog::mode_changed () +{ + // keeps the splitters in sync + if (mode_tab->currentIndex () == 1) { + splitter_new->setSizes (splitter->sizes ()); + show_all (); + } else if (mode_tab->currentIndex () == 0) { + splitter->setSizes (splitter_new->sizes ()); + } +} + +void +SaltManagerDialog::show_all () +{ + search_new_edit->clear (); + + SaltModel *model = dynamic_cast (salt_mine_view->model ()); + if (! model) { + return; + } + + for (int i = model->rowCount (QModelIndex ()); i > 0; ) { + --i; + salt_mine_view->setRowHidden (i, false); + } +} + +void +SaltManagerDialog::show_marked_only () +{ + search_new_edit->clear (); + + SaltModel *model = dynamic_cast (salt_mine_view->model ()); + if (! model) { + return; + } + + for (int i = model->rowCount (QModelIndex ()); i > 0; ) { + --i; + SaltGrain *g = model->grain_from_index (model->index (i, 0, QModelIndex ())); + salt_mine_view->setRowHidden (i, !(g && model->is_marked (g->name ()))); + } +} + +void +SaltManagerDialog::unmark_all () +{ + SaltModel *model = dynamic_cast (salt_mine_view->model ()); + if (model) { + model->clear_marked (); + update_apply_state (); + } +} + +void +SaltManagerDialog::search_text_changed (const QString &text) +{ + QListView *view = 0; + if (sender () == search_installed_edit) { + view = salt_view; + } else if (sender () == search_new_edit) { + view = salt_mine_view; + } else { + return; + } + + SaltModel *model = dynamic_cast (view->model ()); + if (! model) { + return; + } + + if (text.isEmpty ()) { + + for (int i = model->rowCount (QModelIndex ()); i > 0; ) { + --i; + view->setRowHidden (i, false); + } + + } else { + + QRegExp re (text, Qt::CaseInsensitive); + + for (int i = model->rowCount (QModelIndex ()); i > 0; ) { + --i; + QModelIndex index = model->index (i, 0, QModelIndex ()); + SaltGrain *g = model->grain_from_index (index); + bool hidden = (!g || re.indexIn (tl::to_qstring (g->name ())) < 0); + view->setRowHidden (i, hidden); + } + + } +} + +void +SaltManagerDialog::mark_clicked () +{ + SaltModel *model = dynamic_cast (salt_mine_view->model ()); + if (! model) { + return; + } + SaltGrain *g = mine_current_grain (); + if (! g) { + return; + } + + model->set_marked (g->name (), !model->is_marked (g->name ())); + update_apply_state (); +} + +void +SaltManagerDialog::update_apply_state () +{ + int marked = 0; + + SaltModel *model = dynamic_cast (salt_mine_view->model ()); + if (! model) { + return; + } + + for (int i = model->rowCount (QModelIndex ()); i > 0; ) { + --i; + QModelIndex index = model->index (i, 0, QModelIndex ()); + SaltGrain *g = model->grain_from_index (index); + if (g && model->is_marked (g->name ())) { + marked += 1; + } + } + + apply_button->setEnabled (marked > 0); + if (marked == 0) { + apply_label->setText (QString ()); + } else if (marked == 1) { + apply_label->setText (tr ("One package selected")); + } else if (marked > 1) { + apply_label->setText (tr ("%1 packages selected").arg (marked)); + } +} + +void +SaltManagerDialog::apply () +{ +BEGIN_PROTECTED + + lay::SaltDownloadManager manager; + + bool any = false; + + // fetch all marked grains and register for download + SaltModel *model = dynamic_cast (salt_mine_view->model ()); + if (model) { + for (int i = model->rowCount (QModelIndex ()); i > 0; ) { + --i; + QModelIndex index = model->index (i, 0, QModelIndex ()); + SaltGrain *g = model->grain_from_index (index); + if (g && model->is_marked (g->name ())) { + manager.register_download (g->name (), g->url (), g->version ()); + any = true; + } + } + } + + if (! any) { + throw tl::Exception (tl::to_string (tr ("No packages marked for installation or update"))); + } + + manager.compute_dependencies (*mp_salt, *mp_salt_mine); + + if (manager.show_confirmation_dialog (this, *mp_salt)) { + unmark_all (); + manager.execute (*mp_salt); + } + +END_PROTECTED +} + +void +SaltManagerDialog::edit_properties () +{ + SaltGrain *g = current_grain (); + if (g) { + if (mp_properties_dialog->exec_dialog (g, mp_salt)) { + current_changed (); + } + } +} + +void +SaltManagerDialog::create_grain () +{ +BEGIN_PROTECTED + + SaltGrainTemplateSelectionDialog temp_dialog (this, mp_salt); + if (temp_dialog.exec ()) { + + SaltGrain target; + target.set_name (temp_dialog.name ()); + + if (mp_salt->create_grain (temp_dialog.templ (), target)) { + + // select the new one + SaltModel *model = dynamic_cast (salt_view->model ()); + if (model) { + for (int i = model->rowCount (QModelIndex ()); i > 0; ) { + --i; + QModelIndex index = model->index (i, 0, QModelIndex ()); + SaltGrain *g = model->grain_from_index (index); + if (g && g->name () == target.name ()) { + salt_view->setCurrentIndex (index); + break; + } + } + + } + + } else { + throw tl::Exception (tl::to_string (tr ("Initialization of new package failed - see log window (File/Log Viewer) for details"))); + } + + } + +END_PROTECTED +} + +void +SaltManagerDialog::delete_grain () +{ +BEGIN_PROTECTED + + SaltGrain *g = current_grain (); + if (! g) { + throw tl::Exception (tl::to_string (tr ("No package selected to delete"))); + } + + if (QMessageBox::question (this, tr ("Delete Package"), tr ("Are you sure to delete package '%1'?").arg (tl::to_qstring (g->name ())), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) { + mp_salt->remove_grain (*g); + } + +END_PROTECTED +} + +void +SaltManagerDialog::salt_changed () +{ + dm_update_models (); +} + +void +SaltManagerDialog::salt_mine_changed () +{ + dm_update_models (); +} + +void +SaltManagerDialog::update_models () +{ + SaltModel *model = dynamic_cast (salt_view->model ()); + tl_assert (model != 0); + + // NOTE: the disabling of the event handler prevents us from + // letting the model connect to the salt's signal directly. + m_current_changed_enabled = false; + + model->clear_messages (); + + // Establish a message saying that an update is available + for (Salt::flat_iterator g = mp_salt->begin_flat (); g != mp_salt->end_flat (); ++g) { + SaltGrain *gm = mp_salt_mine->grain_by_name ((*g)->name ()); + if (gm && SaltGrain::compare_versions (gm->version (), (*g)->version ()) > 0) { + model->set_message ((*g)->name (), SaltModel::Warning, tl::to_string (tr ("An update to version %1 is available").arg (tl::to_qstring (gm->version ())))); + } + } + + model->update (); + + m_current_changed_enabled = true; + + if (mp_salt->is_empty ()) { + + list_stack->setCurrentIndex (1); + details_frame->hide (); + + } else { + + list_stack->setCurrentIndex (0); + details_frame->show (); + + // select the first grain + if (model->rowCount (QModelIndex ()) > 0) { + salt_view->setCurrentIndex (model->index (0, 0, QModelIndex ())); + } + + } + + SaltModel *mine_model = dynamic_cast (salt_mine_view->model ()); + tl_assert (mine_model != 0); + + // NOTE: the disabling of the event handler prevents us from + // letting the model connect to the salt's signal directly. + m_current_changed_enabled = false; + + mine_model->clear_order (); + mine_model->clear_messages (); + mine_model->enable_all (); + + // Establish a message saying that an update is available + for (Salt::flat_iterator g = mp_salt->begin_flat (); g != mp_salt->end_flat (); ++g) { + SaltGrain *gm = mp_salt_mine->grain_by_name ((*g)->name ()); + if (gm && SaltGrain::compare_versions (gm->version (), (*g)->version ()) > 0) { + mine_model->set_message ((*g)->name (), SaltModel::Warning, tl::to_string (tr ("The installed version is outdated (%1)").arg (tl::to_qstring ((*g)->version ())))); + mine_model->set_order ((*g)->name (), -1); + } else if (gm) { + mine_model->set_message ((*g)->name (), SaltModel::None, tl::to_string (tr ("This package is already installed and up to date"))); + mine_model->set_order ((*g)->name (), 1); + mine_model->set_enabled ((*g)->name (), false); + } + } + + mine_model->update (); + + m_current_changed_enabled = true; + + // select the first grain + if (mine_model->rowCount (QModelIndex ()) > 0) { + salt_mine_view->setCurrentIndex (mine_model->index (0, 0, QModelIndex ())); + } + + mine_current_changed (); + current_changed (); + update_apply_state (); +} + +void +SaltManagerDialog::current_changed () +{ + SaltGrain *g = current_grain (); + details_text->set_grain (g); + if (!g) { + details_frame->setEnabled (false); + delete_button->setEnabled (false); + } else { + details_frame->setEnabled (true); + delete_button->setEnabled (true); + edit_button->setEnabled (! g->is_readonly ()); + } +} + +lay::SaltGrain * +SaltManagerDialog::current_grain () +{ + SaltModel *model = dynamic_cast (salt_view->model ()); + return model ? model->grain_from_index (salt_view->currentIndex ()) : 0; +} + +void +SaltManagerDialog::mine_current_changed () +{ + +BEGIN_PROTECTED + + SaltGrain *g = mine_current_grain (); + details_new_frame->setEnabled (g != 0); + + if (! g) { + details_new_text->set_grain (0); + return; + } + + m_remote_grain.reset (0); + + // Download actual grain definition file + try { + + if (g->url ().empty ()) { + throw tl::Exception (tl::to_string (tr ("No download link available"))); + } + + QString text = tr ( + "" + "" + "" + "

Fetching Package Definition ...

" + "

URL: %1

" + "
" + "" + "" + ) + .arg (tl::to_qstring (SaltGrain::spec_url (g->url ()))); + + details_new_text->setHtml (text); + + QApplication::processEvents (QEventLoop::ExcludeUserInputEvents); + + tl::InputHttpStream http (SaltGrain::spec_url (g->url ())); + tl::InputStream stream (http); + + m_remote_grain.reset (new SaltGrain ()); + m_remote_grain->load (stream); + m_remote_grain->set_url (g->url ()); + + if (g->name () != m_remote_grain->name ()) { + throw tl::Exception (tl::to_string (tr ("Name mismatch between repository and actual package (repository: %1, package: %2)").arg (tl::to_qstring (g->name ())).arg (tl::to_qstring (m_remote_grain->name ())))); + } + if (SaltGrain::compare_versions (g->version (), m_remote_grain->version ()) != 0) { + throw tl::Exception (tl::to_string (tr ("Version mismatch between repository and actual package (repository: %1, package: %2)").arg (tl::to_qstring (g->version ())).arg (tl::to_qstring (m_remote_grain->version ())))); + } + + details_new_text->set_grain (m_remote_grain.get ()); + + } catch (tl::Exception &ex) { + + m_remote_grain.reset (0); + + QString text = tr ( + "" + "" + "" + "

Error Fetching Package Definition

" + "

URL: %1

" + "

Error: %2

" + "
" + "" + "" + ) + .arg (tl::to_qstring (SaltGrain::spec_url (g->url ()))) + .arg (tl::to_qstring (tl::escaped_to_html (ex.msg ()))); + + details_new_text->setHtml (text); + + } + +END_PROTECTED +} + +lay::SaltGrain * +SaltManagerDialog::mine_current_grain () +{ + SaltModel *model = dynamic_cast (salt_mine_view->model ()); + return model ? model->grain_from_index (salt_mine_view->currentIndex ()) : 0; +} + +} diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h new file mode 100644 index 000000000..6e1f40cf4 --- /dev/null +++ b/src/lay/laySaltManagerDialog.h @@ -0,0 +1,139 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_laySaltManagerDialog +#define HDR_laySaltManagerDialog + +#include "ui_SaltManagerDialog.h" +#include "tlDeferredExecution.h" + +#include +#include + +namespace lay +{ + +class Salt; +class SaltGrain; +class SaltGrainPropertiesDialog; + +/** + * @brief The dialog for managing the Salt ("Packages") + */ +class SaltManagerDialog + : public QDialog, private Ui::SaltManagerDialog +{ +Q_OBJECT + +public: + /** + * @brief Constructor + */ + SaltManagerDialog (QWidget *parent); + +private slots: + /** + * @brief Called when the list of packages (grains) has changed + */ + void salt_changed (); + + /** + * @brief Called when the repository (salt mine) has changed + */ + void salt_mine_changed (); + + /** + * @brief Called when the currently selected package (grain) has changed + */ + void current_changed (); + + /** + * @brief Called when the currently selected package from the salt mine has changed + */ + void mine_current_changed (); + + /** + * @brief Called when the "edit" button is pressed + */ + void edit_properties (); + + /** + * @brief Called when the "mark" button is pressed + */ + void mark_clicked (); + + /** + * @brief Called when the "edit" button is pressed + */ + void create_grain (); + + /** + * @brief Called when the "delete" button is pressed + */ + void delete_grain (); + + /** + * @brief Called when the mode tab changed + */ + void mode_changed (); + + /** + * @brief Called when the "apply" button is clicked + */ + void apply (); + + /** + * @brief Called when one search text changed + */ + void search_text_changed (const QString &text); + + /** + * @brief Called to show the marked items only + */ + void show_marked_only (); + + /** + * @brief Called to show all items again + */ + void show_all (); + + /** + * @brief Called to unmark all items + */ + void unmark_all (); + +private: + Salt *mp_salt, *mp_salt_mine; + std::auto_ptr m_remote_grain; + bool m_current_changed_enabled; + SaltGrainPropertiesDialog *mp_properties_dialog; + tl::DeferredMethod dm_update_models; + + SaltGrain *current_grain (); + SaltGrain *mine_current_grain (); + void update_models (); + void update_apply_state (); +}; + +} + +#endif diff --git a/src/lay/laySaltModel.cc b/src/lay/laySaltModel.cc new file mode 100644 index 000000000..fc1a44ef8 --- /dev/null +++ b/src/lay/laySaltModel.cc @@ -0,0 +1,411 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "laySaltModel.h" +#include "laySalt.h" + +#include +#include +#include +#include +#include + +namespace lay +{ + +// -------------------------------------------------------------------------------------- + +SaltItemDelegate::SaltItemDelegate (QObject *parent) + : QStyledItemDelegate (parent) +{ + // .. nothing yet .. +} + +void +SaltItemDelegate::paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyleOptionViewItemV4 optionV4 = option; + initStyleOption (&optionV4, index); + + bool is_enabled = (optionV4.state & QStyle::State_Enabled); + optionV4.state |= QStyle::State_Enabled; + + QStyle *style = optionV4.widget ? optionV4.widget->style () : QApplication::style (); + + QTextDocument doc; + doc.setHtml (optionV4.text); + + optionV4.text = QString (); + style->drawControl (QStyle::CE_ItemViewItem, &optionV4, painter); + + QAbstractTextDocumentLayout::PaintContext ctx; + + if (optionV4.state & QStyle::State_Selected) { + ctx.palette.setColor (QPalette::Text, optionV4.palette.color (QPalette::Active, QPalette::HighlightedText)); + } else if (! is_enabled) { + ctx.palette.setColor (QPalette::Text, optionV4.palette.color (QPalette::Disabled, QPalette::Text)); + } + + QRect textRect = style->subElementRect (QStyle::SE_ItemViewItemText, &optionV4); + painter->save (); + painter->translate (textRect.topLeft ()); + painter->setClipRect (textRect.translated (-textRect.topLeft ())); + doc.documentLayout()->draw (painter, ctx); + painter->restore (); +} + +QSize +SaltItemDelegate::sizeHint (const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + const int textWidth = 500; + + QStyleOptionViewItemV4 optionV4 = option; + initStyleOption (&optionV4, index); + + const QListView *view = dynamic_cast (optionV4.widget); + QSize icon_size (0, 0); + if (view) { + icon_size = view->iconSize (); + } + + QTextDocument doc; + doc.setHtml (optionV4.text); + doc.setTextWidth (textWidth); + return QSize (textWidth + icon_size.width () + 6, std::max (icon_size.height () + 12, int (doc.size ().height ()))); +} + +// -------------------------------------------------------------------------------------- + +SaltModel::SaltModel (QObject *parent, lay::Salt *salt) + : QAbstractItemModel (parent), mp_salt (salt) +{ + create_ordered_list (); +} + +Qt::ItemFlags +SaltModel::flags (const QModelIndex &index) const +{ + Qt::ItemFlags f = QAbstractItemModel::flags (index); + + const lay::SaltGrain *g = grain_from_index (index); + if (g && ! is_enabled (g->name ())) { + f &= ~Qt::ItemIsSelectable; + f &= ~Qt::ItemIsEnabled; + } + + return f; +} + +QVariant +SaltModel::data (const QModelIndex &index, int role) const +{ + if (role == Qt::DisplayRole) { + + const lay::SaltGrain *g = grain_from_index (index); + if (!g) { + return QVariant (); + } + + std::string text = ""; + text += "

"; + text += tl::escaped_to_html (g->name ()); + if (!g->version ().empty ()) { + text += " "; + text += tl::escaped_to_html (g->version ()); + } + if (!g->title ().empty ()) { + text += " - "; + text += tl::escaped_to_html (g->title ()); + } + text += "

"; + if (!g->doc ().empty ()) { + text += "

"; + text += tl::escaped_to_html (g->doc ()); + text += "

"; + } + + std::map >::const_iterator m = m_messages.find (g->name ()); + if (m != m_messages.end ()) { + if (m->second.first == Warning || m->second.first == Error) { + text += "

" + tl::escaped_to_html (m->second.second) + "

"; + } else if (m->second.first == Info) { + text += "

" + tl::escaped_to_html (m->second.second) + "

"; + } else { + text += "

" + tl::escaped_to_html (m->second.second) + "

"; + } + } + + text += ""; + + return tl::to_qstring (text); + + } else if (role == Qt::DecorationRole) { + + int icon_dim = 64; + + const lay::SaltGrain *g = grain_from_index (index); + if (!g) { + return QVariant (); + } + + QImage img; + if (g->icon ().isNull ()) { + img = QImage (":/salt_icon.png"); + } else { + img = g->icon (); + } + + if (img.width () != icon_dim || img.height () != icon_dim) { + + QImage scaled = img.scaled (QSize (icon_dim, icon_dim), Qt::KeepAspectRatio, Qt::SmoothTransformation); + + img = QImage (icon_dim, icon_dim, QImage::Format_ARGB32); + img.fill (QColor (0, 0, 0, 0)); + QPainter painter (&img); + painter.drawImage ((icon_dim - scaled.width ()) / 2, (icon_dim - scaled.height ()) / 2, scaled); + + } + + if (m_marked.find (g->name ()) != m_marked.end ()) { + QPainter painter (&img); + QImage warn (":/marked_64.png"); + painter.drawImage (0, 0, warn); + } + + std::map >::const_iterator m = m_messages.find (g->name ()); + if (m != m_messages.end ()) { + if (m->second.first == Warning) { + QPainter painter (&img); + QImage warn (":/warn_16.png"); + painter.drawImage (0, 0, warn); + } else if (m->second.first == Error) { + QPainter painter (&img); + QImage warn (":/error_16.png"); + painter.drawImage (0, 0, warn); + } else if (m->second.first == Info) { + QPainter painter (&img); + QImage warn (":/info_16.png"); + painter.drawImage (0, 0, warn); + } + } + + return QPixmap::fromImage (img); + + } else { + return QVariant (); + } +} + +QModelIndex +SaltModel::index (int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid ()) { + return QModelIndex (); + } else { + return createIndex (row, column, m_ordered_grains [row]); + } +} + +QModelIndex +SaltModel::parent (const QModelIndex & /*index*/) const +{ + return QModelIndex (); +} + +int +SaltModel::columnCount(const QModelIndex & /*parent*/) const +{ + return 1; +} + +int +SaltModel::rowCount (const QModelIndex &parent) const +{ + if (parent.isValid ()) { + return 0; + } else { + return int (m_ordered_grains.size ()); + } +} + +SaltGrain * +SaltModel::grain_from_index (const QModelIndex &index) const +{ + if (index.isValid ()) { + return static_cast (index.internalPointer ()); + } else { + return 0; + } +} + +bool +SaltModel::is_marked (const std::string &name) const +{ + return m_marked.find (name) != m_marked.end (); +} + +bool +SaltModel::is_enabled (const std::string &name) const +{ + return m_disabled.find (name) == m_disabled.end (); +} + +void +SaltModel::set_marked (const std::string &name, bool marked) +{ + if (marked != is_marked (name)) { + if (! marked) { + m_marked.erase (name); + } else { + m_marked.insert (name); + } + emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); + } +} + +void +SaltModel::clear_marked () +{ + if (! m_marked.empty ()) { + m_marked.clear (); + emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); + } +} + +void +SaltModel::set_enabled (const std::string &name, bool enabled) +{ + if (enabled != is_enabled (name)) { + if (enabled) { + m_disabled.erase (name); + } else { + m_disabled.insert (name); + } + emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); + } +} + +void +SaltModel::enable_all () +{ + if (! m_disabled.empty ()) { + m_disabled.clear (); + emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); + } +} + +void +SaltModel::clear_order () +{ + m_display_order.clear (); +} + +void +SaltModel::reset_order (const std::string &name) +{ + m_display_order.erase (name); +} + +void +SaltModel::set_order (const std::string &name, int order) +{ + m_display_order[name] = order; +} + +void +SaltModel::set_message (const std::string &name, Severity severity, const std::string &message) +{ + bool needs_update = false; + if (message.empty ()) { + if (m_messages.find (name) != m_messages.end ()) { + m_messages.erase (name); + needs_update = true; + } + } else { + std::map >::iterator m = m_messages.find (name); + if (m == m_messages.end () || m->second.second != message || m->second.first != severity) { + m_messages.insert (std::make_pair (name, std::make_pair (severity, message))); + needs_update = true; + } + } + + if (needs_update) { + emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); + } +} + +void +SaltModel::clear_messages () +{ + if (! m_messages.empty ()) { + m_messages.clear (); + emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); + } +} + +void +SaltModel::update () +{ + create_ordered_list (); + reset (); +} + +void +SaltModel::create_ordered_list () +{ + m_ordered_grains.clear (); + + if (m_display_order.empty ()) { + + for (Salt::flat_iterator i = mp_salt->begin_flat (); i != mp_salt->end_flat (); ++i) { + m_ordered_grains.push_back (*i); + } + + } else { + + int min_order = m_display_order.begin ()->second; + int max_order = min_order; + min_order = std::min (min_order, 0); + max_order = std::max (max_order, 0); + + for (std::map::const_iterator i = m_display_order.begin (); i != m_display_order.end (); ++i) { + min_order = std::min (min_order, i->second); + max_order = std::max (max_order, i->second); + } + + for (int o = min_order; o <= max_order; ++o) { + for (Salt::flat_iterator i = mp_salt->begin_flat (); i != mp_salt->end_flat (); ++i) { + std::map::const_iterator d = m_display_order.find ((*i)->name ()); + int oi = 0; + if (d != m_display_order.end ()) { + oi = d->second; + } + if (oi == o) { + m_ordered_grains.push_back (*i); + } + } + } + + } +} + +} diff --git a/src/lay/laySaltModel.h b/src/lay/laySaltModel.h new file mode 100644 index 000000000..d7ba80d92 --- /dev/null +++ b/src/lay/laySaltModel.h @@ -0,0 +1,195 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_laySaltModel +#define HDR_laySaltModel + +#include "layCommon.h" + +#include +#include +#include +#include +#include +#include + +namespace lay +{ + +class Salt; +class SaltGrain; + +/** + * @brief A model representing the salt grains for a QListView + */ +class SaltModel + : public QAbstractItemModel +{ +Q_OBJECT + +public: + /** + * @brief An enum describing the severity of a message + */ + enum Severity + { + None = 0, + Info = 1, + Warning = 2, + Error = 3 + }; + + /** + * @brief Constructor + */ + SaltModel (QObject *parent, lay::Salt *salt); + + /** + * @brief Implementation of the QAbstractItemModel interface + */ + QVariant data (const QModelIndex &index, int role) const; + + /** + * @brief Implementation of the QAbstractItemModel interface + */ + Qt::ItemFlags flags (const QModelIndex &index) const; + + /** + * @brief Implementation of the QAbstractItemModel interface + */ + QModelIndex index (int row, int column, const QModelIndex &parent) const; + + /** + * @brief Implementation of the QAbstractItemModel interface + */ + QModelIndex parent (const QModelIndex & /*index*/) const; + + /** + * @brief Implementation of the QAbstractItemModel interface + */ + int columnCount(const QModelIndex & /*parent*/) const; + + /** + * @brief Implementation of the QAbstractItemModel interface + */ + int rowCount (const QModelIndex &parent) const; + + /** + * @brief Gets the grain pointer from a model index + */ + SaltGrain *grain_from_index (const QModelIndex &index) const; + + /** + * @brief Updates the model + * Needs to be called when the salt has changed. + */ + void update (); + + /** + * @brief Sets or resets the "marked" flag on the grain with the given name + */ + void set_marked (const std::string &name, bool marked); + + /** + * @brief Clears the marked state of all grains + */ + void clear_marked (); + + /** + * @brief Enables or disables the grain with the given name + */ + void set_enabled (const std::string &name, bool enabled); + + /** + * @brief Enables all grains + */ + void enable_all (); + + /** + * @brief Installs a message on the grain with the given name + * Installing an empty message basically removes the message. + */ + void set_message (const std::string &name, Severity severity, const std::string &message); + + /** + * @brief Removes a message + */ + void reset_message (const std::string &name) + { + set_message (name, None, std::string ()); + } + + /** + * @brief Clears all messages + */ + void clear_messages (); + + /** + * @brief Sets the display order + * Specifying a display order for a name will make the grain appear + * before or after other grains. + * "update" needs to be called before the order becomes active. + * Non-assigned items are considered to have order (0). + */ + void set_order (const std::string &name, int order); + + /** + * @brief Resets any display order + */ + void reset_order (const std::string &name); + + /** + * @brief Resets all display order specs + */ + void clear_order (); + +public: + lay::Salt *mp_salt; + std::set m_marked; + std::set m_disabled; + std::map > m_messages; + std::map m_display_order; + std::vector m_ordered_grains; + + bool is_marked (const std::string &name) const; + bool is_enabled (const std::string &name) const; + void create_ordered_list (); +}; + +// -------------------------------------------------------------------------------------- + +/** + * @brief A delegate displaying the summary of a grain + */ +class SaltItemDelegate + : public QStyledItemDelegate +{ +public: + SaltItemDelegate (QObject *parent); + + void paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + QSize sizeHint (const QStyleOptionViewItem &option, const QModelIndex &index) const; +}; + +} + +#endif diff --git a/src/lay/laySaltTemplates.qrc b/src/lay/laySaltTemplates.qrc new file mode 100644 index 000000000..e4155fe58 --- /dev/null +++ b/src/lay/laySaltTemplates.qrc @@ -0,0 +1,47 @@ + + + + + + + salt_templates/font/grain.xml + + + + + + + salt_templates/lib/grain.xml + + + + + + + salt_templates/pcell_lib/grain.xml + + + + + + + salt_templates/macro/grain.xml + + + + + + + salt_templates/pymacro/grain.xml + + + + + + + salt_templates/tech/grain.xml + + + + + diff --git a/src/lay/salt_templates/font/grain.xml b/src/lay/salt_templates/font/grain.xml new file mode 100644 index 000000000..593a48147 --- /dev/null +++ b/src/lay/salt_templates/font/grain.xml @@ -0,0 +1,16 @@ + + + font + 0.0 + Font package + This template provides a font for the Basic.TEXT PCell + + + GPLv3 + + + + + + + diff --git a/src/lay/salt_templates/lib/grain.xml b/src/lay/salt_templates/lib/grain.xml new file mode 100644 index 000000000..a071c2676 --- /dev/null +++ b/src/lay/salt_templates/lib/grain.xml @@ -0,0 +1,16 @@ + + + lib + 0.0 + Static Library + This template provides a static library + + + GPLv3 + + + + + + + diff --git a/src/lay/salt_templates/macro/grain.xml b/src/lay/salt_templates/macro/grain.xml new file mode 100644 index 000000000..ae9c5ffe2 --- /dev/null +++ b/src/lay/salt_templates/macro/grain.xml @@ -0,0 +1,16 @@ + + + macro + 0.0 + Ruby Macro + This template provides a Ruby macro + + + GPLv3 + + + + + + + diff --git a/src/lay/salt_templates/pcell_lib/grain.xml b/src/lay/salt_templates/pcell_lib/grain.xml new file mode 100644 index 000000000..20a656273 --- /dev/null +++ b/src/lay/salt_templates/pcell_lib/grain.xml @@ -0,0 +1,16 @@ + + + macro + 0.0 + PCell library + This template provides a PCell library implemented in Ruby + + + GPLv3 + + + + + + + diff --git a/src/lay/salt_templates/pymacro/grain.xml b/src/lay/salt_templates/pymacro/grain.xml new file mode 100644 index 000000000..2b281fcc9 --- /dev/null +++ b/src/lay/salt_templates/pymacro/grain.xml @@ -0,0 +1,16 @@ + + + macro + 0.0 + Python Macro + This template provides a Python macro + + + GPLv3 + + + + + + + diff --git a/src/lay/salt_templates/tech/grain.xml b/src/lay/salt_templates/tech/grain.xml new file mode 100644 index 000000000..2def15e72 --- /dev/null +++ b/src/lay/salt_templates/tech/grain.xml @@ -0,0 +1,16 @@ + + + macro + 0.0 + Technology + This template provides a technology + + + GPLv3 + + + + + + + diff --git a/src/laybasic/rdbMarkerBrowserPage.cc b/src/laybasic/rdbMarkerBrowserPage.cc index e31087072..f14e57338 100644 --- a/src/laybasic/rdbMarkerBrowserPage.cc +++ b/src/laybasic/rdbMarkerBrowserPage.cc @@ -1820,24 +1820,6 @@ MarkerBrowserPage::set_max_marker_count (size_t max_marker_count) } } -static void -escape_to_html (std::string &out, const std::string &in) -{ - for (const char *cp = in.c_str (); *cp; ++cp) { - if (*cp == '<') { - out += "<"; - } else if (*cp == '>') { - out += ">"; - } else if (*cp == '&') { - out += "&"; - } else if (*cp == '\n') { - out += "
"; - } else { - out += *cp; - } - } -} - void MarkerBrowserPage::enable_updates (bool f) { @@ -1955,13 +1937,13 @@ MarkerBrowserPage::update_info_text () if (category && n_category == 1 && ! category->description ().empty ()) { info += "

"; - escape_to_html (info, category->description ()); + tl::escape_to_html (info, category->description ()); info += "

"; } if (! m_error_text.empty ()) { info += "

"; - escape_to_html (info, m_error_text); + tl::escape_to_html (info, m_error_text); info += "

"; } @@ -1978,7 +1960,7 @@ MarkerBrowserPage::update_info_text () if (v->tag_id () != 0) { const rdb::Tag &tag = mp_database->tags ().tag (v->tag_id ()); info += ""; - escape_to_html (info, tag.name ()); + tl::escape_to_html (info, tag.name ()); info += ":
"; } @@ -1989,7 +1971,7 @@ MarkerBrowserPage::update_info_text () value_string = std::string (value_string.begin (), value_string.begin () + max_length) + "..."; } - escape_to_html (info, value_string); + tl::escape_to_html (info, value_string); info += "
"; diff --git a/src/tl/tl.pro b/src/tl/tl.pro index c59dc2ec9..8018eb796 100644 --- a/src/tl/tl.pro +++ b/src/tl/tl.pro @@ -40,7 +40,9 @@ SOURCES = \ tlVariant.cc \ tlXMLParser.cc \ tlXMLWriter.cc \ - tlFileSystemWatcher.cc + tlFileSystemWatcher.cc \ + tlFileUtils.cc \ + tlWebDAV.cc HEADERS = \ tlAlgorithm.h \ @@ -84,7 +86,9 @@ HEADERS = \ tlFileSystemWatcher.h \ tlCommon.h \ tlMath.h \ - tlCpp.h + tlCpp.h \ + tlFileUtils.h \ + tlWebDAV.h INCLUDEPATH = DEPENDPATH = diff --git a/src/tl/tlFileUtils.cc b/src/tl/tlFileUtils.cc new file mode 100644 index 000000000..9ae545118 --- /dev/null +++ b/src/tl/tlFileUtils.cc @@ -0,0 +1,140 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "tlFileUtils.h" +#include "tlLog.h" +#include "tlInternational.h" + +#include +#include + +namespace tl +{ + +bool +is_parent_path (const QString &parent, const QString &path) +{ + QFileInfo parent_info (parent); + QFileInfo path_info (path); + + while (parent_info != path_info) { + path_info = path_info.path (); + if (path_info.isRoot ()) { + return false; + } + } + + return true; +} + +bool +rm_dir_recursive (const QString &path) +{ + QDir dir (path); + + QStringList entries = dir.entryList (QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + for (QStringList::const_iterator e = entries.begin (); e != entries.end (); ++e) { + QFileInfo fi (dir.absoluteFilePath (*e)); + if (fi.isDir ()) { + if (! rm_dir_recursive (fi.filePath ())) { + return false; + } + } else if (fi.isFile ()) { + if (! dir.remove (*e)) { + tl::error << QObject::tr ("Unable to remove file: %1").arg (dir.absoluteFilePath (*e)); + return false; + } + } + } + + QString name = dir.dirName (); + if (dir.cdUp ()) { + if (! dir.rmdir (name)) { + tl::error << QObject::tr ("Unable to remove directory: %1").arg (dir.absoluteFilePath (name)); + return false; + } + } + + return true; +} + +bool +cp_dir_recursive (const QString &source, const QString &target) +{ + QDir dir (source); + QDir dir_target (target); + + QStringList entries = dir.entryList (QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + for (QStringList::const_iterator e = entries.begin (); e != entries.end (); ++e) { + + QFileInfo fi (dir.absoluteFilePath (*e)); + QFileInfo fi_target (dir_target.absoluteFilePath (*e)); + + if (fi.isDir ()) { + + // Copy subdirectory + if (! fi_target.exists ()) { + if (! dir_target.mkdir (*e)) { + tl::error << QObject::tr ("Unable to create target directory: %1").arg (dir_target.absoluteFilePath (*e)); + return false; + } + } else if (! fi_target.isDir ()) { + tl::error << QObject::tr ("Unable to create target directory (is a file already): %1").arg (dir_target.absoluteFilePath (*e)); + return false; + } + if (! cp_dir_recursive (fi.filePath (), fi_target.filePath ())) { + return false; + } + + // TODO: leave symlinks symlinks? How to copy symlinks with Qt? + } else if (fi.isFile ()) { + + QFile file (fi.filePath ()); + QFile file_target (fi_target.filePath ()); + + if (! file.open (QIODevice::ReadOnly)) { + tl::error << QObject::tr ("Unable to open source file for reading: %1").arg (fi.filePath ()); + return false; + } + if (! file_target.open (QIODevice::WriteOnly)) { + tl::error << QObject::tr ("Unable to open target file for writing: %1").arg (fi_target.filePath ()); + return false; + } + + size_t chunk_size = 64 * 1024; + + while (! file.atEnd ()) { + QByteArray data = file.read (chunk_size); + file_target.write (data); + } + + file.close (); + file_target.close (); + + } + + } + + return true; +} + +} diff --git a/src/tl/tlFileUtils.h b/src/tl/tlFileUtils.h new file mode 100644 index 000000000..be25bd843 --- /dev/null +++ b/src/tl/tlFileUtils.h @@ -0,0 +1,80 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#ifndef HDR_tlFileUtils +#define HDR_tlFileUtils + +#include "tlCommon.h" +#include "tlString.h" +#include + +namespace tl +{ + +/** + * @brief Returns a value indicating whether the parent path is a parent directory of the path + */ +bool TL_PUBLIC is_parent_path (const QString &parent, const QString &path); + +/** + * @brief Returns a value indicating whether the parent path is a parent directory of the path (version with std::string) + */ +inline bool TL_PUBLIC is_parent_path (const std::string &parent, const std::string &path) +{ + return is_parent_path (tl::to_qstring (parent), tl::to_qstring (path)); +} + +/** + * @brief Recursively remove the given directory, the files from that directory and all sub-directories + * @return True, if successful. false otherwise. + */ +bool TL_PUBLIC rm_dir_recursive (const QString &path); + +/** + * @brief Recursively remove the given directory, the files from that directory and all sub-directories (version with std::string) + * @return True, if successful. false otherwise. + */ +inline bool TL_PUBLIC rm_dir_recursive (const std::string &path) +{ + return rm_dir_recursive (tl::to_qstring (path)); +} + +/** + * @brief Recursively copies a given directory to a target directory + * Both target and source directories need to exist. New directories are created in the target + * directory if required. + * @return True, if successful. false otherwise. + */ +bool TL_PUBLIC cp_dir_recursive (const QString &source, const QString &target); + +/** + * @brief Recursively remove the given directory, the files from that directory and all sub-directories (version with std::string) + * @return True, if successful. false otherwise. + */ +inline bool TL_PUBLIC cp_dir_recursive (const std::string &source, const std::string &target) +{ + return cp_dir_recursive (tl::to_qstring (source), tl::to_qstring (target)); +} + +} + +#endif diff --git a/src/tl/tlHttpStream.cc b/src/tl/tlHttpStream.cc index a709586e8..d91bfb905 100644 --- a/src/tl/tlHttpStream.cc +++ b/src/tl/tlHttpStream.cc @@ -78,7 +78,7 @@ public: static QNetworkAccessManager *s_network_manager (0); InputHttpStream::InputHttpStream (const std::string &url) - : m_url (url) + : m_url (url), m_request ("GET"), mp_buffer (0) { if (! s_network_manager) { s_network_manager = new QNetworkAccessManager(0); @@ -87,7 +87,6 @@ InputHttpStream::InputHttpStream (const std::string &url) connect (s_network_manager, SIGNAL (finished (QNetworkReply *)), this, SLOT (finished (QNetworkReply *))); connect (s_network_manager, SIGNAL (authenticationRequired (QNetworkReply *, QAuthenticator *)), this, SLOT (authenticationRequired (QNetworkReply *, QAuthenticator *))); connect (s_network_manager, SIGNAL (proxyAuthenticationRequired (const QNetworkProxy &, QAuthenticator *)), this, SLOT (proxyAuthenticationRequired (const QNetworkProxy &, QAuthenticator *))); - s_network_manager->get (QNetworkRequest (QUrl (tl::to_qstring (url)))); mp_reply = 0; } @@ -97,6 +96,30 @@ InputHttpStream::~InputHttpStream () mp_reply = 0; } +void +InputHttpStream::set_request (const char *r) +{ + m_request = QByteArray (r); +} + +void +InputHttpStream::set_data (const char *data) +{ + m_data = QByteArray (data); +} + +void +InputHttpStream::set_data (const char *data, size_t n) +{ + m_data = QByteArray (data, int (n)); +} + +void +InputHttpStream::add_header (const std::string &name, const std::string &value) +{ + m_headers.insert (std::make_pair (name, value)); +} + void InputHttpStream::authenticationRequired (QNetworkReply *reply, QAuthenticator *auth) { @@ -117,18 +140,39 @@ InputHttpStream::finished (QNetworkReply *reply) QVariant redirect_target = reply->attribute (QNetworkRequest::RedirectionTargetAttribute); if (reply->error () == QNetworkReply::NoError && ! redirect_target.isNull ()) { m_url = tl::to_string (redirect_target.toString ()); - s_network_manager->get (QNetworkRequest (QUrl (redirect_target.toString ()))); + issue_request (QUrl (redirect_target.toString ())); delete reply; } else { mp_reply = reply; } } +void +InputHttpStream::issue_request (const QUrl &url) +{ + delete mp_buffer; + mp_buffer = 0; + + QNetworkRequest request (url); + for (std::map::const_iterator h = m_headers.begin (); h != m_headers.end (); ++h) { + request.setRawHeader (QByteArray (h->first.c_str ()), QByteArray (h->second.c_str ())); + } + if (m_data.isEmpty ()) { + s_network_manager->sendCustomRequest (request, m_request); + } else { + mp_buffer = new QBuffer (&m_data); + s_network_manager->sendCustomRequest (request, m_request, mp_buffer); + } +} + size_t InputHttpStream::read (char *b, size_t n) { + if (mp_reply == 0) { + issue_request (QUrl (tl::to_qstring (m_url))); + } while (mp_reply == 0) { - QCoreApplication::processEvents (QEventLoop::ExcludeUserInputEvents | QEventLoop::WaitForMoreEvents); + QCoreApplication::processEvents (QEventLoop::ExcludeUserInputEvents | QEventLoop::WaitForMoreEvents, 100); } if (mp_reply->error () != QNetworkReply::NoError) { @@ -160,4 +204,3 @@ InputHttpStream::filename () const } } - diff --git a/src/tl/tlHttpStream.h b/src/tl/tlHttpStream.h index 5a2da4d8a..acd3b8635 100644 --- a/src/tl/tlHttpStream.h +++ b/src/tl/tlHttpStream.h @@ -27,6 +27,8 @@ #include "tlStream.h" #include +#include +#include class QNetworkAccessManager; class QNetworkReply; @@ -68,14 +70,38 @@ public: */ virtual ~InputHttpStream (); + /** + * @brief Sets the request verb + * The default verb is "GET" + */ + void set_request (const char *r); + + /** + * @brief Sets data to be sent with the request + * If data is given, it is sent along with the request. + * This version takes a null-terminated string. + */ + void set_data (const char *data); + + /** + * @brief Sets data to be sent with the request + * If data is given, it is sent along with the request. + * This version takes a data plus length. + */ + void set_data (const char *data, size_t n); + + /** + * @brief Sets a header field + */ + void add_header (const std::string &name, const std::string &value); + /** * @brief Read from the stream - * * Implements the basic read method. */ virtual size_t read (char *b, size_t n); - virtual void reset (); + virtual void reset (); virtual std::string source () const { @@ -89,14 +115,20 @@ public: virtual std::string filename () const; -private: - std::string m_url; - QNetworkReply *mp_reply; - private slots: void finished (QNetworkReply *); void authenticationRequired (QNetworkReply *, QAuthenticator *); void proxyAuthenticationRequired (const QNetworkProxy &, QAuthenticator *); + +private: + std::string m_url; + QNetworkReply *mp_reply; + QByteArray m_request; + QByteArray m_data; + QBuffer *mp_buffer; + std::map m_headers; + + void issue_request (const QUrl &url); }; } diff --git a/src/tl/tlString.cc b/src/tl/tlString.cc index 15aa9d5ef..3179df4ac 100644 --- a/src/tl/tlString.cc +++ b/src/tl/tlString.cc @@ -413,6 +413,46 @@ tl::to_word_or_quoted_string (const std::string &s, const char *non_term) } } +void +tl::escape_to_html (std::string &out, const std::string &in, bool replace_newlines) +{ + + + + + + + + + + + + + + + for (const char *cp = in.c_str (); *cp; ++cp) { + if (*cp == '<') { + out += "<"; + } else if (*cp == '>') { + out += ">"; + } else if (*cp == '&') { + out += "&"; + } else if (replace_newlines && *cp == '\n') { + out += "
"; + } else { + out += *cp; + } + } +} + +std::string +tl::escaped_to_html (const std::string &in, bool replace_newlines) +{ + std::string s; + escape_to_html (s, in, replace_newlines); + return s; +} + void tl::from_string (const std::string &s, const char * &result) { diff --git a/src/tl/tlString.h b/src/tl/tlString.h index 5c9aaf630..6840a7fba 100644 --- a/src/tl/tlString.h +++ b/src/tl/tlString.h @@ -326,6 +326,18 @@ TL_PUBLIC int edit_distance (const std::string &a, const std::string &b); */ TL_PUBLIC std::string to_word_or_quoted_string (const std::string &s, const char *non_term = "_.$"); +/** + * @brief Escapes HTML (or XML) characters from in and adds the result to out + * If "replace_newlines" is true, "\n" will be replaced by "
". + */ +TL_PUBLIC void escape_to_html (std::string &out, const std::string &in, bool replace_newlines = true); + +/** + * @brief Escapes HTML (or XML) characters from in and returns the resulting string + * If "replace_newlines" is true, "\n" will be replaced by "
". + */ +TL_PUBLIC std::string escaped_to_html (const std::string &in, bool replace_newlines = true); + /** * @brief Set the number of digits resolution for a micron display */ diff --git a/src/tl/tlWebDAV.cc b/src/tl/tlWebDAV.cc new file mode 100644 index 000000000..04706e78e --- /dev/null +++ b/src/tl/tlWebDAV.cc @@ -0,0 +1,320 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + + +#include "tlWebDAV.h" +#include "tlXMLParser.h" +#include "tlHttpStream.h" +#include "tlStream.h" +#include "tlInternational.h" +#include "tlProgress.h" +#include "tlLog.h" + +#include +#include + +namespace tl +{ + +// --------------------------------------------------------------- +// WebDAVCollection implementation + +WebDAVObject::WebDAVObject () +{ + // .. nothing yet .. +} + +namespace +{ + +/** + * @brief A dummy "DOM" for the WebDAV reply + */ +struct ResourceType +{ + ResourceType () : is_collection (false) { } + + const std::string &collection () const + { + static std::string empty; + return empty; + } + + void set_collection (const std::string &) + { + is_collection = true; + } + + bool is_collection; +}; + +/** + * @brief A dummy "DOM" for the WebDAV reply + */ +struct Prop +{ + ResourceType resourcetype; +}; + +/** + * @brief A dummy "DOM" for the WebDAV reply + */ +struct PropStat +{ + std::string status; + Prop prop; +}; + +/** + * @brief A dummy "DOM" for the WebDAV reply + */ +struct Response +{ + std::string href; + PropStat propstat; +}; + +/** + * @brief A dummy "DOM" for the WebDAV reply + */ +struct MultiStatus +{ + typedef std::list container; + typedef container::const_iterator iterator; + + iterator begin () const { return responses.begin (); } + iterator end () const { return responses.end (); } + void add (const Response &r) { responses.push_back (r); } + + container responses; +}; + +} + +tl::XMLStruct xml_struct ("multistatus", + tl::make_element (&MultiStatus::begin, &MultiStatus::end, &MultiStatus::add, "response", + tl::make_member (&Response::href, "href") + + tl::make_element (&Response::propstat, "propstat", + tl::make_member (&PropStat::status, "status") + + tl::make_element (&PropStat::prop, "prop", + tl::make_element (&Prop::resourcetype, "resourcetype", + tl::make_member (&ResourceType::collection, &ResourceType::set_collection, "collection") + ) + ) + ) + ) +); + +static std::string item_name (const QString &path1, const QString &path2) +{ + QStringList sl1 = path1.split (QChar ('/')); + if (! sl1.empty () && sl1.back ().isEmpty ()) { + sl1.pop_back (); + } + + QStringList sl2 = path2.split (QChar ('/')); + if (! sl2.empty () && sl2.back ().isEmpty ()) { + sl2.pop_back (); + } + + int i = 0; + for ( ; i < sl1.length () && i < sl2.length (); ++i) { + if (sl1 [i] != sl2 [i]) { + throw tl::Exception (tl::to_string (QObject::tr ("Invalid WebDAV response: %1 is not a collection item of %2").arg (path2).arg (path1))); + } + } + if (i == sl2.length ()) { + return std::string (); + } else if (i + 1 == sl2.length ()) { + return tl::to_string (sl2[i]); + } else { + throw tl::Exception (tl::to_string (QObject::tr ("Invalid WebDAV response: %1 is not a collection sub-item of %2").arg (path2).arg (path1))); + } +} + +void +WebDAVObject::read (const std::string &url, int depth) +{ + QUrl base_url = QUrl (tl::to_qstring (url)); + + tl::InputHttpStream http (url); + http.add_header ("User-Agent", "SVN"); + http.add_header ("Depth", tl::to_string (depth)); + http.set_request ("PROPFIND"); + http.set_data (""); + + MultiStatus multistatus; + tl::InputStream stream (http); + tl::XMLStreamSource source (stream); + xml_struct.parse (source, multistatus); + + // TODO: check status .. + + m_items.clear (); + for (MultiStatus::iterator r = multistatus.begin (); r != multistatus.end (); ++r) { + + bool is_collection = r->propstat.prop.resourcetype.is_collection; + QUrl item_url = base_url.resolved (QUrl (tl::to_qstring (r->href))); + + std::string n = item_name (base_url.path (), item_url.path ()); + std::string item_url_string = tl::to_string (item_url.toString ()); + + if (! n.empty ()) { + m_items.push_back (WebDAVItem (is_collection, item_url_string, n)); + } else { + m_is_collection = is_collection; + m_url = item_url_string; + } + + } +} + +namespace +{ + +struct DownloadItem +{ + DownloadItem (const std::string &u, const std::string &p) + { + url = u; + path = p; + } + + std::string url; + std::string path; +}; + +} + +static +void fetch_download_items (const std::string &url, const std::string &target, std::list &items, tl::AbsoluteProgress &progress) +{ + ++progress; + + WebDAVObject object; + object.read (url, 1); + + if (object.is_collection ()) { + + QDir dir (tl::to_qstring (target)); + if (! dir.exists ()) { + throw tl::Exception (tl::to_string (QObject::tr ("Download failed: target directory '%1' does not exists").arg (dir.path ()))); + } + + for (WebDAVObject::iterator i = object.begin (); i != object.end (); ++i) { + + QFileInfo new_item (dir.absoluteFilePath (tl::to_qstring (i->name ()))); + + if (i->is_collection ()) { + + if (! new_item.exists ()) { + if (! dir.mkdir (tl::to_qstring (i->name ()))) { + throw tl::Exception (tl::to_string (QObject::tr ("Download failed: unable to create subdirectory '%2' in '%1'").arg (dir.path ()).arg (tl::to_qstring (i->name ())))); + } + } else if (! new_item.isDir ()) { + throw tl::Exception (tl::to_string (QObject::tr ("Download failed: unable to create subdirectory '%2' in '%1' - is already a file").arg (dir.path ()).arg (tl::to_qstring (i->name ())))); + } else if (! new_item.isWritable ()) { + throw tl::Exception (tl::to_string (QObject::tr ("Download failed: unable to create subdirectory '%2' in '%1' - no write permissions").arg (dir.path ()).arg (tl::to_qstring (i->name ())))); + } + + fetch_download_items (i->url (), tl::to_string (new_item.filePath ()), items, progress); + + } else { + + if (new_item.exists () && ! new_item.isWritable ()) { + throw tl::Exception (tl::to_string (QObject::tr ("Download failed: file is '%2' in '%1' - already exists, but no write permissions").arg (dir.path ()).arg (tl::to_qstring (i->name ())))); + } + + items.push_back (DownloadItem (i->url (), tl::to_string (dir.absoluteFilePath (tl::to_qstring (i->name ()))))); + + } + } + + } else { + items.push_back (DownloadItem (url, target)); + } +} + +bool +WebDAVObject::download (const std::string &url, const std::string &target) +{ + std::list items; + + try { + + tl::info << QObject::tr ("Fetching file structure from ") << url; + tl::AbsoluteProgress progress (tl::to_string (QObject::tr ("Fetching directory structure from %1").arg (tl::to_qstring (url)))); + fetch_download_items (url, target, items, progress); + + } catch (tl::Exception &ex) { + tl::error << QObject::tr ("Error downloading file structure from '") << url << "':" << tl::endl << ex.msg (); + return false; + } + + bool has_errors = false; + + { + tl::info << tl::to_string (QObject::tr ("Downloading %1 file(s) now ..").arg (items.size ())); + + tl::RelativeProgress progress (tl::to_string (QObject::tr ("Downloading file(s) from %1").arg (tl::to_qstring (url))), items.size (), 1); + + for (std::list::const_iterator i = items.begin (); i != items.end (); ++i) { + + tl::info << QObject::tr ("Downloading '%1' to '%2' ..").arg (tl::to_qstring (i->url)).arg (tl::to_qstring (i->path)); + + try { + + tl::InputHttpStream http (i->url); + + QFile file (tl::to_qstring (i->path)); + if (! file.open (QIODevice::WriteOnly)) { + has_errors = true; + tl::error << QObject::tr ("Unable to open file '%1' for writing").arg (tl::to_qstring (i->path)); + } + + const size_t chunk = 65536; + char b[chunk]; + size_t read; + while ((read = http.read (b, sizeof (b))) > 0) { + if (! file.write (b, read)) { + tl::error << QObject::tr ("Unable to write %2 bytes file '%1'").arg (tl::to_qstring (i->path)).arg (int (read)); + has_errors = true; + break; + } + } + + file.close (); + + } catch (tl::Exception &ex) { + tl::error << QObject::tr ("Error downloading file from '") << i->url << "':" << tl::endl << ex.msg (); + has_errors = true; + } + + ++progress; + + } + } + + return ! has_errors; +} + +} diff --git a/src/tl/tlWebDAV.h b/src/tl/tlWebDAV.h new file mode 100644 index 000000000..9e82ffb8e --- /dev/null +++ b/src/tl/tlWebDAV.h @@ -0,0 +1,152 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + + +#ifndef HDR_tlWebDAV +#define HDR_tlWebDAV + +#include "tlCommon.h" +#include +#include + +namespace tl +{ + +/** + * @brief Represents an item in a WebDAV collection + */ +class TL_PUBLIC WebDAVItem +{ +public: + /** + * @brief Default constructor + */ + WebDAVItem () + : m_is_collection (false) + { + // .. nothing yet .. + } + + /** + * @brief Constructor + */ + WebDAVItem (bool is_collection, const std::string &url, const std::string &name) + : m_is_collection (is_collection), m_url (url), m_name (name) + { + // .. nothing yet .. + } + + /** + * @brief Gets a value indicating whether this item is a collection + * If false, it's a file. + */ + bool is_collection () const + { + return m_is_collection; + } + + /** + * @brief Gets the URL of this item + */ + const std::string &url () const + { + return m_url; + } + + /** + * @brief Gets the name of this item + * The name is only valid for sub-items. + */ + const std::string &name () const + { + return m_name; + } + +protected: + bool m_is_collection; + std::string m_url; + std::string m_name; +}; + +/** + * @brief Represents an object from a WebDAV URL + * This object can be a file or collection + */ +class TL_PUBLIC WebDAVObject + : public WebDAVItem +{ +public: + typedef std::vector container; + typedef container::const_iterator iterator; + + /** + * @brief Open a stream with the given URL + */ + WebDAVObject (); + + /** + * @brief Populates the collection from the given URL + * The depth value can be 0 (self only) or 1 (self + collection members). + */ + void read (const std::string &url, int depth); + + /** + * @brief Gets the items of this collection (begin iterator) + */ + iterator begin () const + { + return m_items.begin (); + } + + /** + * @brief Gets the items of this collection (begin iterator) + */ + iterator end () const + { + return m_items.end (); + } + + /** + * @brief Downloads the collection or file with the given URL + * + * This method will download the WebDAV object from url to the file path + * given in "target". + * + * For file download, the target must be the path of the target file. + * For collection download, the target must be a directory path. In this + * case, the target directory must exist already. + * + * Sub-directories are created if required. + * + * This method throws an exception if the directory structure could + * not be obtained or downloading of one file failed. + */ + static bool download (const std::string &url, const std::string &target); + +private: + container m_items; +}; + +} + +#endif + diff --git a/src/tl/tlXMLParser.h b/src/tl/tlXMLParser.h index 2760df1f0..6a1ae3015 100644 --- a/src/tl/tlXMLParser.h +++ b/src/tl/tlXMLParser.h @@ -671,12 +671,12 @@ public: return m_name; } - bool check_name (const std::string &, const std::string &, const std::string &qname) const + bool check_name (const std::string & /*uri*/, const std::string &lname, const std::string & /*qname*/) const { if (m_name == "*") { return true; } else { - return m_name == qname; // no namespace currently + return m_name == lname; // no namespace currently } } diff --git a/src/unit_tests/laySalt.cc b/src/unit_tests/laySalt.cc new file mode 100644 index 000000000..8edff9a14 --- /dev/null +++ b/src/unit_tests/laySalt.cc @@ -0,0 +1,359 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + + +#include "laySaltGrain.h" +#include "laySaltGrains.h" +#include "laySalt.h" +#include "tlFileUtils.h" +#include "utHead.h" + +#include +#include + +static std::string grains_to_string (const lay::SaltGrains &gg) +{ + std::string res; + res += "["; + bool first = true; + for (lay::SaltGrains::grain_iterator g = gg.begin_grains (); g != gg.end_grains (); ++g) { + if (! first) { + res += ","; + } + first = false; + res += g->name (); + } + for (lay::SaltGrains::collection_iterator gc = gg.begin_collections (); gc != gg.end_collections (); ++gc) { + if (! first) { + res += ","; + } + first = false; + res += gc->name (); + res += grains_to_string (*gc); + } + res += "]"; + return res; +} + +static std::string salt_to_string (lay::Salt &salt) +{ + std::string res; + res += "["; + bool first = true; + for (lay::Salt::flat_iterator i = salt.begin_flat (); i != salt.end_flat (); ++i) { + if (! first) { + res += ","; + } + first = false; + res += (*i)->name (); + } + res += "]"; + return res; +} + +TEST (1) +{ + std::string tmp0 = tmp_file ("tmp0"); + + lay::SaltGrain g; + g.save (tmp0); + EXPECT_EQ (g.authored_time ().isNull (), true); + EXPECT_EQ (g.installed_time ().isNull (), true); + + lay::SaltGrain g0; + g0.load (tmp0); + EXPECT_EQ (g0.authored_time ().isNull (), true); + EXPECT_EQ (g0.installed_time ().isNull (), true); + EXPECT_EQ (g == g0, true); + + std::string tmp = tmp_file (); + + g.set_name ("abc"); + EXPECT_EQ (g.name (), "abc"); + g.set_url ("xyz"); + EXPECT_EQ (g.url (), "xyz"); + g.set_version ("1.0"); + EXPECT_EQ (g.version (), "1.0"); + g.set_path ("a/b"); + EXPECT_EQ (g.path (), "a/b"); + g.set_title ("title"); + EXPECT_EQ (g.title (), "title"); + g.set_doc ("doc"); + EXPECT_EQ (g.doc (), "doc"); + g.set_doc_url ("doc-url"); + EXPECT_EQ (g.doc_url (), "doc-url"); + g.set_author ("me"); + EXPECT_EQ (g.author (), "me"); + g.set_author_contact ("ac"); + EXPECT_EQ (g.author_contact (), "ac"); + g.set_license ("free"); + EXPECT_EQ (g.license (), "free"); + g.set_authored_time (QDateTime ()); + EXPECT_EQ (g.authored_time ().isNull (), true); + g.set_authored_time (QDateTime::fromMSecsSinceEpoch (1000000000)); + EXPECT_EQ (QDateTime::fromMSecsSinceEpoch (0).msecsTo (g.authored_time ()), 1000000000); + g.set_installed_time (QDateTime ()); + EXPECT_EQ (g.installed_time ().isNull (), true); + g.set_installed_time (QDateTime::fromMSecsSinceEpoch (2000000000)); + EXPECT_EQ (QDateTime::fromMSecsSinceEpoch (0).msecsTo (g.installed_time ()), 2000000000); + + g.add_dependency (lay::SaltGrain::Dependency ()); + g.dependencies ().back ().name = "depname"; + g.dependencies ().back ().url = "depurl"; + g.dependencies ().back ().version = "0.0"; + EXPECT_EQ (int (g.dependencies ().size ()), 1); + + lay::SaltGrain gg; + EXPECT_EQ (g == gg, false); + EXPECT_EQ (g == g, true); + EXPECT_EQ (g != gg, true); + EXPECT_EQ (g != g, false); + + gg = g; + EXPECT_EQ (g == gg, true); + + gg.set_doc ("blabla"); + EXPECT_EQ (g == gg, false); + + EXPECT_EQ (g == gg, false); + g.save (tmp); + + EXPECT_EQ (g == gg, false); + + gg = lay::SaltGrain (); + gg.load (tmp); + gg.set_path (g.path ()); // path is not set by load(file) + EXPECT_EQ (int (gg.dependencies ().size ()), 1); + EXPECT_EQ (g == gg, true); + + gg.add_dependency (lay::SaltGrain::Dependency ()); + EXPECT_EQ (g == gg, false); + gg.set_path (tl::to_string (QFileInfo (tl::to_qstring (tmp)).absolutePath ())); + gg.save (); + + g = lay::SaltGrain::from_path (gg.path ()); + EXPECT_EQ (g == gg, true); +} + +TEST (2) +{ + EXPECT_EQ (lay::SaltGrain::valid_version (""), true); + EXPECT_EQ (lay::SaltGrain::valid_version ("1"), true); + EXPECT_EQ (lay::SaltGrain::valid_version ("1.2"), true); + EXPECT_EQ (lay::SaltGrain::valid_version ("\t1 . 2.\n3"), true); + EXPECT_EQ (lay::SaltGrain::valid_version ("x"), false); + EXPECT_EQ (lay::SaltGrain::valid_version ("1.2x"), false); + EXPECT_EQ (lay::SaltGrain::valid_name (""), false); + EXPECT_EQ (lay::SaltGrain::valid_name ("x"), true); + EXPECT_EQ (lay::SaltGrain::valid_name ("x1"), true); + EXPECT_EQ (lay::SaltGrain::valid_name ("x1 "), false); + EXPECT_EQ (lay::SaltGrain::valid_name ("x$1"), false); + EXPECT_EQ (lay::SaltGrain::valid_name ("x/y"), true); + EXPECT_EQ (lay::SaltGrain::valid_name ("x_y"), true); + EXPECT_EQ (lay::SaltGrain::compare_versions ("", ""), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1", "2"), -1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1", ""), 1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1", "1"), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("2", "1"), 1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.0", "2.0"), -1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.0", "1.0"), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.1", "1.0"), 1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.0.1", "1.0.0"), 1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.0.1", "1.0"), 1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.0.1", "1"), 1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.0.0", "1"), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1a", "1"), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.a.1", "1.0.1"), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.1a", "1.1"), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.1a", "1.0"), 1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.1a.1", "1.0"), 1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("1.1a.1", "1.1.1"), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("990", "991"), -1); + EXPECT_EQ (lay::SaltGrain::compare_versions ("990", "990"), 0); + EXPECT_EQ (lay::SaltGrain::compare_versions ("991", "990"), 1); +} + +TEST (3) +{ + const QString grain_spec_file = QString::fromUtf8 ("grain.xml"); + + lay::SaltGrain g; + g.set_name ("x"); + + QDir tmp_dir (QFileInfo (tl::to_qstring (tmp_file ())).absolutePath ()); + QDir dir_a (tmp_dir.filePath (QString::fromUtf8 ("a"))); + QDir dir_b (tmp_dir.filePath (QString::fromUtf8 ("b"))); + QDir dir_c (tmp_dir.filePath (QString::fromUtf8 ("c"))); + QDir dir_cu (dir_c.filePath (QString::fromUtf8 ("u"))); + QDir dir_cc (dir_c.filePath (QString::fromUtf8 ("c"))); + QDir dir_ccv (dir_cc.filePath (QString::fromUtf8 ("v"))); + + lay::SaltGrains gg; + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (gg.is_empty (), true); + EXPECT_EQ (grains_to_string (gg), "[]"); + + tmp_dir.mkdir (dir_a.dirName ()); + tmp_dir.mkdir (dir_b.dirName ()); + tmp_dir.mkdir (dir_c.dirName ()); + dir_c.mkdir (dir_cu.dirName ()); + dir_c.mkdir (dir_cc.dirName ()); + dir_cc.mkdir (dir_ccv.dirName ()); + + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (gg.is_empty (), true); + EXPECT_EQ (grains_to_string (gg), "[]"); + EXPECT_EQ (gg.path (), tl::to_string (tmp_dir.path ())); + + g.save (tl::to_string (dir_a.absoluteFilePath (grain_spec_file))); + + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (gg.is_empty (), false); + EXPECT_EQ (grains_to_string (gg), "[a]"); + EXPECT_EQ (gg.begin_grains ()->path (), tl::to_string (dir_a.absolutePath ())); + + g.save (tl::to_string (dir_b.absoluteFilePath (grain_spec_file))); + g.save (tl::to_string (dir_cu.absoluteFilePath (grain_spec_file))); + g.save (tl::to_string (dir_ccv.absoluteFilePath (grain_spec_file))); + + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (gg.is_empty (), false); + EXPECT_EQ (grains_to_string (gg), "[a,b,c[c/u,c/c[c/c/v]]]"); + EXPECT_EQ (gg.begin_collections ()->path (), tl::to_string (dir_c.absolutePath ())); + + std::string gg_path = tmp_file ("gg.tmp"); + gg.save (gg_path); + + lay::SaltGrains ggg; + ggg.load (gg_path); + EXPECT_EQ (grains_to_string (ggg), "[a,b,c[c/u,c/c[c/c/v]]]"); + // NOTE: The path is not set, so this will fail: + // EXPECT_EQ (gg == ggg, true); + + gg.remove_grain (gg.begin_grains (), false); + EXPECT_EQ (grains_to_string (gg), "[b,c[c/u,c/c[c/c/v]]]"); + + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (grains_to_string (gg), "[a,b,c[c/u,c/c[c/c/v]]]"); + gg.remove_grain (gg.begin_grains (), true); + + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (grains_to_string (gg), "[b,c[c/u,c/c[c/c/v]]]"); + + gg.remove_collection (gg.begin_collections (), false); + EXPECT_EQ (grains_to_string (gg), "[b]"); + + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (grains_to_string (gg), "[b,c[c/u,c/c[c/c/v]]]"); + + gg.remove_collection (gg.begin_collections (), true); + EXPECT_EQ (grains_to_string (gg), "[b]"); + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (grains_to_string (gg), "[b]"); +} + +TEST (4) +{ + const QString grain_spec_file = QString::fromUtf8 ("grain.xml"); + + // That's just preparation ... + + lay::SaltGrain g; + g.set_name ("x"); + + QDir tmp_dir (QFileInfo (tl::to_qstring (tmp_file ())).absolutePath ()); + QDir dir_a (tmp_dir.filePath (QString::fromUtf8 ("a"))); + QDir dir_b (tmp_dir.filePath (QString::fromUtf8 ("b"))); + QDir dir_c (tmp_dir.filePath (QString::fromUtf8 ("c"))); + QDir dir_cu (dir_c.filePath (QString::fromUtf8 ("u"))); + QDir dir_cc (dir_c.filePath (QString::fromUtf8 ("c"))); + QDir dir_ccv (dir_cc.filePath (QString::fromUtf8 ("v"))); + + lay::SaltGrains gg; + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (gg.is_empty (), true); + EXPECT_EQ (grains_to_string (gg), "[]"); + + tmp_dir.mkdir (dir_a.dirName ()); + tmp_dir.mkdir (dir_b.dirName ()); + tmp_dir.mkdir (dir_c.dirName ()); + dir_c.mkdir (dir_cu.dirName ()); + dir_c.mkdir (dir_cc.dirName ()); + dir_cc.mkdir (dir_ccv.dirName ()); + + gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (gg.is_empty (), true); + EXPECT_EQ (grains_to_string (gg), "[]"); + EXPECT_EQ (gg.path (), tl::to_string (tmp_dir.path ())); + + g.save (tl::to_string (dir_a.absoluteFilePath (grain_spec_file))); + g.save (tl::to_string (dir_b.absoluteFilePath (grain_spec_file))); + g.save (tl::to_string (dir_cu.absoluteFilePath (grain_spec_file))); + g.save (tl::to_string (dir_ccv.absoluteFilePath (grain_spec_file))); + + // That's the main test part + + lay::Salt salt; + EXPECT_EQ (salt.is_empty (), true); + + QSignalSpy spy (&salt, SIGNAL (collections_changed ())); + EXPECT_EQ (salt_to_string (salt), "[]"); + + spy.clear (); + salt.add_location (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (salt.is_empty (), false); + EXPECT_EQ (spy.count (), 1); + EXPECT_EQ (salt_to_string (salt), "[a,b,c/c/v,c/u]"); + + spy.clear (); + salt.add_location (tl::to_string (tmp_dir.path ())); + EXPECT_EQ (spy.count (), 0); + EXPECT_EQ (salt_to_string (salt), "[a,b,c/c/v,c/u]"); + + spy.clear (); + salt.add_location (tl::to_string (dir_c.path ())); + EXPECT_EQ (spy.count (), 1); + EXPECT_EQ (salt_to_string (salt), "[a,b,c/c/v,c/u,c/v,u]"); + + lay::Salt salt_copy = salt; + (const_cast (*salt_copy.begin ())).remove_grain (salt_copy.begin ()->begin_grains (), true); + + spy.clear (); + salt.refresh (); + EXPECT_EQ (spy.count (), 1); + EXPECT_EQ (salt_to_string (salt), "[b,c/c/v,c/u,c/v,u]"); + + spy.clear (); + salt.remove_location (tl::to_string (dir_c.path ())); + EXPECT_EQ (spy.count (), 1); + EXPECT_EQ (salt_to_string (salt), "[b,c/c/v,c/u]"); + + spy.clear (); + // location already removed + salt.remove_location (tl::to_string (dir_c.path ())); + EXPECT_EQ (spy.count (), 0); + EXPECT_EQ (salt_to_string (salt), "[b,c/c/v,c/u]"); + + EXPECT_EQ (salt.grain_by_name ("x"), 0); + EXPECT_EQ (salt.grain_by_name ("b")->name (), "b"); + EXPECT_EQ (salt.grain_by_name ("c/c/v")->name (), "c/c/v"); +} diff --git a/src/unit_tests/tlFileUtils.cc b/src/unit_tests/tlFileUtils.cc new file mode 100644 index 000000000..7eb3032a8 --- /dev/null +++ b/src/unit_tests/tlFileUtils.cc @@ -0,0 +1,155 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + + +#include "tlFileUtils.h" +#include "utHead.h" + +#include +#include +#include + +TEST (1) +{ + EXPECT_EQ (tl::is_parent_path (std::string ("/home"), "/home/matthias"), true); + EXPECT_EQ (tl::is_parent_path (std::string ("/home"), "/home"), true); + EXPECT_EQ (tl::is_parent_path (std::string (""), ""), true); + EXPECT_EQ (tl::is_parent_path (std::string ("/opt/klayout"), "/home/matthias"), false); + EXPECT_EQ (tl::is_parent_path (std::string ("/home/klayout"), "/home/matthias"), false); +} + +TEST (2) +{ + QDir tmp_dir = QFileInfo (tl::to_qstring (tmp_file ())).absoluteDir (); + tmp_dir.mkdir (QString::fromUtf8 ("a")); + + QDir adir = tmp_dir; + adir.cd (QString::fromUtf8 ("a")); + + EXPECT_EQ (adir.exists (), true); + EXPECT_EQ (tl::rm_dir_recursive (adir.absolutePath ()), true); + EXPECT_EQ (adir.exists (), false); + + tmp_dir.mkdir (QString::fromUtf8 ("a")); + EXPECT_EQ (adir.exists (), true); + + EXPECT_EQ (tl::rm_dir_recursive (tl::to_string (adir.absolutePath ())), true); + EXPECT_EQ (adir.exists (), false); + + tmp_dir.mkdir (QString::fromUtf8 ("a")); + EXPECT_EQ (adir.exists (), true); + + adir.mkdir (QString::fromUtf8 ("b1")); + QDir b1dir = adir; + b1dir.cd (QString::fromUtf8 ("b1")); + + adir.mkdir (QString::fromUtf8 ("b2")); + QDir b2dir = adir; + b2dir.cd (QString::fromUtf8 ("b2")); + + { + QFile file (b2dir.absoluteFilePath (QString::fromUtf8 ("x"))); + file.open (QIODevice::WriteOnly); + file.write ("hello, world!\n"); + file.close (); + } + + { + QFile file (b2dir.absoluteFilePath (QString::fromUtf8 ("y"))); + file.open (QIODevice::WriteOnly); + file.write ("hello, world!\n"); + file.close (); + } + + EXPECT_EQ (adir.exists (), true); + EXPECT_EQ (tl::rm_dir_recursive (adir.absolutePath ()), true); + EXPECT_EQ (adir.exists (), false); + EXPECT_EQ (b1dir.exists (), false); + EXPECT_EQ (b2dir.exists (), false); + EXPECT_EQ (QFileInfo (b2dir.absoluteFilePath (QString::fromUtf8 ("x"))).exists (), false); +} + +TEST (3) +{ + QDir tmp_dir = QFileInfo (tl::to_qstring (tmp_file ())).absoluteDir (); + + tl::rm_dir_recursive (tmp_dir.filePath (QString::fromUtf8 ("a"))); + tmp_dir.mkdir (QString::fromUtf8 ("a")); + + QDir adir = tmp_dir; + adir.cd (QString::fromUtf8 ("a")); + + adir.mkdir (QString::fromUtf8 ("b1")); + QDir b1dir = adir; + b1dir.cd (QString::fromUtf8 ("b1")); + + adir.mkdir (QString::fromUtf8 ("b2")); + QDir b2dir = adir; + b2dir.cd (QString::fromUtf8 ("b2")); + + { + QFile file (b2dir.absoluteFilePath (QString::fromUtf8 ("x"))); + file.open (QIODevice::WriteOnly); + file.write ("hello, world!\n"); + file.close (); + } + + { + QFile file (b2dir.absoluteFilePath (QString::fromUtf8 ("y"))); + file.open (QIODevice::WriteOnly); + file.write ("hello, world II!\n"); + file.close (); + } + + tl::rm_dir_recursive (tmp_dir.filePath (QString::fromUtf8 ("acopy"))); + tmp_dir.mkdir (QString::fromUtf8 ("acopy")); + + tl::cp_dir_recursive (tmp_dir.absoluteFilePath (QString::fromUtf8 ("a")), tmp_dir.absoluteFilePath (QString::fromUtf8 ("acopy"))); + + QDir acopydir = tmp_dir; + EXPECT_EQ (acopydir.cd (QString::fromUtf8 ("acopy")), true); + EXPECT_EQ (acopydir.exists (), true); + + QDir b1copydir = acopydir; + EXPECT_EQ (b1copydir.cd (QString::fromUtf8 ("b1")), true); + EXPECT_EQ (b1copydir.exists (), true); + + QDir b2copydir = acopydir; + EXPECT_EQ (b2copydir.cd (QString::fromUtf8 ("b2")), true); + EXPECT_EQ (b2copydir.exists (), true); + + { + QFile file (b2copydir.absoluteFilePath (QString::fromUtf8 ("x"))); + EXPECT_EQ (file.exists (), true); + file.open (QIODevice::ReadOnly); + EXPECT_EQ (file.readAll ().constData (), "hello, world!\n"); + file.close (); + } + + { + QFile file (b2copydir.absoluteFilePath (QString::fromUtf8 ("y"))); + EXPECT_EQ (file.exists (), true); + file.open (QIODevice::ReadOnly); + EXPECT_EQ (file.readAll ().constData (), "hello, world II!\n"); + file.close (); + } +} diff --git a/src/unit_tests/tlHttpStream.cc b/src/unit_tests/tlHttpStream.cc new file mode 100644 index 000000000..2c493de12 --- /dev/null +++ b/src/unit_tests/tlHttpStream.cc @@ -0,0 +1,75 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + + +#include "tlHttpStream.h" +#include "utHead.h" + +static std::string test_url1 ("http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/text"); +static std::string test_url2 ("http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/dir1"); + +TEST(1) +{ + tl::InputHttpStream stream (test_url1); + + char b[100]; + size_t n = stream.read (b, sizeof (b)); + std::string res (b, n); + EXPECT_EQ (res, "hello, world.\n"); +} + +TEST(2) +{ + tl::InputHttpStream stream (test_url2); + stream.add_header ("User-Agent", "SVN"); + stream.add_header ("Depth", "1"); + stream.set_request ("PROPFIND"); + stream.set_data (""); + + char b[10000]; + size_t n = stream.read (b, sizeof (b)); + std::string res (b, n); + + EXPECT_EQ (res, + "\n" + "\n" + "\n" + "/svn-public/klayout-resources/trunk/testdata/dir1/\n" + "\n" + "\n" + "\n" + "\n" + "HTTP/1.1 200 OK\n" + "\n" + "\n" + "\n" + "/svn-public/klayout-resources/trunk/testdata/dir1/text\n" + "\n" + "\n" + "\n" + "\n" + "HTTP/1.1 200 OK\n" + "\n" + "\n" + "\n" + ); +} diff --git a/src/unit_tests/tlString.cc b/src/unit_tests/tlString.cc index 9c6c3fd19..6b0ad38ac 100644 --- a/src/unit_tests/tlString.cc +++ b/src/unit_tests/tlString.cc @@ -423,3 +423,20 @@ TEST(10) EXPECT_EQ (unescape_string (escape_string ("'a\n\003")), "'a\n\003"); } +TEST(11) +{ + std::string s; + tl::escape_to_html (s, "x"); + EXPECT_EQ (s, "x"); + tl::escape_to_html (s, "<&>"); + EXPECT_EQ (s, "x<&>"); + s = std::string (); + tl::escape_to_html (s, "a\nb"); + EXPECT_EQ (s, "a
b"); + s = std::string (); + tl::escape_to_html (s, "a\nb", false); + EXPECT_EQ (s, "a\nb"); + EXPECT_EQ (tl::escaped_to_html ("x<&>"), "x<&>"); + EXPECT_EQ (tl::escaped_to_html ("a\nb"), "a
b"); + EXPECT_EQ (tl::escaped_to_html ("a\nb", false), "a\nb"); +} diff --git a/src/unit_tests/tlWebDAV.cc b/src/unit_tests/tlWebDAV.cc new file mode 100644 index 000000000..bba9764f8 --- /dev/null +++ b/src/unit_tests/tlWebDAV.cc @@ -0,0 +1,129 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2017 Matthias Koefferlein + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + + +#include "tlWebDAV.h" +#include "utHead.h" + +#include + +static std::string test_url1 ("http://www.klayout.org/svn-public/klayout-resources/trunk/testdata"); +static std::string test_url2 ("http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/text"); + +static std::string collection2string (const tl::WebDAVObject &coll) +{ + std::string s; + for (tl::WebDAVObject::iterator c = coll.begin (); c != coll.end (); ++c) { + if (!s.empty ()) { + s += "\n"; + } + if (c->is_collection ()) { + s += "[dir] "; + } + s += c->name (); + s += " "; + s += c->url (); + } + return s; +} + +TEST(1) +{ + tl::WebDAVObject collection; + collection.read (test_url1, 1); + + EXPECT_EQ (collection.is_collection (), true); + EXPECT_EQ (collection.url (), "http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/"); + + EXPECT_EQ (collection2string (collection), + "[dir] dir1 http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/dir1/\n" + "[dir] dir2 http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/dir2/\n" + "text http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/text\n" + "text2 http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/text2" + ); +} + +TEST(2) +{ + tl::WebDAVObject collection; + collection.read (test_url1, 0); + + EXPECT_EQ (collection.is_collection (), true); + EXPECT_EQ (collection.url (), "http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/"); + EXPECT_EQ (collection2string (collection), ""); +} + +TEST(3) +{ + tl::WebDAVObject collection; + collection.read (test_url2, 1); + + EXPECT_EQ (collection.is_collection (), false); + EXPECT_EQ (collection.url (), "http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/text"); + EXPECT_EQ (collection2string (collection), ""); +} + +TEST(4) +{ + tl::WebDAVObject collection; + collection.read (test_url2, 0); + + EXPECT_EQ (collection.is_collection (), false); + EXPECT_EQ (collection.url (), "http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/text"); + EXPECT_EQ (collection2string (collection), ""); +} + +TEST(5) +{ + tl::WebDAVObject collection; + + QDir tmp_dir (tl::to_qstring (tmp_file ("tmp"))); + EXPECT_EQ (tmp_dir.exists (), false); + + tmp_dir.cdUp (); + tmp_dir.mkdir (tl::to_qstring ("tmp")); + tmp_dir.cd (tl::to_qstring ("tmp")); + + bool res = collection.download (test_url1, tl::to_string (tmp_dir.absolutePath ())); + EXPECT_EQ (res, true); + + QDir dir1 (tmp_dir.absoluteFilePath (QString::fromUtf8 ("dir1"))); + QDir dir2 (tmp_dir.absoluteFilePath (QString::fromUtf8 ("dir2"))); + QDir dir21 (dir2.absoluteFilePath (QString::fromUtf8 ("dir21"))); + EXPECT_EQ (dir1.exists (), true); + EXPECT_EQ (dir2.exists (), true); + EXPECT_EQ (dir21.exists (), true); + + QByteArray ba; + + QFile text1 (dir1.absoluteFilePath (QString::fromUtf8 ("text"))); + text1.open (QIODevice::ReadOnly); + ba = text1.read (10000); + EXPECT_EQ (ba.constData (), "A text.\n"); + text1.close (); + + QFile text21 (dir21.absoluteFilePath (QString::fromUtf8 ("text"))); + text21.open (QIODevice::ReadOnly); + ba = text21.read (10000); + EXPECT_EQ (ba.constData (), "A text II.I.\n"); + text21.close (); +} diff --git a/src/unit_tests/unit_tests.pro b/src/unit_tests/unit_tests.pro index ffe60f38f..7693c5677 100644 --- a/src/unit_tests/unit_tests.pro +++ b/src/unit_tests/unit_tests.pro @@ -96,7 +96,11 @@ SOURCES = \ tlXMLParser.cc \ gsiTest.cc \ tlFileSystemWatcher.cc \ - tlMath.cc + tlMath.cc \ + laySalt.cc \ + tlFileUtils.cc \ + tlHttpStream.cc \ + tlWebDAV.cc # main components: SOURCES += \ diff --git a/src/ut/utMain.cc b/src/ut/utMain.cc index 310d6a0ab..b5ca6d446 100644 --- a/src/ut/utMain.cc +++ b/src/ut/utMain.cc @@ -27,6 +27,7 @@ #include "tlStaticObjects.h" #include "tlTimer.h" #include "tlSystemPaths.h" +#include "tlFileUtils.h" #include "layApplication.h" #include "gsiExpression.h" #include "gsiExternalMain.h" @@ -519,8 +520,12 @@ bool TestBase::do_test (const std::string & /*mode*/) { // Ensures the test temp directory is present QDir dir (testtmp ()); + QDir tmpdir (dir.absoluteFilePath (tl::to_qstring (m_testdir))); + if (tmpdir.exists () && ! tl::rm_dir_recursive (tmpdir.absolutePath ())) { + throw tl::Exception ("Unable to clean temporary dir: " + tl::to_string (tmpdir.absolutePath ())); + } if (! dir.mkpath (tl::to_qstring (m_testdir))) { - throw tl::Exception ("Unable to create path for temporary files: " + tl::to_string (dir.filePath (tl::to_qstring (m_test)))); + throw tl::Exception ("Unable to create path for temporary files: " + tl::to_string (tmpdir.absolutePath ())); } dir.cd (tl::to_qstring (m_testdir));