From e421026366607c6b6c442196ea7d9ed2dd08bb57 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 12 Mar 2017 23:26:04 +0100 Subject: [PATCH 01/27] Started package manager development ('salt') --- src/lay/SaltManagerDialog.ui | 351 ++++++++++++++++++++++++++++++++ src/lay/gsiDeclLayMainWindow.cc | 4 + src/lay/lay.pro | 13 +- src/lay/layMainWindow.cc | 10 +- src/lay/layMainWindow.h | 1 + src/lay/laySalt.cc | 31 +++ src/lay/laySalt.h | 34 ++++ src/lay/laySaltGrain.cc | 183 +++++++++++++++++ src/lay/laySaltGrain.h | 264 ++++++++++++++++++++++++ src/lay/laySaltGrains.cc | 31 +++ src/lay/laySaltGrains.h | 34 ++++ src/lay/laySaltManagerDialog.cc | 36 ++++ src/lay/laySaltManagerDialog.h | 49 +++++ src/unit_tests/laySaltGrain.cc | 99 +++++++++ src/unit_tests/unit_tests.pro | 3 +- 15 files changed, 1139 insertions(+), 4 deletions(-) create mode 100644 src/lay/SaltManagerDialog.ui create mode 100644 src/lay/laySalt.cc create mode 100644 src/lay/laySalt.h create mode 100644 src/lay/laySaltGrain.cc create mode 100644 src/lay/laySaltGrain.h create mode 100644 src/lay/laySaltGrains.cc create mode 100644 src/lay/laySaltGrains.h create mode 100644 src/lay/laySaltManagerDialog.cc create mode 100644 src/lay/laySaltManagerDialog.h create mode 100644 src/unit_tests/laySaltGrain.cc diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui new file mode 100644 index 000000000..0a97e8cda --- /dev/null +++ b/src/lay/SaltManagerDialog.ui @@ -0,0 +1,351 @@ + + + SaltManagerDialog + + + + 0 + 0 + 692 + 440 + + + + Manage Packages + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::NoFocus + + + ... + + + + :/import.png:/import.png + + + true + + + + + + + Qt::Horizontal + + + + 50 + 20 + + + + + + + + Qt::NoFocus + + + ... + + + + :/add.png:/add.png + + + true + + + + + + + Qt::NoFocus + + + ... + + + + :/clear.png:/clear.png + + + true + + + + + + + Qt::NoFocus + + + ... + + + + :/new_folder.png:/new_folder.png + + + true + + + + + + + Qt::NoFocus + + + ... + + + + :/rename.png:/rename.png + + + true + + + + + + + + + + + 0 + 0 + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + false + + + + 1 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + <html><body><center>No packages are installed currently.<br><br>You can use:<br> +</center> +<table> + <tr><td width="30"><a href=":import"><img src=":/import.png"></a></td><td>to import a package from an external source</td></tr> + <tr><td><a href=":add"><img src=":/add.png"></a></td><td>to create a new package</td></tr> +</table> +</body></html> + + + Qt::AlignHCenter|Qt::AlignTop + + + true + + + + + + + + + + + + + + + + 1 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 75 + true + + + + Package Details + + + + + + + + + + + + + + + :/new_folder.png:/new_folder.png + + + New Folder + + + + + + :/add.png:/add.png + + + New + + + New Package + + + + + + :/clear.png:/clear.png + + + Delete + + + Delete Package + + + + + + :/rename.png:/rename.png + + + Rename + + + Rename Package + + + + + + :/import.png:/import.png + + + Import + + + Import Package + + + + + treeWidget + toolButton_4 + toolButton_2 + toolButton_3 + toolButton + toolButton_5 + textBrowser + + + + + + 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/lay.pro b/src/lay/lay.pro index c1451e9b8..7efeae835 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -45,7 +45,11 @@ HEADERS = \ layTextProgress.h \ layVersion.h \ layCommon.h \ - layConfig.h + layConfig.h \ + laySalt.h \ + laySaltGrain.h \ + laySaltGrains.h \ + laySaltManagerDialog.h FORMS = \ ClipDialog.ui \ @@ -90,7 +94,8 @@ FORMS = \ XORToolDialog.ui \ TechLoadOptionsEditorPage.ui \ TechSaveOptionsEditorPage.ui \ - MainConfigPage7.ui + MainConfigPage7.ui \ + SaltManagerDialog.ui SOURCES = \ gsiDeclLayApplication.cc \ @@ -134,6 +139,10 @@ SOURCES = \ layTechSetupDialog.cc \ layTextProgress.cc \ layVersion.cc \ + laySalt.cc \ + laySaltGrain.cc \ + laySaltGrains.cc \ + laySaltManagerDialog.cc RESOURCES = layBuildInMacros.qrc \ layHelpResources.qrc \ diff --git a/src/lay/layMainWindow.cc b/src/lay/layMainWindow.cc index 809e90fd5..70acd1544 100644 --- a/src/lay/layMainWindow.cc +++ b/src/lay/layMainWindow.cc @@ -87,8 +87,8 @@ #include "layLogViewerDialog.h" #include "layLayerToolbox.h" #include "laySettingsForm.h" -#include "laySettingsForm.h" #include "layTechSetupDialog.h" +#include "laySaltManagerDialog.h" #include "layTipDialog.h" #include "laySelectCellViewForm.h" #include "layLayoutPropertiesForm.h" @@ -950,6 +950,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), @@ -4699,6 +4700,13 @@ MainWindow::eventFilter (QObject *obj, QEvent *event) } } +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 96331cc95..ebcd8d4ba 100644 --- a/src/lay/layMainWindow.h +++ b/src/lay/layMainWindow.h @@ -734,6 +734,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/laySalt.cc b/src/lay/laySalt.cc new file mode 100644 index 000000000..897fc0457 --- /dev/null +++ b/src/lay/laySalt.cc @@ -0,0 +1,31 @@ + +/* + + 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" + +namespace lay +{ + + + + +} diff --git a/src/lay/laySalt.h b/src/lay/laySalt.h new file mode 100644 index 000000000..b4bf3aa2a --- /dev/null +++ b/src/lay/laySalt.h @@ -0,0 +1,34 @@ + +/* + + 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 + +namespace lay +{ + + + + +} + +#endif diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc new file mode 100644 index 000000000..feac7ab16 --- /dev/null +++ b/src/lay/laySaltGrain.cc @@ -0,0 +1,183 @@ + +/* + + 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 +#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_version == other.m_version && + m_path == other.m_path && + m_url == other.m_url && + m_title == other.m_title && + m_doc == other.m_doc && + m_dependencies == other.m_dependencies; +} + +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; +} + +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; + } + } + + } +} + +static tl::XMLStruct xml_struct ("salt-grain", + 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::url, &SaltGrain::set_url, "url") + + 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") + ) +); + +void +SaltGrain::load (const std::string &p) +{ + tl::XMLFileSource source (p); + 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); + 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; +} + +bool +SaltGrain::is_grain (const std::string &path) +{ + QDir dir (tl::to_qstring (path)); + QString gf = dir.filePath (tl::to_qstring (grain_filename)); + return QFileInfo (gf).exists (); +} + +} diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h new file mode 100644 index 000000000..644521750 --- /dev/null +++ b/src/lay/laySaltGrain.h @@ -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 + +*/ + +#ifndef HDR_laySaltGrain +#define HDR_laySaltGrain + +#include "layCommon.h" +#include "tlObject.h" + +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 an XML document using + * KLayout's doc format. + */ + 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 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 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 Loads the data from a given file + * This method will *not* set the path. + */ + void load (const std::string &file_path); + + /** + * @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 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 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 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; + std::vector m_dependencies; +}; + +} + +#endif diff --git a/src/lay/laySaltGrains.cc b/src/lay/laySaltGrains.cc new file mode 100644 index 000000000..8134e3178 --- /dev/null +++ b/src/lay/laySaltGrains.cc @@ -0,0 +1,31 @@ + +/* + + 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" + +namespace lay +{ + + + + +} diff --git a/src/lay/laySaltGrains.h b/src/lay/laySaltGrains.h new file mode 100644 index 000000000..74b60b38b --- /dev/null +++ b/src/lay/laySaltGrains.h @@ -0,0 +1,34 @@ + +/* + + 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 + +namespace lay +{ + + + + +} + +#endif diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc new file mode 100644 index 000000000..f63a236cc --- /dev/null +++ b/src/lay/laySaltManagerDialog.cc @@ -0,0 +1,36 @@ + +/* + + 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" + +namespace lay +{ + +SaltManagerDialog::SaltManagerDialog (QWidget *parent) + : QDialog (parent) +{ + Ui::SaltManagerDialog::setupUi (this); + + // ... +} + +} diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h new file mode 100644 index 000000000..42e59a993 --- /dev/null +++ b/src/lay/laySaltManagerDialog.h @@ -0,0 +1,49 @@ + +/* + + 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_laySaltManager +#define HDR_laySaltManager + +#include + +#include "ui_SaltManagerDialog.h" + +namespace lay +{ + +/** + * @brief The dialog for managing the Salt ("Packages") + */ +class SaltManagerDialog + : public QDialog, private Ui::SaltManagerDialog +{ +public: + /** + * @brief Constructor + */ + SaltManagerDialog (QWidget *parent); + +}; + +} + +#endif diff --git a/src/unit_tests/laySaltGrain.cc b/src/unit_tests/laySaltGrain.cc new file mode 100644 index 000000000..516a02698 --- /dev/null +++ b/src/unit_tests/laySaltGrain.cc @@ -0,0 +1,99 @@ + +/* + + 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 "utHead.h" + +TEST (1) +{ + lay::SaltGrain g; + + 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.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); + + std::string tmp = tmp_file (); + + 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); +} + +TEST (2) +{ + 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); +} diff --git a/src/unit_tests/unit_tests.pro b/src/unit_tests/unit_tests.pro index c889a89e2..aba6c9daf 100644 --- a/src/unit_tests/unit_tests.pro +++ b/src/unit_tests/unit_tests.pro @@ -95,7 +95,8 @@ SOURCES = \ tlVariant.cc \ tlXMLParser.cc \ gsiTest.cc \ - tlFileSystemWatcher.cc + tlFileSystemWatcher.cc \ + laySaltGrain.cc # main components: SOURCES += \ From b16d8b941961d304e7323b7ba486343716d235d4 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 12 Mar 2017 23:36:51 +0100 Subject: [PATCH 02/27] Completed unit tests for laySaltGrain.cc --- src/unit_tests/laySaltGrain.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/unit_tests/laySaltGrain.cc b/src/unit_tests/laySaltGrain.cc index 516a02698..3ee047a39 100644 --- a/src/unit_tests/laySaltGrain.cc +++ b/src/unit_tests/laySaltGrain.cc @@ -24,6 +24,8 @@ #include "laySaltGrain.h" #include "utHead.h" +#include + TEST (1) { lay::SaltGrain g; @@ -71,6 +73,14 @@ TEST (1) 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) From b98afd5a8d22a2c4292deff4414896c96e31ec01 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Mon, 13 Mar 2017 23:55:57 +0100 Subject: [PATCH 03/27] WIP: next steps on package manager. --- src/lay/laySaltGrains.cc | 104 +++++++++++++++++++++++++++ src/lay/laySaltGrains.h | 151 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+) diff --git a/src/lay/laySaltGrains.cc b/src/lay/laySaltGrains.cc index 8134e3178..6e2f30f48 100644 --- a/src/lay/laySaltGrains.cc +++ b/src/lay/laySaltGrains.cc @@ -21,11 +21,115 @@ */ #include "laySaltGrains.h" +#include "tlString.h" + +#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); +} + +void +SaltGrains::remove_collection (collection_iterator iter) +{ + // 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) { + m_collections.erase (i); + break; + } + } +} + +void +SaltGrains::add_grain (const SaltGrain &grain) +{ + m_grains.push_back (grain); +} + +void +SaltGrains::remove_grain (grain_iterator iter) +{ + // 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) { + m_grains.erase (i); + break; + } + } +} + +bool +SaltGrains::is_empty () const +{ + return m_collections.empty () && m_grains.empty (); +} + +SaltGrains +SaltGrains::from_path (const std::string &path) +{ + SaltGrains grains; + + QDir dir (tl::to_qstring (path)); + QStringList entries = dir.entryList (QDir::NoFilter, QDir::Name); + for (QStringList::const_iterator e = entries.begin (); e != entries.end (); ++e) { + + std::string epath = tl::to_string (dir.absoluteFilePath (*e)); + if (SaltGrain::is_grain (epath)) { + try { + grains.add_grain (SaltGrain::from_path (epath)); + } catch (...) { + // ignore errors (TODO: what to do here?) + } + } else if (QFileInfo (tl::to_qstring (epath)).isDir ()) { + SaltGrains c = SaltGrains::from_path (epath); + if (! c.is_empty ()) { + grains.add_collection (c); + } + } + + } + + return grains; +} } diff --git a/src/lay/laySaltGrains.h b/src/lay/laySaltGrains.h index 74b60b38b..93f256c58 100644 --- a/src/lay/laySaltGrains.h +++ b/src/lay/laySaltGrains.h @@ -23,11 +23,162 @@ #ifndef HDR_laySaltGrains #define HDR_laySaltGrains +#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.begin (); + } + + /** + * @brief Adds a collection to this collection + */ + void add_collection (const SaltGrains &collection); + + /** + * @brief Removes the collection given by the collection iterator + */ + void remove_collection (collection_iterator iter); + + /** + * @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.begin (); + } + + /** + * @brief Adds a grain to this collection + */ + void add_grain (const SaltGrain &grain); + + /** + * @brief Removes the grain given by the grain iterator + */ + void remove_grain (grain_iterator iter); + + /** + * @brief Gets a value indicating whether the collection is empty + */ + bool is_empty () 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); + +private: + std::string m_name; + std::string m_title; + std::string m_path; + collections_type m_collections; + grains_type m_grains; +}; } From 59dd36f692e20fe17ebf11487747394d9a8d0197 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Tue, 14 Mar 2017 22:59:24 +0100 Subject: [PATCH 04/27] WIP: grains collections for salt package manager. --- src/lay/laySaltGrains.cc | 18 +++++-- src/lay/laySaltGrains.h | 6 +-- src/unit_tests/laySaltGrain.cc | 99 ++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 7 deletions(-) diff --git a/src/lay/laySaltGrains.cc b/src/lay/laySaltGrains.cc index 6e2f30f48..b44c7c05d 100644 --- a/src/lay/laySaltGrains.cc +++ b/src/lay/laySaltGrains.cc @@ -105,23 +105,33 @@ SaltGrains::is_empty () const } SaltGrains -SaltGrains::from_path (const std::string &path) +SaltGrains::from_path (const std::string &path, const std::string &prefix) { SaltGrains grains; + grains.set_path (path); QDir dir (tl::to_qstring (path)); - QStringList entries = dir.entryList (QDir::NoFilter, QDir::Name); + 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 { - grains.add_grain (SaltGrain::from_path (epath)); + 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); + SaltGrains c = SaltGrains::from_path (epath, new_prefix); + c.set_name (new_prefix); if (! c.is_empty ()) { grains.add_collection (c); } diff --git a/src/lay/laySaltGrains.h b/src/lay/laySaltGrains.h index 93f256c58..a2ff4ea98 100644 --- a/src/lay/laySaltGrains.h +++ b/src/lay/laySaltGrains.h @@ -120,7 +120,7 @@ public: */ collection_iterator end_collections () const { - return m_collections.begin (); + return m_collections.end (); } /** @@ -146,7 +146,7 @@ public: */ grain_iterator end_grains () const { - return m_grains.begin (); + return m_grains.end (); } /** @@ -170,7 +170,7 @@ public: * 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); + static SaltGrains from_path (const std::string &path, const std::string &pfx = std::string ()); private: std::string m_name; diff --git a/src/unit_tests/laySaltGrain.cc b/src/unit_tests/laySaltGrain.cc index 3ee047a39..581eda8f5 100644 --- a/src/unit_tests/laySaltGrain.cc +++ b/src/unit_tests/laySaltGrain.cc @@ -22,6 +22,7 @@ #include "laySaltGrain.h" +#include "laySaltGrains.h" #include "utHead.h" #include @@ -107,3 +108,101 @@ TEST (2) EXPECT_EQ (lay::SaltGrain::compare_versions ("990", "990"), 0); EXPECT_EQ (lay::SaltGrain::compare_versions ("991", "990"), 1); } + + +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 bool empty_dir (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 (! empty_dir (fi.filePath ())) { + return false; + } + if (! dir.rmdir (*e)) { + return false; + } + } else if (fi.isFile ()) { + if (! dir.remove (*e)) { + return false; + } + } + } + return true; +} + +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"))); + + tl_assert (empty_dir (tmp_dir.path ())); + + 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 ())); +} From 134534adca9de2fbf5391791c6fa58a02c517ab2 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Tue, 14 Mar 2017 23:32:16 +0100 Subject: [PATCH 05/27] WIP: removal of grains/collections (needs unit tests). --- src/lay/laySaltGrain.cc | 6 ++++ src/lay/laySaltGrain.h | 5 ++++ src/lay/laySaltGrains.cc | 29 ++++++++++++++---- src/lay/laySaltGrains.h | 13 ++++++-- src/tl/tl.pro | 6 ++-- src/tl/tlFileUtils.cc | 54 ++++++++++++++++++++++++++++++++++ src/tl/tlFileUtils.h | 40 +++++++++++++++++++++++++ src/unit_tests/laySaltGrain.cc | 25 ++-------------- 8 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 src/tl/tlFileUtils.cc create mode 100644 src/tl/tlFileUtils.h diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index feac7ab16..de0eccf6a 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -141,6 +141,12 @@ static tl::XMLStruct xml_struct ("salt-grain", ) ); +bool +SaltGrain::is_readonly () const +{ + return QFileInfo (tl::to_qstring (path ())).isWritable (); +} + void SaltGrain::load (const std::string &p) { diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h index 644521750..b86a0e0e7 100644 --- a/src/lay/laySaltGrain.h +++ b/src/lay/laySaltGrain.h @@ -212,6 +212,11 @@ public: 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. diff --git a/src/lay/laySaltGrains.cc b/src/lay/laySaltGrains.cc index b44c7c05d..1d8306176 100644 --- a/src/lay/laySaltGrains.cc +++ b/src/lay/laySaltGrains.cc @@ -22,6 +22,7 @@ #include "laySaltGrains.h" #include "tlString.h" +#include "tlFileUtils.h" #include #include @@ -68,16 +69,21 @@ SaltGrains::add_collection (const SaltGrains &collection) m_collections.push_back (collection); } -void -SaltGrains::remove_collection (collection_iterator iter) +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 (path ()))) { + return false; + } m_collections.erase (i); - break; + return true; } } + + return false; } void @@ -86,16 +92,21 @@ SaltGrains::add_grain (const SaltGrain &grain) m_grains.push_back (grain); } -void -SaltGrains::remove_grain (grain_iterator iter) +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 (path ()))) { + return false; + } m_grains.erase (i); - break; + return true; } } + + return false; } bool @@ -104,6 +115,12 @@ SaltGrains::is_empty () const return m_collections.empty () && m_grains.empty (); } +bool +SaltGrains::is_readonly () const +{ + return QFileInfo (tl::to_qstring (path ())).isWritable (); +} + SaltGrains SaltGrains::from_path (const std::string &path, const std::string &prefix) { diff --git a/src/lay/laySaltGrains.h b/src/lay/laySaltGrains.h index a2ff4ea98..f47b897fb 100644 --- a/src/lay/laySaltGrains.h +++ b/src/lay/laySaltGrains.h @@ -130,8 +130,10 @@ public: /** * @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. */ - void remove_collection (collection_iterator iter); + bool remove_collection (collection_iterator iter, bool with_files = false); /** * @brief Gets the grains (leaf nodes) which are members of this collection (begin iterator) @@ -156,14 +158,21 @@ public: /** * @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. */ - void remove_grain (grain_iterator iter); + 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 Scan grains from a given path * This will scan the grains found within this path and return a collection containing diff --git a/src/tl/tl.pro b/src/tl/tl.pro index 95f8f9870..39a9e5713 100644 --- a/src/tl/tl.pro +++ b/src/tl/tl.pro @@ -39,7 +39,8 @@ SOURCES = \ tlVariant.cc \ tlXMLParser.cc \ tlXMLWriter.cc \ - tlFileSystemWatcher.cc + tlFileSystemWatcher.cc \ + tlFileUtils.cc HEADERS = \ tlAlgorithm.h \ @@ -81,7 +82,8 @@ HEADERS = \ tlXMLParser.h \ tlXMLWriter.h \ tlFileSystemWatcher.h \ - tlCommon.h + tlCommon.h \ + tlFileUtils.h INCLUDEPATH = DEPENDPATH = diff --git a/src/tl/tlFileUtils.cc b/src/tl/tlFileUtils.cc new file mode 100644 index 000000000..518bb7989 --- /dev/null +++ b/src/tl/tlFileUtils.cc @@ -0,0 +1,54 @@ + +/* + + 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 +#include + +namespace tl +{ + +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; + } + if (! dir.rmdir (*e)) { + return false; + } + } else if (fi.isFile ()) { + if (! dir.remove (*e)) { + return false; + } + } + } + return true; +} + +} diff --git a/src/tl/tlFileUtils.h b/src/tl/tlFileUtils.h new file mode 100644 index 000000000..01c171a99 --- /dev/null +++ b/src/tl/tlFileUtils.h @@ -0,0 +1,40 @@ + +/* + + 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 + +namespace tl +{ + +/** + * @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); + +} + +#endif diff --git a/src/unit_tests/laySaltGrain.cc b/src/unit_tests/laySaltGrain.cc index 581eda8f5..fabfab2fd 100644 --- a/src/unit_tests/laySaltGrain.cc +++ b/src/unit_tests/laySaltGrain.cc @@ -23,6 +23,7 @@ #include "laySaltGrain.h" #include "laySaltGrains.h" +#include "tlFileUtils.h" #include "utHead.h" #include @@ -134,28 +135,6 @@ static std::string grains_to_string (const lay::SaltGrains &gg) return res; } -static bool empty_dir (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 (! empty_dir (fi.filePath ())) { - return false; - } - if (! dir.rmdir (*e)) { - return false; - } - } else if (fi.isFile ()) { - if (! dir.remove (*e)) { - return false; - } - } - } - return true; -} - TEST (3) { const QString grain_spec_file = QString::fromUtf8 ("grain.xml"); @@ -171,7 +150,7 @@ TEST (3) QDir dir_cc (dir_c.filePath (QString::fromUtf8 ("c"))); QDir dir_ccv (dir_cc.filePath (QString::fromUtf8 ("v"))); - tl_assert (empty_dir (tmp_dir.path ())); + tl_assert (tl::rm_dir_recursive (tmp_dir.path ())); lay::SaltGrains gg; gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); From 695a5d3169800a9824abe6805cda8720155bde3f Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Wed, 15 Mar 2017 22:56:47 +0100 Subject: [PATCH 06/27] WIP: Salt class and unit tests --- src/lay/laySalt.cc | 99 ++++++++++ src/lay/laySalt.h | 99 ++++++++++ src/lay/laySaltGrains.cc | 4 +- src/lay/laySaltGrains.h | 1 + .../{laySaltGrain.cc => laySalt.cc} | 169 +++++++++++++++--- src/unit_tests/unit_tests.pro | 2 +- 6 files changed, 347 insertions(+), 27 deletions(-) rename src/unit_tests/{laySaltGrain.cc => laySalt.cc} (60%) diff --git a/src/lay/laySalt.cc b/src/lay/laySalt.cc index 897fc0457..24559a2d1 100644 --- a/src/lay/laySalt.cc +++ b/src/lay/laySalt.cc @@ -21,11 +21,110 @@ */ #include "laySalt.h" +#include "tlString.h" + +#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; + } + return *this; +} + +void +Salt::add_location (const std::string &path) +{ + // 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); + mp_flat_grains.clear (); + emit collections_changed (); +} + +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); + mp_flat_grains.clear (); + emit collections_changed (); + 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; + mp_flat_grains.clear (); + emit collections_changed (); + } +} + +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::ensure_flat_present () +{ + if (mp_flat_grains.empty ()) { + add_collection_to_flat (m_root); + std::sort (mp_flat_grains.begin (), mp_flat_grains.end (), NameCompare ()); + } +} } diff --git a/src/lay/laySalt.h b/src/lay/laySalt.h index b4bf3aa2a..39d5c2f02 100644 --- a/src/lay/laySalt.h +++ b/src/lay/laySalt.h @@ -23,11 +23,110 @@ #ifndef HDR_laySalt #define HDR_laySalt +#include "layCommon.h" +#include "laySaltGrain.h" +#include "laySaltGrains.h" + +#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 A flat iterator of (sorted) grains (begin) + */ + flat_iterator begin_flat () + { + ensure_flat_present (); + return mp_flat_grains.begin (); + } + + /** + * @brief A flat iterator of (sorted) grains (end) + */ + flat_iterator end_flat () + { + ensure_flat_present (); + return mp_flat_grains.end (); + } + +signals: + /** + * @brief A signal triggered when one of the collections changed + */ + void collections_changed (); + +private: + SaltGrains m_root; + std::vector mp_flat_grains; + + void ensure_flat_present (); + void add_collection_to_flat (lay::SaltGrains &gg); +}; } diff --git a/src/lay/laySaltGrains.cc b/src/lay/laySaltGrains.cc index 1d8306176..a40bcea64 100644 --- a/src/lay/laySaltGrains.cc +++ b/src/lay/laySaltGrains.cc @@ -75,7 +75,7 @@ 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 (path ()))) { + if (with_files && !tl::rm_dir_recursive (tl::to_qstring (i->path ()))) { return false; } m_collections.erase (i); @@ -98,7 +98,7 @@ 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 (path ()))) { + if (with_files && !tl::rm_dir_recursive (tl::to_qstring (i->path ()))) { return false; } m_grains.erase (i); diff --git a/src/lay/laySaltGrains.h b/src/lay/laySaltGrains.h index f47b897fb..0387000a1 100644 --- a/src/lay/laySaltGrains.h +++ b/src/lay/laySaltGrains.h @@ -23,6 +23,7 @@ #ifndef HDR_laySaltGrains #define HDR_laySaltGrains +#include "layCommon.h" #include "laySaltGrain.h" #include diff --git a/src/unit_tests/laySaltGrain.cc b/src/unit_tests/laySalt.cc similarity index 60% rename from src/unit_tests/laySaltGrain.cc rename to src/unit_tests/laySalt.cc index fabfab2fd..2d761ed62 100644 --- a/src/unit_tests/laySaltGrain.cc +++ b/src/unit_tests/laySalt.cc @@ -23,10 +23,52 @@ #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) { @@ -111,30 +153,6 @@ TEST (2) } -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; -} - TEST (3) { const QString grain_spec_file = QString::fromUtf8 ("grain.xml"); @@ -184,4 +202,107 @@ TEST (3) 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 ())); + + 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"))); + + tl_assert (tl::rm_dir_recursive (tmp_dir.path ())); + + 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; + 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 (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]"); } diff --git a/src/unit_tests/unit_tests.pro b/src/unit_tests/unit_tests.pro index aba6c9daf..547d7616c 100644 --- a/src/unit_tests/unit_tests.pro +++ b/src/unit_tests/unit_tests.pro @@ -96,7 +96,7 @@ SOURCES = \ tlXMLParser.cc \ gsiTest.cc \ tlFileSystemWatcher.cc \ - laySaltGrain.cc + laySalt.cc # main components: SOURCES += \ From b8238a85f96046924d110b70e2647a38496d4f7e Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sat, 18 Mar 2017 00:22:45 +0100 Subject: [PATCH 07/27] WIP: first steps towards package browser --- src/lay/SaltManagerDialog.ui | 199 ++++++++++++++++++--------- src/lay/laySaltManagerDialog.cc | 151 ++++++++++++++++++++ src/laybasic/rdbMarkerBrowserPage.cc | 26 +--- src/tl/tlString.cc | 26 ++++ src/tl/tlString.h | 12 ++ src/unit_tests/tlString.cc | 17 +++ 6 files changed, 346 insertions(+), 85 deletions(-) diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui index 0a97e8cda..4ad0a109b 100644 --- a/src/lay/SaltManagerDialog.ui +++ b/src/lay/SaltManagerDialog.ui @@ -21,6 +21,9 @@ + + 4 + @@ -56,6 +59,9 @@ Qt::NoFocus + + Install Package + ... @@ -76,7 +82,7 @@ 50 - 20 + 0 @@ -86,6 +92,9 @@ Qt::NoFocus + + Create New Package + ... @@ -103,6 +112,9 @@ Qt::NoFocus + + Uninstall Package + ... @@ -115,47 +127,13 @@ - - - - Qt::NoFocus - - - ... - - - - :/new_folder.png:/new_folder.png - - - true - - - - - - - Qt::NoFocus - - - ... - - - - :/rename.png:/rename.png - - - true - - - - + 0 0 @@ -178,15 +156,13 @@ 0 - - - false - - - - 1 - - + + + true + + + true + @@ -214,23 +190,69 @@ QFrame::Raised + + 0 + + + 0 + + + 0 + + + 0 + - - - <html><body><center>No packages are installed currently.<br><br>You can use:<br> + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 218 + 138 + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + <html><body><center>No packages are installed currently.<br><br>You can use:<br> </center> <table> <tr><td width="30"><a href=":import"><img src=":/import.png"></a></td><td>to import a package from an external source</td></tr> <tr><td><a href=":add"><img src=":/add.png"></a></td><td>to create a new package</td></tr> </table> </body></html> - - - Qt::AlignHCenter|Qt::AlignTop - - - true - + + + Qt::AlignHCenter|Qt::AlignTop + + + true + + + + + @@ -250,13 +272,25 @@ - QFrame::StyledPanel + QFrame::NoFrame QFrame::Raised - - + + + 4 + + + 0 + + + 0 + + + 0 + + @@ -269,13 +303,37 @@ - + + + + Edit Package Details + + + Edit + + + + :/edit.png:/edit.png + + + true + + + + + + + + QDialogButtonBox::Close + + + @@ -336,16 +394,31 @@ - treeWidget + salt_view toolButton_4 toolButton_2 toolButton_3 - toolButton - toolButton_5 textBrowser - + + + buttonBox + clicked(QAbstractButton*) + SaltManagerDialog + accept() + + + 635 + 421 + + + 653 + 432 + + + + diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index f63a236cc..5876a1c7a 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -21,15 +21,166 @@ */ #include "laySaltManagerDialog.h" +#include "laySalt.h" +#include "tlString.h" + +#include +#include +#include +#include +#include +#include namespace lay { +class SaltModel + : public QAbstractItemModel +{ +public: + SaltModel (QObject *parent, lay::Salt *salt) + : QAbstractItemModel (parent), mp_salt (salt) + { + // .. nothing yet .. + } + + QVariant data (const QModelIndex &index, int role) const + { + if (role == Qt::DisplayRole) { + + const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; + + 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 += "

