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