1 /**
2  * UGENE - Integrated Bioinformatics Tools.
3  * Copyright (C) 2008-2021 UniPro <ugene@unipro.ru>
4  * http://ugene.net
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19  * MA 02110-1301, USA.
20  */
21 
22 #include "WorkflowSamples.h"
23 
24 #include <QAbstractItemModel>
25 #include <QApplication>
26 #include <QContextMenuEvent>
27 #include <QDir>
28 #include <QHeaderView>
29 #include <QLabel>
30 #include <QLineEdit>
31 #include <QMenu>
32 #include <QPainter>
33 #include <QStyle>
34 #include <QStyledItemDelegate>
35 #include <QTextDocument>
36 #include <QTextStream>
37 #include <QToolButton>
38 #include <QTreeView>
39 #include <QUrl>
40 #include <QVBoxLayout>
41 
42 #include <U2Core/L10n.h>
43 #include <U2Core/Log.h>
44 #include <U2Core/Settings.h>
45 #include <U2Core/U2SafePoints.h>
46 
47 #include <U2Designer/WorkflowGUIUtils.h>
48 
49 #include <U2Lang/HRSchemaSerializer.h>
50 #include <U2Lang/WorkflowSettings.h>
51 #include <U2Lang/WorkflowUtils.h>
52 
53 #include "WorkflowViewController.h"
54 #include "util/SaveSchemaImageUtils.h"
55 
56 Q_DECLARE_METATYPE(QTextDocument *)
57 
58 namespace U2 {
59 
60 const QString SamplesWidget::MIME_TYPE("application/x-ugene-sample-id");
61 QList<SampleCategory> SampleRegistry::data;
62 
63 #define DATA_ROLE Qt::UserRole
64 #define INFO_ROLE Qt::UserRole + 1
65 #define ICON_ROLE Qt::UserRole + 2
66 #define DOC_ROLE Qt::UserRole + 3
67 #define ID_ROLE Qt::UserRole + 4
68 
69 class SampleDelegate : public QStyledItemDelegate {
70 public:
SampleDelegate(QObject * parent=0)71     SampleDelegate(QObject *parent = 0)
72         : QStyledItemDelegate(parent) {
73     }
sizeHint(const QStyleOptionViewItem & option,const QModelIndex & index) const74     QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
75         QVariant value = index.data(Qt::SizeHintRole);
76         if (value.isValid())
77             return qvariant_cast<QSize>(value);
78 
79         QStyleOptionViewItem opt = option;
80         initStyleOption(&opt, index);
81         const QWidget *widget = qobject_cast<QWidget *>(parent());  // QStyledItemDelegatePrivate::widget(option);
82         QStyle *style = widget ? widget->style() : QApplication::style();
83         opt.rect.setSize(widget->size());
84         return style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, QSize(), widget);
85     }
86 };
87 
SamplesWidget(WorkflowScene * scene,QWidget * parent)88 SamplesWidget::SamplesWidget(WorkflowScene *scene, QWidget *parent)
89     : QTreeWidget(parent) {
90     setColumnCount(1);
91     setHeaderHidden(true);
92     setItemDelegate(new SampleDelegate(this));
93     setWordWrap(true);
94 
95     foreach (const SampleCategory &cat, SampleRegistry::getCategories()) {
96         addCategory(cat);
97     }
98 
99     expandAll();
100 
101     glass = new SamplePane(scene);
102 
103     connect(this, SIGNAL(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)), SLOT(handleTreeItem(QTreeWidgetItem *)));
104     connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), SLOT(activateItem(QTreeWidgetItem *)));
105     connect(glass, SIGNAL(itemActivated(QTreeWidgetItem *)), SLOT(activateItem(QTreeWidgetItem *)));
106     connect(glass, SIGNAL(cancel()), SLOT(cancelItem()));
107     connect(WorkflowSettings::watcher, SIGNAL(changed()), this, SLOT(sl_refreshSampesItems()));
108 }
109 
getSampleItem(const QString & category,const QString & id)110 QTreeWidgetItem *SamplesWidget::getSampleItem(const QString &category, const QString &id) {
111     QList<QTreeWidgetItem *> items = findItems(category, Qt::MatchExactly);
112     CHECK(1 == items.size(), nullptr);
113 
114     for (int i = 0; i < items.first()->childCount(); i++) {
115         QTreeWidgetItem *sampleItem = items.first()->child(i);
116         const QString sampleId = sampleItem->data(0, ID_ROLE).toString();
117         if (sampleId == id) {
118             return sampleItem;
119         }
120     }
121 
122     return nullptr;
123 }
124 
activateSample(const QString & category,const QString & id)125 void SamplesWidget::activateSample(const QString &category, const QString &id) {
126     QTreeWidgetItem *sampleItem = getSampleItem(category, id);
127     CHECK(nullptr != sampleItem, );
128 
129     scrollToItem(sampleItem);
130     setCurrentItem(sampleItem);
131     return;
132 }
133 
loadSample(const QString & category,const QString & id)134 void SamplesWidget::loadSample(const QString &category, const QString &id) {
135     QTreeWidgetItem *sampleItem = getSampleItem(category, id);
136     CHECK(nullptr != sampleItem, );
137 
138     activateItem(sampleItem);
139     return;
140 }
141 
activateItem(QTreeWidgetItem * item)142 void SamplesWidget::activateItem(QTreeWidgetItem *item) {
143     if (item && item->data(0, DATA_ROLE).isValid()) {
144         emit sampleSelected(item->data(0, DATA_ROLE).toString());
145     }
146 }
147 
handleTreeItem(QTreeWidgetItem * item)148 void SamplesWidget::handleTreeItem(QTreeWidgetItem *item) {
149     if (item && !item->data(0, DATA_ROLE).isValid()) {
150         item = nullptr;
151     }
152 
153     glass->setItem(item);
154     emit setupGlass(glass);
155 }
156 
cancelItem()157 void SamplesWidget::cancelItem() {
158     selectionModel()->clear();
159     if (isHidden()) {
160         emit setupGlass(nullptr);
161         glass->setItem(nullptr);
162     } else {
163         emit setupGlass(glass);
164     }
165 }
166 
sl_nameFilterChanged(const QString & nameFilter)167 void SamplesWidget::sl_nameFilterChanged(const QString &nameFilter) {
168     revisible(nameFilter);
169 }
170 
revisible(const QString & nameFilter)171 void SamplesWidget::revisible(const QString &nameFilter) {
172     setMouseTracking(false);
173     for (int catIdx = 0; catIdx < topLevelItemCount(); catIdx++) {
174         QTreeWidgetItem *category = topLevelItem(catIdx);
175         bool hasVisibleSamples = false;
176         QString catName = category->text(0);
177         for (int childIdx = 0; childIdx < category->childCount(); childIdx++) {
178             QTreeWidgetItem *sample = category->child(childIdx);
179             QString name = sample->text(0);
180             if (!NameFilterLayout::filterMatched(nameFilter, name) &&
181                 !NameFilterLayout::filterMatched(nameFilter, catName)) {
182                 sample->setHidden(true);
183             } else {
184                 sample->setHidden(false);
185                 hasVisibleSamples = true;
186             }
187         }
188         category->setHidden(!hasVisibleSamples);
189         category->setExpanded(hasVisibleSamples);
190     }
191     setMouseTracking(true);
192 }
193 
addCategory(const SampleCategory & cat)194 void SamplesWidget::addCategory(const SampleCategory &cat) {
195     QTreeWidgetItem *ci = new QTreeWidgetItem(this, QStringList(cat.d.getDisplayName()));
196     ci->setFlags(Qt::ItemIsEnabled);
197     QFont cf;
198     cf.setBold(true);
199     ci->setData(0, Qt::FontRole, cf);
200     ci->setData(0, Qt::BackgroundRole, QColor(255, 255, 160, 127));
201 
202     foreach (const Sample &item, cat.items) {
203         QTreeWidgetItem *ib = new QTreeWidgetItem(ci, QStringList(item.d.getDisplayName()));
204         ib->setData(0, DATA_ROLE, item.content);
205         ib->setData(0, ID_ROLE, item.id);
206         QTextDocument *doc = new QTextDocument(this);
207         ib->setData(0, DOC_ROLE, qVariantFromValue<QTextDocument *>(doc));
208         Descriptor d = item.d;
209         QIcon ico = item.ico;
210         if (ico.isNull()) {
211             const QPixmap pixmap = SaveSchemaImageUtils::generateSchemaSnapshot(item.content.toUtf8());
212             if (!pixmap.isNull()) {
213                 ico.addPixmap(pixmap);
214             }
215         }
216         DesignerGUIUtils::setupSamplesDocument(d, ico, doc);
217     }
218 }
219 
sl_refreshSampesItems()220 void SamplesWidget::sl_refreshSampesItems() {
221     clear();
222     foreach (const SampleCategory &cat, SampleRegistry::getCategories()) {
223         addCategory(cat);
224     }
225     expandAll();
226 }
227 
mouseDoubleClickEvent(QMouseEvent * e)228 void SamplePane::mouseDoubleClickEvent(QMouseEvent *e) {
229     if (!item) {
230         return;
231     }
232 
233     QTextDocument *doc = item->data(0, DOC_ROLE).value<QTextDocument *>();
234     int pageWidth = qMax(width() - 100, 100);
235     int pageHeight = qMax(height() - 100, 100);
236     if (pageWidth != doc->pageSize().width()) {
237         doc->setPageSize(QSize(pageWidth, pageHeight));
238     }
239 
240     QSize ts = doc->size().toSize();
241     QRect textRect(width() / 2 - pageWidth / 2,
242                    height() / 2 - pageHeight / 2,
243                    pageWidth,
244                    pageHeight);
245     textRect.setSize(ts);
246 
247     QPoint position = e->pos();
248     if (textRect.contains(position)) {
249         emit itemActivated(item);
250     } else {
251         item = nullptr;
252         scene->update();
253     }
254 }
255 
keyPressEvent(QKeyEvent * event)256 void SamplePane::keyPressEvent(QKeyEvent *event) {
257     if (event->key() == Qt::Key_Escape) {
258         emit cancel();
259     } else if (event->key() == Qt::Key_Enter) {
260         emit itemActivated(item);
261     }
262 }
263 
SamplePane(WorkflowScene * _scene)264 SamplePane::SamplePane(WorkflowScene *_scene)
265     : item(nullptr), scene(_scene) {
266     m_document = new QTextDocument(this);
267 }
268 
paint(QPainter * painter)269 void SamplePane::paint(QPainter *painter) {
270     const WorkflowView *ctrl = scene->getController();
271     SAFE_POINT(nullptr != ctrl, "NULL workflow controller", );
272     if (!item && ctrl->isShowSamplesHint()) {
273         DesignerGUIUtils::paintSamplesArrow(painter);
274         return;
275     }
276 
277     if (item) {
278         QTextDocument *doc = item->data(0, DOC_ROLE).value<QTextDocument *>();
279         DesignerGUIUtils::paintSamplesDocument(painter, doc, width(), height(), palette());
280     }
281 }
282 
283 const int LoadSamplesTask::maxDepth = 1;
284 
LoadSamplesTask(const QStringList & lst)285 LoadSamplesTask::LoadSamplesTask(const QStringList &lst)
286     : Task(tr("Load workflow samples"), TaskFlag_None), dirs(lst) {
287 }
288 
run()289 void LoadSamplesTask::run() {
290     foreach (const QString &s, dirs) {
291         scanDir(s);
292     }
293 }
294 
scanDir(const QString & s,int depth)295 void LoadSamplesTask::scanDir(const QString &s, int depth) {
296     QDir dir(s);
297     if (!dir.exists()) {
298         ioLog.error(tr("Sample dir does not exist: %1").arg(s));
299         return;
300     }
301     SampleCategory category(s, dir.dirName());
302     QStringList names;
303     foreach (const QString &ext, WorkflowUtils::WD_FILE_EXTENSIONS) {
304         names << "*." + ext;
305     }
306 
307     foreach (const QFileInfo &fi, dir.entryInfoList(names, QDir::Files | QDir::NoSymLinks)) {
308         QFile f(fi.absoluteFilePath());
309         if (!f.open(QIODevice::ReadOnly)) {
310             ioLog.error(tr("Failed to load sample: %1").arg(L10N::errorOpeningFileRead(fi.absoluteFilePath())));
311             continue;
312         }
313 
314         QTextStream in(&f);
315         in.setCodec("UTF-8");
316         Sample sample;
317         sample.content = in.readAll();
318 
319         Metadata meta;
320         QString err = HRSchemaSerializer::string2Schema(sample.content, nullptr, &meta);
321         if (!err.isEmpty()) {
322             coreLog.error(tr("Failed to load sample: %1").arg(err));
323             continue;
324         }
325         sample.d = Descriptor(fi.absoluteFilePath(), meta.name.isEmpty() ? fi.baseName() : meta.name, meta.comment);
326 
327         QString icoName = dir.absoluteFilePath(fi.baseName() + ".png");
328         if (QFile::exists(icoName)) {
329             sample.ico.addFile(icoName);
330         }
331         sample.id = fi.fileName();
332         category.items << sample;
333     }
334     if (!category.items.isEmpty()) {
335         result << category;
336     }
337     if (depth < maxDepth) {
338         foreach (const QFileInfo &fi, dir.entryInfoList(QStringList(), QDir::AllDirs | QDir::NoSymLinks | QDir::NoDotAndDotDot)) {
339             scanDir(fi.absoluteFilePath(), depth + 1);
340         }
341     }
342 }
343 
report()344 Task::ReportResult LoadSamplesTask::report() {
345     SampleRegistry::data = result;
346     return ReportResult_Finished;
347 }
348 
init(const QStringList & lst)349 Task *SampleRegistry::init(const QStringList &lst) {
350     return new LoadSamplesTask(lst);
351 }
352 
353 /************************************************************************/
354 /* NameFilterLayout */
355 /************************************************************************/
NameFilterLayout(QWidget * parent)356 NameFilterLayout::NameFilterLayout(QWidget *parent)
357     : QHBoxLayout(parent) {
358     setContentsMargins(0, 0, 0, 0);
359     setSpacing(6);
360     nameEdit = new QLineEdit();
361     nameEdit->setObjectName("nameFilterLineEdit");
362     nameEdit->setPlaceholderText(tr("Type to filter by name..."));
363 
364     QLabel *label = new QLabel(tr("Name filter:"));
365     label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
366     nameEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
367     addWidget(label);
368     addWidget(nameEdit);
369 
370     delTextAction = new QAction(this);
371     delTextAction->setShortcut(QKeySequence(tr("Esc")));
372     nameEdit->addAction(delTextAction);
373 
374     connect(delTextAction, SIGNAL(triggered()), nameEdit, SLOT(clear()));
375 }
376 
getNameEdit() const377 QLineEdit *NameFilterLayout::getNameEdit() const {
378     return nameEdit;
379 }
380 
filterMatched(const QString & nameFilter,const QString & name)381 bool NameFilterLayout::filterMatched(const QString &nameFilter, const QString &name) {
382     static QRegExp spaces("\\s");
383     QStringList filterWords = nameFilter.split(spaces);
384     foreach (const QString &word, filterWords) {
385         if (!name.contains(word, Qt::CaseInsensitive)) {
386             return false;
387         }
388     }
389     return true;
390 }
391 
392 /************************************************************************/
393 /* SamplesWrapper */
394 /************************************************************************/
SamplesWrapper(SamplesWidget * samples,QWidget * parent)395 SamplesWrapper::SamplesWrapper(SamplesWidget *samples, QWidget *parent)
396     : QWidget(parent) {
397     QVBoxLayout *vl = new QVBoxLayout(this);
398     vl->setContentsMargins(0, 3, 0, 0);
399     vl->setSpacing(3);
400     NameFilterLayout *hl = new NameFilterLayout(nullptr);
401     vl->addLayout(hl);
402     vl->addWidget(samples);
403 
404     connect(hl->getNameEdit(), SIGNAL(textChanged(const QString &)), samples, SLOT(sl_nameFilterChanged(const QString &)));
405     setFocusProxy(hl->getNameEdit());
406 }
407 
408 }  // namespace U2
409