"; + } + text += ""; + + return tl::to_qstring (text); + + } else { + return QVariant (); + } + } + + QModelIndex index (int row, int column, const QModelIndex &parent) const + { + if (parent.isValid ()) { + return QModelIndex (); + } else { + return createIndex (row, column); + } + } + + QModelIndex parent (const QModelIndex & /*index*/) const + { + return QModelIndex (); + } + + int columnCount(const QModelIndex & /*parent*/) const + { + return 1; + } + + int rowCount (const QModelIndex &parent) const + { + if (parent.isValid ()) { + return 0; + } else { + return mp_salt->end_flat () - mp_salt->begin_flat (); + } + } + +public: + lay::Salt *mp_salt; +}; + +class SaltItemDelegate + : public QStyledItemDelegate +{ +public: + SaltItemDelegate (QObject *parent) + : QStyledItemDelegate (parent) + { + // .. nothing yet .. + } + + void paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const + { + QStyleOptionViewItemV4 optionV4 = option; + initStyleOption (&optionV4, index); + + 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)); + } + + 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 sizeHint (const QStyleOptionViewItem &option, const QModelIndex &index) const + { + const int textWidth = 500; + + QStyleOptionViewItemV4 optionV4 = option; + initStyleOption (&optionV4, index); + + QTextDocument doc; + doc.setHtml (optionV4.text); + doc.setTextWidth (textWidth); + return QSize (textWidth, doc.size ().height ()); + } +}; + +// @@@ +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"))); + } +} +// @@@ + SaltManagerDialog::SaltManagerDialog (QWidget *parent) : QDialog (parent) { Ui::SaltManagerDialog::setupUi (this); + salt = lay::Salt (); salt_initialized = false; // @@@ + make_salt (); // @@@ + salt_view->setModel (new SaltModel (this, &salt)); + salt_view->setItemDelegate (new SaltItemDelegate (this)); + // ... } 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/tlString.cc b/src/tl/tlString.cc index 15aa9d5ef..22d05402f 100644 --- a/src/tl/tlString.cc +++ b/src/tl/tlString.cc @@ -413,6 +413,32 @@ 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/unit_tests/tlString.cc b/src/unit_tests/tlString.cc index 72a910cda..c4a111a22 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"); +} From 3a6e4982c859d4e7be7016db464b1de2f0e17236 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sat, 18 Mar 2017 22:36:33 +0100 Subject: [PATCH 08/27] WIP: more functionality for Salt Package Manager --- src/lay/SaltManagerDialog.ui | 151 ++++++++++++++++------- src/lay/images/salt.png | Bin 0 -> 16551 bytes src/lay/images/salt_icon.png | Bin 0 -> 3679 bytes src/lay/lay.pro | 6 +- src/lay/layResources.qrc | 4 +- src/lay/laySalt.h | 8 ++ src/lay/laySaltGrain.cc | 65 +++++++++- src/lay/laySaltGrain.h | 71 +++++++++++ src/lay/laySaltGrainDetailsTextWidget.cc | 148 ++++++++++++++++++++++ src/lay/laySaltGrainDetailsTextWidget.h | 61 +++++++++ src/lay/laySaltGrains.cc | 10 +- src/lay/laySaltManagerDialog.cc | 113 ++++++++++++++++- src/lay/laySaltManagerDialog.h | 19 +++ src/unit_tests/laySalt.cc | 32 ++++- 14 files changed, 626 insertions(+), 62 deletions(-) create mode 100644 src/lay/images/salt.png create mode 100644 src/lay/images/salt_icon.png create mode 100644 src/lay/laySaltGrainDetailsTextWidget.cc create mode 100644 src/lay/laySaltGrainDetailsTextWidget.h diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui index 4ad0a109b..e369924c9 100644 --- a/src/lay/SaltManagerDialog.ui +++ b/src/lay/SaltManagerDialog.ui @@ -11,7 +11,7 @@ - Manage Packages + Salt Package Manager @@ -55,7 +55,7 @@ 0 - + Qt::NoFocus @@ -88,7 +88,7 @@ - + Qt::NoFocus @@ -108,7 +108,7 @@ - + Qt::NoFocus @@ -131,7 +131,7 @@ - + 0 @@ -160,6 +160,12 @@ true + + + 64 + 64 + + true @@ -215,42 +221,101 @@ 0 0 - 218 - 138 + 320 + 204 - - + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + :/salt.png + + + + + + + + 0 + 0 + + + + + 9 + 50 + false + + + + <h1>Salt Package Manager</h1> + + + + - - 0 + + 1 0 - 200 + 0 0 - <html><body><center>No packages are installed currently.<br><br>You can use:<br> -</center> + <html><body><h4>No packages are installed currently.</h4><p>Use<br/> <table> - <tr><td width="30"><a href=":import"><img src=":/import.png"></a></td><td>to import a package from an external source</td></tr> + <tr><td width="30"><a href=":import"><img src=":/import.png"></a></td><td>to import a package from an<br/>external repository</td></tr> + <tr></tr> <tr><td><a href=":add"><img src=":/add.png"></a></td><td>to create a new package</td></tr> </table> </body></html> - Qt::AlignHCenter|Qt::AlignTop + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - true + false + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + @@ -264,7 +329,7 @@ - + 1 @@ -299,7 +364,7 @@
- Package Details + Details @@ -321,7 +386,11 @@ - + + + true + + @@ -335,15 +404,6 @@ - - - - :/new_folder.png:/new_folder.png - - - New Folder - - @@ -353,7 +413,7 @@ New - New Package + New package @@ -365,19 +425,7 @@ Delete - Delete Package - - - - - - :/rename.png:/rename.png - - - Rename - - - Rename Package + Delete package @@ -389,16 +437,23 @@ Import - Import Package + Import package + + + lay::SaltGrainDetailsTextWidget + QTextBrowser +
laySaltGrainDetailsTextWidget.h
+
+
salt_view - toolButton_4 - toolButton_2 - toolButton_3 - textBrowser + install_button + create_button + delete_button + details_text diff --git a/src/lay/images/salt.png b/src/lay/images/salt.png new file mode 100644 index 0000000000000000000000000000000000000000..181409a285a589be0c54877ee2ee28d051f75d4f GIT binary patch literal 16551 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 Cm@23M literal 0 HcmV?d00001 diff --git a/src/lay/images/salt_icon.png b/src/lay/images/salt_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ec11c7ac02d5280a8cd406b672817751117889bc GIT binary patch literal 3679 zcmWkx2{=?;7`|kS!Gx@1O_C-}43T{-Wf>z;jh#%E?EAixr4lk~LfM(h);c6xk;WFk zeTihrzGq1DpPrd}?mYM2bI*6a?|t9zofI?EOPuV&><|QT8eY~j2WRbn2OA3*$!@-r z;Kbr&d`SywwHQ&0l10F0guXt8f#eL>_Wy1G0)}E~? z6)sZg^p}hi)kqGzDuF}{1F2LphwA9#vo4O)I^1sT9#k0_30dI9sW&gyMU|Dh$?~z6 zTgZX01D@YLa_2Y;E9?1-7gH_d5}g%NYHH3G_q#2RQ+SgchYz=C>V3bqNh9c3JVO%- z#sgzHgQPnB>MH?@Ramgk`U$0L{Z-Msq8A)|;m3vw3k%D7Gpo1+g6rnt{?27-9uOSG zhU*Tuc#>>t;_XZ(`;h6x^wH7LRjq&pcxCWPp)eG{rg;SCV8xQh&6Gc@?}u4A^YgalaEd0yKcSO5U`J^2~wKM3!bD+ z*1oV#7t=_+flN|c`@jpbXrM0k?!X7(!qJX`fn2rwK9D||07ok;w=C^Ag@g#+;S$A2 z^MGt|n8{^F4ZruD@l@&pc+kfbz`^!Gg}4wZF|O!T5wG_QsfA6o$cI$ILY)_{M9-EP zA9L_Tpg`>i^5fnQj)=dsd0GCn@~WykWHMMKGBPqy2^XhD|FhLSI5!CIRDo47G-&UgbAgn_X!hn1BTh^TjB;$DPiWw)UHDjb*HkrboT7q42b)i)-=UGZ28mU{hn>jrstSA8B(U zA?jk~vQ&CZotCv?>Jhx^t0MftO+`M@+(tp0Y zVR|NAt1a^P#Px9D_1!V=3z~n{TU{(H6A5^b7!Yt>EA{+TJ z;U-@MphIM&2EV*ba*1({(?~7h0YOOfDL2?4k}8#Fdb1~A1B=t7{q({5Y0G8T-HwPr zkjZfbyxI?dtA*v|Ph`vI4y3u_5c_^Kz@rYlY3oB|IobVE+G~P>5FdM@){(tSSjOJr z;Shh6;3rT3L$okKFsWw6nos}9I-4XQX966s_`Qw1nqskh=I*o#R{|Wy$%zLN9DTW7#+eDm|p4@=P1aRrk|F#r(ARsT7?-Q|(1YLGfWd$OKXAlH+_))!BWMN0R*5e>VOQTO6t#jUDSc4{PmvT*?2} z=65NhYYS3vU!)b)|4 zl<3jE;ke8a3AVG4k81LIEkn~*#0&TJPLtv2y6^$9II%d1&I(oDsZh;DL~(yGZnisD zF--Gnfv`3=o=BUUyJg=6RXcZN#*D4xKm6JGX~z~iYP#P&v*EC_IFRPf5<>v3W-~;6 zV7JpmfTL4yr|hegP*SXH>Z1$ZlbIw23I4Rhj#)p;=gN=Ws{J*2UVLn?MG6ZGkAkT_ zDiORrTR|{0&ml$Mc!5m~Nu~m`@;A32keM{Zs&{6Wslaw8nkS%*;k8*Id2#0rQ`5Nn zt;Pp8qFE?S(kRYWZ570|YcFmT*aSjW*4Fx#mJR<|)*v}rQMnA?^e@E@{L!w$_9qN2 z1y1<3i%vf0YMKcCzOr+0C~wtRq!>_X79z8&aok}*Musi!41bz!Bm|^JJyV+EDuq6; zo8fd6kz?P zOUJHrLU`hUI|Mpi&vib^1EeqJ$nwKS^PvlMT#eu9PhU#~er9-9uXR*1TAn{Gi|GxL znp?T(U!aJ?Hc@gM0G-qL&$2h}FFucMkdvv7JP3lcW%| zi_xbHdHQ~#L-OxP8{YUBL3;%~N>hqAwiS2)((5Ia@XSVD7tCOiZ(DtyCGMh#1JngUYYPz#MmY;{Fv6NJ=z0C|BDwxF@~(ovGJz3A2SFrr~gzoH})hk$$p zGxXMTENB*bf4DnFxq;j*)01-KPM%Sb&N5tAkM72_)5w|u6*gD+m>3y=BLY(U(CB2_ zN)WFOd`jJqf++k}zB%{AyYhpes=QnLryTnzO7_hAc`NH^TxaTMsg1CPyKW>izR#1) zKcHpjF_@Ph>1|6VB?oCDe)nj=-pd2edZx3O=A*SCITG zdBy0$hXf%a;e7YV1=x`kJ;wI?qGPIPbn)8go29RQP|UZrj|Zz+_R)vO3uVpWY}nKV zqiO;k2!DzBQP7}331VM zYdZtC#x^`^Z~BfmpaxD^N$E4NP99pT$otoZSaWF_#~+Xnibs5H`uU!0wg9h}@b<0p zQWj40SJWzH{&iY*Y&{R~ zpGwVvp*V?ua0eaQu4>4ntUuj$Ct$BAU6T>o-roKbE&)e@Lg;#qyZzfJRJw$lVbptm>l6Z5_?Fmt+A`NK(T%h_ z>3C{OBqb>Iw>;SpP6IY7Z}gqFy(fgyv7!f1Y@%mEKNiax zmE1s*fHx4>KRD1)G{6%*9VJbGgDNn`(YczovMpPE$ca z3e=(TZRpnA@9)3_7)Och)Ou#yHiRkdu1=%@u4(YFF%9lVd>m_?_Hmy45bEmc+8dhx z?)RZDVxC9;ShEBC#EBFCeU6YtMUlW-_4OjM_{NSzJrg9rHyZ6j+TmY*Rfqa6x&o95DWLH9U856 zY#m))T|EVKKt^q?ac9taFM2{O^k;_Fc)iWor2OkcamL>3a`6v7Ducg*kfFY*UM0pM F?tdV&+v5NL literal 0 HcmV?d00001 diff --git a/src/lay/lay.pro b/src/lay/lay.pro index 7efeae835..572c78a0d 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -49,7 +49,8 @@ HEADERS = \ laySalt.h \ laySaltGrain.h \ laySaltGrains.h \ - laySaltManagerDialog.h + laySaltManagerDialog.h \ + laySaltGrainDetailsTextWidget.h FORMS = \ ClipDialog.ui \ @@ -142,7 +143,8 @@ SOURCES = \ laySalt.cc \ laySaltGrain.cc \ laySaltGrains.cc \ - laySaltManagerDialog.cc + laySaltManagerDialog.cc \ + laySaltGrainDetailsTextWidget.cc RESOURCES = layBuildInMacros.qrc \ layHelpResources.qrc \ diff --git a/src/lay/layResources.qrc b/src/lay/layResources.qrc index 3c2e3bdd4..883bcff82 100644 --- a/src/lay/layResources.qrc +++ b/src/lay/layResources.qrc @@ -113,8 +113,10 @@ images/upup.png images/waived.png images/yellow_flag.png + images/salt.png + images/salt_icon.png - + syntax/ruby.xml syntax/python.xml diff --git a/src/lay/laySalt.h b/src/lay/laySalt.h index 39d5c2f02..19d21145f 100644 --- a/src/lay/laySalt.h +++ b/src/lay/laySalt.h @@ -96,6 +96,14 @@ public: 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) */ diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index de0eccf6a..80292e6f1 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -41,12 +41,17 @@ bool SaltGrain::operator== (const SaltGrain &other) const { return m_name == other.m_name && - m_version == other.m_version && 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_dependencies == other.m_dependencies; + 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 @@ -85,6 +90,36 @@ SaltGrain::set_doc (const std::string &t) m_doc = t; } +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; +} + int SaltGrain::compare_versions (const std::string &v1, const std::string &v2) { @@ -128,12 +163,38 @@ SaltGrain::compare_versions (const std::string &v1, const std::string &v2) } } +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); + } + } +}; + static tl::XMLStruct xml_struct ("salt-grain", 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::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_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") + diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h index b86a0e0e7..063bade20 100644 --- a/src/lay/laySaltGrain.h +++ b/src/lay/laySaltGrain.h @@ -26,6 +26,8 @@ #include "layCommon.h" #include "tlObject.h" +#include + namespace lay { @@ -142,6 +144,71 @@ public: */ 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 absolute file path of the installed grain * This is the file path to the grain folder. @@ -261,6 +328,10 @@ private: std::string m_url; std::string m_title; std::string m_doc; + std::string m_author; + std::string m_author_contact; + std::string m_license; + QDateTime m_authored_time, m_installed_time; std::vector m_dependencies; }; diff --git a/src/lay/laySaltGrainDetailsTextWidget.cc b/src/lay/laySaltGrainDetailsTextWidget.cc new file mode 100644 index 000000000..ab44d6d22 --- /dev/null +++ b/src/lay/laySaltGrainDetailsTextWidget.cc @@ -0,0 +1,148 @@ + +/* + + 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 + +namespace lay +{ + +SaltGrainDetailsTextWidget::SaltGrainDetailsTextWidget (QWidget *w) + : QTextBrowser (w), mp_grain (0) +{ + // .. nothing yet .. +} + +void SaltGrainDetailsTextWidget::set_grain (SaltGrain *g) +{ + if (mp_grain != g) { + mp_grain = g; + setHtml (details_text ()); + } +} + +QVariant +SaltGrainDetailsTextWidget::loadResource (int type, const QUrl &url) +{ + if (url.path () == QString::fromUtf8 ("/icon")) { + // @@@ + return QImage (":/salt_icon.png"); + // @@@ + } 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 << "
"; + + 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->url ().empty ()) { + stream << "" << QObject::tr ("Documentation link") << ": url ()) << "\">" << tl::to_qstring (tl::escaped_to_html (g->url ())) << ""; + } else { + stream << ""; + stream << QObject::tr ("This package does not have a documentation link. " + "Use the <url> element of the specification file or edit the package properties to provide a link."); + 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/laySaltGrains.cc b/src/lay/laySaltGrains.cc index a40bcea64..9c17944c6 100644 --- a/src/lay/laySaltGrains.cc +++ b/src/lay/laySaltGrains.cc @@ -112,7 +112,15 @@ SaltGrains::remove_grain (grain_iterator iter, bool with_files) bool SaltGrains::is_empty () const { - return m_collections.empty () && m_grains.empty (); + 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 diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 5876a1c7a..9606164f3 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -30,10 +30,17 @@ #include #include #include +#include +#include namespace lay { +// -------------------------------------------------------------------------------------- + +/** + * @brief A model representing the salt grains for a QListView + */ class SaltModel : public QAbstractItemModel { @@ -71,6 +78,12 @@ public: return tl::to_qstring (text); + } else if (role == Qt::DecorationRole) { + + // @@@ + return QIcon (":/salt_icon.png"); + // @@@ + } else { return QVariant (); } @@ -81,7 +94,7 @@ public: if (parent.isValid ()) { return QModelIndex (); } else { - return createIndex (row, column); + return createIndex (row, column, mp_salt->begin_flat () [row]); } } @@ -104,10 +117,29 @@ public: } } + SaltGrain *grain_from_index (const QModelIndex &index) const + { + if (index.isValid ()) { + return static_cast (index.internalPointer ()); + } else { + return 0; + } + } + + void update () + { + // @@@ + } + public: lay::Salt *mp_salt; }; +// -------------------------------------------------------------------------------------- + +/** + * @brief A delegate displaying the summary of a grain + */ class SaltItemDelegate : public QStyledItemDelegate { @@ -152,13 +184,22 @@ public: 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, doc.size ().height ()); + return QSize (textWidth + icon_size.width () + 6, std::max (icon_size.height () + 12, int (doc.size ().height ()))); } }; +// -------------------------------------------------------------------------------------- +// SaltManager implementation + // @@@ lay::Salt salt; static bool salt_initialized = false; @@ -172,16 +213,76 @@ void make_salt () // @@@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) - : QDialog (parent) + : QDialog (parent), + m_current_changed_enabled (true) { Ui::SaltManagerDialog::setupUi (this); - salt = lay::Salt (); salt_initialized = false; // @@@ - make_salt (); // @@@ - salt_view->setModel (new SaltModel (this, &salt)); +// @@@ + salt = lay::Salt (); salt_initialized = false; + make_salt (); + mp_salt = &salt; +// @@@ + + SaltModel *model = new SaltModel (this, mp_salt); + salt_view->setModel (model); salt_view->setItemDelegate (new SaltItemDelegate (this)); + connect (mp_salt, SIGNAL (collections_changed ()), this, SLOT (salt_changed ())); + + // select the first grain + if (model->rowCount (QModelIndex ()) > 0) { + salt_view->setCurrentIndex (model->index (0, 0, QModelIndex ())); + } + + salt_changed (); + + connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); + + // ... } +void +SaltManagerDialog::salt_changed () +{ + SaltModel *model = dynamic_cast (salt_view->model ()); + if (! model) { + return; + } + + m_current_changed_enabled = false; + 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 (); + } + + current_changed (); +} + +void +SaltManagerDialog::current_changed () +{ + SaltModel *model = dynamic_cast (salt_view->model ()); + if (! model) { + return; + } + + SaltGrain *g = model->grain_from_index (salt_view->currentIndex ()); + details_text->set_grain (g); + if (!g) { + details_frame->setEnabled (false); + delete_button->setEnabled (false); + } else { + details_frame->setEnabled (true); + delete_button->setEnabled (true); + } +} + } diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h index 42e59a993..24b55a62e 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -30,18 +30,37 @@ namespace lay { +class Salt; +class SaltGrain; + /** * @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 currently selected package (grain) has changed + */ + void current_changed (); + +private: + lay::Salt *mp_salt; + bool m_current_changed_enabled; }; } diff --git a/src/unit_tests/laySalt.cc b/src/unit_tests/laySalt.cc index 2d761ed62..75d7986ca 100644 --- a/src/unit_tests/laySalt.cc +++ b/src/unit_tests/laySalt.cc @@ -72,7 +72,20 @@ static std::string salt_to_string (lay::Salt &salt) 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"); @@ -86,6 +99,20 @@ TEST (1) EXPECT_EQ (g.title (), "title"); g.set_doc ("doc"); EXPECT_EQ (g.doc (), "doc"); + 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"; @@ -105,8 +132,6 @@ TEST (1) gg.set_doc ("blabla"); EXPECT_EQ (g == gg, false); - std::string tmp = tmp_file (); - EXPECT_EQ (g == gg, false); g.save (tmp); @@ -269,11 +294,14 @@ TEST (4) // 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]"); From 3f9f06a9b9ebc0ac920c2d338dc2f2a4b8ad8565 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 19 Mar 2017 00:42:09 +0100 Subject: [PATCH 09/27] WIP: more information on packages --- src/lay/SaltManagerDialog.ui | 2 +- src/lay/laySaltGrain.cc | 50 +++++++++++ src/lay/laySaltGrain.h | 51 ++++++++++- src/lay/laySaltGrainDetailsTextWidget.cc | 103 ++++++++++++++++++++--- src/lay/laySaltManagerDialog.cc | 14 ++- src/unit_tests/laySalt.cc | 2 + 6 files changed, 204 insertions(+), 18 deletions(-) diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui index e369924c9..d16b5af31 100644 --- a/src/lay/SaltManagerDialog.ui +++ b/src/lay/SaltManagerDialog.ui @@ -167,7 +167,7 @@ - true + false diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index 80292e6f1..266dbeff7 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -26,6 +26,7 @@ #include #include +#include namespace lay { @@ -46,6 +47,9 @@ SaltGrain::operator== (const SaltGrain &other) const 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 && @@ -90,6 +94,12 @@ 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) { @@ -120,6 +130,18 @@ 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) { @@ -184,17 +206,45 @@ struct TimeConverter } }; +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::XMLStruct xml_struct ("salt-grain", 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") + diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h index 063bade20..eda1d63bd 100644 --- a/src/lay/laySaltGrain.h +++ b/src/lay/laySaltGrain.h @@ -27,6 +27,7 @@ #include "tlObject.h" #include +#include namespace lay { @@ -115,8 +116,7 @@ public: /** * @brief Gets the documentation text of the grain * - * The documentation text is an XML document using - * KLayout's doc format. + * The documentation text is a brief description. */ const std::string &doc () const { @@ -128,6 +128,21 @@ public: */ 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 * @@ -209,6 +224,35 @@ public: */ 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. @@ -327,11 +371,12 @@ private: std::string m_path; std::string m_url; std::string m_title; - std::string m_doc; + 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; }; diff --git a/src/lay/laySaltGrainDetailsTextWidget.cc b/src/lay/laySaltGrainDetailsTextWidget.cc index ab44d6d22..730a6daeb 100644 --- a/src/lay/laySaltGrainDetailsTextWidget.cc +++ b/src/lay/laySaltGrainDetailsTextWidget.cc @@ -27,6 +27,7 @@ #include #include #include +#include namespace lay { @@ -49,9 +50,54 @@ QVariant SaltGrainDetailsTextWidget::loadResource (int type, const QUrl &url) { if (url.path () == QString::fromUtf8 ("/icon")) { - // @@@ - return QImage (":/salt_icon.png"); - // @@@ + + if (!mp_grain || mp_grain->icon ().isNull ()) { + return QImage (":/salt_icon.png"); + } else { + QImage img = mp_grain->icon (); + if (img.width () != 64) { + return img.scaled (QSize (64, 64), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + } 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 = 4; + 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); } @@ -72,8 +118,9 @@ SaltGrainDetailsTextWidget::details_text () stream << ""; - stream << ""; - stream << ""; + stream << "
"; + stream << ""; + stream << "
"; stream << "

"; stream << tl::to_qstring (tl::escaped_to_html (g->name ())) << " " << tl::to_qstring (tl::escaped_to_html (g->version ())); @@ -96,8 +143,6 @@ SaltGrainDetailsTextWidget::details_text () stream << "

"; } - stream << "

"; - stream << "


"; if (! g->doc ().empty ()) { stream << tl::to_qstring (tl::escaped_to_html (g->doc ())); @@ -128,16 +173,52 @@ SaltGrainDetailsTextWidget::details_text () stream << "

"; stream << "

"; - if (! g->url ().empty ()) { - stream << "" << QObject::tr ("Documentation link") << ": url ()) << "\">" << tl::to_qstring (tl::escaped_to_html (g->url ())) << ""; + 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 a documentation link. " - "Use the <url> element of the specification file or edit the package properties to provide a link."); + 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)) << " " << tl::to_qstring (tl::escaped_to_html (d->url)) << "
"; + } + stream << "

"; + } + + stream << ""; + stream << ""; stream.flush (); diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 9606164f3..8da3b9934 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -80,9 +80,17 @@ public: } else if (role == Qt::DecorationRole) { - // @@@ - return QIcon (":/salt_icon.png"); - // @@@ + const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; + if (g->icon ().isNull ()) { + return QIcon (":/salt_icon.png"); + } else { + QPixmap px = QPixmap::fromImage (g->icon ()); + if (px.width () == 64) { + return px; + } else { + return px.scaled (QSize (64, 64), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + } + } } else { return QVariant (); diff --git a/src/unit_tests/laySalt.cc b/src/unit_tests/laySalt.cc index 75d7986ca..ae5581e72 100644 --- a/src/unit_tests/laySalt.cc +++ b/src/unit_tests/laySalt.cc @@ -99,6 +99,8 @@ TEST (1) 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"); From b34750f8cdac155998b9a8bc7f8369ee3092b8c6 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 19 Mar 2017 22:44:46 +0100 Subject: [PATCH 10/27] WIP: salt package manager New features like properties editor, proper icon sizing, some styling etc. --- src/lay/SaltGrainPropertiesDialog.ui | 675 +++++++++++++++++++++++ src/lay/lay.pro | 9 +- src/lay/laySaltGrainDetailsTextWidget.cc | 33 +- src/lay/laySaltGrainPropertiesDialog.cc | 268 +++++++++ src/lay/laySaltGrainPropertiesDialog.h | 84 +++ src/lay/laySaltManagerDialog.cc | 52 +- src/lay/laySaltManagerDialog.h | 13 +- 7 files changed, 1109 insertions(+), 25 deletions(-) create mode 100644 src/lay/SaltGrainPropertiesDialog.ui create mode 100644 src/lay/laySaltGrainPropertiesDialog.cc create mode 100644 src/lay/laySaltGrainPropertiesDialog.h diff --git a/src/lay/SaltGrainPropertiesDialog.ui b/src/lay/SaltGrainPropertiesDialog.ui new file mode 100644 index 000000000..f9edcf633 --- /dev/null +++ b/src/lay/SaltGrainPropertiesDialog.ui @@ -0,0 +1,675 @@ + + + SaltGrainPropertiesDialog + + + + 0 + 0 + 754 + 571 + + + + Package Properties + + + + + + + 0 + 1 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + License + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + 0 + 0 + + + + <a href="%1">Open link</a> + + + true + + + + + + + + 1 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 2 + + + 6 + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Select a Screenshot Image + + + ... + + + + :/add.png:/add.png + + + + 64 + 64 + + + + + + + + Reset Icon + + + ... + + + + :/clear_edit.png:/clear_edit.png + + + true + + + + + + + Showcase image +(max 1024x1024) + + + + + + + Select an Icon Image + + + ... + + + + :/salt_icon.png:/salt_icon.png + + + + 64 + 64 + + + + + + + + Reset Screenshot + + + ... + + + + :/clear_edit.png:/clear_edit.png + + + true + + + + + + + Icon +(64x64) + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + 75 + true + + + + Title + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 0 + 0 + + + + + + + + + true + + + + (use numeric versions like "1.5" or "2.1.3") + + + + + + + + 16777215 + 80 + + + + + + + + + 75 + true + + + + Documentation + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 75 + true + + + + +Images + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + + + + 75 + true + + + + +Description + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + 75 + true + + + + Author + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 75 + true + + + + Author contact + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + Qt::Vertical + + + + 736 + 32 + + + + + + + + + 0 + 0 + + + + + true + + + + (enter an URL to provide a documentation link) + + + + + + + + 0 + 0 + + + + + + + + + true + + + + (license information like "GPLv3" or "MIT") + + + + + + + + 75 + true + + + + Version + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + 75 + true + + + + +Depends on + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add new dependency + + + ... + + + + :/add.png:/add.png + + + true + + + + + + + Delete dependency + + + ... + + + + :/clear.png:/clear.png + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + true + + + false + + + true + + + + Name + + + + + Version + + + + + URL + + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + 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/lay.pro b/src/lay/lay.pro index 572c78a0d..da3d53cb0 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -50,7 +50,8 @@ HEADERS = \ laySaltGrain.h \ laySaltGrains.h \ laySaltManagerDialog.h \ - laySaltGrainDetailsTextWidget.h + laySaltGrainDetailsTextWidget.h \ + laySaltGrainPropertiesDialog.h FORMS = \ ClipDialog.ui \ @@ -96,7 +97,8 @@ FORMS = \ TechLoadOptionsEditorPage.ui \ TechSaveOptionsEditorPage.ui \ MainConfigPage7.ui \ - SaltManagerDialog.ui + SaltManagerDialog.ui \ + SaltGrainPropertiesDialog.ui SOURCES = \ gsiDeclLayApplication.cc \ @@ -144,7 +146,8 @@ SOURCES = \ laySaltGrain.cc \ laySaltGrains.cc \ laySaltManagerDialog.cc \ - laySaltGrainDetailsTextWidget.cc + laySaltGrainDetailsTextWidget.cc \ + laySaltGrainPropertiesDialog.cc RESOURCES = layBuildInMacros.qrc \ layHelpResources.qrc \ diff --git a/src/lay/laySaltGrainDetailsTextWidget.cc b/src/lay/laySaltGrainDetailsTextWidget.cc index 730a6daeb..f71bdda14 100644 --- a/src/lay/laySaltGrainDetailsTextWidget.cc +++ b/src/lay/laySaltGrainDetailsTextWidget.cc @@ -40,10 +40,8 @@ SaltGrainDetailsTextWidget::SaltGrainDetailsTextWidget (QWidget *w) void SaltGrainDetailsTextWidget::set_grain (SaltGrain *g) { - if (mp_grain != g) { - mp_grain = g; - setHtml (details_text ()); - } + mp_grain = g; + setHtml (details_text ()); } QVariant @@ -51,15 +49,30 @@ 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 () != 64) { - return img.scaled (QSize (64, 64), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + 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")) { @@ -69,7 +82,7 @@ SaltGrainDetailsTextWidget::loadResource (int type, const QUrl &url) QImage smask (s.size (), QImage::Format_ARGB32_Premultiplied); smask.fill (QColor (0, 0, 0, 0)); { - int border = 4; + int border = 0; int radius = 6; QPainter painter (&smask); @@ -210,9 +223,11 @@ SaltGrainDetailsTextWidget::details_text () stream << "

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

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

" << QObject::tr ("Depends on: ") << "

"; + 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)) << " " << tl::to_qstring (tl::escaped_to_html (d->url)) << "
"; + stream << "    " << tl::to_qstring (tl::escaped_to_html (d->name)) << " "; + stream << tl::to_qstring (tl::escaped_to_html (d->version)) << " - "; + stream << "[" << tl::to_qstring (tl::escaped_to_html (d->url)) << "]
"; } stream << "

"; } diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc new file mode 100644 index 000000000..1c60abd4d --- /dev/null +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -0,0 +1,268 @@ + +/* + + 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 +#include +#include + +namespace lay +{ + +SaltGrainPropertiesDialog::SaltGrainPropertiesDialog (QWidget *parent) + : QDialog (parent), mp_salt (0) +{ + 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))); +} + +void +SaltGrainPropertiesDialog::update_controls () +{ + setWindowTitle (m_title + tl::to_qstring (" - " + m_grain.name ())); + + 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->setText (0, tl::to_qstring (d->name)); + item->setText (1, tl::to_qstring (d->version)); + item->setText (2, tl::to_qstring (d->url)); + 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->text (0).simplified (); + QString version = item->text (1).simplified (); + QString url = item->text (2).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 (column == 0 && mp_salt) { + + // set URL and version for known grains + std::string name = tl::to_string (item->text (0).simplified ()); + if (name == m_grain.name ()) { + + item->setText (1, QString ()); + item->setText (2, tr ("(must not depend on itself)")); + + } else { + + SaltGrain *g = 0; + for (lay::Salt::flat_iterator i = mp_salt->begin_flat (); i != mp_salt->end_flat (); ++i) { + if ((*i)->name () == name) { + g = *i; + } + } + if (g) { + item->setText (1, tl::to_qstring (g->version ())); + item->setText (2, tl::to_qstring (g->url ())); + } else { + item->setText (1, QString ()); + item->setText (2, tr ("(unknown packet)")); + } + + } + + } +} + +void +SaltGrainPropertiesDialog::url_changed (const QString &url) +{ + // inserts the URL into the label + open_label->setText (m_open_label.arg (url)); +} + +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)")); + if (! fileName.isNull ()) { + QImage img = QImage (fileName); + if (img.width () > max_dim || img.height () > max_dim) { + throw tl::Exception (tl::to_string (tr ("Icon image too big -\nmust be %1x%2 pixels max, but is %3x%4").arg (max_dim).arg (max_dim).arg (img.width ()).arg (img.height ()))); + } + 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)")); + if (! fileName.isNull ()) { + QImage img = QImage (fileName); + if (img.width () > max_dim || img.height () > max_dim) { + throw tl::Exception (tl::to_string (tr ("Showcase image too big -\nmust be %1x%2 pixels max, but is %3x%4").arg (max_dim).arg (max_dim).arg (img.width ()).arg (img.height ()))); + } + 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); + } +} + +bool +SaltGrainPropertiesDialog::exec_dialog (lay::SaltGrain *grain, lay::Salt *salt) +{ + m_grain = *grain; + mp_salt = salt; + update_controls (); + + bool res = exec (); + if (res) { + update_data (); + *grain = m_grain; + } + + mp_salt = 0; + return res; +} + +} diff --git a/src/lay/laySaltGrainPropertiesDialog.h b/src/lay/laySaltGrainPropertiesDialog.h new file mode 100644 index 000000000..e9eeae2d7 --- /dev/null +++ b/src/lay/laySaltGrainPropertiesDialog.h @@ -0,0 +1,84 @@ + +/* + + 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); + +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); + +private: + lay::SaltGrain m_grain; + lay::Salt *mp_salt; + QString m_title; + QString m_open_label; + QString m_image_dir; + + void update_controls (); + void update_data (); + void update_icon (); + void update_screenshot (); +}; + +} + +#endif diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 8da3b9934..8c83aa9f8 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -21,6 +21,7 @@ */ #include "laySaltManagerDialog.h" +#include "laySaltGrainPropertiesDialog.h" #include "laySalt.h" #include "tlString.h" @@ -80,16 +81,29 @@ public: } else if (role == Qt::DecorationRole) { + int icon_dim = 64; + const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; if (g->icon ().isNull ()) { return QIcon (":/salt_icon.png"); } else { - QPixmap px = QPixmap::fromImage (g->icon ()); - if (px.width () == 64) { - return px; + + QImage img = g->icon (); + if (img.width () == icon_dim && img.height () == icon_dim) { + return QPixmap::fromImage (img); } else { - return px.scaled (QSize (64, 64), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + + 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 QPixmap::fromImage (final_img); + } + } } else { @@ -225,6 +239,9 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) m_current_changed_enabled (true) { Ui::SaltManagerDialog::setupUi (this); + mp_properties_dialog = new lay::SaltGrainPropertiesDialog (this); + + connect (edit_button, SIGNAL (clicked ()), this, SLOT (edit_properties ())); // @@@ salt = lay::Salt (); salt_initialized = false; @@ -247,8 +264,19 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); + // @@@ +} - // ... +void +SaltManagerDialog::edit_properties () +{ + SaltGrain *g = current_grain (); + if (g) { + if (mp_properties_dialog->exec_dialog (g, mp_salt)) { + current_changed (); + // @@@ + } + } } void @@ -277,12 +305,7 @@ SaltManagerDialog::salt_changed () void SaltManagerDialog::current_changed () { - SaltModel *model = dynamic_cast (salt_view->model ()); - if (! model) { - return; - } - - SaltGrain *g = model->grain_from_index (salt_view->currentIndex ()); + SaltGrain *g = current_grain (); details_text->set_grain (g); if (!g) { details_frame->setEnabled (false); @@ -293,4 +316,11 @@ SaltManagerDialog::current_changed () } } +lay::SaltGrain * +SaltManagerDialog::current_grain () +{ + SaltModel *model = dynamic_cast (salt_view->model ()); + return model ? model->grain_from_index (salt_view->currentIndex ()) : 0; +} + } diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h index 24b55a62e..170e02b7d 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -20,8 +20,8 @@ */ -#ifndef HDR_laySaltManager -#define HDR_laySaltManager +#ifndef HDR_laySaltManagerDialog +#define HDR_laySaltManagerDialog #include @@ -32,6 +32,7 @@ namespace lay class Salt; class SaltGrain; +class SaltGrainPropertiesDialog; /** * @brief The dialog for managing the Salt ("Packages") @@ -58,9 +59,17 @@ private slots: */ void current_changed (); + /** + * @brief Called when the "edit" button is pressed + */ + void edit_properties (); + private: lay::Salt *mp_salt; bool m_current_changed_enabled; + lay::SaltGrainPropertiesDialog *mp_properties_dialog; + + lay::SaltGrain *current_grain (); }; } From 5dab4b19ecdb3e6804bb7555644ddc3f3cb9d2be Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Mon, 20 Mar 2017 10:07:39 +0100 Subject: [PATCH 11/27] WIP: some styling topics. --- src/lay/SaltGrainPropertiesDialog.ui | 3 + src/lay/laySaltGrainPropertiesDialog.cc | 124 +++++++++++++++++++++--- src/lay/laySaltGrainPropertiesDialog.h | 9 ++ 3 files changed, 122 insertions(+), 14 deletions(-) diff --git a/src/lay/SaltGrainPropertiesDialog.ui b/src/lay/SaltGrainPropertiesDialog.ui index f9edcf633..7ef38eda1 100644 --- a/src/lay/SaltGrainPropertiesDialog.ui +++ b/src/lay/SaltGrainPropertiesDialog.ui @@ -578,6 +578,9 @@ Depends on + + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + true diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc index 1c60abd4d..c05dbc63d 100644 --- a/src/lay/laySaltGrainPropertiesDialog.cc +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -28,12 +28,77 @@ #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, SaltGrainPropertiesDialog *dialog, int column) + : QItemDelegate (parent), mp_dialog (dialog), m_column (column) + { + // .. 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 ()); + if (m_column > 0) { + editor->setPlaceholderText (index.model ()->data (index, Qt::EditRole).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 + { + QWidget *editor = createEditor (0, option, index); + QSize size = editor->sizeHint (); + delete editor; + return size; + } + +public: + SaltGrainPropertiesDialog *mp_dialog; + int m_column; +}; + +// ---------------------------------------------------------------------------------------------------- +// SaltGrainPropertiesDialog implementation + SaltGrainPropertiesDialog::SaltGrainPropertiesDialog (QWidget *parent) - : QDialog (parent), mp_salt (0) + : QDialog (parent), mp_salt (0), m_update_enabled (true) { Ui::SaltGrainPropertiesDialog::setupUi (this); @@ -48,6 +113,12 @@ SaltGrainPropertiesDialog::SaltGrainPropertiesDialog (QWidget *parent) 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 (0, new SaltGrainEditDelegate (dependencies, this, 0)); + dependencies->setItemDelegateForColumn (1, new SaltGrainEditDelegate (dependencies, this, 1)); + dependencies->setItemDelegateForColumn (2, new SaltGrainEditDelegate (dependencies, this, 2)); + + url_changed (QString ()); } void @@ -67,9 +138,9 @@ SaltGrainPropertiesDialog::update_controls () 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->setText (0, tl::to_qstring (d->name)); - item->setText (1, tl::to_qstring (d->version)); - item->setText (2, tl::to_qstring (d->url)); + item->setData (0, Qt::UserRole, tl::to_qstring (d->name)); + item->setData (1, Qt::UserRole, tl::to_qstring (d->version)); + item->setData (2, Qt::UserRole, tl::to_qstring (d->url)); dependencies->addTopLevelItem (item); } @@ -120,10 +191,12 @@ SaltGrainPropertiesDialog::update_data () m_grain.dependencies ().clear (); for (int i = 0; i < dependencies->topLevelItemCount (); ++i) { + QTreeWidgetItem *item = dependencies->topLevelItem (i); - QString name = item->text (0).simplified (); - QString version = item->text (1).simplified (); - QString url = item->text (2).simplified (); + 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); @@ -131,20 +204,31 @@ SaltGrainPropertiesDialog::update_data () 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; + if (column == 0 && mp_salt) { + std::string name = tl::to_string (item->data (0, Qt::UserRole).toString ().simplified ()); + item->setData (0, Qt::EditRole, tl::to_qstring (name)); + // set URL and version for known grains - std::string name = tl::to_string (item->text (0).simplified ()); if (name == m_grain.name ()) { - item->setText (1, QString ()); - item->setText (2, tr ("(must not depend on itself)")); + 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 { @@ -155,16 +239,26 @@ SaltGrainPropertiesDialog::dependency_changed (QTreeWidgetItem *item, int column } } if (g) { - item->setText (1, tl::to_qstring (g->version ())); - item->setText (2, tl::to_qstring (g->url ())); + item->setData (1, Qt::UserRole, tl::to_qstring (g->version ())); + item->setData (2, Qt::UserRole, tl::to_qstring (g->url ())); + // placeholder texts: + item->setData (1, Qt::EditRole, tl::to_qstring (g->version ())); + item->setData (2, Qt::EditRole, tl::to_qstring (g->url ())); } else { - item->setText (1, QString ()); - item->setText (2, tr ("(unknown packet)")); + 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 ("(unknown packet)")); } } + } else if (column > 0) { + item->setData (column, Qt::EditRole, item->data (column, Qt::UserRole).toString ()); } + + m_update_enabled = true; } void @@ -172,6 +266,7 @@ 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 @@ -253,6 +348,7 @@ SaltGrainPropertiesDialog::exec_dialog (lay::SaltGrain *grain, lay::Salt *salt) { m_grain = *grain; mp_salt = salt; + update_controls (); bool res = exec (); diff --git a/src/lay/laySaltGrainPropertiesDialog.h b/src/lay/laySaltGrainPropertiesDialog.h index e9eeae2d7..1ea219f98 100644 --- a/src/lay/laySaltGrainPropertiesDialog.h +++ b/src/lay/laySaltGrainPropertiesDialog.h @@ -56,6 +56,14 @@ public: */ 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 (); @@ -72,6 +80,7 @@ private: QString m_title; QString m_open_label; QString m_image_dir; + bool m_update_enabled; void update_controls (); void update_data (); From faf8acb3d1f2fc8a13c3cd35946c0a5d966fa5e7 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Mon, 20 Mar 2017 17:53:47 +0100 Subject: [PATCH 12/27] WIP: completer for name of dependency package --- src/lay/laySaltGrainPropertiesDialog.cc | 59 ++++++++++++++++++------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc index c05dbc63d..c0991894d 100644 --- a/src/lay/laySaltGrainPropertiesDialog.cc +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -30,6 +30,8 @@ #include #include #include +#include +#include namespace lay { @@ -43,8 +45,8 @@ class SaltGrainEditDelegate : public QItemDelegate { public: - SaltGrainEditDelegate (QWidget *parent, SaltGrainPropertiesDialog *dialog, int column) - : QItemDelegate (parent), mp_dialog (dialog), m_column (column) + SaltGrainEditDelegate (QWidget *parent) + : QItemDelegate (parent) { // .. nothing yet .. } @@ -67,9 +69,6 @@ public: QLineEdit *editor = dynamic_cast (widget); if (editor) { editor->setText (index.model ()->data (index, Qt::UserRole).toString ()); - if (m_column > 0) { - editor->setPlaceholderText (index.model ()->data (index, Qt::EditRole).toString ()); - } } } @@ -81,17 +80,43 @@ public: } } - QSize sizeHint (const QStyleOptionViewItem &option, const QModelIndex &index) const + QSize sizeHint (const QStyleOptionViewItem &option, const QModelIndex & /*index*/) const { - QWidget *editor = createEditor (0, option, index); - QSize size = editor->sizeHint (); - delete editor; - return size; + 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: - SaltGrainPropertiesDialog *mp_dialog; - int m_column; + QCompleter *mp_completer; }; // ---------------------------------------------------------------------------------------------------- @@ -114,9 +139,8 @@ SaltGrainPropertiesDialog::SaltGrainPropertiesDialog (QWidget *parent) connect (remove_dependency, SIGNAL (clicked ()), this, SLOT (remove_dependency_clicked ())); connect (dependencies, SIGNAL (itemChanged (QTreeWidgetItem *, int)), this, SLOT (dependency_changed (QTreeWidgetItem *, int))); - dependencies->setItemDelegateForColumn (0, new SaltGrainEditDelegate (dependencies, this, 0)); - dependencies->setItemDelegateForColumn (1, new SaltGrainEditDelegate (dependencies, this, 1)); - dependencies->setItemDelegateForColumn (2, new SaltGrainEditDelegate (dependencies, this, 2)); + dependencies->setItemDelegateForColumn (1, new SaltGrainEditDelegate (dependencies)); + dependencies->setItemDelegateForColumn (2, new SaltGrainEditDelegate (dependencies)); url_changed (QString ()); } @@ -349,6 +373,8 @@ 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 (); @@ -357,6 +383,9 @@ SaltGrainPropertiesDialog::exec_dialog (lay::SaltGrain *grain, lay::Salt *salt) *grain = m_grain; } + delete dependencies->itemDelegateForColumn (0); + dependencies->setItemDelegateForColumn (0, 0); + mp_salt = 0; return res; } From ec415d9251c747db5be47f850c8f7f276be680fb Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Mon, 20 Mar 2017 22:29:22 +0100 Subject: [PATCH 13/27] WIP: functionality of package properties editor Side effect: the log dialog now has an icon indicating whether there are errors or warnings. A new utility widget has been introduced to attach log messages (warnings/errors) to dialogs. This widget is a QToolButton that is invisible initially. It provides warning and errors channels and can be fed messages. If errors or warnings are fed, the tool button becomes visible. If (directly) embedded inside a QFrame, the frame's background will turn red to indicate the region of interest. The button can be pushed to read the log. The button is called lay::AlertLogButton and is found in layLogViewerDialog.h. TODO: move to layBasic. --- src/lay/LogViewerDialog.ui | 226 ++++-- src/lay/SaltGrainPropertiesDialog.ui | 941 ++++++++++++++---------- src/lay/images/warn.png | Bin 0 -> 637 bytes src/lay/layLogViewerDialog.cc | 144 +++- src/lay/layLogViewerDialog.h | 257 ++++++- src/lay/layResources.qrc | 1 + src/lay/laySaltGrain.cc | 22 + src/lay/laySaltGrain.h | 5 + src/lay/laySaltGrainPropertiesDialog.cc | 89 ++- src/lay/laySaltGrainPropertiesDialog.h | 3 + src/unit_tests/laySalt.cc | 6 + 11 files changed, 1174 insertions(+), 520 deletions(-) create mode 100644 src/lay/images/warn.png diff --git a/src/lay/LogViewerDialog.ui b/src/lay/LogViewerDialog.ui index eeb86327f..41834aa0c 100644 --- a/src/lay/LogViewerDialog.ui +++ b/src/lay/LogViewerDialog.ui @@ -1,117 +1,201 @@ - + + 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 - - - + + + + Separator + + + + + + + Copy + + + + + + + Clear + + + + + + + Verbosity + + + + + + QListView::Adjust - + true - - - + + + Qt::Horizontal - - QDialogButtonBox::Close + + + 101 + 0 + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + :/warn.png + + + + + + + There are errors or warnings + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + - + + verbosity_cbx + clear_pb + separator_pb + copy_pb + log_view + + + + buttonBox @@ -119,11 +203,11 @@ LogViewerDialog accept() - + 248 254 - + 157 274 @@ -135,11 +219,11 @@ LogViewerDialog reject() - + 316 260 - + 286 274 diff --git a/src/lay/SaltGrainPropertiesDialog.ui b/src/lay/SaltGrainPropertiesDialog.ui index 7ef38eda1..1f8cc352f 100644 --- a/src/lay/SaltGrainPropertiesDialog.ui +++ b/src/lay/SaltGrainPropertiesDialog.ui @@ -6,8 +6,8 @@ 0 0 - 754 - 571 + 693 + 538 @@ -41,23 +41,17 @@ 0 - - - - - 75 - true - - - - License - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + 16777215 + 80 + - + Qt::Vertical @@ -73,7 +67,10 @@ - + + + + @@ -89,297 +86,6 @@ - - - - - 1 - 0 - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - 2 - - - 6 - - - - - Qt::Vertical - - - - 20 - 0 - - - - - - - - Select a Screenshot Image - - - ... - - - - :/add.png:/add.png - - - - 64 - 64 - - - - - - - - Reset Icon - - - ... - - - - :/clear_edit.png:/clear_edit.png - - - true - - - - - - - Showcase image -(max 1024x1024) - - - - - - - Select an Icon Image - - - ... - - - - :/salt_icon.png:/salt_icon.png - - - - 64 - 64 - - - - - - - - Reset Screenshot - - - ... - - - - :/clear_edit.png:/clear_edit.png - - - true - - - - - - - Icon -(64x64) - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - 75 - true - - - - Title - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - 0 - 0 - - - - - - - - - true - - - - (use numeric versions like "1.5" or "2.1.3") - - - - - - - - 16777215 - 80 - - - - - - - - - 75 - true - - - - Documentation - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 75 - true - - - - -Images - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - - - - - - - - - 75 - true - - - - -Description - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - - - - - - 75 - true - - - - Author - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - @@ -396,66 +102,96 @@ Description - - - - - 0 - 0 - + + + + + 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 - 736 + 20 32 - - - - - 0 - 0 - - + + - true + 75 + true - (enter an URL to provide a documentation link) + Title + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - 0 - 0 - - - - - - + + - true + 75 + true - (license information like "GPLv3" or "MIT") + Depends on + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing @@ -475,41 +211,146 @@ Description - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 5 - - - + + - - + + + + + 0 + 0 + + - 75 - true + true - -Depends on - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + (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 @@ -529,24 +370,20 @@ Depends on 0 - - - - Add new dependency + + + + Qt::Vertical - - ... + + + 20 + 40 + - - - :/add.png:/add.png - - - true - - + - + Delete dependency @@ -563,18 +400,22 @@ Depends on - - - - Qt::Vertical + + + + Add new dependency - - - 20 - 40 - + + ... - + + + :/add.png:/add.png + + + true + + @@ -607,6 +448,317 @@ Depends on + + + + 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 + + + @@ -622,6 +774,13 @@ Depends on + + + lay::AlertLogButton + QToolButton +
layLogViewerDialog.h
+
+
version title diff --git a/src/lay/images/warn.png b/src/lay/images/warn.png new file mode 100644 index 0000000000000000000000000000000000000000..adea92d4f312bbcf55ab14d05e69b9a71042c2ad GIT binary patch literal 637 zcmV-@0)qXCP)FaKj!`_Q(N@y`IA9@_HdAGO&b7I-`l~UxITBnDDwI~ z=FcNMd)z1~C06>~k7$#&KfKt({K;d^C-~i)Vb+=o0Nl=D(b=#qacgnjQmIh|NCG4w zoepAt&JJlUuKh1PsZIKke;a4cc?^I7Fqii8MmEW&DartbdlI-d-MxCr3B+RZPkKX= z5Z@cZ<&}6K99DOFr61ZI7UkNO&z$p^Hxn&_+8A?vg15iVayYC?01k8!47hav@x9Zg z3c1aT954aXV4>Wv9A5;6SF^i!#kQ0Z6oJ#&7Bc{9OW7;O>M4>Q+KGS|;>?dKI9 z8gh#OW*P+2vB-b=Xj#+FI=~;B1Y8*d*AD{F%~HxiX6=max|R=sgp^oJr&M(O$P!Sz z-NKiI$>|)9O=a5*0FAQ?lkmK#C#75vj;7TInP~^rmjGwm41^FMl3fNwHZp4$Y}Yk! z2C6mevW+i5*#{K08N7Zq4j|fP02oLnVyIMVaTta?uPrJxKCIs|3{|wO(8XRi(YJmB X_oS&DXk~uz00000NkvXXu0mjfb)gLI literal 0 HcmV?d00001 diff --git a/src/lay/layLogViewerDialog.cc b/src/lay/layLogViewerDialog.cc index f6be30d4a..0ccec52ca 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; @@ -251,21 +283,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 +321,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/layResources.qrc b/src/lay/layResources.qrc index 883bcff82..ef87d4e5f 100644 --- a/src/lay/layResources.qrc +++ b/src/lay/layResources.qrc @@ -115,6 +115,7 @@ images/yellow_flag.png images/salt.png images/salt_icon.png + images/warn.png
syntax/ruby.xml diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index 266dbeff7..4aee271f1 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -185,6 +185,28 @@ SaltGrain::compare_versions (const std::string &v1, const std::string &v2) } } +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 diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h index eda1d63bd..96af788a4 100644 --- a/src/lay/laySaltGrain.h +++ b/src/lay/laySaltGrain.h @@ -352,6 +352,11 @@ public: */ 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 Detects a grain from the given directory * This method will return a grain constructed from the given directory. diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc index c0991894d..eef2ddd40 100644 --- a/src/lay/laySaltGrainPropertiesDialog.cc +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -31,6 +31,8 @@ #include #include #include +#include + #include namespace lay @@ -149,6 +151,10 @@ 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 ())); @@ -300,15 +306,27 @@ BEGIN_PROTECTED const int max_dim = 256; - QString fileName = QFileDialog::getOpenFileName (this, tr ("Pick Icon Image File"), m_image_dir, tr ("Images (*.png *.jpg)")); + 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) { - throw tl::Exception (tl::to_string (tr ("Icon image too big -\nmust be %1x%2 pixels max, but is %3x%4").arg (max_dim).arg (max_dim).arg (img.width ()).arg (img.height ()))); + 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); + } } - m_grain.set_icon (img); - m_image_dir = QFileInfo (fileName).path (); - update_icon (); + + if (ok) { + m_grain.set_icon (img); + m_image_dir = QFileInfo (fileName).path (); + update_icon (); + } + } END_PROTECTED @@ -328,15 +346,27 @@ BEGIN_PROTECTED const int max_dim = 1024; - QString fileName = QFileDialog::getOpenFileName (this, tr ("Pick Showcase Image File"), m_image_dir, tr ("Images (*.png *.jpg)")); + 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) { - throw tl::Exception (tl::to_string (tr ("Showcase image too big -\nmust be %1x%2 pixels max, but is %3x%4").arg (max_dim).arg (max_dim).arg (img.width ()).arg (img.height ()))); + 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); + } } - m_grain.set_screenshot (img); - m_image_dir = QFileInfo (fileName).path (); - update_screenshot (); + + if (ok) { + m_grain.set_screenshot (img); + m_image_dir = QFileInfo (fileName).path (); + update_screenshot (); + } + } END_PROTECTED @@ -367,6 +397,44 @@ SaltGrainPropertiesDialog::remove_dependency_clicked () } } +void +SaltGrainPropertiesDialog::accept () +{ + update_data (); + + // Perform some checks + license_alert->clear (); + if (m_grain.license ().empty ()) { + license_alert->warn () << tr ("License field is empty. Please consider specifying a license model."); + } + + version_alert->clear (); + if (m_grain.version ().empty ()) { + version_alert->warn () << tr ("Version field is empty. Please consider specifying a version number."); + } 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_alert->clear (); + // @@@ TODO + + dependencies_alert->clear (); + // @@@ TODO + + 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) { @@ -379,7 +447,6 @@ SaltGrainPropertiesDialog::exec_dialog (lay::SaltGrain *grain, lay::Salt *salt) bool res = exec (); if (res) { - update_data (); *grain = m_grain; } diff --git a/src/lay/laySaltGrainPropertiesDialog.h b/src/lay/laySaltGrainPropertiesDialog.h index 1ea219f98..cb2b62285 100644 --- a/src/lay/laySaltGrainPropertiesDialog.h +++ b/src/lay/laySaltGrainPropertiesDialog.h @@ -74,6 +74,9 @@ private slots: void remove_dependency_clicked (); void dependency_changed (QTreeWidgetItem *item, int column); +protected: + void accept (); + private: lay::SaltGrain m_grain; lay::Salt *mp_salt; diff --git a/src/unit_tests/laySalt.cc b/src/unit_tests/laySalt.cc index ae5581e72..7eb557499 100644 --- a/src/unit_tests/laySalt.cc +++ b/src/unit_tests/laySalt.cc @@ -156,6 +156,12 @@ TEST (1) 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::compare_versions ("", ""), 0); EXPECT_EQ (lay::SaltGrain::compare_versions ("1", "2"), -1); EXPECT_EQ (lay::SaltGrain::compare_versions ("1", ""), 1); From 334fca3f7619074c9654b44a1980febbca10cc88 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Tue, 21 Mar 2017 11:24:45 +0100 Subject: [PATCH 14/27] WIP: checking of grain properties. --- src/lay/laySaltGrain.cc | 42 +++++++ src/lay/laySaltGrain.h | 10 ++ src/lay/laySaltGrainPropertiesDialog.cc | 155 +++++++++++++++++++++++- 3 files changed, 203 insertions(+), 4 deletions(-) diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index 4aee271f1..5d8227592 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -185,6 +185,48 @@ SaltGrain::compare_versions (const std::string &v1, const std::string &v2) } } +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 s == n; +} + bool SaltGrain::valid_version (const std::string &v) { diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h index 96af788a4..b237243ef 100644 --- a/src/lay/laySaltGrain.h +++ b/src/lay/laySaltGrain.h @@ -357,6 +357,11 @@ public: */ 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. @@ -365,6 +370,11 @@ public: */ static SaltGrain from_path (const std::string &path); + /** + * @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 */ diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc index eef2ddd40..a3042783e 100644 --- a/src/lay/laySaltGrainPropertiesDialog.cc +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -24,6 +24,7 @@ #include "laySalt.h" #include "tlString.h" #include "tlExceptions.h" +#include "tlHttpStream.h" #include #include @@ -34,6 +35,8 @@ #include #include +#include +#include namespace lay { @@ -397,29 +400,173 @@ SaltGrainPropertiesDialog::remove_dependency_clicked () } } +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."); + 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."); + 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 (); - // @@@ TODO + 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 (); - // @@@ TODO + 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)) { + dependencies_alert->warn () << tr ("'%1' is not a name of a package loaded already").arg (tl::to_qstring (d->name)) << tl::endl + << tr ("You need to specify the details (version, URL) manually"); + } else { + 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 ("Versions help checking dependencies.") << 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 ()) { + dependencies_alert->warn () << tr ("No download URL specified for dependency '%1'").arg (tl::to_qstring (d->name)) << tl::endl + << tr ("A download URL should be specified to ensure the package dependencies can be resolved.") << tl::endl + << tr ("If the dependency package was downloaded itself, the URL is automatically set to the download source"); + } else { + std::string spec_url = SaltGrain::spec_url (d->url); + tl::InputHttpStream stream (spec_url); + try { + char b; + stream.read (&b, 1); + } catch (tl::Exception &ex) { + dependencies_alert->error () << tr ("Attempt to read download URL failed. Error details follow.") << tl::endl + << tr ("URL: ") << spec_url << tl::endl + << tr ("Message: ") << ex.msg (); + } + } + + } if (!license_alert->needs_attention () && !doc_url_alert->needs_attention () && From 10345eea7371e99182bdfb77959e585b9b29c264 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Tue, 21 Mar 2017 12:18:09 +0100 Subject: [PATCH 15/27] Restyling of log viewer Now the entries have icons telling the type of message. This is more convenient when multi-line messages are encountered. --- src/lay/LogViewerDialog.ui | 14 +++++++++++++- src/lay/images/empty_16.png | Bin 0 -> 149 bytes src/lay/images/error_16.png | Bin 0 -> 674 bytes src/lay/images/info_16.png | Bin 0 -> 499 bytes src/lay/images/warn_16.png | Bin 0 -> 637 bytes src/lay/layLogViewerDialog.cc | 17 ++++++++++++----- src/lay/layResources.qrc | 4 ++++ src/lay/laySaltGrainPropertiesDialog.cc | 12 ++++++------ 8 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 src/lay/images/empty_16.png create mode 100644 src/lay/images/error_16.png create mode 100644 src/lay/images/info_16.png create mode 100644 src/lay/images/warn_16.png diff --git a/src/lay/LogViewerDialog.ui b/src/lay/LogViewerDialog.ui index 41834aa0c..889342ade 100644 --- a/src/lay/LogViewerDialog.ui +++ b/src/lay/LogViewerDialog.ui @@ -88,12 +88,24 @@ + + + 16 + 16 + + + + false + QListView::Adjust true + + false + @@ -157,7 +169,7 @@ - :/warn.png + :/warn_16.png diff --git a/src/lay/images/empty_16.png b/src/lay/images/empty_16.png new file mode 100644 index 0000000000000000000000000000000000000000..10b1f1be69386ddc01a2e7bfea83c208f1210e5a GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ zFy8=S#yt0AQ=p(^iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0vQo-U3d l7QM*{60D08M0%JwfQAb&FxLB4^Z{iVJYD@<);T3K0RT{2BXa-% literal 0 HcmV?d00001 diff --git a/src/lay/images/error_16.png b/src/lay/images/error_16.png new file mode 100644 index 0000000000000000000000000000000000000000..c9dad1310711826bf2c1dd147eca79606aefe4b2 GIT binary patch literal 674 zcmV;T0$u%yP)8*v!MpYNrYN-kv4G=nk8kTiF_LMgU`U>xe; zY)*tWPJ!g6fD%Z0|3NuR_Yk=bDiV||f`eO4ai~8iNw4QxV~cYjriI?U9io9q6Djmv z-r@Z|&-=U|zQF&kVzDSElgW=_Jl+umfdoMyNfbMZq8wxc#djGdhUySsqf4R}0&Ab?%>FEcaFi%f>4(`i}Pb;|_+ zfk2>|n3@W`tk)U#_kp7$Fqf-q!1I9H4aV~T&*MR-!+wOr_;!B&#ImgS8~~W6`7AXt z!RYW1xV#)rx!p!8k-%5ed=3Ej0O0$@#fa2u_1?HnPX4N;l@$QMW1o*To6Qjb;JPFj z4PSf92ms?a>Lmz{kN>iKzWYzFq1uy^J!pLpJstpn*=kWR82o8jRyV;TSw7xuHi6O6 zyVrZY*r?SY%krn*3DIaYyIHBgQdI|x4Hh$ox+pqZl?r09ShjDbs_JSa5}`)9Z2z8} zC3||B?76wVkMwza+YW_7q^jzhp66|oYPFhwwYZ3sBtdq$7P*#nx;Lx8`3}4AB3RNj1pk!dH?_b07*qo IM6N<$f&?lsN&o-= literal 0 HcmV?d00001 diff --git a/src/lay/images/info_16.png b/src/lay/images/info_16.png new file mode 100644 index 0000000000000000000000000000000000000000..d1fa366446e37390cac4e7ef610c24115660d29d GIT binary patch literal 499 zcmVMWLn|PCy!h8=)p#fXW*e#HJ`D5dYoe&Hv5+W`KW6t$XPA`}?M8+5ljh zW>_wl#~T59z2GF9-MP)>c56`-VLsPjW?`l$W^Ox<^P29bbm9B{Wj=rCJC0qOPN#_D zuk@gE0NpSQTPqWEI{s;~c+?XS8jmLs(I}OQ0D#fx9n1t~1`)?Zs%}}<8vqyyN$Kn1 za7aom8kv{ErDZCmZU9^)2|Uls5g8xMq=+~HEGE*gn!!w3L=G&=iUmMFrqF7G;th$H|aJpiD$@as5^2MM6rY)+KI^A>=PZQGv-005Li8$i5) zd8V~)C%*JcxBJp!raNXf*IZ$ylyOxkSPws*rDHc5XZL`v1MslC6Aj=|MCxhtp9EFaKj!`_Q(N@y`IA9@_HdAGO&b7I-`l~UxITBnDDwI~ z=FcNMd)z1~C06>~k7$#&KfKt({K;d^C-~i)Vb+=o0Nl=D(b=#qacgnjQmIh|NCG4w zoepAt&JJlUuKh1PsZIKke;a4cc?^I7Fqii8MmEW&DartbdlI-d-MxCr3B+RZPkKX= z5Z@cZ<&}6K99DOFr61ZI7UkNO&z$p^Hxn&_+8A?vg15iVayYC?01k8!47hav@x9Zg z3c1aT954aXV4>Wv9A5;6SF^i!#kQ0Z6oJ#&7Bc{9OW7;O>M4>Q+KGS|;>?dKI9 z8gh#OW*P+2vB-b=Xj#+FI=~;B1Y8*d*AD{F%~HxiX6=max|R=sgp^oJr&M(O$P!Sz z-NKiI$>|)9O=a5*0FAQ?lkmK#C#75vj;7TInP~^rmjGwm41^FMl3fNwHZp4$Y}Yk! z2C6mevW+i5*#{K08N7Zq4j|fP02oLnVyIMVaTta?uPrJxKCIs|3{|wO(8XRi(YJmB X_oS&DXk~uz00000NkvXXu0mjfA= 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) { diff --git a/src/lay/layResources.qrc b/src/lay/layResources.qrc index ef87d4e5f..3ffd9e81e 100644 --- a/src/lay/layResources.qrc +++ b/src/lay/layResources.qrc @@ -116,6 +116,10 @@ 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 syntax/ruby.xml diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc index a3042783e..88fd3d652 100644 --- a/src/lay/laySaltGrainPropertiesDialog.cc +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -519,8 +519,8 @@ SaltGrainPropertiesDialog::accept () 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'"); + << tr ("Valid package names are words (letters, digits, underscores).") << tl::endl + << tr ("Package groups can be specified in the form 'group/package'."); continue; } @@ -539,20 +539,20 @@ SaltGrainPropertiesDialog::accept () 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."); + << 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 ("Versions help checking dependencies.") << tl::endl - << tr ("If the dependency package has a version itself, the version is automatically set to it's current version"); + << 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 ()) { dependencies_alert->warn () << tr ("No download URL specified for dependency '%1'").arg (tl::to_qstring (d->name)) << tl::endl << tr ("A download URL should be specified to ensure the package dependencies can be resolved.") << tl::endl - << tr ("If the dependency package was downloaded itself, the URL is automatically set to the download source"); + << tr ("If the dependency package was downloaded itself, the URL is automatically set to the download source."); } else { std::string spec_url = SaltGrain::spec_url (d->url); tl::InputHttpStream stream (spec_url); From 820c2916234cb148a690a616d3b203013d1a9c38 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Tue, 21 Mar 2017 22:20:24 +0100 Subject: [PATCH 16/27] WIP: salt package manager: features for installer First steps towards installer support. Specifically: - basic installation methods, basic framework - file utilities for directory copy Side effect: temp directories of unit tests are now cleared prior to test run. --- src/lay/lay.pro | 6 +- src/lay/laySalt.cc | 180 +++++++++++++++++++++++- src/lay/laySalt.h | 56 ++++++-- src/lay/laySaltDownloadManager.cc | 40 ++++++ src/lay/laySaltDownloadManager.h | 60 ++++++++ src/lay/laySaltGrainPropertiesDialog.cc | 4 +- src/lay/laySaltManagerDialog.cc | 1 + src/tl/tlFileUtils.cc | 92 +++++++++++- src/tl/tlFileUtils.h | 42 +++++- src/unit_tests/laySalt.cc | 8 +- src/unit_tests/tlFileUtils.cc | 155 ++++++++++++++++++++ src/unit_tests/unit_tests.pro | 3 +- src/ut/utMain.cc | 7 +- 13 files changed, 623 insertions(+), 31 deletions(-) create mode 100644 src/lay/laySaltDownloadManager.cc create mode 100644 src/lay/laySaltDownloadManager.h create mode 100644 src/unit_tests/tlFileUtils.cc diff --git a/src/lay/lay.pro b/src/lay/lay.pro index da3d53cb0..4c4c804f7 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -51,7 +51,8 @@ HEADERS = \ laySaltGrains.h \ laySaltManagerDialog.h \ laySaltGrainDetailsTextWidget.h \ - laySaltGrainPropertiesDialog.h + laySaltGrainPropertiesDialog.h \ + laySaltDownloadManager.h FORMS = \ ClipDialog.ui \ @@ -147,7 +148,8 @@ SOURCES = \ laySaltGrains.cc \ laySaltManagerDialog.cc \ laySaltGrainDetailsTextWidget.cc \ - laySaltGrainPropertiesDialog.cc + laySaltGrainPropertiesDialog.cc \ + laySaltDownloadManager.cc RESOURCES = layBuildInMacros.qrc \ layHelpResources.qrc \ diff --git a/src/lay/laySalt.cc b/src/lay/laySalt.cc index 24559a2d1..156cf0ec3 100644 --- a/src/lay/laySalt.cc +++ b/src/lay/laySalt.cc @@ -21,9 +21,14 @@ */ #include "laySalt.h" +#include "laySaltDownloadManager.h" #include "tlString.h" +#include "tlFileUtils.h" +#include "tlLog.h" +#include "tlInternational.h" #include +#include namespace lay { @@ -47,6 +52,32 @@ Salt &Salt::operator= (const Salt &other) 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) { @@ -60,8 +91,7 @@ Salt::add_location (const std::string &path) lay::SaltGrains gg = lay::SaltGrains::from_path (path); m_root.add_collection (gg); - mp_flat_grains.clear (); - emit collections_changed (); + invalidate (); } void @@ -71,8 +101,7 @@ Salt::remove_location (const std::string &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); - mp_flat_grains.clear (); - emit collections_changed (); + invalidate (); return; } } @@ -87,8 +116,7 @@ Salt::refresh () } if (new_root != m_root) { m_root = new_root; - mp_flat_grains.clear (); - emit collections_changed (); + invalidate (); } } @@ -119,12 +147,150 @@ struct NameCompare } void -Salt::ensure_flat_present () +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; + } + } + + 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 ())); + invalidate (); + return true; + } else { + tl::warn << QObject::tr ("Failed to remove package '%1'.").arg (tl::to_qstring (grain.name ())); + return false; + } +} + +bool +Salt::create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManager &download_manager) +{ + tl_assert (!m_root.is_empty ()); + + const SaltGrains *coll = m_root.begin_collections ().operator-> (); + + 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) { + QDir subdir (target_dir.filePath (tl::to_qstring (*n))); + 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 ()))); + } + } + } + + } catch (tl::Exception &ex) { + tl::error << ex.msg (); + return false; + } + + bool res = true; + + target = templ; + target.set_path (tl::to_string (target_dir.absolutePath ())); + + if (! templ.path ().empty ()) { + + // 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 (! 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 = download_manager.download (templ.url (), target.path ()); + + } + + if (res) { + + tl::info << QObject::tr ("Package '%1' installed").arg (tl::to_qstring (target.name ())); + + target.set_installed_time (QDateTime::currentDateTime ()); + target.save (); + + } 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 index 19d21145f..4ade8acf9 100644 --- a/src/lay/laySalt.h +++ b/src/lay/laySalt.h @@ -29,9 +29,13 @@ #include +#include + namespace lay { +class SaltDownloadManager; + /** * @brief The global salt (package manager) object * This object can be configured to represent a couple of locations. @@ -107,20 +111,48 @@ public: /** * @brief A flat iterator of (sorted) grains (begin) */ - flat_iterator begin_flat () - { - ensure_flat_present (); - return mp_flat_grains.begin (); - } + flat_iterator begin_flat (); /** * @brief A flat iterator of (sorted) grains (end) */ - flat_iterator end_flat () - { - ensure_flat_present (); - return mp_flat_grains.end (); - } + flat_iterator end_flat (); + + /** + * @brief Gets the grain with the given name + */ + SaltGrain *grain_by_name (const std::string &name); + + /** + * @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. + * + * The target's name must be set. If a specific target location is desired, the target's + * path must be set too. + * + * This method refuses to overwrite existing grains, so an update needs to be performed by first + * deleting the grain and then re-installing it. + * + * 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, SaltDownloadManager &download_manager); signals: /** @@ -131,8 +163,10 @@ signals: private: SaltGrains m_root; std::vector mp_flat_grains; + std::map m_grains_by_name; - void ensure_flat_present (); + void validate (); + void invalidate (); void add_collection_to_flat (lay::SaltGrains &gg); }; diff --git a/src/lay/laySaltDownloadManager.cc b/src/lay/laySaltDownloadManager.cc new file mode 100644 index 000000000..f6970b851 --- /dev/null +++ b/src/lay/laySaltDownloadManager.cc @@ -0,0 +1,40 @@ + +/* + + 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" + +namespace lay +{ + +SaltDownloadManager::SaltDownloadManager () +{ + // .. nothing yet .. +} + +bool +SaltDownloadManager::download (const std::string &url, const std::string &target_dir) +{ + // @@@ + return false; +} + +} diff --git a/src/lay/laySaltDownloadManager.h b/src/lay/laySaltDownloadManager.h new file mode 100644 index 000000000..141df238a --- /dev/null +++ b/src/lay/laySaltDownloadManager.h @@ -0,0 +1,60 @@ + +/* + + 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 +#include + +namespace lay +{ + +/** + * @brief The download manager + * This class is responsible for handling the downloads for + * grains. + */ +class LAY_PUBLIC SaltDownloadManager + : public QObject +{ +Q_OBJECT + +public: + /** + * @brief Default constructor + */ + SaltDownloadManager (); + + /** + * @brief Downloads the files from the given URL to the given target location + * The target directory needs to exist. + * Returns true, if the download was successful, false otherwise. + */ + bool download (const std::string &url, const std::string &target_dir); +}; + +} + +#endif diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc index 88fd3d652..ab4e08d43 100644 --- a/src/lay/laySaltGrainPropertiesDialog.cc +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -593,8 +593,10 @@ SaltGrainPropertiesDialog::exec_dialog (lay::SaltGrain *grain, lay::Salt *salt) update_controls (); bool res = exec (); - if (res) { + if (res && *grain != m_grain) { *grain = m_grain; + // save modified grain + grain->save (); } delete dependencies->itemDelegateForColumn (0); diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 8c83aa9f8..7730310bc 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -313,6 +313,7 @@ SaltManagerDialog::current_changed () } else { details_frame->setEnabled (true); delete_button->setEnabled (true); + edit_button->setEnabled (! g->is_readonly ()); } } diff --git a/src/tl/tlFileUtils.cc b/src/tl/tlFileUtils.cc index 518bb7989..9ae545118 100644 --- a/src/tl/tlFileUtils.cc +++ b/src/tl/tlFileUtils.cc @@ -21,6 +21,8 @@ */ #include "tlFileUtils.h" +#include "tlLog.h" +#include "tlInternational.h" #include #include @@ -28,10 +30,27 @@ 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)); @@ -39,15 +58,82 @@ rm_dir_recursive (const QString &path) if (! rm_dir_recursive (fi.filePath ())) { return false; } - if (! dir.rmdir (*e)) { - 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 index 01c171a99..be25bd843 100644 --- a/src/tl/tlFileUtils.h +++ b/src/tl/tlFileUtils.h @@ -24,17 +24,57 @@ #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. + * @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/unit_tests/laySalt.cc b/src/unit_tests/laySalt.cc index 7eb557499..62a62826a 100644 --- a/src/unit_tests/laySalt.cc +++ b/src/unit_tests/laySalt.cc @@ -201,8 +201,6 @@ TEST (3) QDir dir_cc (dir_c.filePath (QString::fromUtf8 ("c"))); QDir dir_ccv (dir_cc.filePath (QString::fromUtf8 ("v"))); - tl_assert (tl::rm_dir_recursive (tmp_dir.path ())); - lay::SaltGrains gg; gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); EXPECT_EQ (gg.is_empty (), true); @@ -275,8 +273,6 @@ TEST (4) QDir dir_cc (dir_c.filePath (QString::fromUtf8 ("c"))); QDir dir_ccv (dir_cc.filePath (QString::fromUtf8 ("v"))); - tl_assert (tl::rm_dir_recursive (tmp_dir.path ())); - lay::SaltGrains gg; gg = lay::SaltGrains::from_path (tl::to_string (tmp_dir.path ())); EXPECT_EQ (gg.is_empty (), true); @@ -341,4 +337,8 @@ TEST (4) 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/unit_tests.pro b/src/unit_tests/unit_tests.pro index 547d7616c..31dd201a4 100644 --- a/src/unit_tests/unit_tests.pro +++ b/src/unit_tests/unit_tests.pro @@ -96,7 +96,8 @@ SOURCES = \ tlXMLParser.cc \ gsiTest.cc \ tlFileSystemWatcher.cc \ - laySalt.cc + laySalt.cc \ + tlFileUtils.cc # main components: SOURCES += \ diff --git a/src/ut/utMain.cc b/src/ut/utMain.cc index 8a563e177..359e2bad5 100644 --- a/src/ut/utMain.cc +++ b/src/ut/utMain.cc @@ -26,6 +26,7 @@ #include "pya.h" #include "tlStaticObjects.h" #include "tlTimer.h" +#include "tlFileUtils.h" #include "layApplication.h" #include "gsiExpression.h" #include "gsiExternalMain.h" @@ -518,8 +519,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)); From 1353c9dfb0403061373001a891408232400f7c88 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Tue, 21 Mar 2017 22:53:04 +0100 Subject: [PATCH 17/27] WIP: download capabilities for salt grains. --- src/lay/laySaltGrain.cc | 22 +++++++++++++++++++++- src/lay/laySaltGrain.h | 14 ++++++++++++++ src/lay/laySaltGrainPropertiesDialog.cc | 15 +++++++++------ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index 5d8227592..49152e4ae 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -23,6 +23,7 @@ #include "laySaltGrain.h" #include "tlString.h" #include "tlXMLParser.h" +#include "tlHttpStream.h" #include #include @@ -319,7 +320,7 @@ static tl::XMLStruct xml_struct ("salt-grain", bool SaltGrain::is_readonly () const { - return QFileInfo (tl::to_qstring (path ())).isWritable (); + return !QFileInfo (tl::to_qstring (path ())).isWritable (); } void @@ -329,6 +330,13 @@ SaltGrain::load (const std::string &p) xml_struct.parse (source, *this); } +void +SaltGrain::load (tl::InputStream &p) +{ + tl::XMLStreamSource source (p); + xml_struct.parse (source, *this); +} + void SaltGrain::save () const { @@ -353,6 +361,18 @@ SaltGrain::from_path (const std::string &path) return g; } +SaltGrain +SaltGrain::from_url (const std::string &url) +{ + 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) { diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h index b237243ef..ce21ae0f1 100644 --- a/src/lay/laySaltGrain.h +++ b/src/lay/laySaltGrain.h @@ -25,6 +25,7 @@ #include "layCommon.h" #include "tlObject.h" +#include "tlStream.h" #include #include @@ -334,6 +335,11 @@ public: */ 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 */ @@ -370,6 +376,14 @@ public: */ 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 */ diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc index ab4e08d43..3059cb653 100644 --- a/src/lay/laySaltGrainPropertiesDialog.cc +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -554,14 +554,17 @@ SaltGrainPropertiesDialog::accept () << tr ("A download URL should be specified to ensure the package dependencies can be resolved.") << tl::endl << tr ("If the dependency package was downloaded itself, the URL is automatically set to the download source."); } else { - std::string spec_url = SaltGrain::spec_url (d->url); - tl::InputHttpStream stream (spec_url); + SaltGrain gdep; try { - char b; - stream.read (&b, 1); + 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 read download URL failed. Error details follow.") << tl::endl - << tr ("URL: ") << spec_url << tl::endl + 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 (); } } From 4a904791870e11d69d32089131900395d283746f Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Thu, 23 Mar 2017 22:19:13 +0100 Subject: [PATCH 18/27] WIP: package template selection dialog Plus some initial package templates (without files). --- src/lay/SaltGrainTemplateSelectionDialog.ui | 237 ++++++++++++++++++++ src/lay/lay.pro | 4 +- src/lay/laySalt.cc | 14 +- src/lay/laySaltGrain.cc | 37 ++- src/lay/laySaltGrains.cc | 108 +++++++-- src/lay/laySaltManagerDialog.cc | 107 +++++++++ src/lay/laySaltManagerDialog.h | 15 ++ src/lay/laySaltTemplates.qrc | 47 ++++ src/lay/salt_templates/font/grain.xml | 16 ++ src/lay/salt_templates/lib/grain.xml | 16 ++ src/lay/salt_templates/macro/grain.xml | 16 ++ src/lay/salt_templates/pcell_lib/grain.xml | 16 ++ src/lay/salt_templates/pymacro/grain.xml | 16 ++ src/lay/salt_templates/tech/grain.xml | 16 ++ 14 files changed, 633 insertions(+), 32 deletions(-) create mode 100644 src/lay/SaltGrainTemplateSelectionDialog.ui create mode 100644 src/lay/laySaltTemplates.qrc create mode 100644 src/lay/salt_templates/font/grain.xml create mode 100644 src/lay/salt_templates/lib/grain.xml create mode 100644 src/lay/salt_templates/macro/grain.xml create mode 100644 src/lay/salt_templates/pcell_lib/grain.xml create mode 100644 src/lay/salt_templates/pymacro/grain.xml create mode 100644 src/lay/salt_templates/tech/grain.xml 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/lay.pro b/src/lay/lay.pro index 4c4c804f7..7b684827e 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -99,7 +99,8 @@ FORMS = \ TechSaveOptionsEditorPage.ui \ MainConfigPage7.ui \ SaltManagerDialog.ui \ - SaltGrainPropertiesDialog.ui + SaltGrainPropertiesDialog.ui \ + SaltGrainTemplateSelectionDialog.ui SOURCES = \ gsiDeclLayApplication.cc \ @@ -156,6 +157,7 @@ RESOURCES = layBuildInMacros.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/laySalt.cc b/src/lay/laySalt.cc index 156cf0ec3..6e3ba10cb 100644 --- a/src/lay/laySalt.cc +++ b/src/lay/laySalt.cc @@ -81,11 +81,15 @@ Salt::grain_by_name (const std::string &name) void Salt::add_location (const std::string &path) { - // 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; + 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; + } } } diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index 49152e4ae..af3f39130 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -28,6 +28,7 @@ #include #include #include +#include namespace lay { @@ -326,8 +327,28 @@ SaltGrain::is_readonly () const void SaltGrain::load (const std::string &p) { - tl::XMLFileSource source (p); - xml_struct.parse (source, *this); + tl_assert (!p.empty ()); + + if (p[0] != ':') { + + tl::XMLFileSource source (p); + 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); + xml_struct.parse (source, *this); + + } } void @@ -376,9 +397,15 @@ SaltGrain::from_url (const std::string &url) bool SaltGrain::is_grain (const std::string &path) { - QDir dir (tl::to_qstring (path)); - QString gf = dir.filePath (tl::to_qstring (grain_filename)); - return QFileInfo (gf).exists (); + 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/laySaltGrains.cc b/src/lay/laySaltGrains.cc index 9c17944c6..5c80cc884 100644 --- a/src/lay/laySaltGrains.cc +++ b/src/lay/laySaltGrains.cc @@ -26,6 +26,7 @@ #include #include +#include namespace lay { @@ -129,37 +130,102 @@ 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); - 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) { + 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); + } + } - 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); + } + } + } + } } diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 7730310bc..71a34dba3 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -23,6 +23,7 @@ #include "laySaltManagerDialog.h" #include "laySaltGrainPropertiesDialog.h" #include "laySalt.h" +#include "ui_SaltGrainTemplateSelectionDialog.h" #include "tlString.h" #include @@ -33,6 +34,7 @@ #include #include #include +#include namespace lay { @@ -219,6 +221,56 @@ public: } }; +// -------------------------------------------------------------------------------------- + +/** + * @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) + : QDialog (parent) + { + 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); + + g->set_name (tl::to_string (name_edit->text ().simplified ())); + + return *g; + } + + 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 { + QDialog::accept (); + } + } + +private: + lay::Salt m_salt_templates; +}; + // -------------------------------------------------------------------------------------- // SaltManager implementation @@ -242,6 +294,9 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) 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 (install_button, SIGNAL (clicked ()), this, SLOT (install_grain ())); // @@@ salt = lay::Salt (); salt_initialized = false; @@ -279,6 +334,58 @@ SaltManagerDialog::edit_properties () } } +// @@@ +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 .. + } +}; + +} +// @@@ + +void +SaltManagerDialog::create_grain () +{ + SaltGrainTemplateSelectionDialog temp_dialog (this); + if (temp_dialog.exec ()) { + + // @@@ + + } +} + +void +SaltManagerDialog::delete_grain () +{ + + // @@@ + +} + +void +SaltManagerDialog::install_grain () +{ + + // @@@ + +} + void SaltManagerDialog::salt_changed () { diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h index 170e02b7d..232d40403 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -64,6 +64,21 @@ private slots: */ void edit_properties (); + /** + * @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 "install" button is pressed + */ + void install_grain (); + private: lay::Salt *mp_salt; bool m_current_changed_enabled; 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 + + + + + + + From 10b09c35756928dded2284984ff156c0f557b27d Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Thu, 23 Mar 2017 23:46:25 +0100 Subject: [PATCH 19/27] WIP: create packages from templates Now, packages are actually created. --- src/lay/laySalt.cc | 124 +++++++++++++++++++++++++++++--- src/lay/laySalt.h | 2 +- src/lay/laySaltGrain.cc | 2 +- src/lay/laySaltManagerDialog.cc | 94 ++++++++++++++---------- src/unit_tests/laySalt.cc | 8 ++- 5 files changed, 182 insertions(+), 48 deletions(-) diff --git a/src/lay/laySalt.cc b/src/lay/laySalt.cc index 6e3ba10cb..b71cbc107 100644 --- a/src/lay/laySalt.cc +++ b/src/lay/laySalt.cc @@ -29,6 +29,7 @@ #include #include +#include namespace lay { @@ -215,8 +216,91 @@ Salt::remove_grain (const SaltGrain &grain) } } +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, SaltDownloadManager &download_manager) +Salt::create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManager *download_manager) { tl_assert (!m_root.is_empty ()); @@ -243,15 +327,23 @@ Salt::create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManag // 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) { - QDir subdir (target_dir.filePath (tl::to_qstring (*n))); - if (! subdir.exists ()) { + + 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) { @@ -261,30 +353,46 @@ Salt::create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManag 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 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 ()); + 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 ()) { + tl_assert (download_manager != 0); + // 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 = download_manager.download (templ.url (), target.path ()); + res = download_manager->download (templ.url (), target.path ()); } 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 ())); diff --git a/src/lay/laySalt.h b/src/lay/laySalt.h index 4ade8acf9..ad578c737 100644 --- a/src/lay/laySalt.h +++ b/src/lay/laySalt.h @@ -152,7 +152,7 @@ public: * * Returns true, if the package could be created successfully. */ - bool create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManager &download_manager); + bool create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManager *download_manager = 0); signals: /** diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index af3f39130..246d44278 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -226,7 +226,7 @@ SaltGrain::valid_name (const std::string &n) // this captures the cases where the extractor skips blanks // TODO: the extractor should have a "non-blank-skipping" mode - return s == n; + return res == n; } bool diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 71a34dba3..568040404 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -25,6 +25,7 @@ #include "laySalt.h" #include "ui_SaltGrainTemplateSelectionDialog.h" #include "tlString.h" +#include "tlExceptions.h" #include #include @@ -152,7 +153,7 @@ public: void update () { - // @@@ + reset (); } public: @@ -230,8 +231,8 @@ class SaltGrainTemplateSelectionDialog : public QDialog, private Ui::SaltGrainTemplateSelectionDialog { public: - SaltGrainTemplateSelectionDialog (QWidget *parent) - : QDialog (parent) + SaltGrainTemplateSelectionDialog (QWidget *parent, lay::Salt *salt) + : QDialog (parent), mp_salt (salt) { Ui::SaltGrainTemplateSelectionDialog::setupUi (this); @@ -249,11 +250,14 @@ public: SaltGrain *g = model->grain_from_index (salt_view->currentIndex ()); tl_assert (g != 0); - g->set_name (tl::to_string (name_edit->text ().simplified ())); - return *g; } + std::string name () const + { + return tl::to_string (name_edit->text ()); + } + void accept () { name_alert->clear (); @@ -263,12 +267,23 @@ public: } 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; }; // -------------------------------------------------------------------------------------- @@ -310,11 +325,6 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) connect (mp_salt, SIGNAL (collections_changed ()), this, SLOT (salt_changed ())); - // select the first grain - if (model->rowCount (QModelIndex ()) > 0) { - salt_view->setCurrentIndex (model->index (0, 0, QModelIndex ())); - } - salt_changed (); connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); @@ -334,40 +344,41 @@ SaltManagerDialog::edit_properties () } } -// @@@ -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 .. - } -}; - -} -// @@@ - void SaltManagerDialog::create_grain () { - SaltGrainTemplateSelectionDialog temp_dialog (this); +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 @@ -399,11 +410,20 @@ SaltManagerDialog::salt_changed () 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 ())); + } + } current_changed (); diff --git a/src/unit_tests/laySalt.cc b/src/unit_tests/laySalt.cc index 62a62826a..9d3ef0a36 100644 --- a/src/unit_tests/laySalt.cc +++ b/src/unit_tests/laySalt.cc @@ -162,6 +162,13 @@ TEST (2) 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); @@ -185,7 +192,6 @@ TEST (2) EXPECT_EQ (lay::SaltGrain::compare_versions ("991", "990"), 1); } - TEST (3) { const QString grain_spec_file = QString::fromUtf8 ("grain.xml"); From d160a5c27c626c8e0609b22a490f9decad996198 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sat, 25 Mar 2017 00:08:17 +0100 Subject: [PATCH 20/27] WIP: delete function, installation dialog (first steps) --- src/lay/SaltGrainInstallationDialog.ui | 313 ++++++++++++++++++++++ src/lay/lay.pro | 11 +- src/lay/laySalt.cc | 7 + src/lay/laySalt.h | 25 ++ src/lay/laySaltGrain.cc | 21 +- src/lay/laySaltGrain.h | 6 + src/lay/laySaltGrainInstallationDialog.cc | 101 +++++++ src/lay/laySaltGrainInstallationDialog.h | 76 ++++++ src/lay/laySaltGrains.cc | 28 ++ src/lay/laySaltGrains.h | 15 ++ src/lay/laySaltManagerDialog.cc | 209 ++------------- src/lay/laySaltModel.cc | 207 ++++++++++++++ src/lay/laySaltModel.h | 110 ++++++++ src/unit_tests/laySalt.cc | 9 + 14 files changed, 941 insertions(+), 197 deletions(-) create mode 100644 src/lay/SaltGrainInstallationDialog.ui create mode 100644 src/lay/laySaltGrainInstallationDialog.cc create mode 100644 src/lay/laySaltGrainInstallationDialog.h create mode 100644 src/lay/laySaltModel.cc create mode 100644 src/lay/laySaltModel.h diff --git a/src/lay/SaltGrainInstallationDialog.ui b/src/lay/SaltGrainInstallationDialog.ui new file mode 100644 index 000000000..ab54731cb --- /dev/null +++ b/src/lay/SaltGrainInstallationDialog.ui @@ -0,0 +1,313 @@ + + + SaltGrainInstallationDialog + + + + 0 + 0 + 692 + 440 + + + + Salt Mine Repository Browser + + + + + + Qt::Horizontal + + + + + 4 + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Repository + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Install + + + + + + + Qt::Horizontal + + + + 126 + 20 + + + + + + + + + 0 + 0 + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 64 + 64 + + + + false + + + + + + + + + + + + 1 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 4 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Details + + + + + + + true + + + + + + + + + + + QDialogButtonBox::Apply|QDialogButtonBox::Close + + + + + + + + :/add.png:/add.png + + + New + + + New package + + + + + + :/clear.png:/clear.png + + + Delete + + + Delete package + + + + + + :/import.png:/import.png + + + Import + + + Import package + + + + + + lay::DecoratedLineEdit + QLineEdit +
layWidgets.h
+
+ + lay::SaltGrainDetailsTextWidget + QTextBrowser +
laySaltGrainDetailsTextWidget.h
+
+
+ + details_text + + + + + + + button_box + rejected() + SaltGrainInstallationDialog + reject() + + + 559 + 425 + + + 576 + 437 + + + + +
diff --git a/src/lay/lay.pro b/src/lay/lay.pro index 7b684827e..0d1251609 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -52,7 +52,9 @@ HEADERS = \ laySaltManagerDialog.h \ laySaltGrainDetailsTextWidget.h \ laySaltGrainPropertiesDialog.h \ - laySaltDownloadManager.h + laySaltDownloadManager.h \ + laySaltModel.h \ + laySaltGrainInstallationDialog.h FORMS = \ ClipDialog.ui \ @@ -100,7 +102,8 @@ FORMS = \ MainConfigPage7.ui \ SaltManagerDialog.ui \ SaltGrainPropertiesDialog.ui \ - SaltGrainTemplateSelectionDialog.ui + SaltGrainTemplateSelectionDialog.ui \ + SaltGrainInstallationDialog.ui SOURCES = \ gsiDeclLayApplication.cc \ @@ -150,7 +153,9 @@ SOURCES = \ laySaltManagerDialog.cc \ laySaltGrainDetailsTextWidget.cc \ laySaltGrainPropertiesDialog.cc \ - laySaltDownloadManager.cc + laySaltDownloadManager.cc \ + laySaltModel.cc \ + laySaltGrainInstallationDialog.cc RESOURCES = layBuildInMacros.qrc \ layHelpResources.qrc \ diff --git a/src/lay/laySalt.cc b/src/lay/laySalt.cc index b71cbc107..c16bc8e56 100644 --- a/src/lay/laySalt.cc +++ b/src/lay/laySalt.cc @@ -189,6 +189,7 @@ bool remove_from_collection (SaltGrains &collection, const std::string &name) ++gnext; collection.remove_grain (g, true); res = true; + g = gnext; } } @@ -207,9 +208,15 @@ 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; diff --git a/src/lay/laySalt.h b/src/lay/laySalt.h index ad578c737..ff16c0280 100644 --- a/src/lay/laySalt.h +++ b/src/lay/laySalt.h @@ -123,6 +123,31 @@ public: */ SaltGrain *grain_by_name (const std::string &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 * diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index 246d44278..61ceb8fef 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -297,7 +297,7 @@ struct ImageConverter } }; -static tl::XMLStruct xml_struct ("salt-grain", +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") + @@ -315,8 +315,15 @@ static tl::XMLStruct xml_struct ("salt-grain", 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 @@ -332,7 +339,7 @@ SaltGrain::load (const std::string &p) if (p[0] != ':') { tl::XMLFileSource source (p); - xml_struct.parse (source, *this); + s_xml_struct.parse (source, *this); } else { @@ -346,7 +353,7 @@ SaltGrain::load (const std::string &p) std::string str_data (data.constData (), data.size ()); tl::XMLStringSource source (str_data); - xml_struct.parse (source, *this); + s_xml_struct.parse (source, *this); } } @@ -355,7 +362,7 @@ void SaltGrain::load (tl::InputStream &p) { tl::XMLStreamSource source (p); - xml_struct.parse (source, *this); + s_xml_struct.parse (source, *this); } void @@ -368,7 +375,7 @@ void SaltGrain::save (const std::string &p) const { tl::OutputStream os (p, tl::OutputStream::OM_Plain); - xml_struct.write (os, *this); + s_xml_struct.write (os, *this); } SaltGrain diff --git a/src/lay/laySaltGrain.h b/src/lay/laySaltGrain.h index ce21ae0f1..df30e7f49 100644 --- a/src/lay/laySaltGrain.h +++ b/src/lay/laySaltGrain.h @@ -26,6 +26,7 @@ #include "layCommon.h" #include "tlObject.h" #include "tlStream.h" +#include "tlXMLParser.h" #include #include @@ -350,6 +351,11 @@ public: */ 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. diff --git a/src/lay/laySaltGrainInstallationDialog.cc b/src/lay/laySaltGrainInstallationDialog.cc new file mode 100644 index 000000000..f12d7f576 --- /dev/null +++ b/src/lay/laySaltGrainInstallationDialog.cc @@ -0,0 +1,101 @@ + +/* + + 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 "laySaltGrainInstallationDialog.h" +#include "laySaltModel.h" +#include "laySaltGrainPropertiesDialog.h" +#include "laySalt.h" +#include "ui_SaltGrainTemplateSelectionDialog.h" +#include "tlString.h" +#include "tlExceptions.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lay +{ + +// -------------------------------------------------------------------------------------- +// SaltGrainInstallation implementation + +SaltGrainInstallationDialog::SaltGrainInstallationDialog (QWidget *parent, lay::Salt *salt) + : QDialog (parent), mp_salt (salt) +{ + Ui::SaltGrainInstallationDialog::setupUi (this); + + // TODO: cache package list + // @@@ + m_salt_mine.load ("/home/matthias/salt.mine"); + // @@@ + + SaltModel *model = new SaltModel (this, &m_salt_mine); + salt_view->setModel (model); + salt_view->setItemDelegate (new SaltItemDelegate (this)); + salt_view->setCurrentIndex (model->index (0, 0, QModelIndex ())); + + connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); + connect (mark_button, SIGNAL (clicked ()), this, SLOT (mark ())); + connect (button_box->button (QDialogButtonBox::Apply), SIGNAL (clicked ()), this, SLOT (apply ())); + + current_changed (); +} + +void +SaltGrainInstallationDialog::current_changed () +{ + SaltGrain *g = current_grain (); + details_text->set_grain (g); + details_frame->setEnabled (g != 0); +} + +lay::SaltGrain * +SaltGrainInstallationDialog::current_grain () +{ + SaltModel *model = dynamic_cast (salt_view->model ()); + return model ? model->grain_from_index (salt_view->currentIndex ()) : 0; +} + +void +SaltGrainInstallationDialog::apply () +{ + + // @@@ + +} + +void +SaltGrainInstallationDialog::mark () +{ + + // @@@ + +} + +} diff --git a/src/lay/laySaltGrainInstallationDialog.h b/src/lay/laySaltGrainInstallationDialog.h new file mode 100644 index 000000000..9c7986fb2 --- /dev/null +++ b/src/lay/laySaltGrainInstallationDialog.h @@ -0,0 +1,76 @@ + +/* + + 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_laySaltGrainInstallationDialog +#define HDR_laySaltGrainInstallationDialog + +#include "laySalt.h" + +#include + +#include "ui_SaltGrainInstallationDialog.h" + +namespace lay +{ + +class Salt; + +/** + * @brief The dialog for managing the Salt ("Packages") + */ +class SaltGrainInstallationDialog + : public QDialog, private Ui::SaltGrainInstallationDialog +{ +Q_OBJECT + +public: + /** + * @brief Constructor + */ + SaltGrainInstallationDialog (QWidget *parent, lay::Salt *salt); + +private slots: + /** + * @brief Called when the currently selected package (grain) has changed + */ + void current_changed (); + + /** + * @brief Called when the Apply button is clicked + */ + void apply (); + + /** + * @brief Called when the Mark button is pressed + */ + void mark (); + +private: + lay::Salt *mp_salt; + lay::Salt m_salt_mine; + + lay::SaltGrain *current_grain (); +}; + +} + +#endif diff --git a/src/lay/laySaltGrains.cc b/src/lay/laySaltGrains.cc index 5c80cc884..221861340 100644 --- a/src/lay/laySaltGrains.cc +++ b/src/lay/laySaltGrains.cc @@ -233,4 +233,32 @@ SaltGrains::from_path (const std::string &path, const std::string &prefix) 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 index 0387000a1..2dc524a5a 100644 --- a/src/lay/laySaltGrains.h +++ b/src/lay/laySaltGrains.h @@ -174,6 +174,21 @@ public: */ 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 diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 568040404..c0d0934be 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -21,209 +21,29 @@ */ #include "laySaltManagerDialog.h" +#include "laySaltModel.h" #include "laySaltGrainPropertiesDialog.h" +#include "laySaltGrainInstallationDialog.h" #include "laySalt.h" #include "ui_SaltGrainTemplateSelectionDialog.h" #include "tlString.h" #include "tlExceptions.h" -#include -#include -#include #include #include #include #include #include #include +#include +#include +#include namespace lay { // -------------------------------------------------------------------------------------- -/** - * @brief A model representing the salt grains for a QListView - */ -class SaltModel - : public QAbstractItemModel -{ -public: - SaltModel (QObject *parent, lay::Salt *salt) - : QAbstractItemModel (parent), mp_salt (salt) - { - // .. nothing yet .. - } - - QVariant data (const QModelIndex &index, int role) const - { - if (role == Qt::DisplayRole) { - - const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; - - 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 += "

"; - } - text += ""; - - return tl::to_qstring (text); - - } else if (role == Qt::DecorationRole) { - - int icon_dim = 64; - - const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; - if (g->icon ().isNull ()) { - return QIcon (":/salt_icon.png"); - } else { - - QImage img = g->icon (); - if (img.width () == icon_dim && img.height () == icon_dim) { - return QPixmap::fromImage (img); - } else { - - 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 QPixmap::fromImage (final_img); - - } - - } - - } else { - return QVariant (); - } - } - - QModelIndex index (int row, int column, const QModelIndex &parent) const - { - if (parent.isValid ()) { - return QModelIndex (); - } else { - return createIndex (row, column, mp_salt->begin_flat () [row]); - } - } - - QModelIndex parent (const QModelIndex & /*index*/) const - { - return QModelIndex (); - } - - int columnCount(const QModelIndex & /*parent*/) const - { - return 1; - } - - int rowCount (const QModelIndex &parent) const - { - if (parent.isValid ()) { - return 0; - } else { - return mp_salt->end_flat () - mp_salt->begin_flat (); - } - } - - SaltGrain *grain_from_index (const QModelIndex &index) const - { - if (index.isValid ()) { - return static_cast (index.internalPointer ()); - } else { - return 0; - } - } - - void update () - { - reset (); - } - -public: - lay::Salt *mp_salt; -}; - -// -------------------------------------------------------------------------------------- - -/** - * @brief A delegate displaying the summary of a grain - */ -class SaltItemDelegate - : public QStyledItemDelegate -{ -public: - SaltItemDelegate (QObject *parent) - : QStyledItemDelegate (parent) - { - // .. nothing yet .. - } - - void paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const - { - QStyleOptionViewItemV4 optionV4 = option; - initStyleOption (&optionV4, index); - - 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)); - } - - 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 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 ()))); - } -}; - -// -------------------------------------------------------------------------------------- - /** * @brief A tiny dialog to select a template and a name for the grain */ @@ -384,17 +204,30 @@ 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::install_grain () { +BEGIN_PROTECTED - // @@@ + // @@@ TODO: cache this somewhere - don't recreate this dialog always + SaltGrainInstallationDialog inst_dialog (this, mp_salt); + inst_dialog.exec (); +END_PROTECTED } void @@ -405,6 +238,8 @@ SaltManagerDialog::salt_changed () return; } + // 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->update (); m_current_changed_enabled = true; diff --git a/src/lay/laySaltModel.cc b/src/lay/laySaltModel.cc new file mode 100644 index 000000000..ded796c86 --- /dev/null +++ b/src/lay/laySaltModel.cc @@ -0,0 +1,207 @@ + +/* + + 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); + + 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)); + } + + 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) +{ + // .. nothing yet .. +} + +QVariant +SaltModel::data (const QModelIndex &index, int role) const +{ + if (role == Qt::DisplayRole) { + + const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; + + 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 += "

"; + } + text += ""; + + return tl::to_qstring (text); + + } else if (role == Qt::DecorationRole) { + + int icon_dim = 64; + + const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; + if (g->icon ().isNull ()) { + return QIcon (":/salt_icon.png"); + } else { + + QImage img = g->icon (); + if (img.width () == icon_dim && img.height () == icon_dim) { + return QPixmap::fromImage (img); + } else { + + 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 QPixmap::fromImage (final_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, mp_salt->begin_flat () [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 mp_salt->end_flat () - mp_salt->begin_flat (); + } +} + +SaltGrain * +SaltModel::grain_from_index (const QModelIndex &index) const +{ + if (index.isValid ()) { + return static_cast (index.internalPointer ()); + } else { + return 0; + } +} + +void +SaltModel::update () +{ + reset (); +} + +} diff --git a/src/lay/laySaltModel.h b/src/lay/laySaltModel.h new file mode 100644 index 000000000..1ffa5f86e --- /dev/null +++ b/src/lay/laySaltModel.h @@ -0,0 +1,110 @@ + +/* + + 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 + +namespace lay +{ + +class Salt; +class SaltGrain; + +/** + * @brief A model representing the salt grains for a QListView + */ +class SaltModel + : public QAbstractItemModel +{ +Q_OBJECT + +public: + /** + * @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 + */ + 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 (); + +public: + lay::Salt *mp_salt; +}; + +// -------------------------------------------------------------------------------------- + +/** + * @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/unit_tests/laySalt.cc b/src/unit_tests/laySalt.cc index 9d3ef0a36..8edff9a14 100644 --- a/src/unit_tests/laySalt.cc +++ b/src/unit_tests/laySalt.cc @@ -240,6 +240,15 @@ TEST (3) 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]]]"); From fee806704d74e882b10e79cab4ef75ba2aecf4f4 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sat, 25 Mar 2017 10:15:24 +0100 Subject: [PATCH 21/27] WIP: combined installation and browser dialog --- src/lay/SaltGrainInstallationDialog.ui | 313 ------- src/lay/SaltManagerDialog.ui | 968 ++++++++++++++-------- src/lay/lay.pro | 9 +- src/lay/laySaltGrainInstallationDialog.cc | 101 --- src/lay/laySaltGrainInstallationDialog.h | 76 -- src/lay/laySaltManagerDialog.cc | 103 ++- src/lay/laySaltManagerDialog.h | 18 +- 7 files changed, 737 insertions(+), 851 deletions(-) delete mode 100644 src/lay/SaltGrainInstallationDialog.ui delete mode 100644 src/lay/laySaltGrainInstallationDialog.cc delete mode 100644 src/lay/laySaltGrainInstallationDialog.h diff --git a/src/lay/SaltGrainInstallationDialog.ui b/src/lay/SaltGrainInstallationDialog.ui deleted file mode 100644 index ab54731cb..000000000 --- a/src/lay/SaltGrainInstallationDialog.ui +++ /dev/null @@ -1,313 +0,0 @@ - - - SaltGrainInstallationDialog - - - - 0 - 0 - 692 - 440 - - - - Salt Mine Repository Browser - - - - - - Qt::Horizontal - - - - - 4 - - - - - - 0 - 0 - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 75 - true - - - - Repository - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Install - - - - - - - Qt::Horizontal - - - - 126 - 20 - - - - - - - - - 0 - 0 - - - - - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - true - - - - 64 - 64 - - - - false - - - - - - - - - - - - 1 - 0 - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 4 - - - 0 - - - 0 - - - 0 - - - - - - 75 - true - - - - Details - - - - - - - true - - - - - - - - - - - QDialogButtonBox::Apply|QDialogButtonBox::Close - - - - - - - - :/add.png:/add.png - - - New - - - New package - - - - - - :/clear.png:/clear.png - - - Delete - - - Delete package - - - - - - :/import.png:/import.png - - - Import - - - Import package - - - - - - lay::DecoratedLineEdit - QLineEdit -
layWidgets.h
-
- - lay::SaltGrainDetailsTextWidget - QTextBrowser -
laySaltGrainDetailsTextWidget.h
-
-
- - details_text - - - - - - - button_box - rejected() - SaltGrainInstallationDialog - reject() - - - 559 - 425 - - - 576 - 437 - - - - -
diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui index d16b5af31..0a4bcf96c 100644 --- a/src/lay/SaltManagerDialog.ui +++ b/src/lay/SaltManagerDialog.ui @@ -15,187 +15,43 @@ - - - Qt::Horizontal + + + 0 - - - - 4 - + + + Installed Packages + + - - - - 0 - 0 - + + + Qt::Horizontal - - QFrame::NoFrame - - - QFrame::Raised - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::NoFocus - - - Install Package - - - ... - - - - :/import.png:/import.png - - - true - - - - - - - Qt::Horizontal - - - - 50 - 0 - - - - - - - - Qt::NoFocus - - - Create New Package - - - ... - - - - :/add.png:/add.png - - - true - - - - - - - Qt::NoFocus - - - Uninstall Package - - - ... - - - - :/clear.png:/clear.png - - - true - - - - - - - - - - - 0 - 0 - - - - 0 - - - - - 0 - - - 0 - + + - 0 - - - 0 + 4 - - - true + + + + 0 + 0 + - - - 64 - 64 - - - - false - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - QFrame::StyledPanel + QFrame::NoFrame QFrame::Raised - + + + 2 + 0 @@ -209,187 +65,621 @@ 0 - - - QFrame::NoFrame + + + Qt::NoFocus - + + Create New Package + + + ... + + + + :/add.png:/add.png + + true - - - - 0 - 0 - 320 - 204 - - - - - - - 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 width="30"><a href=":import"><img src=":/import.png"></a></td><td>to import a package from an<br/>external repository</td></tr> - <tr></tr> - <tr><td><a href=":add"><img src=":/add.png"></a></td><td>to create a new package</td></tr> -</table> -</body></html> - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - false - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - + + + + + + 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 + + + - - - - 1 - 0 - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 4 - - - 0 - - - 0 - - - 0 - - - - - - 75 - true - + + + Install New Packages + + + + + + + 0 + 1 + - - Details + + Qt::Horizontal + + + + 4 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Mark for installation + + + Mark + + + + + + + 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 + + + + + - - - - Edit Package Details + + + + QFrame::NoFrame - - Edit - - - - :/edit.png:/edit.png - - - true - - - - - - - true + + QFrame::Raised + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Apply + + + false + + + false + + + + + + + Qt::Horizontal + + + + 563 + 20 + + + + + @@ -442,6 +732,11 @@ + + lay::DecoratedLineEdit + QLineEdit +
layWidgets.h
+
lay::SaltGrainDetailsTextWidget QTextBrowser @@ -449,11 +744,18 @@
+ mode_tab + search_installed_edit salt_view - install_button - create_button - delete_button details_text + edit_button + search_new_edit + mark_button + salt_mine_view + details_new_text + toolButton + apply_button + scrollArea diff --git a/src/lay/lay.pro b/src/lay/lay.pro index 0d1251609..7d1bff20d 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -53,8 +53,7 @@ HEADERS = \ laySaltGrainDetailsTextWidget.h \ laySaltGrainPropertiesDialog.h \ laySaltDownloadManager.h \ - laySaltModel.h \ - laySaltGrainInstallationDialog.h + laySaltModel.h FORMS = \ ClipDialog.ui \ @@ -102,8 +101,7 @@ FORMS = \ MainConfigPage7.ui \ SaltManagerDialog.ui \ SaltGrainPropertiesDialog.ui \ - SaltGrainTemplateSelectionDialog.ui \ - SaltGrainInstallationDialog.ui + SaltGrainTemplateSelectionDialog.ui SOURCES = \ gsiDeclLayApplication.cc \ @@ -154,8 +152,7 @@ SOURCES = \ laySaltGrainDetailsTextWidget.cc \ laySaltGrainPropertiesDialog.cc \ laySaltDownloadManager.cc \ - laySaltModel.cc \ - laySaltGrainInstallationDialog.cc + laySaltModel.cc RESOURCES = layBuildInMacros.qrc \ layHelpResources.qrc \ diff --git a/src/lay/laySaltGrainInstallationDialog.cc b/src/lay/laySaltGrainInstallationDialog.cc deleted file mode 100644 index f12d7f576..000000000 --- a/src/lay/laySaltGrainInstallationDialog.cc +++ /dev/null @@ -1,101 +0,0 @@ - -/* - - 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 "laySaltGrainInstallationDialog.h" -#include "laySaltModel.h" -#include "laySaltGrainPropertiesDialog.h" -#include "laySalt.h" -#include "ui_SaltGrainTemplateSelectionDialog.h" -#include "tlString.h" -#include "tlExceptions.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace lay -{ - -// -------------------------------------------------------------------------------------- -// SaltGrainInstallation implementation - -SaltGrainInstallationDialog::SaltGrainInstallationDialog (QWidget *parent, lay::Salt *salt) - : QDialog (parent), mp_salt (salt) -{ - Ui::SaltGrainInstallationDialog::setupUi (this); - - // TODO: cache package list - // @@@ - m_salt_mine.load ("/home/matthias/salt.mine"); - // @@@ - - SaltModel *model = new SaltModel (this, &m_salt_mine); - salt_view->setModel (model); - salt_view->setItemDelegate (new SaltItemDelegate (this)); - salt_view->setCurrentIndex (model->index (0, 0, QModelIndex ())); - - connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); - connect (mark_button, SIGNAL (clicked ()), this, SLOT (mark ())); - connect (button_box->button (QDialogButtonBox::Apply), SIGNAL (clicked ()), this, SLOT (apply ())); - - current_changed (); -} - -void -SaltGrainInstallationDialog::current_changed () -{ - SaltGrain *g = current_grain (); - details_text->set_grain (g); - details_frame->setEnabled (g != 0); -} - -lay::SaltGrain * -SaltGrainInstallationDialog::current_grain () -{ - SaltModel *model = dynamic_cast (salt_view->model ()); - return model ? model->grain_from_index (salt_view->currentIndex ()) : 0; -} - -void -SaltGrainInstallationDialog::apply () -{ - - // @@@ - -} - -void -SaltGrainInstallationDialog::mark () -{ - - // @@@ - -} - -} diff --git a/src/lay/laySaltGrainInstallationDialog.h b/src/lay/laySaltGrainInstallationDialog.h deleted file mode 100644 index 9c7986fb2..000000000 --- a/src/lay/laySaltGrainInstallationDialog.h +++ /dev/null @@ -1,76 +0,0 @@ - -/* - - 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_laySaltGrainInstallationDialog -#define HDR_laySaltGrainInstallationDialog - -#include "laySalt.h" - -#include - -#include "ui_SaltGrainInstallationDialog.h" - -namespace lay -{ - -class Salt; - -/** - * @brief The dialog for managing the Salt ("Packages") - */ -class SaltGrainInstallationDialog - : public QDialog, private Ui::SaltGrainInstallationDialog -{ -Q_OBJECT - -public: - /** - * @brief Constructor - */ - SaltGrainInstallationDialog (QWidget *parent, lay::Salt *salt); - -private slots: - /** - * @brief Called when the currently selected package (grain) has changed - */ - void current_changed (); - - /** - * @brief Called when the Apply button is clicked - */ - void apply (); - - /** - * @brief Called when the Mark button is pressed - */ - void mark (); - -private: - lay::Salt *mp_salt; - lay::Salt m_salt_mine; - - lay::SaltGrain *current_grain (); -}; - -} - -#endif diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index c0d0934be..09d4a66ed 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -23,7 +23,6 @@ #include "laySaltManagerDialog.h" #include "laySaltModel.h" #include "laySaltGrainPropertiesDialog.h" -#include "laySaltGrainInstallationDialog.h" #include "laySalt.h" #include "ui_SaltGrainTemplateSelectionDialog.h" #include "tlString.h" @@ -119,6 +118,25 @@ void make_salt () 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.load ("/home/matthias/salt.mine"); +} +lay::Salt *get_salt_mine () +{ + make_salt_mine(); + return &salt_mine; +} // @@@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) @@ -131,27 +149,46 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) 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 (install_button, SIGNAL (clicked ()), this, SLOT (install_grain ())); -// @@@ - salt = lay::Salt (); salt_initialized = false; - make_salt (); - mp_salt = &salt; -// @@@ + mp_salt = get_salt (); + mp_salt_mine = get_salt_mine (); - SaltModel *model = new SaltModel (this, mp_salt); + SaltModel *model; + + model = new SaltModel (this, mp_salt); salt_view->setModel (model); salt_view->setItemDelegate (new SaltItemDelegate (this)); + model = new SaltModel (this, mp_salt_mine); + salt_mine_view->setModel (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 ())); salt_changed (); + salt_mine_changed (); connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); + connect (salt_mine_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (mine_current_changed ())); // @@@ } +void +SaltManagerDialog::mode_changed () +{ + // keeps the splitters in sync + if (mode_tab->currentIndex () == 1) { + splitter_new->setSizes (splitter->sizes ()); + } else if (mode_tab->currentIndex () == 0) { + splitter->setSizes (splitter_new->sizes ()); + } +} + void SaltManagerDialog::edit_properties () { @@ -159,7 +196,6 @@ SaltManagerDialog::edit_properties () if (g) { if (mp_properties_dialog->exec_dialog (g, mp_salt)) { current_changed (); - // @@@ } } } @@ -218,18 +254,6 @@ BEGIN_PROTECTED END_PROTECTED } -void -SaltManagerDialog::install_grain () -{ -BEGIN_PROTECTED - - // @@@ TODO: cache this somewhere - don't recreate this dialog always - SaltGrainInstallationDialog inst_dialog (this, mp_salt); - inst_dialog.exec (); - -END_PROTECTED -} - void SaltManagerDialog::salt_changed () { @@ -286,4 +310,41 @@ SaltManagerDialog::current_grain () return model ? model->grain_from_index (salt_view->currentIndex ()) : 0; } +void +SaltManagerDialog::salt_mine_changed () +{ + SaltModel *model = dynamic_cast (salt_mine_view->model ()); + if (! model) { + return; + } + + // 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->update (); + m_current_changed_enabled = true; + + // select the first grain + if (model->rowCount (QModelIndex ()) > 0) { + salt_mine_view->setCurrentIndex (model->index (0, 0, QModelIndex ())); + } + + mine_current_changed (); +} + +void +SaltManagerDialog::mine_current_changed () +{ + SaltGrain *g = mine_current_grain (); + details_new_text->set_grain (g); + details_new_frame->setEnabled (g != 0); +} + +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 index 232d40403..305969f73 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -54,11 +54,21 @@ private slots: */ 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 */ @@ -79,12 +89,18 @@ private slots: */ void install_grain (); + /** + * @brief Called when the mode tab changed + */ + void mode_changed (); + private: - lay::Salt *mp_salt; + lay::Salt *mp_salt, *mp_salt_mine; bool m_current_changed_enabled; lay::SaltGrainPropertiesDialog *mp_properties_dialog; lay::SaltGrain *current_grain (); + lay::SaltGrain *mine_current_grain (); }; } From b72655c94b675cc752bf4dd14a2ccb8398c8e7ca Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sat, 25 Mar 2017 23:33:07 +0100 Subject: [PATCH 22/27] WIP: preview download and search feature in package manager --- src/lay/SaltManagerDialog.ui | 4 +- src/lay/laySaltManagerDialog.cc | 102 +++++++++++++++++++++++++++++++- src/lay/laySaltManagerDialog.h | 11 +++- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui index 0a4bcf96c..9194d7715 100644 --- a/src/lay/SaltManagerDialog.ui +++ b/src/lay/SaltManagerDialog.ui @@ -21,7 +21,7 @@
- Installed Packages + Browse Installed Packages @@ -410,7 +410,7 @@ - Install New Packages + Install or Update Packages diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 09d4a66ed..5ce7cd4eb 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -27,6 +27,7 @@ #include "ui_SaltGrainTemplateSelectionDialog.h" #include "tlString.h" #include "tlExceptions.h" +#include "tlHttpStream.h" #include #include @@ -130,6 +131,7 @@ lay::Salt *get_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 () @@ -175,7 +177,10 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); connect (salt_mine_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (mine_current_changed ())); - // @@@ + 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 &))); } void @@ -189,6 +194,45 @@ SaltManagerDialog::mode_changed () } } +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::edit_properties () { @@ -335,9 +379,63 @@ SaltManagerDialog::salt_mine_changed () void SaltManagerDialog::mine_current_changed () { +BEGIN_PROTECTED + SaltGrain *g = mine_current_grain (); - details_new_text->set_grain (g); 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"))); + } + + 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 * diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h index 305969f73..68576602a 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -23,10 +23,11 @@ #ifndef HDR_laySaltManagerDialog #define HDR_laySaltManagerDialog -#include - #include "ui_SaltManagerDialog.h" +#include +#include + namespace lay { @@ -94,8 +95,14 @@ private slots: */ void mode_changed (); + /** + * @brief Called when one search text changed + */ + void search_text_changed (const QString &text); + private: lay::Salt *mp_salt, *mp_salt_mine; + std::auto_ptr m_remote_grain; bool m_current_changed_enabled; lay::SaltGrainPropertiesDialog *mp_properties_dialog; From 9e2c4cb927b9fc9e6b1451c9f0fd63ae404f7a08 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 26 Mar 2017 01:02:40 +0100 Subject: [PATCH 23/27] WIP: marked mode, update required message --- src/lay/SaltManagerDialog.ui | 15 +++++-- src/lay/images/marked_16.png | Bin 0 -> 889 bytes src/lay/images/marked_24.png | Bin 0 -> 1452 bytes src/lay/images/marked_64.png | Bin 0 -> 3353 bytes src/lay/layResources.qrc | 3 ++ src/lay/laySaltManagerDialog.cc | 61 ++++++++++++++++++++++---- src/lay/laySaltManagerDialog.h | 5 +++ src/lay/laySaltModel.cc | 73 ++++++++++++++++++++++++++------ src/lay/laySaltModel.h | 17 ++++++++ 9 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 src/lay/images/marked_16.png create mode 100644 src/lay/images/marked_24.png create mode 100644 src/lay/images/marked_64.png diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui index 9194d7715..a51993a12 100644 --- a/src/lay/SaltManagerDialog.ui +++ b/src/lay/SaltManagerDialog.ui @@ -17,7 +17,7 @@ - 0 + 1 @@ -231,8 +231,8 @@ 0 0 - 343 - 207 + 537 + 284 @@ -456,11 +456,18 @@ - Mark for installation + Mark or unmark for installation Mark + + + :/marked_16.png:/marked_16.png + + + true + diff --git a/src/lay/images/marked_16.png b/src/lay/images/marked_16.png new file mode 100644 index 0000000000000000000000000000000000000000..aa152411be8cfc02a42015b1b194db9e08d7321e GIT binary patch literal 889 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 literal 0 HcmV?d00001 diff --git a/src/lay/layResources.qrc b/src/lay/layResources.qrc index 3ffd9e81e..52e7bc522 100644 --- a/src/lay/layResources.qrc +++ b/src/lay/layResources.qrc @@ -120,6 +120,9 @@ 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 diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 5ce7cd4eb..57ce3fbf5 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -155,19 +155,27 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) mp_salt = get_salt (); mp_salt_mine = get_salt_mine (); - SaltModel *model; - - model = new SaltModel (this, mp_salt); + SaltModel *model = new SaltModel (this, mp_salt); salt_view->setModel (model); salt_view->setItemDelegate (new SaltItemDelegate (this)); - model = new SaltModel (this, mp_salt_mine); - salt_mine_view->setModel (model); + SaltModel *mine_model = new SaltModel (this, mp_salt_mine); + salt_mine_view->setModel (mine_model); salt_mine_view->setItemDelegate (new SaltItemDelegate (this)); + // 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 (), tl::to_string (tr ("An update to version %1 is available").arg (tl::to_qstring (gm->version ())))); + mine_model->set_message ((*g)->name (), tl::to_string (tr ("The installed version is outdated (%1)").arg (tl::to_qstring ((*g)->version ())))); + } + } + 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 ())); @@ -175,12 +183,14 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) salt_mine_changed (); connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ())); - connect (salt_mine_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (mine_current_changed ())); + connect (salt_mine_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (mine_current_changed ()), Qt::QueuedConnection); 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 ())); } void @@ -233,6 +243,21 @@ SaltManagerDialog::search_text_changed (const QString &text) } } +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 ())); +} + void SaltManagerDialog::edit_properties () { @@ -379,6 +404,7 @@ SaltManagerDialog::salt_mine_changed () void SaltManagerDialog::mine_current_changed () { + BEGIN_PROTECTED SaltGrain *g = mine_current_grain (); @@ -398,6 +424,22 @@ BEGIN_PROTECTED 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); @@ -422,9 +464,10 @@ BEGIN_PROTECTED "" "" "" - "

