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 ¤tPixmapRect) 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