File indexing completed on 2024-05-12 16:34:42

0001 /* Part of the Calligra project
0002  * Copyright (C) 2010-2014 Yue Liu <yue.liu@mail.com>
0003  *
0004  * This library is free software; you can redistribute it and/or
0005  * modify it under the terms of the GNU Library General Public
0006  * License as published by the Free Software Foundation; either
0007  * version 2 of the License, or (at your option) any later version.
0008  *
0009  * This library is distributed in the hope that it will be useful,
0010  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0011  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0012  * Library General Public License for more details.
0013  *
0014  * You should have received a copy of the GNU Library General Public License
0015  * along with this library; see the file COPYING.LIB.  If not, write to
0016  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0017  * Boston, MA 02110-1301, USA.
0018  */
0019 
0020 #include "StencilBoxDocker.h"
0021 
0022 #include "StencilBoxDocker_p.h"
0023 #include "StencilShapeFactory.h"
0024 #include "CollectionItemModel.h"
0025 #include "CollectionTreeWidget.h"
0026 #include "StencilBoxDebug.h"
0027 
0028 #include <KoShapeFactoryBase.h>
0029 #include <KoShapeRegistry.h>
0030 #include <KoCanvasController.h>
0031 #include <KoCreateShapesTool.h>
0032 #include <KoShape.h>
0033 #include <KoShapeGroup.h>
0034 #include <KoZoomHandler.h>
0035 #include <KoShapePaintingContext.h>
0036 #include <KoProperties.h>
0037 #include <KoIcon.h>
0038 
0039 #include <kiconeffect.h>
0040 #include <kcolorscheme.h>
0041 #include <klocalizedstring.h>
0042 #include <kdesktopfile.h>
0043 #include <kconfiggroup.h>
0044 #include <kmessagebox.h>
0045 #include <klineedit.h>
0046 
0047 #ifdef GHNS
0048 #include <knewstuff3/downloaddialog.h>
0049 #endif
0050 
0051 #include <QStandardPaths>
0052 #include <QVBoxLayout>
0053 #include <QListView>
0054 #include <QStandardItemModel>
0055 #include <QRegExp>
0056 #include <QSortFilterProxyModel>
0057 #include <QList>
0058 #include <QSize>
0059 #include <QToolButton>
0060 #include <QDir>
0061 #include <QFile>
0062 #include <QMenu>
0063 #include <QPainter>
0064 #include <QDesktopServices>
0065 #include <QPixmapCache>
0066 
0067 #define StencilShapeId "StencilShape"
0068 
0069 StencilBoxDocker::StencilBoxDocker(QWidget* parent)
0070     : QDockWidget(parent)
0071 {
0072     setWindowTitle(i18n("Stencil Box"));
0073     QWidget* mainWidget = new QWidget(this);
0074     mainWidget->setAcceptDrops(true);
0075     setWidget(mainWidget);
0076 
0077     m_menu = new QMenu();
0078 #ifdef GHNS
0079     QAction *ghnsAction = m_menu->addAction(koIcon("get-hot-new-stuff"), i18n("Stencils Online"));
0080     connect(ghnsAction, SIGNAL(triggered()), this, SLOT(getHotNewStuff()));
0081 #endif
0082     QAction *installAction = m_menu->addAction(koIcon("document-open-folder"), i18n("Add/Remove Stencil"));
0083     connect(installAction, SIGNAL(triggered()), this, SLOT(manageStencilsFolder()));
0084 
0085     m_button = new QToolButton;
0086     /*
0087     m_button->setFixedHeight(qApp->fontMetrics().height()+3);
0088     m_button->setAutoFillBackground(true);
0089     m_button->setStyleSheet("\
0090         QToolButton {\
0091             border: 1px solid #a0a0a0;\
0092             border-top: 0px;\
0093             border-left: 0px;\
0094             border-right: 0px;\
0095             background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,\
0096             stop:0 #ffffff, stop:0.5 #e0e0e0, stop:1 #ffffff);\
0097         }");*/
0098     m_button->setIcon(koIcon("list-add"));
0099     m_button->setToolTip(i18n("More shapes"));
0100     m_button->setMenu(m_menu);
0101     m_button->setPopupMode(QToolButton::InstantPopup);
0102 
0103     m_filterLineEdit = new KLineEdit;
0104     m_filterLineEdit->setPlaceholderText(i18n("Filter"));
0105     m_filterLineEdit->setClearButtonShown(true);
0106 
0107     m_treeWidget = new CollectionTreeWidget(mainWidget);
0108     m_treeWidget->setSelectionMode(QListView::SingleSelection);
0109     m_treeWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0110 
0111     m_panelLayout = new QHBoxLayout();
0112     m_panelLayout->addWidget(m_button);
0113     m_panelLayout->addWidget(m_filterLineEdit);
0114 
0115     m_layout = new QVBoxLayout(mainWidget);
0116     m_layout->addLayout(m_panelLayout);
0117     m_layout->addWidget(m_treeWidget);
0118 
0119     // Load the stencils
0120     m_loader = new StencilBoxDockerLoader(this);
0121     m_loader->moveToThread(&loaderThread);
0122     connect(&loaderThread, SIGNAL(started()), this, SLOT(threadStarted()));
0123     connect(this , SIGNAL(startLoading()), m_loader, SLOT(loadShapeCollections()));
0124     connect(&loaderThread, SIGNAL(finished()), m_loader, SLOT(deleteLater()));
0125     connect(m_loader, SIGNAL(resultReady()), this, SLOT(collectionsLoaded()));
0126     loaderThread.start();
0127 }
0128 
0129 StencilBoxDocker::~StencilBoxDocker()
0130 {
0131     loaderThread.quit();
0132     loaderThread.wait();
0133     qDeleteAll(m_modelMap);
0134 }
0135 
0136 void StencilBoxDocker::threadStarted()
0137 {
0138     Q_EMIT startLoading();
0139 }
0140 
0141 void StencilBoxDocker::collectionsLoaded()
0142 {
0143     debugStencilBox;
0144     m_modelMap = m_loader->m_modelMap;
0145     m_treeWidget->setFamilyMap(m_modelMap);
0146     m_treeWidget->regenerateFilteredMap();
0147     connect(this, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
0148             this, SLOT(locationChanged(Qt::DockWidgetArea)));
0149     connect(m_filterLineEdit, SIGNAL(textEdited(QString)), this, SLOT(reapplyFilter()));
0150 
0151     loaderThread.quit();
0152 }
0153 
0154 #ifdef GHNS
0155 void StencilBoxDocker::getHotNewStuff()
0156 {
0157     KNS3::DownloadDialog dialog("calligra_stencils.knsrc", this);
0158     dialog.exec();
0159     if(!dialog.installedEntries().isEmpty()) {
0160         KMessageBox::information(0, i18n("Stencils successfully installed."));
0161     }
0162     else if(!dialog.changedEntries().isEmpty()) {
0163         KMessageBox::information(0, i18n("Stencils successfully uninstalled."));
0164     }
0165 }
0166 #endif
0167 
0168 void StencilBoxDocker::manageStencilsFolder()
0169 {
0170     const QString destination = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/calligra/stencils");
0171     QDir().mkpath(destination);
0172     QFile file(destination + "/readme.txt");
0173     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0174         debugStencilBox << "could not open" << destination + "/readme.txt" << "for writing";
0175     } else {
0176         QTextStream out(&file);
0177         out << i18n("\
0178 This is the user stencils directory.\n\
0179 From here you can add / remove stencils for use in the Stencil Box docker.\n\
0180 \n\
0181 Stencils are organized in collections, a collection is a folder containing a text file 'collection.desktop':\n\
0182 \n\
0183 collection.desktop\n\
0184 \n\
0185 [Desktop Entry]\n\
0186 Name=Your Stencil Collection Name\n\
0187 \n\
0188 A stencil is an ODG/SVG file, a desktop file, an optional PNG icon file, all with the same name under its collection folder:\n\
0189 \n\
0190 foo.odg or foo.svgz or foo.svg\n\
0191 \n\
0192 ODF elements for stencil should be a <draw:g> element or <draw:custom-shape> element\n\
0193 No special requirements to SVG file\n\
0194 \n\
0195 foo.desktop\n\
0196 \n\
0197 [Desktop Entry]\n\
0198 Name=Foo\n\
0199 CS-KeepAspectRatio=1\n\
0200 \n\
0201 If CS-KeepAspectRatio=1, the stencil added to canvas will have geometry aspect ratio locked, by default it's 0.\n\
0202 \n\
0203 foo.png\n\
0204 \n\
0205 Should have size 32x32 pixel, if the png file is not included, the ODG/SVG file will be rendered as the icon,\n\
0206 but it won't look good under small pixels when the stencil stroke is complicated.\n");
0207         file.close();
0208     }
0209     QDesktopServices::openUrl(QUrl::fromLocalFile(destination));
0210 }
0211 
0212 void StencilBoxDocker::locationChanged(Qt::DockWidgetArea area)
0213 {
0214     switch(area) {
0215         case Qt::TopDockWidgetArea:
0216         case Qt::BottomDockWidgetArea:
0217             break;
0218         case Qt::LeftDockWidgetArea:
0219         case Qt::RightDockWidgetArea:
0220             break;
0221         default:
0222             break;
0223     }
0224     m_layout->setSizeConstraint(QLayout::SetMinAndMaxSize);
0225     m_layout->invalidate();
0226 }
0227 
0228 void StencilBoxDocker::reapplyFilter()
0229 {
0230     QRegExp regExp(m_filterLineEdit->originalText(), Qt::CaseInsensitive, QRegExp::RegExp2);
0231     m_treeWidget->setFilter(regExp);
0232 }
0233 
0234 /// Load shape collections to m_modelMap and register in the KoShapeRegistry
0235 void StencilBoxDockerLoader::loadShapeCollections()
0236 {
0237     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("calligra/stencils"), QStandardPaths::LocateDirectory);
0238     foreach(const QString& path, dirs)
0239     {
0240         debugStencilBox << path;
0241         QDir dir(path);
0242         QStringList collectionDirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0243         foreach(const QString & collectionDirName, collectionDirs) {
0244             addCollection(path + QLatin1Char('/') + collectionDirName);
0245             debugStencilBox << path + collectionDirName;
0246         }
0247     }
0248     emit resultReady();
0249 }
0250 
0251 bool StencilBoxDockerLoader::addCollection(const QString& path)
0252 {
0253     QDir dir(path);
0254 
0255     if(!dir.exists("collection.desktop"))
0256         return false;
0257 
0258     KDesktopFile collection(dir.absoluteFilePath("collection.desktop"));
0259     KConfigGroup dg = collection.desktopGroup();
0260     QString family = dg.readEntry("Name");
0261 
0262     if(!m_modelMap.contains(family)) {
0263         CollectionItemModel* model = new CollectionItemModel();
0264         m_modelMap.insert(family, model);
0265     }
0266 
0267     CollectionItemModel* model = m_modelMap[family];
0268     QList<KoCollectionItem> templateList = model->shapeTemplateList();
0269     QStringList stencils = dir.entryList(QStringList("*.desktop"));
0270 
0271     KStatefulBrush brushForeground(KColorScheme::Window, KColorScheme::NormalText);
0272     KStatefulBrush brushBackground(KColorScheme::Window, KColorScheme::NormalBackground);
0273     const QColor blackColor = brushForeground.brush(q).color();
0274     const QColor whiteColor = brushBackground.brush(q).color();
0275 
0276     foreach(const QString & stencil, stencils) {
0277         if(stencil == "collection.desktop")
0278             continue;
0279 
0280         KDesktopFile entry(dir.absoluteFilePath(stencil));
0281         KConfigGroup content = entry.desktopGroup();
0282         QString name = content.readEntry("Name");
0283         bool keepAspectRatio = content.readEntry("CS-KeepAspectRatio", false);
0284         KoProperties* props = new KoProperties();
0285         props->setProperty("keepAspectRatio", keepAspectRatio);
0286 
0287         // find data file path
0288         QString filename = dir.absoluteFilePath(stencil);
0289         filename.chop(7); // remove 'desktop'
0290         static const char * const suffix[3] = { "odg", "svgz", "svg"};
0291         static const int suffixCount = sizeof(suffix)/sizeof(suffix[0]);
0292 
0293         QString source;
0294         for (int i = 0; i < suffixCount; ++i) {
0295             source = filename + QLatin1String(suffix[i]);
0296             if (QFile::exists(source)) {
0297                 break;
0298             }
0299             source.clear();
0300         }
0301         if (source.isEmpty()) {
0302             debugStencilBox << filename << "not found";
0303             continue;
0304         }
0305 
0306         // register shape factory
0307         StencilShapeFactory* factory = new StencilShapeFactory(source, name, props);
0308         KoShapeRegistry::instance()->add(source, factory);
0309 
0310         KoCollectionItem temp;
0311         temp.id = source;
0312         temp.name = name;
0313         temp.toolTip = name;
0314 
0315         QImage img;
0316         const QString thumbnailFile = filename + QStringLiteral("png");
0317         if (QFile::exists(thumbnailFile)) {
0318             img = QIcon(thumbnailFile).pixmap(QSize(22, 22)).toImage();
0319         } else {
0320             // generate icon using factory
0321             QPixmap pix(22, 22);
0322             pix.fill(Qt::white);
0323             if (!QPixmapCache::find(source, &pix)) {
0324                 KoShape* shape = factory->createDefaultShape();
0325                 if (shape) {
0326                     KoZoomHandler converter;
0327                     qreal diffx = 20 / converter.documentToViewX(shape->size().width());
0328                     qreal diffy = 20 / converter.documentToViewY(shape->size().height());
0329                     converter.setZoom(qMin(diffx, diffy));
0330                     QPainter painter(&pix);
0331                     painter.setRenderHint(QPainter::Antialiasing, true);
0332                     painter.translate(1, 1);
0333                     KoShapePaintingContext paintContext;
0334                     shape->paint(painter, converter, paintContext);
0335                     painter.end();
0336                     QPixmapCache::insert(source, pix);
0337                     delete shape;
0338                 }
0339             }
0340             img = pix.toImage();
0341         }
0342         KIconEffect::toMonochrome(img, blackColor, whiteColor, 1.0f);
0343         temp.icon = QIcon(QPixmap::fromImage(img));
0344         templateList.append(temp);
0345     }
0346     model->setShapeTemplateList(templateList);
0347     return true;
0348 }
0349 
0350 void StencilBoxDocker::removeCollection(const QString& family)
0351 {
0352     if(m_modelMap.contains(family))
0353     {
0354         CollectionItemModel* model = m_modelMap[family];
0355         QList<KoCollectionItem> list = model->shapeTemplateList();
0356         foreach(const KoCollectionItem & temp, list)
0357         {
0358             KoShapeFactoryBase* factory = KoShapeRegistry::instance()->get(temp.id);
0359             KoShapeRegistry::instance()->remove(temp.id);
0360             delete factory;
0361         }
0362 
0363         m_modelMap.remove(family);
0364         delete model;
0365         m_treeWidget->regenerateFilteredMap();
0366     }
0367 }