Error Fetching Package Definition

" - "

URL: %1

" - "

Error: %2

" + "

Error Fetching Package Definition

" + "

URL: %1

" + "

Error: %2

" + "
" "" "" ) diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h index 68576602a..6a5068217 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -75,6 +75,11 @@ private slots: */ void edit_properties (); + /** + * @brief Called when the "mark" button is pressed + */ + void mark_clicked (); + /** * @brief Called when the "edit" button is pressed */ diff --git a/src/lay/laySaltModel.cc b/src/lay/laySaltModel.cc index ded796c86..57dd44212 100644 --- a/src/lay/laySaltModel.cc +++ b/src/lay/laySaltModel.cc @@ -120,6 +120,12 @@ SaltModel::data (const QModelIndex &index, int role) const text += tl::escaped_to_html (g->doc ()); text += "

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

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

"; + } + text += ""; return tl::to_qstring (text); @@ -129,28 +135,39 @@ SaltModel::data (const QModelIndex &index, int role) const int icon_dim = 64; const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; + + QImage img; if (g->icon ().isNull ()) { - return QIcon (":/salt_icon.png"); + img = QImage (":/salt_icon.png"); } else { + img = g->icon (); + } - QImage img = g->icon (); - if (img.width () == icon_dim && img.height () == icon_dim) { - return QPixmap::fromImage (img); - } else { + if (img.width () != icon_dim || img.height () != icon_dim) { - img = img.scaled (QSize (icon_dim, icon_dim), Qt::KeepAspectRatio, Qt::SmoothTransformation); + QImage scaled = 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 QPixmap::fromImage (final_img); - - } + 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); + } + + if (m_messages.find (g->name ()) != m_messages.end ()) { + QPainter painter (&img); + QImage warn (":/warn_16.png"); + painter.drawImage (0, 0, warn); + } + + return QPixmap::fromImage (img); + } else { return QVariant (); } @@ -198,6 +215,34 @@ SaltModel::grain_from_index (const QModelIndex &index) const } } +bool +SaltModel::is_marked (const std::string &name) const +{ + return m_marked.find (name) != m_marked.end (); +} + +void +SaltModel::set_marked (const std::string &name, bool marked) +{ + 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::set_message (const std::string &name, const std::string &message) +{ + if (message.empty ()) { + m_messages.erase (name); + } else { + m_messages.insert (std::make_pair (name, message)); + } + emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); +} + void SaltModel::update () { diff --git a/src/lay/laySaltModel.h b/src/lay/laySaltModel.h index 1ffa5f86e..20a537e2c 100644 --- a/src/lay/laySaltModel.h +++ b/src/lay/laySaltModel.h @@ -29,6 +29,8 @@ #include #include #include +#include +#include namespace lay { @@ -86,8 +88,23 @@ public: */ 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 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, const std::string &message); + public: lay::Salt *mp_salt; + std::set m_marked; + std::map m_messages; + + bool is_marked (const std::string &name) const; }; // -------------------------------------------------------------------------------------- From a5d0461284f1c68270ce7a845155b6f1797e3b27 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 26 Mar 2017 23:26:45 +0200 Subject: [PATCH 24/27] WIP: removed obsolete method. --- src/lay/laySaltManagerDialog.h | 5 ----- src/unit_tests/tlHttpStream.cc | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/unit_tests/tlHttpStream.cc diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h index 6a5068217..cbbfac0a4 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -90,11 +90,6 @@ private slots: */ void delete_grain (); - /** - * @brief Called when the "install" button is pressed - */ - void install_grain (); - /** * @brief Called when the mode tab changed */ diff --git a/src/unit_tests/tlHttpStream.cc b/src/unit_tests/tlHttpStream.cc new file mode 100644 index 000000000..184416f1f --- /dev/null +++ b/src/unit_tests/tlHttpStream.cc @@ -0,0 +1,33 @@ + +/* + + 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" + +std::string test_url1 ("http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/text"); + +TEST(1) +{ + EXPECT_EQ (to_string (12.5), "12.5"); +} + From d98495c18a9c1884a865b1086ee2da50188c931c Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 26 Mar 2017 23:27:51 +0200 Subject: [PATCH 25/27] WIP: new features for HTTP streams. --- src/tl/tlHttpStream.cc | 39 ++++++++++++++++++++++++++++++---- src/tl/tlHttpStream.h | 38 +++++++++++++++++++++++++++------ src/unit_tests/tlHttpStream.cc | 7 +++++- src/unit_tests/unit_tests.pro | 3 ++- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/tl/tlHttpStream.cc b/src/tl/tlHttpStream.cc index a709586e8..216e31c76 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,7 @@ 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)))); + issue_request (QUrl (tl::to_qstring (url))); mp_reply = 0; } @@ -97,6 +97,24 @@ 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::authenticationRequired (QNetworkReply *reply, QAuthenticator *auth) { @@ -117,13 +135,27 @@ 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; + + if (m_data.isEmpty ()) { + s_network_manager->sendCustomRequest (QNetworkRequest (url), m_request); + } else { + mp_buffer = new QBuffer (&m_data); + s_network_manager->sendCustomRequest (QNetworkRequest (url), m_request, mp_buffer); + } +} + size_t InputHttpStream::read (char *b, size_t n) { @@ -160,4 +192,3 @@ InputHttpStream::filename () const } } - diff --git a/src/tl/tlHttpStream.h b/src/tl/tlHttpStream.h index 5a2da4d8a..7f20c0aad 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,33 @@ 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 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 +110,19 @@ 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; + + void issue_request (const QUrl &url); }; } diff --git a/src/unit_tests/tlHttpStream.cc b/src/unit_tests/tlHttpStream.cc index 184416f1f..1783b2ccb 100644 --- a/src/unit_tests/tlHttpStream.cc +++ b/src/unit_tests/tlHttpStream.cc @@ -28,6 +28,11 @@ std::string test_url1 ("http://www.klayout.org/svn-public/klayout-resources/trun TEST(1) { - EXPECT_EQ (to_string (12.5), "12.5"); + 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"); } diff --git a/src/unit_tests/unit_tests.pro b/src/unit_tests/unit_tests.pro index 31dd201a4..608f86786 100644 --- a/src/unit_tests/unit_tests.pro +++ b/src/unit_tests/unit_tests.pro @@ -97,7 +97,8 @@ SOURCES = \ gsiTest.cc \ tlFileSystemWatcher.cc \ laySalt.cc \ - tlFileUtils.cc + tlFileUtils.cc \ + tlHttpStream.cc # main components: SOURCES += \ From cb589dc2d3b690609918192b90bcac8c0d0ee9dd Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Mon, 27 Mar 2017 15:46:01 +0200 Subject: [PATCH 26/27] WIP: downloading of packages - Support for WebDAV - Download manager implemented - Apply button functionality implemented (needs testing) --- .../SaltManagerInstallConfirmationDialog.ui | 156 +++++++++ src/lay/lay.pro | 3 +- src/lay/laySalt.cc | 21 +- src/lay/laySalt.h | 22 +- src/lay/laySaltDownloadManager.cc | 175 +++++++++- src/lay/laySaltDownloadManager.h | 67 +++- src/lay/laySaltManagerDialog.cc | 38 +++ src/lay/laySaltManagerDialog.h | 5 + src/tl/tl.pro | 6 +- src/tl/tlHttpStream.cc | 20 +- src/tl/tlHttpStream.h | 6 + src/tl/tlWebDAV.cc | 316 ++++++++++++++++++ src/tl/tlWebDAV.h | 152 +++++++++ src/tl/tlXMLParser.h | 4 +- src/unit_tests/tlHttpStream.cc | 39 ++- src/unit_tests/tlWebDAV.cc | 129 +++++++ src/unit_tests/unit_tests.pro | 3 +- 17 files changed, 1130 insertions(+), 32 deletions(-) create mode 100644 src/lay/SaltManagerInstallConfirmationDialog.ui create mode 100644 src/tl/tlWebDAV.cc create mode 100644 src/tl/tlWebDAV.h create mode 100644 src/unit_tests/tlWebDAV.cc diff --git a/src/lay/SaltManagerInstallConfirmationDialog.ui b/src/lay/SaltManagerInstallConfirmationDialog.ui new file mode 100644 index 000000000..0920a619b --- /dev/null +++ b/src/lay/SaltManagerInstallConfirmationDialog.ui @@ -0,0 +1,156 @@ + + + SaltManagerInstallConfirmationDialog + + + + 0 + 0 + 495 + 478 + + + + Ready for Installation + + + + + + The following packages are now ready for installation or update: + + + 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/lay.pro b/src/lay/lay.pro index 7d1bff20d..0daa9efc7 100644 --- a/src/lay/lay.pro +++ b/src/lay/lay.pro @@ -101,7 +101,8 @@ FORMS = \ MainConfigPage7.ui \ SaltManagerDialog.ui \ SaltGrainPropertiesDialog.ui \ - SaltGrainTemplateSelectionDialog.ui + SaltGrainTemplateSelectionDialog.ui \ + SaltManagerInstallConfirmationDialog.ui SOURCES = \ gsiDeclLayApplication.cc \ diff --git a/src/lay/laySalt.cc b/src/lay/laySalt.cc index c16bc8e56..2812d2095 100644 --- a/src/lay/laySalt.cc +++ b/src/lay/laySalt.cc @@ -21,11 +21,11 @@ */ #include "laySalt.h" -#include "laySaltDownloadManager.h" #include "tlString.h" #include "tlFileUtils.h" #include "tlLog.h" #include "tlInternational.h" +#include "tlWebDAV.h" #include #include @@ -307,12 +307,23 @@ public: } bool -Salt::create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManager *download_manager) +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; @@ -383,11 +394,11 @@ Salt::create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManag } else if (! templ.url ().empty ()) { - tl_assert (download_manager != 0); - // 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 = download_manager->download (templ.url (), target.path ()); + res = tl::WebDAVObject::download (templ.url (), target.path ()); + + target.set_url (templ.url ()); } diff --git a/src/lay/laySalt.h b/src/lay/laySalt.h index ff16c0280..d238476ae 100644 --- a/src/lay/laySalt.h +++ b/src/lay/laySalt.h @@ -34,8 +34,6 @@ namespace lay { -class SaltDownloadManager; - /** * @brief The global salt (package manager) object * This object can be configured to represent a couple of locations. @@ -123,6 +121,14 @@ public: */ 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 */ @@ -165,11 +171,11 @@ public: * 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. * - * The target's name must be set. If a specific target location is desired, the target's - * path must be set too. - * - * This method refuses to overwrite existing grains, so an update needs to be performed by first - * deleting the grain and then re-installing it. + * 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 @@ -177,7 +183,7 @@ public: * * Returns true, if the package could be created successfully. */ - bool create_grain (const SaltGrain &templ, SaltGrain &target, SaltDownloadManager *download_manager = 0); + bool create_grain (const SaltGrain &templ, SaltGrain &target); signals: /** diff --git a/src/lay/laySaltDownloadManager.cc b/src/lay/laySaltDownloadManager.cc index f6970b851..c3e587551 100644 --- a/src/lay/laySaltDownloadManager.cc +++ b/src/lay/laySaltDownloadManager.cc @@ -21,20 +21,189 @@ */ #include "laySaltDownloadManager.h" +#include "laySalt.h" +#include "tlFileUtils.h" +#include "tlWebDAV.h" + +#include "ui_SaltManagerInstallConfirmationDialog.h" + +#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->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)); + } +}; + +// ---------------------------------------------------------------------------------- + SaltDownloadManager::SaltDownloadManager () { // .. nothing yet .. } -bool -SaltDownloadManager::download (const std::string &url, const std::string &target_dir) +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 .."))); + + while (needs_iteration ()) { + + fetch_missing (salt_mine, progress); + + std::map 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 (); + } + + p->second.grain = SaltGrain::from_url (p->second.url); + p->second.downloaded = true; + + } + + } +} + +bool +SaltDownloadManager::show_confirmation_dialog (QWidget *parent, const lay::Salt &salt) +{ + 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) { + dialog.add_info (p->first, true, g->version () + "->" + 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 index 141df238a..89fd74620 100644 --- a/src/lay/laySaltDownloadManager.h +++ b/src/lay/laySaltDownloadManager.h @@ -24,17 +24,27 @@ #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. + * 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 @@ -48,11 +58,58 @@ public: SaltDownloadManager (); /** - * @brief Downloads the files from the given URL to the given target location - * The target directory needs to exist. - * Returns true, if the download was successful, false otherwise. + * @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. */ - bool download (const std::string &url, const std::string &target_dir); + 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); }; } diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 57ce3fbf5..978dc9c46 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -23,6 +23,7 @@ #include "laySaltManagerDialog.h" #include "laySaltModel.h" #include "laySaltGrainPropertiesDialog.h" +#include "laySaltDownloadManager.h" #include "laySalt.h" #include "ui_SaltGrainTemplateSelectionDialog.h" #include "tlString.h" @@ -151,6 +152,7 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) 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 (); @@ -258,6 +260,42 @@ SaltManagerDialog::mark_clicked () model->set_marked (g->name (), !model->is_marked (g->name ())); } +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)) { + manager.execute (*mp_salt); + } + +END_PROTECTED +} + void SaltManagerDialog::edit_properties () { diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h index cbbfac0a4..c6e4ff893 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -95,6 +95,11 @@ private slots: */ void mode_changed (); + /** + * @brief Called when the "apply" button is clicked + */ + void apply (); + /** * @brief Called when one search text changed */ diff --git a/src/tl/tl.pro b/src/tl/tl.pro index 39a9e5713..0a427e5e2 100644 --- a/src/tl/tl.pro +++ b/src/tl/tl.pro @@ -40,7 +40,8 @@ SOURCES = \ tlXMLParser.cc \ tlXMLWriter.cc \ tlFileSystemWatcher.cc \ - tlFileUtils.cc + tlFileUtils.cc \ + tlWebDAV.cc HEADERS = \ tlAlgorithm.h \ @@ -83,7 +84,8 @@ HEADERS = \ tlXMLWriter.h \ tlFileSystemWatcher.h \ tlCommon.h \ - tlFileUtils.h + tlFileUtils.h \ + tlWebDAV.h INCLUDEPATH = DEPENDPATH = diff --git a/src/tl/tlHttpStream.cc b/src/tl/tlHttpStream.cc index 216e31c76..d91bfb905 100644 --- a/src/tl/tlHttpStream.cc +++ b/src/tl/tlHttpStream.cc @@ -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 *))); - issue_request (QUrl (tl::to_qstring (url))); mp_reply = 0; } @@ -115,6 +114,12 @@ 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) { @@ -148,19 +153,26 @@ 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 (QNetworkRequest (url), m_request); + s_network_manager->sendCustomRequest (request, m_request); } else { mp_buffer = new QBuffer (&m_data); - s_network_manager->sendCustomRequest (QNetworkRequest (url), m_request, mp_buffer); + 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) { diff --git a/src/tl/tlHttpStream.h b/src/tl/tlHttpStream.h index 7f20c0aad..acd3b8635 100644 --- a/src/tl/tlHttpStream.h +++ b/src/tl/tlHttpStream.h @@ -90,6 +90,11 @@ public: */ 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. @@ -121,6 +126,7 @@ private: QByteArray m_request; QByteArray m_data; QBuffer *mp_buffer; + std::map m_headers; void issue_request (const QUrl &url); }; diff --git a/src/tl/tlWebDAV.cc b/src/tl/tlWebDAV.cc new file mode 100644 index 000000000..19c2a004e --- /dev/null +++ b/src/tl/tlWebDAV.cc @@ -0,0 +1,316 @@ + +/* + + 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 files 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; + } + + } + } + + 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/tlHttpStream.cc b/src/unit_tests/tlHttpStream.cc index 1783b2ccb..2c493de12 100644 --- a/src/unit_tests/tlHttpStream.cc +++ b/src/unit_tests/tlHttpStream.cc @@ -24,7 +24,8 @@ #include "tlHttpStream.h" #include "utHead.h" -std::string test_url1 ("http://www.klayout.org/svn-public/klayout-resources/trunk/testdata/text"); +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) { @@ -36,3 +37,39 @@ TEST(1) 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/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 608f86786..85b821dd9 100644 --- a/src/unit_tests/unit_tests.pro +++ b/src/unit_tests/unit_tests.pro @@ -98,7 +98,8 @@ SOURCES = \ tlFileSystemWatcher.cc \ laySalt.cc \ tlFileUtils.cc \ - tlHttpStream.cc + tlHttpStream.cc \ + tlWebDAV.cc # main components: SOURCES += \ From 7228efc7bd7ba2ea783a77ce789425a5ee437ae6 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Mon, 27 Mar 2017 23:55:26 +0200 Subject: [PATCH 27/27] WIP: various enhancements for salt manager * double-click * salt mine context menu * bug fixes * etc. --- src/lay/SaltManagerDialog.ui | 30 ++- .../SaltManagerInstallConfirmationDialog.ui | 9 + src/lay/images/marked_16.png | Bin 889 -> 336 bytes src/lay/images/marked_24.png | Bin 1452 -> 424 bytes src/lay/laySalt.cc | 1 + src/lay/laySaltDownloadManager.cc | 40 +++- src/lay/laySaltGrain.cc | 4 + src/lay/laySaltGrainDetailsTextWidget.cc | 7 +- src/lay/laySaltGrainPropertiesDialog.cc | 62 ++++-- src/lay/laySaltManagerDialog.cc | 188 ++++++++++++---- src/lay/laySaltManagerDialog.h | 33 ++- src/lay/laySaltModel.cc | 207 ++++++++++++++++-- src/lay/laySaltModel.h | 72 +++++- src/tl/tlString.cc | 14 ++ src/tl/tlWebDAV.cc | 6 +- 15 files changed, 566 insertions(+), 107 deletions(-) diff --git a/src/lay/SaltManagerDialog.ui b/src/lay/SaltManagerDialog.ui index a51993a12..77651bd95 100644 --- a/src/lay/SaltManagerDialog.ui +++ b/src/lay/SaltManagerDialog.ui @@ -231,8 +231,8 @@ 0 0 - 537 - 284 + 343 + 207 @@ -673,6 +673,13 @@ + + + + Set in code + + + @@ -725,16 +732,19 @@ Delete package - - - - :/import.png:/import.png - + - Import + Unmark all - - Import package + + + + Show marked only + + + + + Show all diff --git a/src/lay/SaltManagerInstallConfirmationDialog.ui b/src/lay/SaltManagerInstallConfirmationDialog.ui index 0920a619b..d8dcf20e4 100644 --- a/src/lay/SaltManagerInstallConfirmationDialog.ui +++ b/src/lay/SaltManagerInstallConfirmationDialog.ui @@ -26,6 +26,15 @@ + + Qt::NoFocus + + + false + + + true + Package diff --git a/src/lay/images/marked_16.png b/src/lay/images/marked_16.png index aa152411be8cfc02a42015b1b194db9e08d7321e..777a625a7262019d2b6404dad56282026f5a0bb1 100644 GIT binary patch delta 250 zcmV7#7)P> zwp?A>931==AK{PlxiJMD=Q#9CZxJNi#u3Wvo+_?(2hN6MF0Q*07*qoM6N<$f-19a A^Z)<= delta 808 zcmV+@1K0e}0{I4zI|~Av0007;0h?-OvXMq7e**SNL_t(IjeV0_Ow(r=hoApfdQhfP z+oIMeAVo7Og%%>{W>Y*QTegra1Kl(%CPpO-iDC1CY|Lmf*_an@alyF@v*;3)Y>P3A za}m>(8H~!*jzehcVYF3@mKBPO0&Txt(W&Tumv8dCzxSx{tlCgvS?V&W^;=XzWdTUY ze}q3WF>(^ez;7s|q?9GVkw2nWax94SFNw@=*JRlZPJLO{LX8oC2a^$}Cwi^= za<4a1f4}x1(EO#p*-*8(dO(?m^*z@2&6)-KKgp{z&dJD}Q5b+asii_Gmo0sJ6_Ylb z-35)mDAUgQvvN*jnGyow4pK;miP{@_$dEH z&hwd1DWx?1*}$>8t^D*#J0VqUWNZ6d27zU%YxTTh!~ek!>EMGKyEt~Yl}$@(DL1~# zx%dTGp5s*Y9J+cbYj&n-wpu#_Hetdp?KIrj!RgR(=48y}MEMy8GJ=eWF`YNoe?KY0 zBmPA-nktGYLd$jVJzR|Oa&X5}y zE($2XjG2|GmF(<#m!I$d2taXGDevX(q?cX*BoZm56rGdZUG+*7axWW?8`IRrmD;}cpkM=>2|QuQ_lGZ zXVdL$%dMkGZzn(?9qWj8MIR=jrxe;yS#9eB7ngmX_nNzc@Obd)L24J!xxSB}Iz*T- z0KA?*rMrjNjiVRLfn&OT?$BezN@mIYOjEARO zZo4#})kXEb_Eqs1;p@h=brqs1yF zlTkr13zt>=z#<}86v1FMh-k8jf`UQUFt@lYd&}Lq`-4q8^SraO?=X~`sooOtHN2Kl zpa)y{!DAWtHtta^1K&Y}nWFJN>^AX68F-_KzhSIMyn?+Z9)DrENPGYX3H(<0nnF^= zQ3C(QbgRLY4)GzJCGbe-o|aj}TnG4h3V%T@Z&E+5aDsZyxM5tR@CXa-tgMcAd}1Ag zsrMte&cH8)1J+iS5)&W%ZCedD8Tc0_+QsASr;uaZW#ADuI>liW&n>{@^CbL}t}-k1 zbzXt_Zt*yqIX(E{e{sS|&D#49j2BS=CWW?57FaC`kF%1%OS`2`m2^wR851t?uJQ{- W-)*78vH0=;0000>ZGQzRNklGwZePr??Q-Z zU?niHw6!=vx+AzWHbqmkrG8;JtcoTjX)#8$8l?hU(y!=s?sKv$a19xGkJs=nWJF`N z5TYlL4-8pzYB`RWoO)9SZ~-QBq-|vYkvl4Kn3xbf)|?U&k3`U3Oaj4~YYpscIXL-X z>#@gED<9k>gnzgfi9rkhm`AkQ6I*D%rr-%mY!Rv=@4d?sF>;}8mNh}^)2V|1mpoS( zEsqw7mRM>2?=MquaX*FG$Fa!j&YKeyU{X!ezP=#|8E@NWt09WkCAbwUIoepPSf$rb z`uQxcIqWXp5a71UDy8;g*j7&0- z12WH-On)Ws+}8jMjU3L?i8FEH#Lzs7AO9pOQZ4d@#G)gaq3LpRNL;7zZgeA)Or!wJ zI5vfE&+i66mSo;dTZbYkR8T>3NE}v0lhB=G)H}i>h`^2=!|*fh zsDtmD_qNxbNu0&-C_7Cwahc0VNF-`828h0%(lBk%k!>VI7|Oe=OUzSjhr+znLxeY`{Hvy-yzr=%4<33 zl~Hz+TH%!eJbwMn>9wy4zY<_=`lrZ}++oqJT9NhvAxh|NgROQqQh23Q^2)vi@PF@9 z!HC9cKgQae+F5#5bT%*8Ub-Xv;a(GYsFwpL&JMx43^&VaHhL6Ed7q*6wE)VEVqjeB zf#z=B`yxzXvGd~R%KI-0t>V9X~i2-g?SxJ)1QkJrn z;|WP!2H%hnLJ~quuW_C!eC@3Ux0000 +#include namespace lay { @@ -47,10 +48,17 @@ public: 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); + } } }; @@ -72,11 +80,25 @@ SaltDownloadManager::compute_dependencies (const lay::Salt &salt, const lay::Sal { 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); - std::map registry = m_registry; + 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) { @@ -147,7 +169,12 @@ SaltDownloadManager::fetch_missing (const lay::Salt &salt_mine, tl::AbsoluteProg p->second.url = g->url (); } - p->second.grain = SaltGrain::from_url (p->second.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; } @@ -158,13 +185,20 @@ SaltDownloadManager::fetch_missing (const lay::Salt &salt_mine, tl::AbsoluteProg 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) { - dialog.add_info (p->first, true, g->version () + "->" + p->second.version, p->second.url); + // \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); } } diff --git a/src/lay/laySaltGrain.cc b/src/lay/laySaltGrain.cc index 61ceb8fef..63c7f5689 100644 --- a/src/lay/laySaltGrain.cc +++ b/src/lay/laySaltGrain.cc @@ -392,6 +392,10 @@ SaltGrain::from_path (const std::string &path) 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); diff --git a/src/lay/laySaltGrainDetailsTextWidget.cc b/src/lay/laySaltGrainDetailsTextWidget.cc index f71bdda14..310a18a6e 100644 --- a/src/lay/laySaltGrainDetailsTextWidget.cc +++ b/src/lay/laySaltGrainDetailsTextWidget.cc @@ -226,8 +226,11 @@ SaltGrainDetailsTextWidget::details_text () 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)) << " - "; - stream << "[" << tl::to_qstring (tl::escaped_to_html (d->url)) << "]
"; + 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 << "

