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