2#include <score/model/Skin.hpp>
3#include <score/tools/ThreadPool.hpp>
4#include <score/widgets/Pixmap.hpp>
6#include <core/document/DocumentTemplates.hpp>
7#include <core/document/ProjectInfo.hpp>
8#include <core/presenter/AboutWidget.hpp>
9#include <core/view/QRecentFilesMenu.h>
11#include <QApplication>
14#include <QDesktopServices>
17#include <QFontMetrics>
24#include <QNetworkAccessManager>
25#include <QNetworkReply>
26#include <QNetworkRequest>
28#include <QPainterPath>
30#include <QPlainTextEdit>
34#include <QStackedWidget>
36#include <QVersionNumber>
38#include <score_git_info.hpp>
52template <
typename OnSuccess,
typename OnError>
53class HTTPGet final :
public QNetworkAccessManager
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)}
60 connect(
this, &QNetworkAccessManager::finished,
this, [
this](QNetworkReply* reply) {
63 qDebug() << reply->errorString();
68 m_callback(reply->readAll());
75 QNetworkRequest req{std::move(url)};
76 req.setRawHeader(
"User-Agent",
"curl/7.35.0");
77 req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute,
true);
79 QNetworkRequest::RedirectPolicyAttribute,
80 QNetworkRequest::UserVerifiedRedirectPolicy);
81 req.setAttribute(QNetworkRequest::Http2AllowedAttribute,
true);
83 auto reply = get(req);
84 connect(reply, &QNetworkReply::redirected, reply, &QNetworkReply::redirectAllowed);
95namespace StartScreenColors
97static const QColor Panel{
98 0x21, 0x1f, 0x1f, 205};
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"};
110QPixmap coverPixmap(
const QImage& img, QSize size)
115 = img.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
117 (scaled.width() - size.width()) / 2, (scaled.height() - size.height()) / 2,
118 size.width(), size.height()};
119 return QPixmap::fromImage(scaled.copy(crop));
136 const QFont& font,
const QString& text,
const QString& url,
137 QWidget* parent =
nullptr);
139 void setOpenExternalLink(
bool val) { m_openExternalLink = val; }
140 void setPixmaps(
const QPixmap& pixmap,
const QPixmap& pixmapOn);
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; }
149 void setItemHeight(
int px);
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; }
156 void setText(
const QString& text);
157 const QString& text() const noexcept {
return m_title; }
158 const QString& url() const noexcept {
return m_url; }
160 void labelPressed(
const QString& file) W_SIGNAL(labelPressed, file)
161 void hovered(
bool state) W_SIGNAL(hovered, state)
163 QSize sizeHint()
const override;
164 QSize minimumSizeHint()
const override;
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;
174 bool highlighted() const noexcept {
return m_checked || (m_interactive && m_hovered); }
175 int iconWidth() const noexcept {
return m_pixmap.isNull() ? 0 : m_iconColumn; }
184 QColor m_activeColor{StartScreenColors::Hover};
185 QColor m_inactiveColor{StartScreenColors::Text};
186 QColor m_checkedBackground;
188 Qt::TextElideMode m_elideMode{Qt::ElideRight};
189 int m_leftPadding{4};
190 int m_iconColumn{34};
193 bool m_openExternalLink{};
194 bool m_interactive{
true};
201InteractiveLabel::InteractiveLabel(
202 const QFont& font,
const QString& title,
const QString& url, QWidget* parent)
208 setCursor(score::Skin::instance().CursorPointingHand);
209 setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
210 setFixedHeight(m_height);
213void InteractiveLabel::setPixmaps(
const QPixmap& pixmap,
const QPixmap& pixmapOn)
216 m_pixmapOn = pixmapOn;
218 const int h = std::max(
219 int(m_pixmap.height() / m_pixmap.devicePixelRatio()),
220 int(m_pixmapOn.height() / m_pixmapOn.devicePixelRatio()));
222 setItemHeight(h + 4);
226void InteractiveLabel::setItemHeight(
int px)
233void InteractiveLabel::disableInteractivity()
235 m_interactive =
false;
236 setCursor(score::Skin::instance().CursorPointer);
240void InteractiveLabel::setActiveColor(
const QColor& c)
246void InteractiveLabel::setInactiveColor(
const QColor& c)
252void InteractiveLabel::setChecked(
bool b)
260void InteractiveLabel::setText(
const QString& text)
267QSize InteractiveLabel::sizeHint()
const
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};
275QSize InteractiveLabel::minimumSizeHint()
const
278 int w = m_leftPadding + iconWidth();
279 if(!m_title.isEmpty())
281 return {w, m_height};
285static QPixmap tintedPixmap(
const QPixmap& source,
const QColor& color)
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())
294 QPixmap res{source.size()};
295 res.setDevicePixelRatio(source.devicePixelRatio());
296 res.fill(Qt::transparent);
299 p.drawPixmap(0, 0, source);
300 p.setCompositionMode(QPainter::CompositionMode_SourceIn);
301 p.fillRect(res.rect(), color);
303 return cache.emplace(key, res).first->second;
306void InteractiveLabel::paintEvent(QPaintEvent* event)
308 QPainter painter(
this);
309 painter.setRenderHint(QPainter::Antialiasing,
true);
310 painter.setRenderHint(QPainter::TextAntialiasing,
true);
311 painter.setRenderHint(QPainter::SmoothPixmapTransform,
true);
313 const bool hl = highlighted();
315 if(m_checked && m_checkedBackground.isValid())
317 painter.fillRect(rect(), m_checkedBackground);
318 painter.fillRect(QRect{0, 0, 3, height()}, m_activeColor);
321 QRectF textRect = rect().adjusted(m_leftPadding, 0, -4, 0);
324 const QColor color = hl ? m_activeColor : m_inactiveColor;
325 const QPixmap pm = tintedPixmap(m_pixmap, color);
328 const qreal w = pm.width() / pm.devicePixelRatio();
329 const qreal h = pm.height() / pm.devicePixelRatio();
332 QPointF{textRect.x() + (m_iconColumn - 8 - w) / 2., (height() - h) / 2.}, pm);
333 textRect.setX(textRect.x() + m_iconColumn);
336 if(!m_title.isEmpty())
338 painter.setPen(QPen{color});
339 painter.setFont(m_font);
341 = QFontMetrics{m_font}.elidedText(m_title, m_elideMode,
int(textRect.width()));
342 painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, txt);
346void InteractiveLabel::enterEvent(QEnterEvent* event)
356void InteractiveLabel::leaveEvent(QEvent* event)
367void InteractiveLabel::mousePressEvent(QMouseEvent* event)
369 if(!m_interactive || event->button() != Qt::LeftButton)
370 return QWidget::mousePressEvent(event);
375void InteractiveLabel::mouseReleaseEvent(QMouseEvent* event)
377 if(!m_pressed || event->button() != Qt::LeftButton)
378 return QWidget::mouseReleaseEvent(event);
382 if(!rect().contains(event->pos()))
388 if(m_openExternalLink)
389 QDesktopServices::openUrl(QUrl(m_url));
400 static constexpr int Width = 264;
401 static constexpr int ThumbHeight = 158;
402 static constexpr int Margin = 6;
403 static constexpr int DescriptionLines = 2;
405 ThumbnailPopup(
const QFont& titleFont,
const QFont& subFont, QWidget* parent)
407 , m_titleFont{titleFont}
410 setAttribute(Qt::WA_TransparentForMouseEvents);
415 const QImage& thumbnail,
const QString& title,
const QString& author,
416 const QString& description,
const QString& path)
418 m_thumbnail = coverPixmap(thumbnail, {Width - 2 * Margin, ThumbHeight});
421 m_description = description.simplified();
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())
430 if(!m_description.isEmpty())
431 h += 4 + DescriptionLines * sm.lineSpacing();
432 if(!m_path.isEmpty())
433 h += 4 + sm.height();
439 void showFor(QWidget* anchor,
const QRect& bounds)
441 const QRect a{anchor->mapTo(parentWidget(), QPoint{0, 0}), anchor->size()};
442 int x = a.left() - width() - 12;
443 if(x < bounds.left())
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()));
455 void paintEvent(QPaintEvent*)
override
458 p.setRenderHint(QPainter::Antialiasing,
true);
459 p.setRenderHint(QPainter::SmoothPixmapTransform,
true);
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});
467 const int textX = Margin + 2;
468 const int textW = width() - 2 * Margin - 4;
469 const QFontMetrics tm{m_titleFont}, sm{m_subFont};
471 if(!m_thumbnail.isNull())
473 p.drawPixmap(Margin, y, m_thumbnail);
474 y += ThumbHeight + Margin;
477 p.setPen(StartScreenColors::Text);
478 p.setFont(m_titleFont);
480 QRect{textX, y, textW, tm.height()}, Qt::AlignLeft | Qt::AlignVCenter,
481 tm.elidedText(m_title, Qt::ElideMiddle, textW));
484 p.setFont(m_subFont);
485 if(!m_author.isEmpty())
487 p.setPen(StartScreenColors::Hover);
489 QRect{textX, y, textW, sm.height()}, Qt::AlignLeft | Qt::AlignVCenter,
490 sm.elidedText(m_author, Qt::ElideRight, textW));
493 if(!m_description.isEmpty())
496 p.setPen(StartScreenColors::Text);
497 const QRect descRect{textX, y, textW, DescriptionLines * sm.lineSpacing()};
499 p.setClipRect(descRect);
501 descRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, m_description);
503 y += descRect.height();
505 if(!m_path.isEmpty())
508 p.setPen(StartScreenColors::Muted);
510 QRect{textX, y, textW, sm.height()}, Qt::AlignLeft | Qt::AlignVCenter,
511 sm.elidedText(m_path, Qt::ElideMiddle, textW));
521 QString m_description;
531 static constexpr int Width = 190;
532 static constexpr int ThumbHeight = 119;
533 static constexpr int Height = ThumbHeight + 66;
536 const QFont& titleFont,
const QFont& subFont,
const QString& title,
537 const QString& subtitle,
const QString& path, QWidget* parent)
539 , m_titleFont{titleFont}
542 , m_subtitle{subtitle}
545 setFixedSize(Width, Height);
546 setCursor(score::Skin::instance().CursorPointingHand);
547 setToolTip(QDir::toNativeSeparators(path));
550 const QString& path()
const noexcept {
return m_path; }
552 void setInfo(
const ProjectInfo::Info& info)
554 if(!info.name.isEmpty())
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);
569 std::function<void(
const QString&)> onActivated;
572 void paintEvent(QPaintEvent*)
override
575 p.setRenderHint(QPainter::Antialiasing,
true);
576 p.setRenderHint(QPainter::SmoothPixmapTransform,
true);
579 path.addRoundedRect(QRectF{rect()}.adjusted(0.5, 0.5, -0.5, -0.5), 4, 4);
580 p.fillPath(path, StartScreenColors::Card);
585 const QRect thumbRect{0, 0, Width, ThumbHeight};
586 if(!m_thumbnail.isNull())
588 p.drawPixmap(thumbRect, m_thumbnail);
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();
598 p.drawPixmap(QPointF{(Width - w) / 2., (ThumbHeight - h) / 2.}, placeholder);
603 p.setPen(QPen{m_hovered ? StartScreenColors::Hover : StartScreenColors::Outline, 1});
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};
612 QRect{textX, ThumbHeight + 6, textW, tm.height()},
613 Qt::AlignLeft | Qt::AlignVCenter, tm.elidedText(m_title, Qt::ElideRight, textW));
615 p.setPen(StartScreenColors::Muted);
616 p.setFont(m_subFont);
617 const QFontMetrics sm{m_subFont};
619 QRect{textX, ThumbHeight + 6 + tm.height(), textW, sm.height()},
620 Qt::AlignLeft | Qt::AlignVCenter,
621 sm.elidedText(m_subtitle, Qt::ElideRight, textW));
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));
632 void enterEvent(QEnterEvent*)
override
637 void leaveEvent(QEvent*)
override
643 void mousePressEvent(QMouseEvent* e)
override
645 if(e->button() == Qt::LeftButton)
651 void mouseReleaseEvent(QMouseEvent* e)
override
654 = m_pressed && e->button() == Qt::LeftButton && rect().contains(e->pos());
656 if(activate && onActivated)
698 StartScreen(
const QPointer<QRecentFilesMenu>& recentFiles, QWidget* parent =
nullptr);
700 void openNewDocument() W_SIGNAL(openNewDocument)
701 void openFile(
const QString& file) W_SIGNAL(openFile, file)
704 void openFileDialog() W_SIGNAL(openFileDialog)
705 void loadCrashedSession() W_SIGNAL(loadCrashedSession)
708 void exitApp() W_SIGNAL(exitApp)
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;
726 void paintEvent(QPaintEvent* event) override;
727 void keyPressEvent(QKeyEvent* event) override;
728 void closeEvent(QCloseEvent* event) override;
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);
755 int addPage(
const QString& name,
const QString& icon, QWidget* page);
756 void setCurrentPage(
int index);
758 QLabel* makeSectionTitle(
const QString& text, QWidget* parent);
759 QLabel* makeHint(
const QString& text, QWidget* parent);
761 const QString& text,
const QString& icon,
const QString& url, QWidget* parent);
764 const QString& text,
const QString& icon,
const QString& url, QWidget* parent);
768 const QString& text,
const QString& icon,
const QString& path,
bool asTemplate,
773 template <
typename F>
774 void choose(F&& emitSignal)
778 m_actionTaken =
true;
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);
788 void checkForNewVersion();
789 void showUpdateAvailable(
const QString& version);
797 QPixmap m_background;
799 QStackedWidget* m_pages{};
800 std::vector<InteractiveLabel*> m_navItems;
801 std::vector<std::function<QWidget*()>> m_pageFactories;
802 QVBoxLayout* m_navLayout{};
804 InteractiveLabel* m_updateLabel{};
805 InteractiveLabel* m_crashLabel{};
806 InteractiveLabel* m_joinLabel{};
807 int m_templatesPage{};
808 ThumbnailPopup* m_preview{};
810 std::map<QString, std::optional<ProjectInfo::Info>> m_infos;
811 std::vector<ExampleCard*> m_cards;
814 bool m_actionTaken{};
817StartScreen::StartScreen(
const QPointer<QRecentFilesMenu>& recentFiles, QWidget* parent)
820 auto& skin = score::Skin::instance();
822 setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
823 setWindowModality(Qt::ApplicationModal);
824 setFocusPolicy(Qt::StrongFocus);
825 setFixedSize(Width, Height);
829 m_firstRun = !s.value(
"score/StartScreenSeen",
false).toBool();
830 s.setValue(
"score/StartScreenSeen",
true);
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);
856 m_background = score::get_pixmap(
":/startscreen/startscreensplash.png");
858 auto mainLayout =
new QVBoxLayout{
this};
859 mainLayout->setContentsMargins(0, 0, 0, 0);
860 mainLayout->setSpacing(0);
862 mainLayout->addWidget(createHeader());
864 auto body =
new QHBoxLayout;
865 body->setContentsMargins(0, 0, 0, 0);
868 m_pages =
new QStackedWidget{
this};
869 body->addWidget(createNavigation());
870 body->addWidget(m_pages, 1);
871 mainLayout->addLayout(body, 1);
873 addPage(tr(
"Home"),
"home", createHomePage(recentFiles));
875 = addPage(tr(
"Templates"),
"new_file", [
this] {
return createTemplatesPage(); });
876 addPage(tr(
"Examples"),
"load_examples", [
this] {
return createExamplesPage(); });
878 tr(
"Learn"),
"learn",
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",
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", {}},
894 tr(
"Community"),
"community",
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",
904 {tr(
"Support ossia score through the Suite SAT"),
"https://suite.sat.qc.ca/en",
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!")},
912 addPage(tr(
"About"),
"about", [
this] {
return createAboutPage(); });
914 m_navLayout->addStretch();
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(); });
922 m_navLayout->addWidget(exitLabel);
925 m_preview =
new ThumbnailPopup{m_itemFont, m_smallFont,
this};
929 checkForNewVersion();
932QWidget* StartScreen::createHeader()
936 auto header =
new QWidget{
this};
937 header->setFixedHeight(HeaderHeight);
938 auto lay =
new QHBoxLayout{header};
939 lay->setContentsMargins(0, 10, 10, 0);
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);
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(); });
964 lay->addWidget(closeLabel, 0, Qt::AlignTop);
969QWidget* StartScreen::createNavigation()
971 auto nav =
new QWidget{
this};
972 nav->setFixedWidth(NavWidth);
973 m_navLayout =
new QVBoxLayout{nav};
975 m_navLayout->setContentsMargins(0, 0, 0, 16);
976 m_navLayout->setSpacing(4);
980int StartScreen::addPage(
const QString& name,
const QString& icon, QWidget* page)
982 const int index = addPage(name, icon, std::function<QWidget*()>{});
983 m_pages->widget(index)->layout()->addWidget(page);
987int StartScreen::addPage(
988 const QString& name,
const QString& icon, std::function<QWidget*()> make)
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);
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);
1007 m_navItems.push_back(item);
1008 m_navLayout->addWidget(item);
1012void StartScreen::setCurrentPage(
int index)
1014 if(index >= 0 && index <
int(m_pageFactories.size()))
1016 if(
auto make = std::exchange(m_pageFactories[index], {}))
1017 m_pages->widget(index)->layout()->addWidget(make());
1019 m_pages->setCurrentIndex(index);
1020 for(
int i = 0; i < int(m_navItems.size()); i++)
1021 m_navItems[i]->setChecked(i == index);
1026QLabel* StartScreen::makeSectionTitle(
const QString& text, QWidget* parent)
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);
1036QLabel* StartScreen::makeHint(
const QString& text, QWidget* parent)
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);
1047InteractiveLabel* StartScreen::makeItem(
1048 const QString& text,
const QString& icon,
const QString& url, QWidget* parent)
1050 auto label =
new InteractiveLabel{m_itemFont, text, url, parent};
1054 score::get_pixmap(QString(
":/icons/%1_off.png").arg(icon)),
1055 score::get_pixmap(QString(
":/icons/%1_on.png").arg(icon)));
1060InteractiveLabel* StartScreen::makeAccentItem(
1061 const QString& text,
const QString& icon,
const QString& url, QWidget* parent)
1064 auto label = makeItem(text, icon, url, parent);
1065 label->setInactiveColor(StartScreenColors::Accent);
1069InteractiveLabel* StartScreen::makeExternalLink(
const Link& link, QWidget* parent)
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);
1077InteractiveLabel* StartScreen::makeScoreItem(
1078 const QString& text,
const QString& icon,
const QString& path,
bool asTemplate,
1081 auto label = makeItem(text, icon, path, parent);
1082 label->setElideMode(Qt::ElideMiddle);
1085 connect(label, &InteractiveLabel::labelPressed,
this, [
this](
const QString& file) {
1091 connect(label, &InteractiveLabel::labelPressed,
this, [
this](
const QString& file) {
1092 choose([&] { openFile(file); });
1095 connect(label, &InteractiveLabel::hovered,
this, [
this, label](
bool on) {
1106QWidget* StartScreen::createHomePage(
const QPointer<QRecentFilesMenu>& recentFiles)
1108 auto page =
new QWidget;
1109 auto lay =
new QHBoxLayout{page};
1110 lay->setContentsMargins(28, 24, 28, 24);
1111 lay->setSpacing(32);
1117 auto col =
new QVBoxLayout;
1119 col->addWidget(makeSectionTitle(tr(
"Start"), page));
1121 auto newLabel = makeItem(tr(
"New empty score"),
"new_file",
"", page);
1122 connect(newLabel, &InteractiveLabel::labelPressed,
this, [
this] {
1123 choose([
this] { openNewDocument(); });
1125 col->addWidget(newLabel);
1127 auto templatesLabel = makeItem(tr(
"Start from a template..."),
"new_file",
"", page);
1128 connect(templatesLabel, &InteractiveLabel::labelPressed,
this, [
this] {
1129 setCurrentPage(m_templatesPage);
1131 col->addWidget(templatesLabel);
1133 auto openLabel = makeItem(tr(
"Open a score file..."),
"load",
"", page);
1134 connect(openLabel, &InteractiveLabel::labelPressed,
this, [
this] {
1135 choose([
this] { openFileDialog(); });
1137 col->addWidget(openLabel);
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] {
1146 col->addWidget(m_joinLabel);
1148 col->addSpacing(18);
1149 col->addWidget(makeSectionTitle(tr(
"New to ossia score?"), page));
1151 if(!firstRunScore.isEmpty())
1155 col->addWidget(makeHint(
1156 tr(
"Welcome! This guided score shows you around the interface and "
1157 "the main concepts in a few minutes."),
1160 auto guided = makeAccentItem(
1161 tr(
"Open the demo project"),
"version", firstRunScore, page);
1162 connect(guided, &InteractiveLabel::labelPressed,
this, [
this](
const QString& f) {
1165 col->addWidget(guided);
1167 col->addWidget(makeExternalLink(
1168 {tr(
"Read the quick start guide"),
1169 "https://ossia.io/score-docs/quick-start.html",
1176 col->addWidget(makeHint(
1177 tr(
"Start with the quick start guide: it walks you through the "
1178 "interface and your first score."),
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);
1188 lay->addLayout(col, 1);
1193 auto col =
new QVBoxLayout;
1195 col->addWidget(makeSectionTitle(tr(
"Recent"), page));
1200 for(
const auto& action : recentFiles->actions())
1202 if(shown++ >= MaxRecentFiles)
1205 const QString path = action->data().toString();
1207 makeScoreItem(QFileInfo{path}.fileName(),
"load", path,
false, page));
1213 auto none = makeItem(tr(
"No recent scores yet"),
"",
"", page);
1214 none->disableInteractivity();
1215 none->setInactiveColor(StartScreenColors::Muted);
1216 col->addWidget(none);
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(); });
1228 col->addWidget(m_crashLabel);
1231 lay->addLayout(col, 1);
1237QWidget* StartScreen::createTemplatesPage()
1239 return createCardsPage(
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.")
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/",
1254QWidget* StartScreen::createAboutPage()
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;
1267 auto page =
new QWidget;
1268 auto lay =
new QVBoxLayout{page};
1269 lay->setContentsMargins(28, 20, 28, 16);
1274QWidget* StartScreen::createExamplesPage()
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.")
1282 availableExampleDocuments(), [
this](
const QString& path) { openExample(path); },
1283 {tr(
"More examples online"),
1284 "https://ossia.io/score-docs/examples",
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)
1294 auto page =
new QWidget;
1295 auto lay =
new QVBoxLayout{page};
1296 lay->setContentsMargins(28, 24, 28, 16);
1299 lay->addWidget(makeSectionTitle(title, page));
1303 lay->addWidget(makeHint(emptyHint, page));
1307 lay->addWidget(makeHint(hint, page));
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);
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);
1322 static constexpr int columns = 3;
1324 for(
const auto& doc : docs)
1326 const QString subtitle = !doc.category.isEmpty() ? doc.category
1327 : doc.source ==
"library" ? tr(
"User library")
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);
1337 grid->setColumnStretch(columns, 1);
1338 grid->setRowStretch((i + columns - 1) / columns, 1);
1340 scroll->setWidget(container);
1341 lay->addWidget(scroll, 1);
1344 lay->addWidget(makeExternalLink(more, page));
1353StartScreen::createLinksPage(
const QString& title,
const std::vector<Link>& links)
1355 auto page =
new QWidget;
1356 auto lay =
new QVBoxLayout{page};
1357 lay->setContentsMargins(28, 24, 28, 24);
1360 lay->addWidget(makeSectionTitle(title, page));
1361 for(
const auto& link : links)
1362 lay->addWidget(makeExternalLink(link, page));
1368void StartScreen::openExample(
const QString& path)
1375 std::optional<ProjectInfo::Info> info;
1376 if(
auto it = m_infos.find(path); it != m_infos.end() && it->second)
1379 info = ProjectInfo::peek(path);
1381 if(info && !info->url.isEmpty())
1383 if(
const QUrl url{info->url}; url.isValid() && !url.scheme().isEmpty())
1384 QDesktopServices::openUrl(url);
1390void StartScreen::requestInfo(
const QString& path)
1392 if(m_infos.find(path) != m_infos.end())
1394 m_infos.emplace(path, std::nullopt);
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)] {
1402 self->onInfoLoaded(path, info);
1403 }, Qt::QueuedConnection);
1407void StartScreen::onInfoLoaded(
1408 const QString& path,
const std::optional<ProjectInfo::Info>& info)
1410 m_infos[path] = info;
1414 for(
auto card : m_cards)
1415 if(card->path() == path)
1416 card->setInfo(*info);
1419void StartScreen::showPreview(InteractiveLabel* label)
1421 const QString& path = label->url();
1422 ProjectInfo::Info info;
1423 if(
auto it = m_infos.find(path); it != m_infos.end() && it->second)
1426 m_preview->setContent(
1427 info.thumbnail, info.name.isEmpty() ? label->text() : info.name, info.author,
1428 info.description, QDir::toNativeSeparators(path));
1431 NavWidth + 8, HeaderHeight + 8, Width - NavWidth - 16,
1432 Height - HeaderHeight - 16});
1437 m_crashLabel->show();
1443 m_joinLabel->show();
1449 m_actionTaken =
true;
1455 m_actionTaken =
false;
1461void StartScreen::checkForNewVersion()
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))
1474 QMetaObject::invokeMethod(QCoreApplication::instance(), [self, version] {
1476 self->showUpdateAvailable(version);
1480 connect(getLastVersion, &QObject::destroyed, t, [] {
1481 QMetaObject::invokeMethod(QCoreApplication::instance(), [] {
1482 auto& tp = score::ThreadPool::instance();
1489void StartScreen::showUpdateAvailable(
const QString& version)
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();
1497void StartScreen::paintEvent(QPaintEvent* event)
1499 QPainter painter(
this);
1500 painter.setRenderHint(QPainter::Antialiasing,
true);
1501 painter.setRenderHint(QPainter::TextAntialiasing,
true);
1502 painter.setRenderHint(QPainter::SmoothPixmapTransform,
true);
1506 painter.fillRect(rect(), Qt::black);
1508 if(!m_background.isNull())
1512 const QSizeF logical = m_background.deviceIndependentSize();
1513 scale = std::max(width() / logical.width(), height() / logical.height());
1515 QRectF{QPointF{}, logical * scale}, m_background, m_background.rect());
1519 painter.setFont(m_versionFont);
1520 painter.setPen(QPen{StartScreenColors::Version});
1523 const qreal x = 217 * scale;
1524 const QFontMetrics fm{m_versionFont};
1526 QPointF(x, 188 * scale), fm.elidedText(
1527 QCoreApplication::applicationVersion(),
1528 Qt::ElideMiddle,
int(width() - x - 60)));
1533 QRect{0, HeaderHeight, NavWidth, Height - HeaderHeight}, QColor{0, 0, 0, 120});
1538 QRect{NavWidth, HeaderHeight, Width - NavWidth, Height - HeaderHeight},
1539 StartScreenColors::Panel);
1542void StartScreen::keyPressEvent(QKeyEvent* event)
1544 switch(event->key())
1546 case Qt::Key_Escape:
1547 choose([
this] { openNewDocument(); });
1553 (m_pages->currentIndex() + m_pages->count() - 1) % m_pages->count());
1558 setCurrentPage((m_pages->currentIndex() + 1) % m_pages->count());
1562 QWidget::keyPressEvent(event);
1566void StartScreen::closeEvent(QCloseEvent* event)
1570 choose([
this] { openNewDocument(); });
1571 QWidget::closeEvent(event);
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).
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