"; } diff --git a/src/lay/laySaltGrainPropertiesDialog.cc b/src/lay/laySaltGrainPropertiesDialog.cc index 3059cb653..d78ee94bf 100644 --- a/src/lay/laySaltGrainPropertiesDialog.cc +++ b/src/lay/laySaltGrainPropertiesDialog.cc @@ -169,12 +169,19 @@ SaltGrainPropertiesDialog::update_controls () 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 (); @@ -249,9 +256,11 @@ SaltGrainPropertiesDialog::dependency_changed (QTreeWidgetItem *item, int column } 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) { - std::string name = tl::to_string (item->data (0, Qt::UserRole).toString ().simplified ()); item->setData (0, Qt::EditRole, tl::to_qstring (name)); // set URL and version for known grains @@ -265,30 +274,48 @@ SaltGrainPropertiesDialog::dependency_changed (QTreeWidgetItem *item, int column } else { - SaltGrain *g = 0; - for (lay::Salt::flat_iterator i = mp_salt->begin_flat (); i != mp_salt->end_flat (); ++i) { - if ((*i)->name () == name) { - g = *i; - } - } if (g) { item->setData (1, Qt::UserRole, tl::to_qstring (g->version ())); - item->setData (2, Qt::UserRole, tl::to_qstring (g->url ())); + item->setData (2, Qt::UserRole, QString ()); // placeholder texts: item->setData (1, Qt::EditRole, tl::to_qstring (g->version ())); - item->setData (2, Qt::EditRole, tl::to_qstring (g->url ())); + 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 ("(unknown packet)")); + item->setData (2, Qt::EditRole, tr ("(from repository)")); } } - } else if (column > 0) { - item->setData (column, Qt::EditRole, item->data (column, Qt::UserRole).toString ()); + } 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; @@ -531,10 +558,7 @@ SaltGrainPropertiesDialog::accept () } dep_seen.insert (d->name); - if (! dep.is_valid_name (d->name)) { - dependencies_alert->warn () << tr ("'%1' is not a name of a package loaded already").arg (tl::to_qstring (d->name)) << tl::endl - << tr ("You need to specify the details (version, URL) manually"); - } else { + 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) { @@ -549,11 +573,7 @@ SaltGrainPropertiesDialog::accept () << tr ("If the dependency package has a version itself, the version is automatically set to it's current version."); } - if (d->url.empty ()) { - dependencies_alert->warn () << tr ("No download URL specified for dependency '%1'").arg (tl::to_qstring (d->name)) << tl::endl - << tr ("A download URL should be specified to ensure the package dependencies can be resolved.") << tl::endl - << tr ("If the dependency package was downloaded itself, the URL is automatically set to the download source."); - } else { + if (!d->url.empty ()) { SaltGrain gdep; try { gdep = SaltGrain::from_url (d->url); diff --git a/src/lay/laySaltManagerDialog.cc b/src/lay/laySaltManagerDialog.cc index 978dc9c46..aa3ecd745 100644 --- a/src/lay/laySaltManagerDialog.cc +++ b/src/lay/laySaltManagerDialog.cc @@ -144,7 +144,7 @@ lay::Salt *get_salt_mine () SaltManagerDialog::SaltManagerDialog (QWidget *parent) : QDialog (parent), - m_current_changed_enabled (true) + m_current_changed_enabled (true), dm_update_models (this, &SaltManagerDialog::update_models) { Ui::SaltManagerDialog::setupUi (this); mp_properties_dialog = new lay::SaltGrainPropertiesDialog (this); @@ -165,15 +165,6 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) salt_mine_view->setModel (mine_model); salt_mine_view->setItemDelegate (new SaltItemDelegate (this)); - // 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 (), tl::to_string (tr ("An update to version %1 is available").arg (tl::to_qstring (gm->version ())))); - mine_model->set_message ((*g)->name (), tl::to_string (tr ("The installed version is outdated (%1)").arg (tl::to_qstring ((*g)->version ())))); - } - } - mode_tab->setCurrentIndex (mp_salt->is_empty () ? 1 : 0); connect (mode_tab, SIGNAL (currentChanged (int)), this, SLOT (mode_changed ())); @@ -181,11 +172,12 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) connect (mp_salt, SIGNAL (collections_changed ()), this, SLOT (salt_changed ())); connect (mp_salt_mine, SIGNAL (collections_changed ()), this, SLOT (salt_mine_changed ())); - salt_changed (); - 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); @@ -193,6 +185,18 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent) 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 @@ -201,11 +205,55 @@ 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) { @@ -258,6 +306,36 @@ SaltManagerDialog::mark_clicked () } 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 @@ -290,6 +368,7 @@ BEGIN_PROTECTED manager.compute_dependencies (*mp_salt, *mp_salt_mine); if (manager.show_confirmation_dialog (this, *mp_salt)) { + unmark_all (); manager.execute (*mp_salt); } @@ -363,16 +442,38 @@ 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 ()); - if (! model) { - return; - } + 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 ()) { @@ -392,7 +493,42 @@ SaltManagerDialog::salt_changed () } + 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 @@ -417,28 +553,6 @@ SaltManagerDialog::current_grain () return model ? model->grain_from_index (salt_view->currentIndex ()) : 0; } -void -SaltManagerDialog::salt_mine_changed () -{ - SaltModel *model = dynamic_cast (salt_mine_view->model ()); - if (! model) { - return; - } - - // 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->update (); - m_current_changed_enabled = true; - - // select the first grain - if (model->rowCount (QModelIndex ()) > 0) { - salt_mine_view->setCurrentIndex (model->index (0, 0, QModelIndex ())); - } - - mine_current_changed (); -} - void SaltManagerDialog::mine_current_changed () { diff --git a/src/lay/laySaltManagerDialog.h b/src/lay/laySaltManagerDialog.h index c6e4ff893..6e1f40cf4 100644 --- a/src/lay/laySaltManagerDialog.h +++ b/src/lay/laySaltManagerDialog.h @@ -24,6 +24,7 @@ #define HDR_laySaltManagerDialog #include "ui_SaltManagerDialog.h" +#include "tlDeferredExecution.h" #include #include @@ -105,14 +106,32 @@ private slots: */ void search_text_changed (const QString &text); -private: - lay::Salt *mp_salt, *mp_salt_mine; - std::auto_ptr m_remote_grain; - bool m_current_changed_enabled; - lay::SaltGrainPropertiesDialog *mp_properties_dialog; + /** + * @brief Called to show the marked items only + */ + void show_marked_only (); - lay::SaltGrain *current_grain (); - lay::SaltGrain *mine_current_grain (); + /** + * @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 (); }; } diff --git a/src/lay/laySaltModel.cc b/src/lay/laySaltModel.cc index 57dd44212..fc1a44ef8 100644 --- a/src/lay/laySaltModel.cc +++ b/src/lay/laySaltModel.cc @@ -46,6 +46,9 @@ SaltItemDelegate::paint (QPainter *painter, const QStyleOptionViewItem &option, 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; @@ -58,6 +61,8 @@ SaltItemDelegate::paint (QPainter *painter, const QStyleOptionViewItem &option, 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); @@ -93,15 +98,32 @@ SaltItemDelegate::sizeHint (const QStyleOptionViewItem &option, const QModelInde SaltModel::SaltModel (QObject *parent, lay::Salt *salt) : QAbstractItemModel (parent), mp_salt (salt) { - // .. nothing yet .. + create_ordered_list (); } -QVariant +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 = mp_salt->begin_flat ()[index.row ()]; + const lay::SaltGrain *g = grain_from_index (index); + if (!g) { + return QVariant (); + } std::string text = ""; text += "

