1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "gettingstartedwelcomepage.h"
27 
28 #include "exampleslistmodel.h"
29 #include "screenshotcropper.h"
30 
31 #include <utils/fileutils.h>
32 #include <utils/pathchooser.h>
33 #include <utils/theme/theme.h>
34 #include <utils/winutils.h>
35 
36 #include <coreplugin/coreconstants.h>
37 #include <coreplugin/documentmanager.h>
38 #include <coreplugin/icore.h>
39 #include <coreplugin/helpmanager.h>
40 #include <coreplugin/modemanager.h>
41 #include <coreplugin/welcomepagehelper.h>
42 #include <projectexplorer/projectexplorer.h>
43 #include <projectexplorer/project.h>
44 
45 #include <QComboBox>
46 #include <QDesktopServices>
47 #include <QDialogButtonBox>
48 #include <QElapsedTimer>
49 #include <QGridLayout>
50 #include <QLabel>
51 #include <QMessageBox>
52 #include <QPainter>
53 #include <QPushButton>
54 #include <QStyledItemDelegate>
55 #include <QTimer>
56 
57 using namespace Core;
58 using namespace Utils;
59 
60 namespace QtSupport {
61 namespace Internal {
62 
63 const char C_FALLBACK_ROOT[] = "ProjectsFallbackRoot";
64 
ExamplesWelcomePage(bool showExamples)65 ExamplesWelcomePage::ExamplesWelcomePage(bool showExamples)
66     : m_showExamples(showExamples)
67 {
68 }
69 
title() const70 QString ExamplesWelcomePage::title() const
71 {
72     return m_showExamples ? tr("Examples") : tr("Tutorials");
73 }
74 
priority() const75 int ExamplesWelcomePage::priority() const
76 {
77     return m_showExamples ? 30 : 40;
78 }
79 
id() const80 Id ExamplesWelcomePage::id() const
81 {
82     return m_showExamples ? "Examples" : "Tutorials";
83 }
84 
copyToAlternativeLocation(const QFileInfo & proFileInfo,QStringList & filesToOpen,const QStringList & dependencies)85 QString ExamplesWelcomePage::copyToAlternativeLocation(const QFileInfo& proFileInfo, QStringList &filesToOpen, const QStringList& dependencies)
86 {
87     const QString projectDir = proFileInfo.canonicalPath();
88     QDialog d(ICore::dialogParent());
89     auto lay = new QGridLayout(&d);
90     auto descrLbl = new QLabel;
91     d.setWindowTitle(tr("Copy Project to writable Location?"));
92     descrLbl->setTextFormat(Qt::RichText);
93     descrLbl->setWordWrap(false);
94     const QString nativeProjectDir = QDir::toNativeSeparators(projectDir);
95     descrLbl->setText(QString::fromLatin1("<blockquote>%1</blockquote>").arg(nativeProjectDir));
96     descrLbl->setMinimumWidth(descrLbl->sizeHint().width());
97     descrLbl->setWordWrap(true);
98     descrLbl->setText(tr("<p>The project you are about to open is located in the "
99                          "write-protected location:</p><blockquote>%1</blockquote>"
100                          "<p>Please select a writable location below and click \"Copy Project and Open\" "
101                          "to open a modifiable copy of the project or click \"Keep Project and Open\" "
102                          "to open the project in location.</p><p><b>Note:</b> You will not "
103                          "be able to alter or compile your project in the current location.</p>")
104                       .arg(nativeProjectDir));
105     lay->addWidget(descrLbl, 0, 0, 1, 2);
106     auto txt = new QLabel(tr("&Location:"));
107     auto chooser = new PathChooser;
108     txt->setBuddy(chooser);
109     chooser->setExpectedKind(PathChooser::ExistingDirectory);
110     chooser->setHistoryCompleter(QLatin1String("Qt.WritableExamplesDir.History"));
111     const QString defaultRootDirectory = DocumentManager::projectsDirectory().toString();
112     QtcSettings *settings = ICore::settings();
113     chooser->setPath(settings->value(C_FALLBACK_ROOT, defaultRootDirectory).toString());
114     lay->addWidget(txt, 1, 0);
115     lay->addWidget(chooser, 1, 1);
116     enum { Copy = QDialog::Accepted + 1, Keep = QDialog::Accepted + 2 };
117     auto bb = new QDialogButtonBox;
118     QPushButton *copyBtn = bb->addButton(tr("&Copy Project and Open"), QDialogButtonBox::AcceptRole);
119     connect(copyBtn, &QAbstractButton::released, &d, [&d] { d.done(Copy); });
120     copyBtn->setDefault(true);
121     QPushButton *keepBtn = bb->addButton(tr("&Keep Project and Open"), QDialogButtonBox::RejectRole);
122     connect(keepBtn, &QAbstractButton::released, &d, [&d] { d.done(Keep); });
123     lay->addWidget(bb, 2, 0, 1, 2);
124     connect(chooser, &PathChooser::validChanged, copyBtn, &QWidget::setEnabled);
125     int code = d.exec();
126     if (code == Copy) {
127         QString exampleDirName = proFileInfo.dir().dirName();
128         QString destBaseDir = chooser->filePath().toString();
129         settings->setValueWithDefault(C_FALLBACK_ROOT, destBaseDir, defaultRootDirectory);
130         QDir toDirWithExamplesDir(destBaseDir);
131         if (toDirWithExamplesDir.cd(exampleDirName)) {
132             toDirWithExamplesDir.cdUp(); // step out, just to not be in the way
133             QMessageBox::warning(ICore::dialogParent(),
134                                  tr("Cannot Use Location"),
135                                  tr("The specified location already exists. "
136                                     "Please specify a valid location."),
137                                  QMessageBox::Ok,
138                                  QMessageBox::NoButton);
139             return QString();
140         } else {
141             QString error;
142             QString targetDir = destBaseDir + QLatin1Char('/') + exampleDirName;
143             if (FileUtils::copyRecursively(FilePath::fromString(projectDir),
144                     FilePath::fromString(targetDir), &error)) {
145                 // set vars to new location
146                 const QStringList::Iterator end = filesToOpen.end();
147                 for (QStringList::Iterator it = filesToOpen.begin(); it != end; ++it)
148                     it->replace(projectDir, targetDir);
149 
150                 foreach (const QString &dependency, dependencies) {
151                     const FilePath targetFile = FilePath::fromString(targetDir)
152                             .pathAppended(QDir(dependency).dirName());
153                     if (!FileUtils::copyRecursively(FilePath::fromString(dependency), targetFile,
154                             &error)) {
155                         QMessageBox::warning(ICore::dialogParent(),
156                                              tr("Cannot Copy Project"),
157                                              error);
158                         // do not fail, just warn;
159                     }
160                 }
161 
162 
163                 return targetDir + QLatin1Char('/') + proFileInfo.fileName();
164             } else {
165                 QMessageBox::warning(ICore::dialogParent(), tr("Cannot Copy Project"), error);
166             }
167 
168         }
169     }
170     if (code == Keep)
171         return proFileInfo.absoluteFilePath();
172     return QString();
173 }
174 
openProject(const ExampleItem * item)175 void ExamplesWelcomePage::openProject(const ExampleItem *item)
176 {
177     using namespace ProjectExplorer;
178     QString proFile = item->projectPath;
179     if (proFile.isEmpty())
180         return;
181 
182     QStringList filesToOpen = item->filesToOpen;
183     if (!item->mainFile.isEmpty()) {
184         // ensure that the main file is opened on top (i.e. opened last)
185         filesToOpen.removeAll(item->mainFile);
186         filesToOpen.append(item->mainFile);
187     }
188 
189     QFileInfo proFileInfo(proFile);
190     if (!proFileInfo.exists())
191         return;
192 
193     // If the Qt is a distro Qt on Linux, it will not be writable, hence compilation will fail
194     // Same if it is installed in non-writable location for other reasons
195     const bool needsCopy = withNtfsPermissions<bool>([proFileInfo] {
196         QFileInfo pathInfo(proFileInfo.path());
197         return !proFileInfo.isWritable()
198                 || !pathInfo.isWritable() /* path of .pro file */
199                 || !QFileInfo(pathInfo.path()).isWritable() /* shadow build directory */;
200     });
201     if (needsCopy)
202         proFile = copyToAlternativeLocation(proFileInfo, filesToOpen, item->dependencies);
203 
204     // don't try to load help and files if loading the help request is being cancelled
205     if (proFile.isEmpty())
206         return;
207     ProjectExplorerPlugin::OpenProjectResult result = ProjectExplorerPlugin::openProject(proFile);
208     if (result) {
209         ICore::openFiles(filesToOpen);
210         ModeManager::activateMode(Core::Constants::MODE_EDIT);
211         QUrl docUrl = QUrl::fromUserInput(item->docUrl);
212         if (docUrl.isValid())
213             HelpManager::showHelpUrl(docUrl, HelpManager::ExternalHelpAlways);
214         ModeManager::activateMode(ProjectExplorer::Constants::MODE_SESSION);
215     } else {
216         ProjectExplorerPlugin::showOpenProjectError(result);
217     }
218 }
219 
220 class ExampleDelegate : public ListItemDelegate
221 {
222 public:
223 
setShowExamples(bool showExamples)224     void setShowExamples(bool showExamples) { m_showExamples = showExamples; goon(); }
225 
226 protected:
clickAction(const ListItem * item) const227     void clickAction(const ListItem *item) const override
228     {
229         QTC_ASSERT(item, return);
230         const auto exampleItem = static_cast<const ExampleItem *>(item);
231 
232         if (exampleItem->isVideo)
233             QDesktopServices::openUrl(QUrl::fromUserInput(exampleItem->videoUrl));
234         else if (exampleItem->hasSourceCode)
235             ExamplesWelcomePage::openProject(exampleItem);
236         else
237             HelpManager::showHelpUrl(QUrl::fromUserInput(exampleItem->docUrl),
238                                      HelpManager::ExternalHelpAlways);
239     }
240 
drawPixmapOverlay(const ListItem * item,QPainter * painter,const QStyleOptionViewItem & option,const QRect & currentPixmapRect) const241     void drawPixmapOverlay(const ListItem *item, QPainter *painter,
242                            const QStyleOptionViewItem &option,
243                            const QRect &currentPixmapRect) const override
244     {
245         QTC_ASSERT(item, return);
246         const auto exampleItem = static_cast<const ExampleItem *>(item);
247         if (exampleItem->isVideo) {
248             QFont f = option.widget->font();
249             f.setPixelSize(13);
250             painter->setFont(f);
251             QString videoLen = exampleItem->videoLength;
252             painter->drawText(currentPixmapRect.adjusted(0, 0, 0, painter->font().pixelSize() + 3),
253                               videoLen, Qt::AlignBottom | Qt::AlignHCenter);
254         }
255     }
256 
adjustPixmapRect(QRect * pixmapRect) const257     void adjustPixmapRect(QRect *pixmapRect) const override
258     {
259         if (!m_showExamples)
260             *pixmapRect = pixmapRect->adjusted(6, 20, -6, -15);
261     }
262 
263     bool m_showExamples = true;
264 };
265 
266 class ExamplesPageWidget : public QWidget
267 {
268 public:
ExamplesPageWidget(bool isExamples)269     ExamplesPageWidget(bool isExamples)
270         : m_isExamples(isExamples)
271     {
272         m_exampleDelegate.setShowExamples(isExamples);
273         const int sideMargin = 27;
274         static auto s_examplesModel = new ExamplesListModel(this);
275         m_examplesModel = s_examplesModel;
276 
277         auto filteredModel = new ExamplesListModelFilter(m_examplesModel, !m_isExamples, this);
278 
279         auto searchBox = new SearchBox(this);
280         m_searcher = searchBox->m_lineEdit;
281 
282         auto vbox = new QVBoxLayout(this);
283         vbox->setContentsMargins(30, sideMargin, 0, 0);
284 
285         auto hbox = new QHBoxLayout;
286         if (m_isExamples) {
287             m_searcher->setPlaceholderText(ExamplesWelcomePage::tr("Search in Examples..."));
288 
289             auto exampleSetSelector = new QComboBox(this);
290             QPalette pal = exampleSetSelector->palette();
291             // for macOS dark mode
292             pal.setColor(QPalette::Text, Utils::creatorTheme()->color(Theme::Welcome_TextColor));
293             exampleSetSelector->setPalette(pal);
294             exampleSetSelector->setMinimumWidth(GridProxyModel::GridItemWidth);
295             exampleSetSelector->setMaximumWidth(GridProxyModel::GridItemWidth);
296             ExampleSetModel *exampleSetModel = m_examplesModel->exampleSetModel();
297             exampleSetSelector->setModel(exampleSetModel);
298             exampleSetSelector->setCurrentIndex(exampleSetModel->selectedExampleSet());
299             connect(exampleSetSelector, QOverload<int>::of(&QComboBox::activated),
300                     exampleSetModel, &ExampleSetModel::selectExampleSet);
301             connect(exampleSetModel, &ExampleSetModel::selectedExampleSetChanged,
302                     exampleSetSelector, &QComboBox::setCurrentIndex);
303 
304             hbox->setSpacing(17);
305             hbox->addWidget(exampleSetSelector);
306         } else {
307             m_searcher->setPlaceholderText(ExamplesWelcomePage::tr("Search in Tutorials..."));
308         }
309         hbox->addWidget(searchBox);
310         hbox->addSpacing(sideMargin);
311         vbox->addItem(hbox);
312 
313         m_gridModel.setSourceModel(filteredModel);
314 
315         auto gridView = new GridView(this);
316         gridView->setModel(&m_gridModel);
317         gridView->setItemDelegate(&m_exampleDelegate);
318         vbox->addWidget(gridView);
319 
320         connect(&m_exampleDelegate, &ExampleDelegate::tagClicked,
321                 this, &ExamplesPageWidget::onTagClicked);
322         connect(m_searcher, &QLineEdit::textChanged,
323                 filteredModel, &ExamplesListModelFilter::setSearchString);
324     }
325 
bestColumnCount() const326     int bestColumnCount() const
327     {
328         return qMax(1, width() / (GridProxyModel::GridItemWidth + GridProxyModel::GridItemGap));
329     }
330 
resizeEvent(QResizeEvent * ev)331     void resizeEvent(QResizeEvent *ev) final
332     {
333         QWidget::resizeEvent(ev);
334         m_gridModel.setColumnCount(bestColumnCount());
335     }
336 
onTagClicked(const QString & tag)337     void onTagClicked(const QString &tag)
338     {
339         QString text = m_searcher->text();
340         m_searcher->setText(text + QString("tag:\"%1\" ").arg(tag));
341     }
342 
343     const bool m_isExamples;
344     ExampleDelegate m_exampleDelegate;
345     QPointer<ExamplesListModel> m_examplesModel;
346     QLineEdit *m_searcher;
347     GridProxyModel m_gridModel;
348 };
349 
createWidget() const350 QWidget *ExamplesWelcomePage::createWidget() const
351 {
352     return new ExamplesPageWidget(m_showExamples);
353 }
354 
355 } // namespace Internal
356 } // namespace QtSupport
357