Loading...
Searching...
No Matches
StartScreen.hpp
1#pragma once
2#include <score/model/Skin.hpp>
3#include <score/tools/ThreadPool.hpp>
4#include <score/widgets/Pixmap.hpp>
5
6#include <core/document/DocumentTemplates.hpp>
7#include <core/document/ProjectInfo.hpp>
8#include <core/presenter/AboutWidget.hpp>
9#include <core/view/QRecentFilesMenu.h>
10
11#include <QApplication>
12#include <QCloseEvent>
13#include <QDate>
14#include <QDesktopServices>
15#include <QDir>
16#include <QFileInfo>
17#include <QFontMetrics>
18#include <QGridLayout>
19#include <QHBoxLayout>
20#include <QKeyEvent>
21#include <QLabel>
22#include <QListWidget>
23#include <QMouseEvent>
24#include <QNetworkAccessManager>
25#include <QNetworkReply>
26#include <QNetworkRequest>
27#include <QPainter>
28#include <QPainterPath>
29#include <QPixmap>
30#include <QPlainTextEdit>
31#include <QPointer>
32#include <QScrollArea>
33#include <QSettings>
34#include <QStackedWidget>
35#include <QVBoxLayout>
36#include <QVersionNumber>
37
38#include <score_git_info.hpp>
39
40#include <algorithm>
41#include <functional>
42#include <map>
43#include <memory>
44#include <optional>
45#include <vector>
46#include <verdigris>
47
48namespace score
49{
50namespace
51{
52template <typename OnSuccess, typename OnError>
53class HTTPGet final : public QNetworkAccessManager
54{
55public:
56 explicit HTTPGet(QUrl url, OnSuccess on_success, OnError on_error) noexcept
57 : m_callback{std::move(on_success)}
58 , m_error{std::move(on_error)}
59 {
60 connect(this, &QNetworkAccessManager::finished, this, [this](QNetworkReply* reply) {
61 if(reply->error())
62 {
63 qDebug() << reply->errorString();
64 m_error();
65 }
66 else
67 {
68 m_callback(reply->readAll());
69 }
70
71 reply->deleteLater();
72 this->deleteLater();
73 });
74
75 QNetworkRequest req{std::move(url)};
76 req.setRawHeader("User-Agent", "curl/7.35.0");
77 req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
78 req.setAttribute(
79 QNetworkRequest::RedirectPolicyAttribute,
80 QNetworkRequest::UserVerifiedRedirectPolicy);
81 req.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
82
83 auto reply = get(req);
84 connect(reply, &QNetworkReply::redirected, reply, &QNetworkReply::redirectAllowed);
85 }
86
87private:
88 OnSuccess m_callback;
89 OnError m_error;
90};
91
92// Brand colors of the start screen. They match the splash artwork and the
93// start screen icon set (see src/lib/resources/icons_svg/readme.md), and are
94// deliberately independent from the editor skin.
95namespace StartScreenColors
96{
97static const QColor Panel{
98 0x21, 0x1f, 0x1f, 205}; // translucent: the artwork shows through
99static const QColor Card{"#2b2929"};
100static const QColor CardDark{"#161514"};
101static const QColor Outline{"#3d3a3a"};
102static const QColor Text{"#f0f0f0"};
103static const QColor Muted{"#8a8a8a"};
104static const QColor Hover{"#03C3DD"};
105static const QColor Accent{"#f6a019"};
106static const QColor Version{"#0092CF"};
107}
108
109// Crops an image to the given aspect and scales it to exactly `size`.
110QPixmap coverPixmap(const QImage& img, QSize size)
111{
112 if(img.isNull())
113 return {};
114 QImage scaled
115 = img.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
116 const QRect crop{
117 (scaled.width() - size.width()) / 2, (scaled.height() - size.height()) / 2,
118 size.width(), size.height()};
119 return QPixmap::fromImage(scaled.copy(crop));
120}
121}
122
130class InteractiveLabel : public QWidget
131{
132 W_OBJECT(InteractiveLabel)
133
134public:
136 const QFont& font, const QString& text, const QString& url,
137 QWidget* parent = nullptr);
138
139 void setOpenExternalLink(bool val) { m_openExternalLink = val; }
140 void setPixmaps(const QPixmap& pixmap, const QPixmap& pixmapOn);
141
142 void disableInteractivity();
143 void setActiveColor(const QColor& c);
144 void setInactiveColor(const QColor& c);
145 void setElideMode(Qt::TextElideMode mode) { m_elideMode = mode; }
146 void setLeftPadding(int px) { m_leftPadding = px; }
148 void setIconColumn(int px) { m_iconColumn = px; }
149 void setItemHeight(int px);
150
151 void setCheckable(bool b) { m_checkable = b; }
152 void setCheckedBackground(const QColor& c) { m_checkedBackground = c; }
153 void setChecked(bool b);
154 bool isChecked() const noexcept { return m_checked; }
155
156 void setText(const QString& text);
157 const QString& text() const noexcept { return m_title; }
158 const QString& url() const noexcept { return m_url; }
159
160 void labelPressed(const QString& file) W_SIGNAL(labelPressed, file)
161 void hovered(bool state) W_SIGNAL(hovered, state)
162
163 QSize sizeHint() const override;
164 QSize minimumSizeHint() const override;
165
166protected:
167 void paintEvent(QPaintEvent* event) override;
168 void enterEvent(QEnterEvent* event) override;
169 void leaveEvent(QEvent* event) override;
170 void mousePressEvent(QMouseEvent* event) override;
171 void mouseReleaseEvent(QMouseEvent* event) override;
172
173private:
174 bool highlighted() const noexcept { return m_checked || (m_interactive && m_hovered); }
175 int iconWidth() const noexcept { return m_pixmap.isNull() ? 0 : m_iconColumn; }
176
177 QFont m_font;
178 QString m_title;
179 QString m_url;
180
181 QPixmap m_pixmap;
182 QPixmap m_pixmapOn;
183
184 QColor m_activeColor{StartScreenColors::Hover};
185 QColor m_inactiveColor{StartScreenColors::Text};
186 QColor m_checkedBackground;
187
188 Qt::TextElideMode m_elideMode{Qt::ElideRight};
189 int m_leftPadding{4};
190 int m_iconColumn{34};
191 int m_height{30};
192
193 bool m_openExternalLink{};
194 bool m_interactive{true};
195 bool m_hovered{};
196 bool m_pressed{};
197 bool m_checkable{};
198 bool m_checked{};
199};
200
201InteractiveLabel::InteractiveLabel(
202 const QFont& font, const QString& title, const QString& url, QWidget* parent)
203 : QWidget{parent}
204 , m_font(font)
205 , m_title(title)
206 , m_url(url)
207{
208 setCursor(score::Skin::instance().CursorPointingHand);
209 setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
210 setFixedHeight(m_height);
211}
212
213void InteractiveLabel::setPixmaps(const QPixmap& pixmap, const QPixmap& pixmapOn)
214{
215 m_pixmap = pixmap;
216 m_pixmapOn = pixmapOn;
217 // Never let a big icon overflow the label
218 const int h = std::max(
219 int(m_pixmap.height() / m_pixmap.devicePixelRatio()),
220 int(m_pixmapOn.height() / m_pixmapOn.devicePixelRatio()));
221 if(h + 4 > m_height)
222 setItemHeight(h + 4);
223 updateGeometry();
224}
225
226void InteractiveLabel::setItemHeight(int px)
227{
228 m_height = px;
229 setFixedHeight(px);
230 updateGeometry();
231}
232
233void InteractiveLabel::disableInteractivity()
234{
235 m_interactive = false;
236 setCursor(score::Skin::instance().CursorPointer);
237 update();
238}
239
240void InteractiveLabel::setActiveColor(const QColor& c)
241{
242 m_activeColor = c;
243 update();
244}
245
246void InteractiveLabel::setInactiveColor(const QColor& c)
247{
248 m_inactiveColor = c;
249 update();
250}
251
252void InteractiveLabel::setChecked(bool b)
253{
254 if(m_checked == b)
255 return;
256 m_checked = b;
257 update();
258}
259
260void InteractiveLabel::setText(const QString& text)
261{
262 m_title = text;
263 updateGeometry();
264 update();
265}
266
267QSize InteractiveLabel::sizeHint() const
268{
269 int w = m_leftPadding + iconWidth();
270 if(!m_title.isEmpty())
271 w += QFontMetrics{m_font}.horizontalAdvance(m_title) + 6;
272 return {w, m_height};
273}
274
275QSize InteractiveLabel::minimumSizeHint() const
276{
277 // Allow the layout to squeeze us: the text gets elided.
278 int w = m_leftPadding + iconWidth();
279 if(!m_title.isEmpty())
280 w += 40;
281 return {w, m_height};
282}
283
285static QPixmap tintedPixmap(const QPixmap& source, const QColor& color)
286{
287 if(source.isNull())
288 return source;
289 static std::map<std::pair<qint64, QRgb>, QPixmap> cache;
290 const auto key = std::make_pair(source.cacheKey(), color.rgba());
291 if(auto it = cache.find(key); it != cache.end())
292 return it->second;
293
294 QPixmap res{source.size()};
295 res.setDevicePixelRatio(source.devicePixelRatio());
296 res.fill(Qt::transparent);
297 {
298 QPainter p{&res};
299 p.drawPixmap(0, 0, source);
300 p.setCompositionMode(QPainter::CompositionMode_SourceIn);
301 p.fillRect(res.rect(), color);
302 }
303 return cache.emplace(key, res).first->second;
304}
305
306void InteractiveLabel::paintEvent(QPaintEvent* event)
307{
308 QPainter painter(this);
309 painter.setRenderHint(QPainter::Antialiasing, true);
310 painter.setRenderHint(QPainter::TextAntialiasing, true);
311 painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
312
313 const bool hl = highlighted();
314
315 if(m_checked && m_checkedBackground.isValid())
316 {
317 painter.fillRect(rect(), m_checkedBackground);
318 painter.fillRect(QRect{0, 0, 3, height()}, m_activeColor);
319 }
320
321 QRectF textRect = rect().adjusted(m_leftPadding, 0, -4, 0);
322
323 // The icon always takes the color of the text, whatever the state
324 const QColor color = hl ? m_activeColor : m_inactiveColor;
325 const QPixmap pm = tintedPixmap(m_pixmap, color);
326 if(!pm.isNull())
327 {
328 const qreal w = pm.width() / pm.devicePixelRatio();
329 const qreal h = pm.height() / pm.devicePixelRatio();
330 // Icons of different sizes share the same column, centered on the same axis
331 painter.drawPixmap(
332 QPointF{textRect.x() + (m_iconColumn - 8 - w) / 2., (height() - h) / 2.}, pm);
333 textRect.setX(textRect.x() + m_iconColumn);
334 }
335
336 if(!m_title.isEmpty())
337 {
338 painter.setPen(QPen{color});
339 painter.setFont(m_font);
340 const QString txt
341 = QFontMetrics{m_font}.elidedText(m_title, m_elideMode, int(textRect.width()));
342 painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, txt);
343 }
344}
345
346void InteractiveLabel::enterEvent(QEnterEvent* event)
347{
348 m_hovered = true;
349 if(m_interactive)
350 {
351 update();
352 hovered(true);
353 }
354}
355
356void InteractiveLabel::leaveEvent(QEvent* event)
357{
358 m_hovered = false;
359 m_pressed = false;
360 if(m_interactive)
361 {
362 update();
363 hovered(false);
364 }
365}
366
367void InteractiveLabel::mousePressEvent(QMouseEvent* event)
368{
369 if(!m_interactive || event->button() != Qt::LeftButton)
370 return QWidget::mousePressEvent(event);
371 m_pressed = true;
372 event->accept();
373}
374
375void InteractiveLabel::mouseReleaseEvent(QMouseEvent* event)
376{
377 if(!m_pressed || event->button() != Qt::LeftButton)
378 return QWidget::mouseReleaseEvent(event);
379 m_pressed = false;
380 event->accept();
381
382 if(!rect().contains(event->pos()))
383 return;
384
385 if(m_checkable)
386 setChecked(true);
387
388 if(m_openExternalLink)
389 QDesktopServices::openUrl(QUrl(m_url));
390 else
391 labelPressed(m_url);
392}
393
397class ThumbnailPopup final : public QWidget
398{
399public:
400 static constexpr int Width = 264;
401 static constexpr int ThumbHeight = 158;
402 static constexpr int Margin = 6;
403 static constexpr int DescriptionLines = 2;
404
405 ThumbnailPopup(const QFont& titleFont, const QFont& subFont, QWidget* parent)
406 : QWidget{parent}
407 , m_titleFont{titleFont}
408 , m_subFont{subFont}
409 {
410 setAttribute(Qt::WA_TransparentForMouseEvents);
411 hide();
412 }
413
414 void setContent(
415 const QImage& thumbnail, const QString& title, const QString& author,
416 const QString& description, const QString& path)
417 {
418 m_thumbnail = coverPixmap(thumbnail, {Width - 2 * Margin, ThumbHeight});
419 m_title = title;
420 m_author = author;
421 m_description = description.simplified();
422 m_path = path;
423
424 const QFontMetrics tm{m_titleFont}, sm{m_subFont};
425 int h = 2 * Margin + tm.height();
426 if(!m_thumbnail.isNull())
427 h += ThumbHeight + Margin;
428 if(!m_author.isEmpty())
429 h += sm.height();
430 if(!m_description.isEmpty())
431 h += 4 + DescriptionLines * sm.lineSpacing();
432 if(!m_path.isEmpty())
433 h += 4 + sm.height();
434 resize(Width, h);
435 update();
436 }
437
439 void showFor(QWidget* anchor, const QRect& bounds)
440 {
441 const QRect a{anchor->mapTo(parentWidget(), QPoint{0, 0}), anchor->size()};
442 int x = a.left() - width() - 12;
443 if(x < bounds.left())
444 x = a.right() + 12;
445 if(x + width() > bounds.right())
446 x = bounds.right() - width();
447 int y = a.center().y() - height() / 2;
448 y = std::clamp(y, bounds.top(), std::max(bounds.top(), bounds.bottom() - height()));
449 move(x, y);
450 raise();
451 show();
452 }
453
454protected:
455 void paintEvent(QPaintEvent*) override
456 {
457 QPainter p{this};
458 p.setRenderHint(QPainter::Antialiasing, true);
459 p.setRenderHint(QPainter::SmoothPixmapTransform, true);
460
461 QPainterPath path;
462 path.addRoundedRect(QRectF{rect()}.adjusted(0.5, 0.5, -0.5, -0.5), 4, 4);
463 p.fillPath(path, StartScreenColors::CardDark);
464 p.setPen(QPen{StartScreenColors::Outline});
465 p.drawPath(path);
466
467 const int textX = Margin + 2;
468 const int textW = width() - 2 * Margin - 4;
469 const QFontMetrics tm{m_titleFont}, sm{m_subFont};
470 int y = Margin;
471 if(!m_thumbnail.isNull())
472 {
473 p.drawPixmap(Margin, y, m_thumbnail);
474 y += ThumbHeight + Margin;
475 }
476
477 p.setPen(StartScreenColors::Text);
478 p.setFont(m_titleFont);
479 p.drawText(
480 QRect{textX, y, textW, tm.height()}, Qt::AlignLeft | Qt::AlignVCenter,
481 tm.elidedText(m_title, Qt::ElideMiddle, textW));
482 y += tm.height();
483
484 p.setFont(m_subFont);
485 if(!m_author.isEmpty())
486 {
487 p.setPen(StartScreenColors::Hover);
488 p.drawText(
489 QRect{textX, y, textW, sm.height()}, Qt::AlignLeft | Qt::AlignVCenter,
490 sm.elidedText(m_author, Qt::ElideRight, textW));
491 y += sm.height();
492 }
493 if(!m_description.isEmpty())
494 {
495 y += 4;
496 p.setPen(StartScreenColors::Text);
497 const QRect descRect{textX, y, textW, DescriptionLines * sm.lineSpacing()};
498 p.save();
499 p.setClipRect(descRect);
500 p.drawText(
501 descRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, m_description);
502 p.restore();
503 y += descRect.height();
504 }
505 if(!m_path.isEmpty())
506 {
507 y += 4;
508 p.setPen(StartScreenColors::Muted);
509 p.drawText(
510 QRect{textX, y, textW, sm.height()}, Qt::AlignLeft | Qt::AlignVCenter,
511 sm.elidedText(m_path, Qt::ElideMiddle, textW));
512 }
513 }
514
515private:
516 QFont m_titleFont;
517 QFont m_subFont;
518 QPixmap m_thumbnail;
519 QString m_title;
520 QString m_author;
521 QString m_description;
522 QString m_path;
523};
524
528class ExampleCard final : public QWidget
529{
530public:
531 static constexpr int Width = 190;
532 static constexpr int ThumbHeight = 119;
533 static constexpr int Height = ThumbHeight + 66;
534
536 const QFont& titleFont, const QFont& subFont, const QString& title,
537 const QString& subtitle, const QString& path, QWidget* parent)
538 : QWidget{parent}
539 , m_titleFont{titleFont}
540 , m_subFont{subFont}
541 , m_title{title}
542 , m_subtitle{subtitle}
543 , m_path{path}
544 {
545 setFixedSize(Width, Height);
546 setCursor(score::Skin::instance().CursorPointingHand);
547 setToolTip(QDir::toNativeSeparators(path));
548 }
549
550 const QString& path() const noexcept { return m_path; }
551
552 void setInfo(const ProjectInfo::Info& info)
553 {
554 if(!info.name.isEmpty())
555 m_title = info.name;
556 if(!info.author.isEmpty())
557 m_subtitle = info.author;
558 m_thumbnail = coverPixmap(info.thumbnail, {Width, ThumbHeight});
559 QString tip = m_title;
560 if(!info.author.isEmpty())
561 tip += "\n" + info.author;
562 if(!info.description.isEmpty())
563 tip += "\n\n" + info.description;
564 tip += "\n\n" + QDir::toNativeSeparators(m_path);
565 setToolTip(tip);
566 update();
567 }
568
569 std::function<void(const QString&)> onActivated;
570
571protected:
572 void paintEvent(QPaintEvent*) override
573 {
574 QPainter p{this};
575 p.setRenderHint(QPainter::Antialiasing, true);
576 p.setRenderHint(QPainter::SmoothPixmapTransform, true);
577
578 QPainterPath path;
579 path.addRoundedRect(QRectF{rect()}.adjusted(0.5, 0.5, -0.5, -0.5), 4, 4);
580 p.fillPath(path, StartScreenColors::Card);
581
582 // Thumbnail area
583 p.save();
584 p.setClipPath(path);
585 const QRect thumbRect{0, 0, Width, ThumbHeight};
586 if(!m_thumbnail.isNull())
587 {
588 p.drawPixmap(thumbRect, m_thumbnail);
589 }
590 else
591 {
592 p.fillRect(thumbRect, StartScreenColors::CardDark);
593 static const QPixmap placeholder
594 = score::get_pixmap(":/icons/load_examples_off.png");
595 const qreal w = placeholder.width() / placeholder.devicePixelRatio();
596 const qreal h = placeholder.height() / placeholder.devicePixelRatio();
597 p.setOpacity(0.35);
598 p.drawPixmap(QPointF{(Width - w) / 2., (ThumbHeight - h) / 2.}, placeholder);
599 p.setOpacity(1.);
600 }
601 p.restore();
602
603 p.setPen(QPen{m_hovered ? StartScreenColors::Hover : StartScreenColors::Outline, 1});
604 p.drawPath(path);
605
606 const int textX = 8;
607 const int textW = Width - 16;
608 p.setPen(m_hovered ? StartScreenColors::Hover : StartScreenColors::Text);
609 p.setFont(m_titleFont);
610 const QFontMetrics tm{m_titleFont};
611 p.drawText(
612 QRect{textX, ThumbHeight + 6, textW, tm.height()},
613 Qt::AlignLeft | Qt::AlignVCenter, tm.elidedText(m_title, Qt::ElideRight, textW));
614
615 p.setPen(StartScreenColors::Muted);
616 p.setFont(m_subFont);
617 const QFontMetrics sm{m_subFont};
618 p.drawText(
619 QRect{textX, ThumbHeight + 6 + tm.height(), textW, sm.height()},
620 Qt::AlignLeft | Qt::AlignVCenter,
621 sm.elidedText(m_subtitle, Qt::ElideRight, textW));
622
623 // Full path, so that the user knows which file is going to be opened
624 p.setOpacity(0.7);
625 p.drawText(
626 QRect{textX, ThumbHeight + 8 + tm.height() + sm.height(), textW, sm.height()},
627 Qt::AlignLeft | Qt::AlignVCenter,
628 sm.elidedText(QDir::toNativeSeparators(m_path), Qt::ElideMiddle, textW));
629 p.setOpacity(1.);
630 }
631
632 void enterEvent(QEnterEvent*) override
633 {
634 m_hovered = true;
635 update();
636 }
637 void leaveEvent(QEvent*) override
638 {
639 m_hovered = false;
640 m_pressed = false;
641 update();
642 }
643 void mousePressEvent(QMouseEvent* e) override
644 {
645 if(e->button() == Qt::LeftButton)
646 {
647 m_pressed = true;
648 e->accept();
649 }
650 }
651 void mouseReleaseEvent(QMouseEvent* e) override
652 {
653 const bool activate
654 = m_pressed && e->button() == Qt::LeftButton && rect().contains(e->pos());
655 m_pressed = false;
656 if(activate && onActivated)
657 onActivated(m_path);
658 }
659
660private:
661 QFont m_titleFont;
662 QFont m_subFont;
663 QString m_title;
664 QString m_subtitle;
665 QString m_path;
666 QPixmap m_thumbnail;
667 bool m_hovered{};
668 bool m_pressed{};
669};
670
694class StartScreen : public QWidget
695{
696 W_OBJECT(StartScreen)
697public:
698 StartScreen(const QPointer<QRecentFilesMenu>& recentFiles, QWidget* parent = nullptr);
699
700 void openNewDocument() W_SIGNAL(openNewDocument)
701 void openFile(const QString& file) W_SIGNAL(openFile, file)
703 void openTemplate(const QString& file) W_SIGNAL(openTemplate, file)
704 void openFileDialog() W_SIGNAL(openFileDialog)
705 void loadCrashedSession() W_SIGNAL(loadCrashedSession)
707 void joinSession() W_SIGNAL(joinSession)
708 void exitApp() W_SIGNAL(exitApp)
709
713 void addJoinSession();
715 void dismiss();
717 void reopen();
718
719 static constexpr int Width = 880;
720 static constexpr int Height = 640;
721 static constexpr int HeaderHeight = 220;
722 static constexpr int NavWidth = 190;
723 static constexpr int MaxRecentFiles = 8;
724
725protected:
726 void paintEvent(QPaintEvent* event) override;
727 void keyPressEvent(QKeyEvent* event) override;
728 void closeEvent(QCloseEvent* event) override;
729
730private:
731 struct Link
732 {
733 QString text;
734 QString url;
735 QString icon; // base name in :/icons, without the _on / _off suffix
736 QString tooltip;
737 };
738
739 QWidget* createHeader();
740 QWidget* createNavigation();
743 int addPage(const QString& name, const QString& icon, std::function<QWidget*()> make);
744 QWidget* createHomePage(const QPointer<QRecentFilesMenu>& recentFiles);
745 QWidget* createTemplatesPage();
746 QWidget* createAboutPage();
747 QWidget* createExamplesPage();
749 QWidget* createCardsPage(
750 const QString& title, const QString& hint, const QString& emptyHint,
751 const std::vector<DocumentTemplate>& docs,
752 std::function<void(const QString&)> onActivated, const Link& more);
753 QWidget* createLinksPage(const QString& title, const std::vector<Link>& links);
754
755 int addPage(const QString& name, const QString& icon, QWidget* page);
756 void setCurrentPage(int index);
757
758 QLabel* makeSectionTitle(const QString& text, QWidget* parent);
759 QLabel* makeHint(const QString& text, QWidget* parent);
760 InteractiveLabel* makeItem(
761 const QString& text, const QString& icon, const QString& url, QWidget* parent);
763 InteractiveLabel* makeAccentItem(
764 const QString& text, const QString& icon, const QString& url, QWidget* parent);
765 InteractiveLabel* makeExternalLink(const Link& link, QWidget* parent);
767 InteractiveLabel* makeScoreItem(
768 const QString& text, const QString& icon, const QString& path, bool asTemplate,
769 QWidget* parent);
770
771 // Wraps the emission of the choice signals: once the user chose something,
772 // closing the window must not additionally open a new document.
773 template <typename F>
774 void choose(F&& emitSignal)
775 {
776 if(m_actionTaken)
777 return;
778 m_actionTaken = true;
779 emitSignal();
780 }
781
783 void openExample(const QString& path);
784 void requestInfo(const QString& path);
785 void onInfoLoaded(const QString& path, const std::optional<ProjectInfo::Info>& info);
786 void showPreview(InteractiveLabel* label);
787
788 void checkForNewVersion();
789 void showUpdateAvailable(const QString& version);
790
791 QFont m_navFont;
792 QFont m_sectionFont;
793 QFont m_itemFont;
794 QFont m_smallFont;
795 QFont m_versionFont;
796
797 QPixmap m_background;
798
799 QStackedWidget* m_pages{};
800 std::vector<InteractiveLabel*> m_navItems;
801 std::vector<std::function<QWidget*()>> m_pageFactories;
802 QVBoxLayout* m_navLayout{};
803
804 InteractiveLabel* m_updateLabel{};
805 InteractiveLabel* m_crashLabel{};
806 InteractiveLabel* m_joinLabel{};
807 int m_templatesPage{};
808 ThumbnailPopup* m_preview{};
809
810 std::map<QString, std::optional<ProjectInfo::Info>> m_infos;
811 std::vector<ExampleCard*> m_cards;
812
813 bool m_firstRun{};
814 bool m_actionTaken{};
815};
816
817StartScreen::StartScreen(const QPointer<QRecentFilesMenu>& recentFiles, QWidget* parent)
818 : QWidget(parent)
819{
820 auto& skin = score::Skin::instance();
821 setCursor(skin.CursorPointer);
822 setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
823 setWindowModality(Qt::ApplicationModal);
824 setFocusPolicy(Qt::StrongFocus);
825 setFixedSize(Width, Height);
826
827 {
828 QSettings s;
829 m_firstRun = !s.value("score/StartScreenSeen", false).toBool();
830 s.setValue("score/StartScreenSeen", true);
831 }
832
833 // px, not pt: pt would shrink on macOS' 72 DPI.
834 m_navFont = QFont("Montserrat");
835 m_navFont.setPixelSize(16);
836 m_navFont.setWeight(QFont::DemiBold);
837 m_sectionFont = QFont("Montserrat");
838 m_sectionFont.setPixelSize(13);
839 m_sectionFont.setWeight(QFont::Medium);
840 m_sectionFont.setCapitalization(QFont::AllUppercase);
841 m_sectionFont.setLetterSpacing(QFont::PercentageSpacing, 108);
842 m_itemFont = QFont("Ubuntu");
843 m_itemFont.setPixelSize(16);
844 m_itemFont.setHintingPreference(QFont::HintingPreference::PreferFullHinting);
845 m_itemFont.setStyleStrategy(QFont::PreferAntialias);
846 m_smallFont = QFont("Ubuntu");
847 m_smallFont.setPixelSize(13);
848 m_smallFont.setHintingPreference(QFont::HintingPreference::PreferFullHinting);
849 m_smallFont.setStyleStrategy(QFont::PreferAntialias);
850 m_versionFont = QFont("Ubuntu");
851 m_versionFont.setPixelSize(19);
852 m_versionFont.setWeight(QFont::Light);
853 m_versionFont.setHintingPreference(QFont::HintingPreference::PreferFullHinting);
854 m_versionFont.setStyleStrategy(QFont::PreferAntialias);
855
856 m_background = score::get_pixmap(":/startscreen/startscreensplash.png");
857
858 auto mainLayout = new QVBoxLayout{this};
859 mainLayout->setContentsMargins(0, 0, 0, 0);
860 mainLayout->setSpacing(0);
861
862 mainLayout->addWidget(createHeader());
863
864 auto body = new QHBoxLayout;
865 body->setContentsMargins(0, 0, 0, 0);
866 body->setSpacing(0);
867
868 m_pages = new QStackedWidget{this};
869 body->addWidget(createNavigation());
870 body->addWidget(m_pages, 1);
871 mainLayout->addLayout(body, 1);
872
873 addPage(tr("Home"), "home", createHomePage(recentFiles));
874 m_templatesPage
875 = addPage(tr("Templates"), "new_file", [this] { return createTemplatesPage(); });
876 addPage(tr("Examples"), "load_examples", [this] { return createExamplesPage(); });
877 addPage(
878 tr("Learn"), "learn",
879 createLinksPage(
880 tr("Learn ossia score"),
881 {{tr("Quick start guide"), "https://ossia.io/score-docs/quick-start.html",
882 "version", tr("The recommended starting point if you are new to score")},
883 {tr("Video tutorials"),
884 "https://www.youtube.com/"
885 "watch?v=R-3d8K6gQkw&list=PLIHLSiZpIa6YoY1_aW1yetDgZ7tZcxfEC",
886 "tutorials",
887 {}},
888 {tr("Documentation"), "https://ossia.io/score-docs/", "learn", {}},
889 {tr("Process reference"), "https://ossia.io/score-docs/processes.html", "learn", {}},
890 {tr("Device reference"), "https://ossia.io/score-docs/devices.html", "learn", {}},
891 {tr("Web version"), "https://ossia.io/score-web/", "tutorials", {}},
892 }));
893 addPage(
894 tr("Community"), "community",
895 createLinksPage(
896 tr("Get involved"),
897 {{tr("GitHub Discussions"), "https://github.com/ossia/score/discussions",
898 "forum", tr("Ask questions and share what you do with score")},
899 {tr("Discord Chat"), "https://discord.gg/8Hzm4UduaS", "chat", {}},
900 {tr("Report a bug or suggest a feature"),
901 "https://github.com/ossia/score/issues",
902 "new_file",
903 {}},
904 {tr("Support ossia score through the Suite SAT"), "https://suite.sat.qc.ca/en",
905 "contribute",
906 tr("ossia score is free software: donations fund its development")},
907 {tr("Donate on Open Collective"), "https://opencollective.com/ossia",
908 "contribute", tr("ossia score is free software: donations fund its development")},
909 {tr("Contributing"), "https://ossia.io/score-docs/development", "contribute",
910 tr("Come implement your dream feature!")},
911 }));
912 addPage(tr("About"), "about", [this] { return createAboutPage(); });
913
914 m_navLayout->addStretch();
915
916 auto exitLabel = makeItem(tr("Exit"), "exit", "", this);
917 exitLabel->setLeftPadding(20);
918 exitLabel->setItemHeight(40);
919 connect(exitLabel, &InteractiveLabel::labelPressed, this, [this] {
920 choose([this] { exitApp(); });
921 });
922 m_navLayout->addWidget(exitLabel);
923
924 // Created last so that it is above every page
925 m_preview = new ThumbnailPopup{m_itemFont, m_smallFont, this};
926
927 setCurrentPage(0);
928
929 checkForNewVersion();
930}
931
932QWidget* StartScreen::createHeader()
933{
934 // The artwork itself is painted in paintEvent; this widget only hosts the
935 // controls that sit on top of it.
936 auto header = new QWidget{this};
937 header->setFixedHeight(HeaderHeight);
938 auto lay = new QHBoxLayout{header};
939 lay->setContentsMargins(0, 10, 10, 0);
940 lay->setSpacing(12);
941 lay->addStretch();
942
943 m_updateLabel = new InteractiveLabel{
944 m_navFont, {}, "https://github.com/ossia/score/releases/latest/", header};
945 m_updateLabel->setOpenExternalLink(true);
946 m_updateLabel->setPixmaps(
947 score::get_pixmap(":/icons/version_off.png"),
948 score::get_pixmap(":/icons/version_on.png"));
949 m_updateLabel->setInactiveColor(StartScreenColors::Accent);
950 m_updateLabel->hide();
951 lay->addWidget(m_updateLabel, 0, Qt::AlignTop);
952
953 auto closeLabel = new InteractiveLabel{m_navFont, {}, "", header};
954 closeLabel->setPixmaps(
955 score::get_pixmap(":/icons/close_window_off.png"),
956 score::get_pixmap(":/icons/close_window_on.png"));
957 closeLabel->setLeftPadding(0);
958 closeLabel->setIconColumn(32);
959 closeLabel->setFixedWidth(32);
960 closeLabel->setToolTip(tr("Close this window and start with an empty score"));
961 connect(closeLabel, &InteractiveLabel::labelPressed, this, [this] {
962 choose([this] { openNewDocument(); });
963 });
964 lay->addWidget(closeLabel, 0, Qt::AlignTop);
965
966 return header;
967}
968
969QWidget* StartScreen::createNavigation()
970{
971 auto nav = new QWidget{this};
972 nav->setFixedWidth(NavWidth);
973 m_navLayout = new QVBoxLayout{nav};
974 // The first entry starts exactly where the page panel starts
975 m_navLayout->setContentsMargins(0, 0, 0, 16);
976 m_navLayout->setSpacing(4);
977 return nav;
978}
979
980int StartScreen::addPage(const QString& name, const QString& icon, QWidget* page)
981{
982 const int index = addPage(name, icon, std::function<QWidget*()>{});
983 m_pages->widget(index)->layout()->addWidget(page);
984 return index;
985}
986
987int StartScreen::addPage(
988 const QString& name, const QString& icon, std::function<QWidget*()> make)
989{
990 // A host widget per page; the content is added to it now or on first display
991 auto host = new QWidget;
992 auto hostLayout = new QVBoxLayout{host};
993 hostLayout->setContentsMargins(0, 0, 0, 0);
994 const int index = m_pages->addWidget(host);
995 m_pageFactories.resize(index + 1);
996 m_pageFactories[index] = std::move(make);
997
998 auto item = makeItem(name, icon, "", this);
999 item->setCheckable(true);
1000 item->setCheckedBackground(StartScreenColors::Panel);
1001 item->setLeftPadding(20);
1002 item->setItemHeight(44);
1003 connect(item, &InteractiveLabel::labelPressed, this, [this, index] {
1004 setCurrentPage(index);
1005 });
1006
1007 m_navItems.push_back(item);
1008 m_navLayout->addWidget(item);
1009 return index;
1010}
1011
1012void StartScreen::setCurrentPage(int index)
1013{
1014 if(index >= 0 && index < int(m_pageFactories.size()))
1015 {
1016 if(auto make = std::exchange(m_pageFactories[index], {}))
1017 m_pages->widget(index)->layout()->addWidget(make());
1018 }
1019 m_pages->setCurrentIndex(index);
1020 for(int i = 0; i < int(m_navItems.size()); i++)
1021 m_navItems[i]->setChecked(i == index);
1022 if(m_preview)
1023 m_preview->hide();
1024}
1025
1026QLabel* StartScreen::makeSectionTitle(const QString& text, QWidget* parent)
1027{
1028 auto label = new QLabel{text, parent};
1029 label->setFont(m_sectionFont);
1030 QPalette pal = label->palette();
1031 pal.setColor(QPalette::WindowText, StartScreenColors::Muted);
1032 label->setPalette(pal);
1033 return label;
1034}
1035
1036QLabel* StartScreen::makeHint(const QString& text, QWidget* parent)
1037{
1038 auto hint = new QLabel{text, parent};
1039 hint->setFont(m_itemFont);
1040 hint->setWordWrap(true);
1041 QPalette pal = hint->palette();
1042 pal.setColor(QPalette::WindowText, StartScreenColors::Muted);
1043 hint->setPalette(pal);
1044 return hint;
1045}
1046
1047InteractiveLabel* StartScreen::makeItem(
1048 const QString& text, const QString& icon, const QString& url, QWidget* parent)
1049{
1050 auto label = new InteractiveLabel{m_itemFont, text, url, parent};
1051 if(!icon.isEmpty())
1052 {
1053 label->setPixmaps(
1054 score::get_pixmap(QString(":/icons/%1_off.png").arg(icon)),
1055 score::get_pixmap(QString(":/icons/%1_on.png").arg(icon)));
1056 }
1057 return label;
1058}
1059
1060InteractiveLabel* StartScreen::makeAccentItem(
1061 const QString& text, const QString& icon, const QString& url, QWidget* parent)
1062{
1063 // Accent text at rest; hovering behaves like every other item
1064 auto label = makeItem(text, icon, url, parent);
1065 label->setInactiveColor(StartScreenColors::Accent);
1066 return label;
1067}
1068
1069InteractiveLabel* StartScreen::makeExternalLink(const Link& link, QWidget* parent)
1070{
1071 auto label = makeItem(link.text, link.icon, link.url, parent);
1072 label->setOpenExternalLink(true);
1073 label->setToolTip(link.tooltip.isEmpty() ? link.url : link.tooltip);
1074 return label;
1075}
1076
1077InteractiveLabel* StartScreen::makeScoreItem(
1078 const QString& text, const QString& icon, const QString& path, bool asTemplate,
1079 QWidget* parent)
1080{
1081 auto label = makeItem(text, icon, path, parent);
1082 label->setElideMode(Qt::ElideMiddle);
1083 if(asTemplate)
1084 {
1085 connect(label, &InteractiveLabel::labelPressed, this, [this](const QString& file) {
1086 choose([&] { openTemplate(file); });
1087 });
1088 }
1089 else
1090 {
1091 connect(label, &InteractiveLabel::labelPressed, this, [this](const QString& file) {
1092 choose([&] { openFile(file); });
1093 });
1094 }
1095 connect(label, &InteractiveLabel::hovered, this, [this, label](bool on) {
1096 if(on)
1097 showPreview(label);
1098 else
1099 m_preview->hide();
1100 });
1101
1102 requestInfo(path);
1103 return label;
1104}
1105
1106QWidget* StartScreen::createHomePage(const QPointer<QRecentFilesMenu>& recentFiles)
1107{
1108 auto page = new QWidget;
1109 auto lay = new QHBoxLayout{page};
1110 lay->setContentsMargins(28, 24, 28, 24);
1111 lay->setSpacing(32);
1112
1113 const QString firstRunScore = firstRunDocumentTemplate();
1114
1115 // Left column: create
1116 {
1117 auto col = new QVBoxLayout;
1118 col->setSpacing(6);
1119 col->addWidget(makeSectionTitle(tr("Start"), page));
1120
1121 auto newLabel = makeItem(tr("New empty score"), "new_file", "", page);
1122 connect(newLabel, &InteractiveLabel::labelPressed, this, [this] {
1123 choose([this] { openNewDocument(); });
1124 });
1125 col->addWidget(newLabel);
1126
1127 auto templatesLabel = makeItem(tr("Start from a template..."), "new_file", "", page);
1128 connect(templatesLabel, &InteractiveLabel::labelPressed, this, [this] {
1129 setCurrentPage(m_templatesPage);
1130 });
1131 col->addWidget(templatesLabel);
1132
1133 auto openLabel = makeItem(tr("Open a score file..."), "load", "", page);
1134 connect(openLabel, &InteractiveLabel::labelPressed, this, [this] {
1135 choose([this] { openFileDialog(); });
1136 });
1137 col->addWidget(openLabel);
1138
1139 m_joinLabel
1140 = makeItem(tr("Join a collaborative session..."), "net_session", "", page);
1141 m_joinLabel->setToolTip(tr("Connect to a score session hosted on another computer"));
1142 m_joinLabel->hide();
1143 connect(m_joinLabel, &InteractiveLabel::labelPressed, this, [this] {
1144 choose([this] { joinSession(); });
1145 });
1146 col->addWidget(m_joinLabel);
1147
1148 col->addSpacing(18);
1149 col->addWidget(makeSectionTitle(tr("New to ossia score?"), page));
1150
1151 if(!firstRunScore.isEmpty())
1152 {
1153 if(m_firstRun)
1154 {
1155 col->addWidget(makeHint(
1156 tr("Welcome! This guided score shows you around the interface and "
1157 "the main concepts in a few minutes."),
1158 page));
1159 }
1160 auto guided = makeAccentItem(
1161 tr("Open the demo project"), "version", firstRunScore, page);
1162 connect(guided, &InteractiveLabel::labelPressed, this, [this](const QString& f) {
1163 choose([&] { openTemplate(f); });
1164 });
1165 col->addWidget(guided);
1166
1167 col->addWidget(makeExternalLink(
1168 {tr("Read the quick start guide"),
1169 "https://ossia.io/score-docs/quick-start.html",
1170 "version",
1171 {}},
1172 page));
1173 }
1174 else
1175 {
1176 col->addWidget(makeHint(
1177 tr("Start with the quick start guide: it walks you through the "
1178 "interface and your first score."),
1179 page));
1180 auto guide = makeAccentItem(
1181 tr("Read the quick start guide"), "version",
1182 "https://ossia.io/score-docs/quick-start.html", page);
1183 guide->setOpenExternalLink(true);
1184 col->addWidget(guide);
1185 }
1186
1187 col->addStretch();
1188 lay->addLayout(col, 1);
1189 }
1190
1191 // Right column: open
1192 {
1193 auto col = new QVBoxLayout;
1194 col->setSpacing(6);
1195 col->addWidget(makeSectionTitle(tr("Recent"), page));
1196
1197 int shown = 0;
1198 if(recentFiles)
1199 {
1200 for(const auto& action : recentFiles->actions())
1201 {
1202 if(shown++ >= MaxRecentFiles)
1203 break;
1204
1205 const QString path = action->data().toString();
1206 col->addWidget(
1207 makeScoreItem(QFileInfo{path}.fileName(), "load", path, false, page));
1208 }
1209 }
1210
1211 if(shown == 0)
1212 {
1213 auto none = makeItem(tr("No recent scores yet"), "", "", page);
1214 none->disableInteractivity();
1215 none->setInactiveColor(StartScreenColors::Muted);
1216 col->addWidget(none);
1217 }
1218
1219 col->addSpacing(6);
1220
1221 m_crashLabel = makeAccentItem(tr("Restore last session"), "reload_crash", "", page);
1222 m_crashLabel->setToolTip(
1223 tr("score did not exit cleanly last time: reopen the documents that were open"));
1224 m_crashLabel->hide();
1225 connect(m_crashLabel, &InteractiveLabel::labelPressed, this, [this] {
1226 choose([this] { loadCrashedSession(); });
1227 });
1228 col->addWidget(m_crashLabel);
1229
1230 col->addStretch();
1231 lay->addLayout(col, 1);
1232 }
1233
1234 return page;
1235}
1236
1237QWidget* StartScreen::createTemplatesPage()
1238{
1239 return createCardsPage(
1240 tr("Templates"),
1241 tr("A template opens as a new untitled score with devices, processes and a "
1242 "structure already in place."),
1243 tr("No templates are installed yet. Templates are .score files in the "
1244 "Templates folder of your user library (%1) or of an installed package.")
1245 .arg(QDir::toNativeSeparators(libraryRootPath())),
1246 availableDocumentTemplates(),
1247 [this](const QString& path) { choose([&] { openTemplate(path); }); },
1248 {tr("Learn how to write your own templates"),
1249 "https://ossia.io/score-docs/",
1250 "learn",
1251 {}});
1252}
1253
1254QWidget* StartScreen::createAboutPage()
1255{
1256 // Shared with Help > About
1258 style.sectionFont = m_sectionFont;
1259 style.itemFont = m_itemFont;
1260 style.smallFont = m_smallFont;
1261 style.text = StartScreenColors::Text;
1262 style.muted = StartScreenColors::Muted;
1263 style.hover = StartScreenColors::Hover;
1264 style.version = StartScreenColors::Version;
1265 style.outline = StartScreenColors::Outline;
1266
1267 auto page = new QWidget;
1268 auto lay = new QVBoxLayout{page};
1269 lay->setContentsMargins(28, 20, 28, 16);
1270 lay->addWidget(new score::AboutWidget{style, page});
1271 return page;
1272}
1273
1274QWidget* StartScreen::createExamplesPage()
1275{
1276 return createCardsPage(
1277 tr("Example scores"),
1278 tr("Each example opens as a new, untitled score."),
1279 tr("No example scores are installed yet. Examples are .score files in the "
1280 "Examples folder of your user library (%1) or of an installed package.")
1281 .arg(QDir::toNativeSeparators(libraryRootPath())),
1282 availableExampleDocuments(), [this](const QString& path) { openExample(path); },
1283 {tr("More examples online"),
1284 "https://ossia.io/score-docs/examples",
1285 "load_examples",
1286 {}});
1287}
1288
1289QWidget* StartScreen::createCardsPage(
1290 const QString& title, const QString& hint, const QString& emptyHint,
1291 const std::vector<DocumentTemplate>& docs,
1292 std::function<void(const QString&)> onActivated, const Link& more)
1293{
1294 auto page = new QWidget;
1295 auto lay = new QVBoxLayout{page};
1296 lay->setContentsMargins(28, 24, 28, 16);
1297 lay->setSpacing(8);
1298
1299 lay->addWidget(makeSectionTitle(title, page));
1300
1301 if(docs.empty())
1302 {
1303 lay->addWidget(makeHint(emptyHint, page));
1304 }
1305 else
1306 {
1307 lay->addWidget(makeHint(hint, page));
1308
1309 auto scroll = new QScrollArea{page};
1310 scroll->setFrameShape(QFrame::NoFrame);
1311 scroll->setWidgetResizable(true);
1312 scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1313 scroll->viewport()->setAutoFillBackground(false);
1314
1315 auto container = new QWidget;
1316 container->setAutoFillBackground(false);
1317 auto grid = new QGridLayout{container};
1318 grid->setContentsMargins(0, 4, 8, 4);
1319 grid->setHorizontalSpacing(14);
1320 grid->setVerticalSpacing(14);
1321
1322 static constexpr int columns = 3;
1323 int i = 0;
1324 for(const auto& doc : docs)
1325 {
1326 const QString subtitle = !doc.category.isEmpty() ? doc.category
1327 : doc.source == "library" ? tr("User library")
1328 : doc.source;
1329 auto card = new ExampleCard{m_itemFont, m_smallFont, doc.name,
1330 subtitle, doc.path, container};
1331 card->onActivated = onActivated;
1332 grid->addWidget(card, i / columns, i % columns, Qt::AlignLeft | Qt::AlignTop);
1333 m_cards.push_back(card);
1334 requestInfo(doc.path);
1335 i++;
1336 }
1337 grid->setColumnStretch(columns, 1);
1338 grid->setRowStretch((i + columns - 1) / columns, 1);
1339
1340 scroll->setWidget(container);
1341 lay->addWidget(scroll, 1);
1342 }
1343
1344 lay->addWidget(makeExternalLink(more, page));
1345
1346 if(docs.empty())
1347 lay->addStretch();
1348
1349 return page;
1350}
1351
1352QWidget*
1353StartScreen::createLinksPage(const QString& title, const std::vector<Link>& links)
1354{
1355 auto page = new QWidget;
1356 auto lay = new QVBoxLayout{page};
1357 lay->setContentsMargins(28, 24, 28, 24);
1358 lay->setSpacing(6);
1359
1360 lay->addWidget(makeSectionTitle(title, page));
1361 for(const auto& link : links)
1362 lay->addWidget(makeExternalLink(link, page));
1363 lay->addStretch();
1364
1365 return page;
1366}
1367
1368void StartScreen::openExample(const QString& path)
1369{
1370 if(m_actionTaken)
1371 return;
1372
1373 // The information is normally already loaded in the background; if the
1374 // user was faster than the parser, read it now.
1375 std::optional<ProjectInfo::Info> info;
1376 if(auto it = m_infos.find(path); it != m_infos.end() && it->second)
1377 info = it->second;
1378 else
1379 info = ProjectInfo::peek(path);
1380
1381 if(info && !info->url.isEmpty())
1382 {
1383 if(const QUrl url{info->url}; url.isValid() && !url.scheme().isEmpty())
1384 QDesktopServices::openUrl(url);
1385 }
1386
1387 choose([&] { openTemplate(path); });
1388}
1389
1390void StartScreen::requestInfo(const QString& path)
1391{
1392 if(m_infos.find(path) != m_infos.end())
1393 return;
1394 m_infos.emplace(path, std::nullopt);
1395
1396 // Parsing a big .score file can take a moment: do it off the GUI thread.
1397 score::TaskPool::instance().post([path, self = QPointer{this}] {
1398 auto info = ProjectInfo::peek(path);
1399 QMetaObject::invokeMethod(
1400 QCoreApplication::instance(), [self, path, info = std::move(info)] {
1401 if(self)
1402 self->onInfoLoaded(path, info);
1403 }, Qt::QueuedConnection);
1404 });
1405}
1406
1407void StartScreen::onInfoLoaded(
1408 const QString& path, const std::optional<ProjectInfo::Info>& info)
1409{
1410 m_infos[path] = info;
1411 if(!info)
1412 return;
1413
1414 for(auto card : m_cards)
1415 if(card->path() == path)
1416 card->setInfo(*info);
1417}
1418
1419void StartScreen::showPreview(InteractiveLabel* label)
1420{
1421 const QString& path = label->url();
1422 ProjectInfo::Info info;
1423 if(auto it = m_infos.find(path); it != m_infos.end() && it->second)
1424 info = *it->second;
1425
1426 m_preview->setContent(
1427 info.thumbnail, info.name.isEmpty() ? label->text() : info.name, info.author,
1428 info.description, QDir::toNativeSeparators(path));
1429 m_preview->showFor(
1430 label, QRect{
1431 NavWidth + 8, HeaderHeight + 8, Width - NavWidth - 16,
1432 Height - HeaderHeight - 16});
1433}
1434
1436{
1437 m_crashLabel->show();
1438 update();
1439}
1440
1442{
1443 m_joinLabel->show();
1444 update();
1445}
1446
1448{
1449 m_actionTaken = true;
1450 close();
1451}
1452
1454{
1455 m_actionTaken = false;
1456 show();
1457 raise();
1458 activateWindow();
1459}
1460
1461void StartScreen::checkForNewVersion()
1462{
1463 // The request itself is asynchronous (QNetworkAccessManager); the reply is
1464 // handled on the GUI thread and only then touches the widgets.
1465 auto& tp = score::ThreadPool::instance();
1466 auto t = tp.acquireThread();
1467 QMetaObject::invokeMethod(t, [t, self = QPointer{this}] {
1468 auto getLastVersion = new HTTPGet{
1469 QUrl("https://ossia.io/score-last-version.txt"), [self](const QByteArray& data) {
1470 const auto version = QString::fromUtf8(data.simplified());
1471 if(QVersionNumber::fromString(version)
1472 > QVersionNumber::fromString(SCORE_TAG_NO_V))
1473 {
1474 QMetaObject::invokeMethod(QCoreApplication::instance(), [self, version] {
1475 if(self)
1476 self->showUpdateAvailable(version);
1477 });
1478 }
1479 }, [] { }};
1480 connect(getLastVersion, &QObject::destroyed, t, [] {
1481 QMetaObject::invokeMethod(QCoreApplication::instance(), [] {
1482 auto& tp = score::ThreadPool::instance();
1483 tp.releaseThread();
1484 });
1485 });
1486 });
1487}
1488
1489void StartScreen::showUpdateAvailable(const QString& version)
1490{
1491 m_updateLabel->setText(
1492 tr("New version %1 is available, click to update").arg(version));
1493 m_updateLabel->setToolTip(tr("Open the download page in your browser"));
1494 m_updateLabel->show();
1495}
1496
1497void StartScreen::paintEvent(QPaintEvent* event)
1498{
1499 QPainter painter(this);
1500 painter.setRenderHint(QPainter::Antialiasing, true);
1501 painter.setRenderHint(QPainter::TextAntialiasing, true);
1502 painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
1503
1504 // The splash artwork covers the whole window: logo and tagline in the
1505 // header, decorations behind the navigation column.
1506 painter.fillRect(rect(), Qt::black);
1507 qreal scale = 1.;
1508 if(!m_background.isNull())
1509 {
1510 // Scaled uniformly to cover the window, anchored top-left so that the logo
1511 // and tagline keep their place; the overflow is cropped at the bottom.
1512 const QSizeF logical = m_background.deviceIndependentSize();
1513 scale = std::max(width() / logical.width(), height() / logical.height());
1514 painter.drawPixmap(
1515 QRectF{QPointF{}, logical * scale}, m_background, m_background.rect());
1516 }
1517
1518 // The version sits under the tagline, which is part of the artwork
1519 painter.setFont(m_versionFont);
1520 painter.setPen(QPen{StartScreenColors::Version});
1521 {
1522 // Branch builds carry the branch name: keep it inside the header
1523 const qreal x = 217 * scale;
1524 const QFontMetrics fm{m_versionFont};
1525 painter.drawText(
1526 QPointF(x, 188 * scale), fm.elidedText(
1527 QCoreApplication::applicationVersion(),
1528 Qt::ElideMiddle, int(width() - x - 60)));
1529 }
1530
1531 // Dim the artwork behind the navigation so that its text stays readable
1532 painter.fillRect(
1533 QRect{0, HeaderHeight, NavWidth, Height - HeaderHeight}, QColor{0, 0, 0, 120});
1534
1535 // Page panel. The selected navigation item paints itself with the panel
1536 // color so that it visually connects to the page.
1537 painter.fillRect(
1538 QRect{NavWidth, HeaderHeight, Width - NavWidth, Height - HeaderHeight},
1539 StartScreenColors::Panel);
1540}
1541
1542void StartScreen::keyPressEvent(QKeyEvent* event)
1543{
1544 switch(event->key())
1545 {
1546 case Qt::Key_Escape:
1547 choose([this] { openNewDocument(); });
1548 event->accept();
1549 return;
1550 case Qt::Key_Left:
1551 case Qt::Key_Up:
1552 setCurrentPage(
1553 (m_pages->currentIndex() + m_pages->count() - 1) % m_pages->count());
1554 event->accept();
1555 return;
1556 case Qt::Key_Right:
1557 case Qt::Key_Down:
1558 setCurrentPage((m_pages->currentIndex() + 1) % m_pages->count());
1559 event->accept();
1560 return;
1561 default:
1562 QWidget::keyPressEvent(event);
1563 }
1564}
1565
1566void StartScreen::closeEvent(QCloseEvent* event)
1567{
1568 // Closed by other means (e.g. the window manager): make sure that the user
1569 // does not end up with an empty application window.
1570 choose([this] { openNewDocument(); });
1571 QWidget::closeEvent(event);
1572}
1573}
Version, partners and third-party licenses of ossia score.
Definition AboutWidget.hpp:18
A score presented as a card: thumbnail, name, author.
Definition StartScreen.hpp:529
A clickable label with an optional icon and a hover state.
Definition StartScreen.hpp:131
void setIconColumn(int px)
Width reserved for the icon, so that texts align whatever the icon size.
Definition StartScreen.hpp:148
The window shown when score starts without a document.
Definition StartScreen.hpp:695
void joinSession()
Join a collaborative session hosted by another instance of score.
void addLoadCrashedSession()
Shows the "Restore last session" entry.
Definition StartScreen.hpp:1435
void dismiss()
Closes the start screen without opening anything (something else took over).
Definition StartScreen.hpp:1447
void addJoinSession()
Shows the "Join a collaborative session" entry.
Definition StartScreen.hpp:1441
void reopen()
Shows the start screen again after an action that led nowhere (cancelled dialog......
Definition StartScreen.hpp:1453
void openTemplate(const QString &file)
Opens the file as a new untitled document (templates, examples).
Preview of a score shown next to a hovered entry: thumbnail, name, author.
Definition StartScreen.hpp:398
void showFor(QWidget *anchor, const QRect &bounds)
Shows the popup next to anchor, inside bounds (parent coordinates).
Definition StartScreen.hpp:439
Base toolkit upon which the software is built.
Definition Application.cpp:117
QString libraryRootPath()
Root of the user library, from the settings.
Definition DocumentTemplates.cpp:22
void setCursor(Qt::CursorShape c)
setCursor sets the cursor safely.
Definition ClearLayout.cpp:8
QString firstRunDocumentTemplate()
The guided score for first-time users, or an empty string.
Definition DocumentTemplates.cpp:144
Definition AboutWidget.hpp:21
QFont smallFont
Captions, license text.
Definition AboutWidget.hpp:24
QFont sectionFont
Tabs (uppercase)
Definition AboutWidget.hpp:22
QFont itemFont
Regular text.
Definition AboutWidget.hpp:23