"; @@ -121,9 +143,15 @@ SaltModel::data (const QModelIndex &index, int role) const text += "

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

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

"; + 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 += ""; @@ -134,7 +162,10 @@ SaltModel::data (const QModelIndex &index, int role) const int icon_dim = 64; - const lay::SaltGrain *g = mp_salt->begin_flat ()[index.row ()]; + const lay::SaltGrain *g = grain_from_index (index); + if (!g) { + return QVariant (); + } QImage img; if (g->icon ().isNull ()) { @@ -160,10 +191,21 @@ SaltModel::data (const QModelIndex &index, int role) const painter.drawImage (0, 0, warn); } - if (m_messages.find (g->name ()) != m_messages.end ()) { - QPainter painter (&img); - QImage warn (":/warn_16.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); @@ -179,7 +221,7 @@ SaltModel::index (int row, int column, const QModelIndex &parent) const if (parent.isValid ()) { return QModelIndex (); } else { - return createIndex (row, column, mp_salt->begin_flat () [row]); + return createIndex (row, column, m_ordered_grains [row]); } } @@ -201,7 +243,7 @@ SaltModel::rowCount (const QModelIndex &parent) const if (parent.isValid ()) { return 0; } else { - return mp_salt->end_flat () - mp_salt->begin_flat (); + return int (m_ordered_grains.size ()); } } @@ -221,32 +263,149 @@ 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) { - m_marked.erase (name); - } else { - m_marked.insert (name); + 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 ())); } - emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); } void -SaltModel::set_message (const std::string &name, const std::string &message) +SaltModel::clear_marked () { - if (message.empty ()) { - m_messages.erase (name); - } else { - m_messages.insert (std::make_pair (name, message)); + if (! m_marked.empty ()) { + m_marked.clear (); + emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); } - emit dataChanged (index (0, 0, QModelIndex ()), index (rowCount (QModelIndex ()) - 1, 0, QModelIndex ())); } -void +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 index 20a537e2c..d7ba80d92 100644 --- a/src/lay/laySaltModel.h +++ b/src/lay/laySaltModel.h @@ -47,6 +47,17 @@ class SaltModel Q_OBJECT public: + /** + * @brief An enum describing the severity of a message + */ + enum Severity + { + None = 0, + Info = 1, + Warning = 2, + Error = 3 + }; + /** * @brief Constructor */ @@ -57,6 +68,11 @@ public: */ 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 */ @@ -93,18 +109,70 @@ public: */ 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, const std::string &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::map m_messages; + 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 (); }; // -------------------------------------------------------------------------------------- diff --git a/src/tl/tlString.cc b/src/tl/tlString.cc index 22d05402f..3179df4ac 100644 --- a/src/tl/tlString.cc +++ b/src/tl/tlString.cc @@ -416,6 +416,20 @@ 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 += "<"; diff --git a/src/tl/tlWebDAV.cc b/src/tl/tlWebDAV.cc index 19c2a004e..04706e78e 100644 --- a/src/tl/tlWebDAV.cc +++ b/src/tl/tlWebDAV.cc @@ -273,8 +273,10 @@ WebDAVObject::download (const std::string &url, const std::string &target) bool has_errors = false; { - tl::info << tl::to_string (QObject::tr ("Downloading %1 files now").arg (items.size ())); + 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)); @@ -307,6 +309,8 @@ WebDAVObject::download (const std::string &url, const std::string &target) has_errors = true; } + ++progress; + } }