// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "projectwelcomepage.h" #include "projectexplorer.h" #include "projectexplorertr.h" #include "projectmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace Core::WelcomePageHelpers; using namespace Utils; using namespace Utils::StyleHelper::SpacingTokens; const char PROJECT_BASE_ID[] = "Welcome.OpenRecentProject"; namespace ProjectExplorer { namespace Internal { constexpr TextFormat projectNameTF {Theme::Token_Text_Accent, StyleHelper::UiElementH5}; constexpr TextFormat projectPathTF {Theme::Token_Text_Muted, StyleHelper::UiElementH6}; constexpr TextFormat sessionNameTF = projectNameTF; constexpr TextFormat sessionProjectNameTF {Theme::Token_Text_Default, projectNameTF.uiElement}; constexpr TextFormat shortcutNumberTF {Theme::Token_Text_Default, StyleHelper::UiElementCaptionStrong, Qt::AlignCenter | Qt::TextDontClip}; constexpr TextFormat actionTF {Theme::Token_Text_Default, StyleHelper::UiElementIconActive, Qt::AlignCenter | Qt::TextDontClip}; constexpr TextFormat actionDisabledTF {Theme::Token_Text_Subtle, actionTF.uiElement, actionTF.drawTextFlags}; constexpr int shortcutNumberWidth = 6; constexpr int actionSepWidth = 1; constexpr int sessionScrollBarGap = HPaddingXs; static int s(const int metric) { constexpr int shrinkWhenAbove = 150; // Above this session count, increasingly reduce scale constexpr qreal maxScale = 1.0; // Spacings as defined by design constexpr qreal minScale = 0.2; // Maximum "condensed" layout const int sessionsCount = SessionManager::sessionsCount(); const qreal scaling = sessionsCount < shrinkWhenAbove ? maxScale : qMax(minScale, maxScale - (sessionsCount - shrinkWhenAbove) * 0.065); return int(qMax(1.0, scaling * metric)); } static int itemSpacing() { return qMax(int(s(VGapL)), VGapS); } static bool withIcon() { return s(100) > 60; // Hide icons if spacings are scaled to below 60% } ProjectModel::ProjectModel(QObject *parent) : QAbstractListModel(parent) { connect(ProjectExplorerPlugin::instance(), &ProjectExplorerPlugin::recentProjectsChanged, this, &ProjectModel::resetProjects); } int ProjectModel::rowCount(const QModelIndex &) const { return int(m_projects.count()); } QVariant ProjectModel::data(const QModelIndex &index, int role) const { if (m_projects.count() <= index.row()) return {}; RecentProjectsEntry data = m_projects.at(index.row()); switch (role) { case Qt::DisplayRole: return data.displayName; case Qt::ToolTipRole: case FilePathRole: return data.filePath.toVariant(); case PrettyFilePathRole: return data.filePath.withTildeHomePath(); // FIXME: FilePath::displayName() ? case ShortcutRole: { const Id projectBase = PROJECT_BASE_ID; if (Command *cmd = ActionManager::command(projectBase.withSuffix(index.row() + 1))) return cmd->keySequence().toString(QKeySequence::NativeText); return {}; } default: return {}; } } QHash ProjectModel::roleNames() const { static QHash extraRoles{ {Qt::DisplayRole, "displayName"}, {FilePathRole, "filePath"}, {PrettyFilePathRole, "prettyFilePath"} }; return extraRoles; } void ProjectModel::resetProjects() { beginResetModel(); m_projects = ProjectExplorerPlugin::recentProjects(); endResetModel(); } /////////////////// ProjectWelcomePage::ProjectWelcomePage() { } Utils::Id ProjectWelcomePage::id() const { return "Develop"; } void ProjectWelcomePage::reloadWelcomeScreenData() const { if (m_sessionModel) m_sessionModel->resetSessions(); if (m_projectModel) m_projectModel->resetProjects(); } void ProjectWelcomePage::newProject() { ProjectExplorerPlugin::openNewProjectDialog(); } void ProjectWelcomePage::openProject() { ProjectExplorerPlugin::openOpenProjectDialog(); } void ProjectWelcomePage::openSessionAt(int index) { QTC_ASSERT(m_sessionModel, return); m_sessionModel->switchToSession(m_sessionModel->sessionAt(index)); } void ProjectWelcomePage::openProjectAt(int index) { QTC_ASSERT(m_projectModel, return); const QVariant projectFile = m_projectModel->data(m_projectModel->index(index, 0), ProjectModel::FilePathRole); ProjectExplorerPlugin::openProjectWelcomePage(FilePath::fromVariant(projectFile)); } void ProjectWelcomePage::createActions() { static bool actionsRegistered = false; if (actionsRegistered) return; actionsRegistered = true; const int actionsCount = 9; Context welcomeContext(Core::Constants::C_WELCOME_MODE); const Id projectBase = PROJECT_BASE_ID; const Id sessionBase = SESSION_BASE_ID; for (int i = 1; i <= actionsCount; ++i) { auto act = new QAction(Tr::tr("Open Session #%1").arg(i), this); Command *cmd = ActionManager::registerAction(act, sessionBase.withSuffix(i), welcomeContext); cmd->setDefaultKeySequence(QKeySequence((useMacShortcuts ? Tr::tr("Ctrl+Meta+%1") : Tr::tr("Ctrl+Alt+%1")).arg(i))); connect(act, &QAction::triggered, this, [this, i] { if (i <= m_sessionModel->rowCount()) openSessionAt(i - 1); }); act = new QAction(Tr::tr("Open Recent Project #%1").arg(i), this); cmd = ActionManager::registerAction(act, projectBase.withSuffix(i), welcomeContext); cmd->setDefaultKeySequence(QKeySequence(Tr::tr("Ctrl+Shift+%1").arg(i))); connect(act, &QAction::triggered, this, [this, i] { if (i <= m_projectModel->rowCount(QModelIndex())) openProjectAt(i - 1); }); } } /////////////////// static QColor themeColor(Theme::Color role) { return Utils::creatorTheme()->color(role); } static QPixmap pixmap(const QString &id, const Theme::Color color) { const QString fileName = QString(":/welcome/images/%1.png").arg(id); return Icon({{FilePath::fromString(fileName), color}}, Icon::Tint).pixmap(); } static void drawBackgroundRect(QPainter *painter, const QRectF &rect, bool hovered) { const QColor fill(themeColor(hovered ? cardHoverBackground : cardDefaultBackground)); const QPen pen(themeColor(hovered ? cardHoverStroke : cardDefaultStroke)); const qreal rounding = s(defaultCardBackgroundRounding * 1000) / 1000.0; const qreal saneRounding = rounding <= 2 ? 0 : rounding; WelcomePageHelpers::drawCardBackground(painter, rect, fill, pen, saneRounding); } class BaseDelegate : public QAbstractItemDelegate { protected: virtual QString entryType() = 0; virtual QRect toolTipArea(const QRect &itemRect, const QModelIndex &) const { return itemRect; } virtual int shortcutRole() const = 0; bool helpEvent(QHelpEvent *ev, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &idx) final { if (!toolTipArea(option.rect, idx).contains(ev->pos())) { QToolTip::hideText(); return false; } QString shortcut = idx.data(shortcutRole()).toString(); QString name = idx.data(Qt::DisplayRole).toString(); QString tooltipText; const QString type = entryType(); if (shortcut.isEmpty()) tooltipText = Tr::tr("Open %1 \"%2\"").arg(type, name); else tooltipText = Tr::tr("Open %1 \"%2\" (%3)").arg(type, name, shortcut); if (tooltipText.isEmpty()) return false; QToolTip::showText(ev->globalPos(), tooltipText, view); return true; } }; class SessionDelegate : public BaseDelegate { protected: bool expanded(const QModelIndex &idx) const { return m_expandedSessions.contains(idx.data(Qt::DisplayRole).toString()); } QString entryType() override { return Tr::tr("session", "Appears in \"Open session \""); } QRect toolTipArea(const QRect &itemRect, const QModelIndex &idx) const override { // in expanded state bottom contains 'Clone', 'Rename', etc links, where the tool tip // would be confusing return expanded(idx) ? itemRect.adjusted(0, 0, 0, -actionButtonHeight()) : itemRect; } int shortcutRole() const override { return SessionModel::ShortcutRole; } static int actionButtonHeight() { return s(VPaddingXxs) + actionTF.lineHeight() + s(VPaddingXxs); } static const QPixmap &icon() { static const QPixmap icon = pixmap("session", Theme::Token_Text_Muted); return icon; } public: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const final { // visible on withIcon() Gap + arrow visible on hover Extra margin right of project item // | | | // +----------+----------+ +--------+-------+ +----------+----------+ // | | | | | | // // +------------+--------+--------+------------+--------+-------------+--------+-------+------------+---------------------+ --+ // | | | |(VPaddingXs)| |(VPaddingXs) | | | | | | // | | | +------------+ +-------------+ | | | | | // |(HPaddingXs)||(HGapXs)| |(HGapXs)||(HGapXs)|| | | +-- Header // | |(w:6) | +------------+ +-------------+ | | | | | // | | | |(VPaddingXs)| |(VPaddingXs) | | | | | | // |------------+--------+--------+------------+--------+-------------+--------+-------+ | | --+ // | +-- | (VPaddingXxs) | | | | // | | +------------------------------+(HPaddingXs)| | | // | | | | | | | // | | +------------------------------+ | | | // | Per project in session --+ | (ExPaddingGapS) | |(sessionScrollBarGap)| | // | | +------------------------------+ | | | // | | | | | | | // | | +------------------------------+ | | +-- Expansion // | +-- | (VPaddingXxs) | | | | // +----------------------------------------------------+------------------------------+------------+ | | // | (VPaddingXs) | | | // +---------------------------------------+--------------+-----------------------------------------+ | | // +-- | || | | | // | +---------------------------------------+--------------+-----------------------------------------+ | | // | | (VPaddingXs) | | | // | +------------------------------------------------------------------------------------------------+---------------------+ --+ // | | (VGapL) | +-- Gap between session items // | +----------------------------------------------------------------------------------------------------------------------+ --+ // | // \ session action "buttons" and dividers // +-----------------------------------------------+--------+---------+--------+ // | (VGapXs) | | | | // +----------------+-------------+----------------+ | | | // |(EXSPaddingGapM)||(EXSPaddingGapM)|(HGapXs)||(HGapXs)| // +----------------+-------------+----------------+ |(w:1) | | // | (VGapXs) | | | | // +-----------------------------------------------+--------+---------+--------+ // // | | // +-------------+-------------+ // | // omitted after last button const QPoint mousePos = option.widget->mapFromGlobal(QCursor::pos()); const bool hovered = option.rect.contains(mousePos); const bool expanded = this->expanded(idx); const QRect bgR = option.rect.adjusted(0, 0, -sessionScrollBarGap, -itemSpacing()); const QRect hdR(bgR.topLeft(), QSize(bgR.width(), expanded ? headerHeight() : bgR.height())); const QSize iconS = icon().deviceIndependentSize().toSize(); static const QPixmap arrow = Icon({{FilePath::fromString(":/core/images/expandarrow"), Theme::Token_Text_Muted}}, Icon::Tint).pixmap(); const QSize arrowS = arrow.deviceIndependentSize().toSize(); const bool arrowVisible = hovered || expanded; const QString sessionName = idx.data(Qt::DisplayRole).toString(); const int x = bgR.x(); const int y = bgR.y(); const int numberX = x + s(HPaddingXs); const int iconX = numberX + shortcutNumberWidth + s(HGapXs); const int arrowX = bgR.right() - s(HPaddingXs) - arrowS.width(); const QRect arrowHoverR(arrowX - s(HGapXs) + 1, y, s(HGapXs) + arrowS.width() + s(HPaddingXs), hdR.height()); const int textX = withIcon() ? iconX + iconS.width() + s(HGapXs) : iconX; const int iconY = y + (hdR.height() - iconS.height()) / 2; const int arrowY = y + (hdR.height() - arrowS.height()) / 2; { drawBackgroundRect(painter, bgR, hovered); } if (idx.row() < 9) { painter->setPen(shortcutNumberTF.color()); painter->setFont(shortcutNumberTF.font()); const QRect numberR(numberX, y, shortcutNumberWidth, hdR.height()); const QString numberString = QString::number(idx.row() + 1); painter->drawText(numberR, shortcutNumberTF.drawTextFlags, numberString); } if (withIcon()) { painter->drawPixmap(iconX, iconY, icon()); } { const bool isLastSession = idx.data(SessionModel::LastSessionRole).toBool(); const bool isActiveSession = idx.data(SessionModel::ActiveSessionRole).toBool(); const bool isDefaultVirgin = SessionManager::isDefaultVirgin(); const int sessionNameWidth = hdR.right() - (arrowVisible ? arrowHoverR.width(): s(HPaddingXs)) - textX; const int sessionNameHeight = sessionNameTF.lineHeight(); const int sessionNameY = y + (hdR.height() - sessionNameHeight) / 2; const QRect sessionNameR(textX, sessionNameY, sessionNameWidth, sessionNameHeight); QString fullSessionName = sessionName; if (isLastSession && isDefaultVirgin) fullSessionName = Tr::tr("%1 (last session)").arg(fullSessionName); if (isActiveSession && !isDefaultVirgin) fullSessionName = Tr::tr("%1 (current session)").arg(fullSessionName); const QRect switchR(x, y, hdR.width() - arrowHoverR.width(), arrowHoverR.height()); const bool switchActive = switchR.contains(mousePos); painter->setPen(sessionNameTF.color()); painter->setFont(sessionNameTF.font(switchActive)); const QString fullSessionNameElided = painter->fontMetrics().elidedText( fullSessionName, Qt::ElideRight, sessionNameWidth); painter->drawText(sessionNameR, sessionNameTF.drawTextFlags, fullSessionNameElided); if (switchActive) m_activeSwitchToRect = switchR; } if (arrowVisible) { if (arrowHoverR.adjusted(0, 0, 0, expanded ? 0 : s(VGapL)).contains(mousePos)) { m_activeExpandRect = arrowHoverR; } else { painter->save(); painter->setClipRect(arrowHoverR); drawBackgroundRect(painter, bgR, false); painter->restore(); } static const QPixmap arrowDown = QPixmap::fromImage(arrow.toImage().mirrored(false, true)); painter->drawPixmap(arrowX, arrowY, expanded ? arrowDown : arrow); } int yy = hdR.bottom(); if (expanded) { const QFont projectNameFont = sessionProjectNameTF.font(); const QFontMetrics projectNameFm(projectNameFont); const int projectNameLineHeight = sessionProjectNameTF.lineHeight(); const QFont projectPathFont = projectPathTF.font(); const QFontMetrics projectPathFm(projectPathFont); const int projectPathLineHeight = projectPathTF.lineHeight(); const int textWidth = bgR.right() - s(HPaddingXs) - textX; const FilePaths projects = ProjectManager::projectsForSessionName(sessionName); for (const FilePath &projectPath : projects) { yy += s(VPaddingXxs); { painter->setFont(projectNameFont); painter->setPen(sessionProjectNameTF.color()); const QRect projectNameR(textX, yy, textWidth, projectNameLineHeight); const QString projectNameElided = projectNameFm.elidedText(projectPath.completeBaseName(), Qt::ElideMiddle, textWidth); painter->drawText(projectNameR, sessionProjectNameTF.drawTextFlags, projectNameElided); yy += projectNameLineHeight; yy += s(ExPaddingGapS); } { const QString displayPath = projectPath.osType() == OsTypeWindows ? projectPath.displayName() : projectPath.withTildeHomePath(); painter->setFont(projectPathFont); painter->setPen(projectPathTF.color()); const QRect projectPathR(textX, yy, textWidth, projectPathLineHeight); const QString projectPathElided = projectPathFm.elidedText(displayPath, Qt::ElideMiddle, textWidth); painter->drawText(projectPathR, projectPathTF.drawTextFlags, projectPathElided); yy += projectPathLineHeight; } yy += s(VPaddingXxs); } yy += s(VGapXs); const QStringList actions = { Tr::tr("Clone"), Tr::tr("Rename"), Tr::tr("Delete"), }; const QFont actionFont = actionTF.font(); const QFontMetrics actionFm(actionTF.font()); const int gapWidth = s(HGapXs) + actionSepWidth + s(HGapXs); int actionsTotalWidth = gapWidth * int(actions.count() - 1); // dividers const auto textWidths = Utils::transform(actions, [&] (const QString &action) { const int width = actionFm.horizontalAdvance(action); actionsTotalWidth += s(ExPaddingGapM) + width + s(ExPaddingGapM); return width; }); const int buttonHeight = this->actionButtonHeight(); int xx = (bgR.width() - actionsTotalWidth) / 2; for (int i = 0; i < actions.count(); ++i) { const QString &action = actions.at(i); const int ww = textWidths.at(i); const QRect actionR(xx, yy, s(ExPaddingGapM) + ww + s(ExPaddingGapM), buttonHeight); const bool isDisabled = i > 0 && SessionManager::isDefaultSession(sessionName); const bool isActive = actionR.adjusted(-s(VPaddingXs), 0, s(VPaddingXs) + 1, 0) .contains(mousePos) && !isDisabled; if (isActive) { WelcomePageHelpers::drawCardBackground(painter, actionR, Qt::transparent, themeColor(Theme::Token_Text_Muted)); m_activeActionRects[i] = actionR; } painter->setFont(actionFont); painter->setPen((isDisabled ? actionDisabledTF : actionTF).color()); const QRect actionTextR = actionR.adjusted(0, 0, 0, -1); painter->drawText(actionTextR, actionTF.drawTextFlags, action); xx += actionR.width(); if (i < actions.count() - 1) { const QRect dividerR(xx + s(HGapXs), yy, actionSepWidth, buttonHeight); painter->fillRect(dividerR, themeColor(Theme::Token_Text_Muted)); } xx += gapWidth; } yy += buttonHeight; yy += s(VGapXs); } QTC_CHECK(option.rect.bottom() == yy + itemSpacing()); } static int headerHeight() { const int paddingsHeight = s(VPaddingXs + VPaddingXs); const int heightForSessionName = sessionNameTF.lineHeight() + paddingsHeight; const int heightForIcon = withIcon() ? int(icon().deviceIndependentSize().height()) + paddingsHeight : 0; return qMax(heightForSessionName, heightForIcon); } QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &idx) const final { int h = headerHeight(); if (expanded(idx)) { const QString sessionName = idx.data(Qt::DisplayRole).toString(); const FilePaths projects = ProjectManager::projectsForSessionName(sessionName); const int projectEntryHeight = s(VPaddingXxs) + projectNameTF.lineHeight() + s(ExPaddingGapS) + projectPathTF.lineHeight() + s(VPaddingXxs); h += projects.size() * projectEntryHeight + s(VGapXs) + actionButtonHeight() + s(VGapXs); } return QSize(-1, h + itemSpacing()); } bool editorEvent(QEvent *ev, QAbstractItemModel *model, const QStyleOptionViewItem &, const QModelIndex &idx) final { if (ev->type() == QEvent::MouseButtonRelease) { const QMouseEvent *mouseEvent = static_cast(ev); const Qt::MouseButtons button = mouseEvent->button(); const QPoint pos = static_cast(ev)->pos(); const QString sessionName = idx.data(Qt::DisplayRole).toString(); if (m_activeExpandRect.contains(pos) || button == Qt::RightButton) { // The expand/collapse "button". if (m_expandedSessions.contains(sessionName)) m_expandedSessions.removeOne(sessionName); else m_expandedSessions.append(sessionName); emit model->layoutChanged({QPersistentModelIndex(idx)}); return true; } if (button == Qt::LeftButton) { // One of the action links? const auto sessionModel = qobject_cast(model); QTC_ASSERT(sessionModel, return false); if (m_activeSwitchToRect.contains(pos)) sessionModel->switchToSession(sessionName); else if (m_activeActionRects[0].contains(pos)) sessionModel->cloneSession(ICore::dialogParent(), sessionName); else if (m_activeActionRects[1].contains(pos)) sessionModel->renameSession(ICore::dialogParent(), sessionName); else if (m_activeActionRects[2].contains(pos)) sessionModel->deleteSessions(QStringList(sessionName)); return true; } } if (ev->type() == QEvent::MouseMove) { emit model->layoutChanged({QPersistentModelIndex(idx)}); // Somewhat brutish. return false; } return false; } private: QStringList m_expandedSessions; mutable QRect m_activeExpandRect; mutable QRect m_activeSwitchToRect; mutable QRect m_activeActionRects[3]; }; class ProjectDelegate : public BaseDelegate { QString entryType() override { return Tr::tr("project", "Appears in \"Open project \""); } int shortcutRole() const override { return ProjectModel::ShortcutRole; } public: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const final { // visible on with Icon() Extra margin right of project item // | | // +--------+-------+ +------+-----+ // | | | | // // +------------+--------+--------+------+---------+-------------+------------+------------+ // | | | | | | (VPaddingXs)| | | // | | | | | +-------------+ | | // | | | | | || | | // | | | | | +-------------+ | | // |(HPaddingXs)||(HGapXs)||(HGapXxs)| (VGapXs) |(HPaddingXs)|(HPaddingXs)| // | |(w:6) | | | +-------------+ | | // | | | | | || | | // | | | | | +-------------+ | | // | | | | | | (VPaddingXs)| | | // +------------+--------+--------+------+---------+-------------+------------+------------+ --+ // | (VGapL) | +-- Gap between project items // +---------------------------------------------------------------------------------------+ --+ const bool hovered = option.widget->isActiveWindow() && option.state & QStyle::State_MouseOver; const QRect bgR = option.rect.adjusted(0, 0, -s(HPaddingXs), -itemSpacing()); static const QPixmap icon = pixmap("project", Theme::Token_Text_Muted); const QSize iconS = icon.deviceIndependentSize().toSize(); const int x = bgR.x(); const int numberX = x + s(HPaddingXs); const int iconX = numberX + shortcutNumberWidth + s(HGapXs); const int iconWidth = iconS.width(); const int textX = withIcon() ? iconX + iconWidth + s(HGapXs) : iconX; const int textWidth = bgR.width() - s(HPaddingXs) - textX; const int y = bgR.y(); const int iconHeight = iconS.height(); const int iconY = y + (bgR.height() - iconHeight) / 2; const int projectNameY = y + s(VPaddingXs); const QRect projectNameR(textX, projectNameY, textWidth, projectNameTF.lineHeight()); const int projectPathY = projectNameY + projectNameR.height() + s(VGapXs); const QRect projectPathR(textX, projectPathY, textWidth, projectPathTF.lineHeight()); QTC_CHECK(option.rect.bottom() == projectPathR.bottom() + s(VPaddingXs) + itemSpacing()); { drawBackgroundRect(painter, bgR, hovered); } if (idx.row() < 9) { painter->setPen(shortcutNumberTF.color()); painter->setFont(shortcutNumberTF.font()); const QRect numberR(numberX, y, shortcutNumberWidth, bgR.height()); const QString numberString = QString::number(idx.row() + 1); painter->drawText(numberR, shortcutNumberTF.drawTextFlags, numberString); } if (withIcon()) { painter->drawPixmap(iconX, iconY, icon); } { painter->setPen(projectNameTF.color()); painter->setFont(projectNameTF.font(hovered)); const QString projectName = idx.data(Qt::DisplayRole).toString(); const QString projectNameElided = painter->fontMetrics().elidedText(projectName, Qt::ElideRight, textWidth); painter->drawText(projectNameR, projectNameTF.drawTextFlags, projectNameElided); } { painter->setPen(projectPathTF.color()); painter->setFont(projectPathTF.font()); const FilePath projectPath = FilePath::fromVariant(idx.data(ProjectModel::FilePathRole)); const QString displayPath = projectPath.osType() == OsTypeWindows ? projectPath.displayName() : projectPath.withTildeHomePath(); const QString displayPathElided = painter->fontMetrics().elidedText(displayPath, Qt::ElideMiddle, textWidth); painter->drawText(projectPathR, projectPathTF.drawTextFlags, displayPathElided); } } QSize sizeHint([[maybe_unused]] const QStyleOptionViewItem &option, [[maybe_unused]] const QModelIndex &idx) const override { return QSize(-1, itemHeight() + itemSpacing()); } bool editorEvent(QEvent *ev, QAbstractItemModel *model, const QStyleOptionViewItem &, const QModelIndex &idx) final { if (ev->type() == QEvent::MouseButtonRelease) { const QMouseEvent *mouseEvent = static_cast(ev); const Qt::MouseButtons button = mouseEvent->button(); if (button == Qt::LeftButton) { const QVariant projectFile = idx.data(ProjectModel::FilePathRole); ProjectExplorerPlugin::openProjectWelcomePage(FilePath::fromVariant(projectFile)); return true; } if (button == Qt::RightButton) { QMenu contextMenu; QAction *action = new QAction(Tr::tr("Remove Project from Recent Projects")); const auto projectModel = qobject_cast(model); contextMenu.addAction(action); connect(action, &QAction::triggered, this, [idx, projectModel] { const QVariant projectFile = idx.data(ProjectModel::FilePathRole); ProjectExplorerPlugin::removeFromRecentProjects(FilePath::fromVariant(projectFile)); projectModel->resetProjects(); }); contextMenu.addSeparator(); action = new QAction(Tr::tr("Clear Recent Project List")); connect(action, &QAction::triggered, this, [projectModel] { ProjectExplorerPlugin::clearRecentProjects(); projectModel->resetProjects(); }); contextMenu.addAction(action); contextMenu.exec(mouseEvent->globalPosition().toPoint()); return true; } } return false; } private: static int itemHeight() { const int height = s(VPaddingXs) + projectNameTF.lineHeight() + s(VGapXs) + projectPathTF.lineHeight() + s(VPaddingXs); return height; } }; class TreeView : public QTreeView { public: TreeView(QWidget *parent, const QString &name) : QTreeView(parent) { setObjectName(name); header()->hide(); setMouseTracking(true); // To enable hover. setIndentation(0); setSelectionMode(QAbstractItemView::NoSelection); setFrameShape(QFrame::NoFrame); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); setFocusPolicy(Qt::NoFocus); setBackgroundColor(viewport(), Theme::Token_Background_Default); } }; class SessionsPage : public QWidget { public: explicit SessionsPage(ProjectWelcomePage *projectWelcomePage) { // FIXME: Remove once facilitateQml() is gone. if (!projectWelcomePage->m_sessionModel) projectWelcomePage->m_sessionModel = new SessionModel(this); if (!projectWelcomePage->m_projectModel) projectWelcomePage->m_projectModel = new ProjectModel(this); using namespace Layouting; auto sessions = new QWidget; { auto sessionsLabel = new Label(Tr::tr("Sessions"), Label::Primary); auto manageSessionsButton = new Button(Tr::tr("Manage..."), Button::MediumSecondary); auto sessionsList = new TreeView(this, "Sessions"); sessionsList->setModel(projectWelcomePage->m_sessionModel); sessionsList->header()->setSectionHidden(1, true); // The "last modified" column. sessionsList->setItemDelegate(&m_sessionDelegate); sessionsList->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); QSizePolicy sessionsSp(QSizePolicy::Expanding, QSizePolicy::Expanding); sessionsSp.setHorizontalStretch(3); sessions->setSizePolicy(sessionsSp); Column { Row { sessionsLabel, st, manageSessionsButton, customMargin({HPaddingS, 0, sessionScrollBarGap, 0}), }, sessionsList, spacing(ExPaddingGapL), customMargin({ExVPaddingGapXl, ExVPaddingGapXl, 0, 0}), }.attachTo(sessions); connect(manageSessionsButton, &Button::clicked, this, &SessionManager::showSessionManager); } auto projects = new QWidget; { auto projectsLabel = new Label(Tr::tr("Projects"), Label::Primary); auto projectsList = new TreeView(this, "Recent Projects"); projectsList->setUniformRowHeights(true); projectsList->setModel(projectWelcomePage->m_projectModel); projectsList->setItemDelegate(&m_projectDelegate); projectsList->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); QSizePolicy projectsSP(QSizePolicy::Expanding, QSizePolicy::Expanding); projectsSP.setHorizontalStretch(5); projects->setSizePolicy(projectsSP); Column { Row { projectsLabel, customMargin({HPaddingS, 0, 0, 0}), }, projectsList, spacing(ExPaddingGapL), customMargin({ExVPaddingGapXl - sessionScrollBarGap, ExVPaddingGapXl, 0, 0}), }.attachTo(projects); } Row { sessions, projects, spacing(0), noMargin(), }.attachTo(this); } SessionDelegate m_sessionDelegate; ProjectDelegate m_projectDelegate; }; QWidget *ProjectWelcomePage::createWidget() const { auto that = const_cast(this); QWidget *widget = new SessionsPage(that); that->createActions(); return widget; } } // namespace Internal } // namespace ProjectExplorer