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 "DashboardWidget.h"
23 
24 #include <QApplication>
25 #include <QDesktopServices>
26 #include <QDir>
27 #include <QFileInfo>
28 #include <QHBoxLayout>
29 #include <QLabel>
30 #include <QMessageBox>
31 #include <QUrl>
32 
33 #include <U2Core/AppContext.h>
34 #include <U2Core/L10n.h>
35 #include <U2Core/ProjectModel.h>
36 #include <U2Core/Task.h>
37 #include <U2Core/U2SafePoints.h>
38 
39 namespace U2 {
40 
DashboardWidget(const QString & title,QWidget * contentWidget)41 DashboardWidget::DashboardWidget(const QString &title, QWidget *contentWidget) {
42     auto layout = new QHBoxLayout();
43     layout->setMargin(0);
44     layout->setSpacing(0);
45     setLayout(layout);
46 
47     setStyleSheet("QWidget#tabWidgetStyleRoot {"
48                   " border-radius: 6px;"
49                   " border: 1px solid #ddd;"
50                   "}");
51 
52     auto styleRootWidget = new QWidget();
53     styleRootWidget->setObjectName("tabWidgetStyleRoot");
54     layout->addWidget(styleRootWidget);
55 
56     auto styleRootWidgetLayout = new QVBoxLayout();
57     styleRootWidgetLayout->setMargin(0);
58     styleRootWidgetLayout->setSpacing(0);
59     styleRootWidget->setLayout(styleRootWidgetLayout);
60 
61     auto titleLabel = new QLabel(title);
62     titleLabel->setStyleSheet("background: rgb(239, 239, 239);"
63                               "color: #222;"
64                               "padding: 5px;"
65                               "border-top-left-radius: 6px;"
66                               "border-top-right-radius: 6px;");
67     styleRootWidgetLayout->addWidget(titleLabel);
68 
69     auto contentStyleWidget = new QWidget();
70     contentStyleWidget->setObjectName("tabWidgetContentStyleRoot");
71     contentStyleWidget->setStyleSheet("QWidget#tabWidgetContentStyleRoot {"
72                                       " background: white;"
73                                       " border-bottom-left-radius: 6px;"
74                                       " border-bottom-right-radius: 6px;"
75                                       "}");
76     styleRootWidgetLayout->addWidget(contentStyleWidget);
77 
78     auto contentStyleWidgetLayout = new QVBoxLayout();
79     contentStyleWidgetLayout->setMargin(0);
80     contentStyleWidgetLayout->setSpacing(0);
81     contentStyleWidget->setLayout(contentStyleWidgetLayout);
82 
83     contentStyleWidgetLayout->addWidget(contentWidget);
84 }
85 
addTableHeadersRow(QGridLayout * gridLayout,const QStringList & headerNameList)86 void DashboardWidgetUtils::addTableHeadersRow(QGridLayout *gridLayout, const QStringList &headerNameList) {
87     QString commonHeaderStyle = "border: 1px solid #999; background-color: rgb(101, 101, 101);";
88     for (int i = 0; i < headerNameList.size(); i++) {
89         auto headerNameWidget = new QWidget();
90         headerNameWidget->setObjectName("tableHeaderCell");
91         if (i == 0) {
92             headerNameWidget->setStyleSheet("#tableHeaderCell { " + commonHeaderStyle + "border-top-left-radius: 4px; border-right: 0px;}");
93         } else if (i == headerNameList.size() - 1) {
94             headerNameWidget->setStyleSheet("#tableHeaderCell { " + commonHeaderStyle + "border-left: 1px solid white; border-top-right-radius: 4px;}");
95         } else {
96             headerNameWidget->setStyleSheet("#tableHeaderCell { " + commonHeaderStyle + "border-left: 1px solid white; border-right: 0px;}");
97         }
98         auto headerNameWidgetLayout = new QVBoxLayout();
99         headerNameWidgetLayout->setContentsMargins(0, 0, 0, 0);
100         headerNameWidget->setLayout(headerNameWidgetLayout);
101         auto headerNameWidgetLabel = new QLabel(headerNameList.at(i));
102         headerNameWidgetLabel->setStyleSheet("color: white; padding: 5px 10px;");
103         headerNameWidgetLayout->addWidget(headerNameWidgetLabel);
104         gridLayout->addWidget(headerNameWidget, 0, i);
105     }
106 }
107 
108 #define ID_KEY "DashboardWidget-Row-Id"
109 
110 static QString cellStyle = "border: 1px solid #ddd; border-top: 0px; border-right: 0px;";
111 static QString rightCellStyle = "border-right: 1px solid #ddd;";
112 static QString lastRowLeftCellStyle = "border-bottom-left-radius: 4px;";
113 static QString lastRowRightCellStyle = "border-bottom-right-radius: 4px;";
114 
addTableCell(QGridLayout * gridLayout,const QString & rowId,QWidget * widget,int row,int column,bool isLastRow,bool isLastColumn)115 void DashboardWidgetUtils::addTableCell(QGridLayout *gridLayout, const QString &rowId, QWidget *widget, int row, int column, bool isLastRow, bool isLastColumn) {
116     auto cellWidget = new QWidget();
117     cellWidget->setObjectName("tableCell");
118     QString extraCellStyle = "";
119     if (isLastColumn) {
120         extraCellStyle += rightCellStyle;
121     }
122     if (isLastRow) {
123         extraCellStyle += column == 0 ? lastRowLeftCellStyle : "";
124         extraCellStyle += isLastColumn ? lastRowRightCellStyle : "";
125     }
126     cellWidget->setStyleSheet("#tableCell {" + cellStyle + extraCellStyle + "}");
127     auto cellWidgetLayout = new QVBoxLayout();
128     cellWidgetLayout->setContentsMargins(10, 7, 10, 7);
129     cellWidget->setLayout(cellWidgetLayout);
130     cellWidgetLayout->addWidget(widget);
131     cellWidgetLayout->addStretch();
132 
133     auto layoutItem = gridLayout->itemAtPosition(row, column);
134     if (layoutItem != nullptr) {
135         QWidget *oldWidget = layoutItem->widget();
136         gridLayout->replaceWidget(oldWidget, cellWidget, Qt::FindDirectChildrenOnly);
137         delete oldWidget;
138     } else {
139         gridLayout->addWidget(cellWidget, row, column);
140     }
141     cellWidget->setProperty(ID_KEY, rowId);
142 }
143 
addTableCell(QGridLayout * gridLayout,const QString & rowId,const QString & text,int row,int column,bool isLastRow,bool isLastColumn)144 void DashboardWidgetUtils::addTableCell(QGridLayout *gridLayout, const QString &rowId, const QString &text, int row, int column, bool isLastRow, bool isLastColumn) {
145     auto cellLabel = new QLabel(text);
146     cellLabel->setWordWrap(true);
147     cellLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
148     cellLabel->setStyleSheet("color: #333");
149     addTableCell(gridLayout, rowId, cellLabel, row, column, isLastRow, isLastColumn);
150 }
151 
addTableRow(QGridLayout * gridLayout,const QString & rowId,const QStringList & valueList)152 void DashboardWidgetUtils::addTableRow(QGridLayout *gridLayout, const QString &rowId, const QStringList &valueList) {
153     // Update last border style for the old last.
154     int lastRowIndex = gridLayout->rowCount() - 1;
155     if (lastRowIndex > 0) {  // row = 0 is a header.
156         auto leftCellLayoutItem = gridLayout->itemAtPosition(lastRowIndex, 0);
157         auto leftCellWidget = leftCellLayoutItem->widget();
158         leftCellWidget->setStyleSheet("#tableCell {" + cellStyle + "}");
159 
160         auto rightCellLayoutItem = gridLayout->itemAtPosition(lastRowIndex, gridLayout->columnCount() - 1);
161         auto rightCellWidget = rightCellLayoutItem->widget();
162         rightCellWidget->setStyleSheet("#tableCell {" + cellStyle + rightCellStyle + "}");
163     }
164 
165     for (int columnIndex = 0; columnIndex < valueList.size(); columnIndex++) {
166         QString text = valueList.at(columnIndex);
167         bool isLastColumn = columnIndex == valueList.size() - 1;
168         addTableCell(gridLayout, rowId, text, lastRowIndex + 1, columnIndex, true, isLastColumn);
169     }
170 }
171 
addOrUpdateTableRow(QGridLayout * gridLayout,const QString & rowId,const QStringList & valueList)172 bool DashboardWidgetUtils::addOrUpdateTableRow(QGridLayout *gridLayout, const QString &rowId, const QStringList &valueList) {
173     bool isUpdated = false;
174     for (int rowIndex = 0; rowIndex < gridLayout->rowCount(); rowIndex++) {
175         auto cellLayoutItem = gridLayout->itemAtPosition(rowIndex, 0);
176         auto cellWidget = cellLayoutItem == nullptr ? nullptr : cellLayoutItem->widget();
177         if (cellWidget != nullptr && cellWidget->property(ID_KEY).toString() == rowId) {
178             for (int columnIndex = 0; columnIndex < valueList.size(); columnIndex++) {
179                 cellLayoutItem = gridLayout->itemAtPosition(rowIndex, columnIndex);
180                 auto cellLabel = qobject_cast<QLabel *>(cellLayoutItem == nullptr ? nullptr : cellLayoutItem->widget()->findChild<QLabel *>());
181                 if (cellLabel != nullptr) {
182                     cellLabel->setText(valueList.at(columnIndex));
183                 }
184             }
185             isUpdated = true;
186             break;
187         }
188     }
189     if (!isUpdated) {
190         addTableRow(gridLayout, rowId, valueList);
191     }
192     return !isUpdated;
193 }
194 
parseOpenUrlValueFromOnClick(const QString & onclickValue)195 QString DashboardWidgetUtils::parseOpenUrlValueFromOnClick(const QString &onclickValue) {
196     int prefixLen = QString("agent.openUrl('").length();
197     int suffixLen = QString("')").length();
198     return onclickValue.length() > prefixLen + suffixLen ? onclickValue.mid(prefixLen, onclickValue.length() - prefixLen - suffixLen) : QString();
199 }
200 
DashboardPopupMenu(QAbstractButton * button,QWidget * parent)201 DashboardPopupMenu::DashboardPopupMenu(QAbstractButton *button, QWidget *parent)
202     : QMenu(parent), button(button) {
203 }
204 
showEvent(QShowEvent * event)205 void DashboardPopupMenu::showEvent(QShowEvent *event) {
206     Q_UNUSED(event);
207     QPoint position = this->pos();
208     QRect rect = button->geometry();
209     this->move(position.x() + rect.width() - this->geometry().width(), position.y());
210 }
211 
212 #define FILE_URL_KEY "file-url"
213 
DashboardFileButton(const QStringList & urlList,const QString & dashboardDir,const WorkflowMonitor * monitor,bool isFolderMode)214 DashboardFileButton::DashboardFileButton(const QStringList &urlList, const QString &dashboardDir, const WorkflowMonitor *monitor, bool isFolderMode)
215     : urlList(urlList), dashboardDirInfo(dashboardDir), isFolderMode(isFolderMode) {
216     setObjectName("DashboardFileButton");
217     QString buttonText = urlList.size() != 1 ? tr("%1 file(s)").arg(urlList.size()) : QFileInfo(urlList[0]).fileName();
218     if (buttonText.length() > 27) {
219         buttonText = buttonText.left(27) + "…";
220     }
221     setText(buttonText);
222     setToolTip(buttonText);
223     setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
224     setStyleSheet("QToolButton {"
225                   "  height: 1.33em; border-radius: 4px;"
226                   "  border: 1px solid #aaa; background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f6f7fa, stop: 1 #dadbde);"
227                   "}"
228                   "QToolButton:pressed {"
229                   "  background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #dadbde, stop: 1 #f6f7fa);"
230                   "}"
231                   "QToolButton::menu-button {"
232                   "  border: 1px solid #aaa;"
233                   "  border-top-right-radius: 4px; border-bottom-right-radius: 4px;"
234                   "  background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f6f7fa, stop: 1 #dadbde);"
235                   "  width: 1.5em;"
236                   "}");
237 
238     connect(this, SIGNAL(clicked()), SLOT(sl_openFileClicked()));
239     if (monitor != nullptr) {
240         connect(monitor, SIGNAL(si_dirSet(const QString &)), SLOT(sl_dashboardDirChanged(const QString &)));
241     }
242     if (urlList.size() == 1) {
243         QString url = urlList[0];
244         if (isFolderMode) {
245             setProperty(FILE_URL_KEY, "file\n" + url);
246         } else {
247             setProperty(FILE_URL_KEY, "ugene\n" + url);
248             auto menu = new DashboardPopupMenu(this, this);
249             addUrlActionsToMenu(menu, url);
250             setMenu(menu);
251             setPopupMode(QToolButton::MenuButtonPopup);
252         }
253     } else {
254         auto menu = new DashboardPopupMenu(this);
255         for (int i = 0, n = qMin(urlList.size(), 50); i < n; i++) {
256             QString url = urlList[i];
257             auto perUrlMenu = new QMenu(QFileInfo(url).fileName());
258             addUrlActionsToMenu(perUrlMenu, url, !isFolderMode);
259             menu->addMenu(perUrlMenu);
260         }
261         setMenu(menu);
262         setPopupMode(QToolButton::InstantPopup);
263     }
264 }
265 
addUrlActionsToMenu(QMenu * menu,const QString & url,bool addOpenByUgeneAction)266 void DashboardFileButton::addUrlActionsToMenu(QMenu *menu, const QString &url, bool addOpenByUgeneAction) {
267     if (addOpenByUgeneAction) {
268         auto openFolderAction = new QAction(tr("Open file with UGENE"), this);
269         openFolderAction->setProperty(FILE_URL_KEY, "ugene\n" + url);
270         connect(openFolderAction, SIGNAL(triggered()), SLOT(sl_openFileClicked()));
271         menu->addAction(openFolderAction);
272     }
273 
274     auto openFolderAction = new QAction(tr("Open folder with the file"), this);
275     openFolderAction->setProperty(FILE_URL_KEY, "folder\n" + url);
276     connect(openFolderAction, SIGNAL(triggered()), SLOT(sl_openFileClicked()));
277     menu->addAction(openFolderAction);
278 
279     auto openFileAction = new QAction(tr("Open file by OS"), this);
280     openFileAction->setProperty(FILE_URL_KEY, "file\n" + url);
281     connect(openFileAction, SIGNAL(triggered()), SLOT(sl_openFileClicked()));
282     menu->addAction(openFileAction);
283 }
284 
285 /**
286  * Finds a file in the given dashboard dir by path suffix of the 'fileInfo'.
287  * Returns new file info or the old one if the file detection algorithm is failed.
288  * This method is designed to find dashboard output files in moved dashboard.
289  */
findFileOpenCandidateInTheDashboardOutputDir(const QFileInfo & dashboardDirInfo,const QFileInfo & fileInfo)290 static QFileInfo findFileOpenCandidateInTheDashboardOutputDir(const QFileInfo &dashboardDirInfo, const QFileInfo &fileInfo) {
291     // Split 'fileInfo' into path tokens: list of dirs + file name.
292     QStringList fileInfoPathTokens;
293     QFileInfo currentPathInfo(QDir::cleanPath(fileInfo.absoluteFilePath()));
294     while (!currentPathInfo.isRoot()) {
295         fileInfoPathTokens.prepend(currentPathInfo.fileName());
296         currentPathInfo = QFileInfo(currentPathInfo.path());
297     }
298     // Try to find the file by the path suffix inside dashboard dir. Check the longest possible variant first.
299     while (!fileInfoPathTokens.isEmpty()) {
300         QFileInfo resultFileInfo(dashboardDirInfo.absoluteFilePath() + fileInfoPathTokens.join("/"));
301         if (resultFileInfo.exists()) {
302             return resultFileInfo;
303         }
304         fileInfoPathTokens.removeFirst();
305     }
306     return fileInfo;
307 }
308 
sl_dashboardDirChanged(const QString & dashboardDir)309 void DashboardFileButton::sl_dashboardDirChanged(const QString &dashboardDir) {
310     dashboardDirInfo = QFileInfo(dashboardDir);
311 }
312 
313 /** Returns true if the url must be opened with OS, but not with UGENE. */
isOpenWithOsOverride(const QString & url)314 static bool isOpenWithOsOverride(const QString &url) {
315     QString extension = QFileInfo(url).suffix().toLower();
316     return extension == "html" || extension == "htm";
317 }
318 
sl_openFileClicked()319 void DashboardFileButton::sl_openFileClicked() {
320     QString typeAndUrl = sender()->property(FILE_URL_KEY).toString();
321     QStringList tokens = typeAndUrl.split("\n");
322     CHECK(tokens.size() == 2, );
323     QString type = tokens[0];
324     QString url = tokens[1];
325     QFileInfo fileInfo(url);
326     bool isOpenParentDir = type == "folder";  // A parent dir of the url must be opened.
327     if (isOpenParentDir) {
328         fileInfo = QFileInfo(fileInfo.absolutePath());
329     }
330     if (!fileInfo.exists()) {
331         fileInfo = findFileOpenCandidateInTheDashboardOutputDir(dashboardDirInfo, fileInfo);
332         if (!fileInfo.exists()) {
333             if (isOpenParentDir || isFolderMode) {
334                 // We can't locate the original dashboard sub-folder. Opening the dashboard folder instead of error message.
335                 fileInfo = dashboardDirInfo;
336             } else {
337                 QMessageBox::critical(QApplication::activeWindow(), L10N::errorTitle(), DashboardWidget::tr("File is not found: %1").arg(fileInfo.absoluteFilePath()));
338                 return;
339             }
340         }
341     }
342     // Some known file types, like auto-generated HTML reports should be opened by OS by default.
343     if (type == "ugene" && !isOpenWithOsOverride(url)) {
344         QVariantMap hints;
345         hints[ProjectLoaderHint_OpenBySystemIfFormatDetectionFailed] = true;
346         Task *task = AppContext::getProjectLoader()->openWithProjectTask(fileInfo.absoluteFilePath(), hints);
347         CHECK(task != nullptr, );
348         AppContext::getTaskScheduler()->registerTopLevelTask(task);
349     } else {
350         QDesktopServices::openUrl(QUrl::fromLocalFile(fileInfo.absoluteFilePath()));
351     }
352 }
353 
354 }  // namespace U2
355