Commit 92638f18 authored by Eugene Popov's avatar Eugene Popov 🇺🇦 Committed by Christoph Cullmann
Browse files

New welcome screen UI

The current welcome screen UI doesn't suit me very well, so I decided to design
a new one. Pros of the new UI:
- it has a cohesive design and is visually separated from the rest of the view
- using QScrollArea as a basis avoids squeezing its contents and doesn't limit
  the minimum window size
- it's more compact and suitable for small screens
- it also displays saved sessions
parent ff74abce
Pipeline #246544 passed with stage
in 10 minutes and 47 seconds
......@@ -66,7 +66,7 @@ if (TARGET KUserFeedbackWidgets)
target_compile_definitions(kateprivate PUBLIC -DWITH_KUSERFEEDBACK)
endif()
ki18n_wrap_ui(kateprivate ui/sessionconfigwidget.ui welcomeview.ui session/katesessionmanagedialog.ui)
ki18n_wrap_ui(kateprivate ui/sessionconfigwidget.ui welcomeview/welcomeview.ui session/katesessionmanagedialog.ui)
target_sources(
kateprivate
......@@ -118,9 +118,10 @@ target_sources(
diff/gitdiff.cpp
filehistorywidget.cpp
recentitemsmodel.cpp
welcomeview.cpp
welcomeview/welcomeview.cpp
welcomeview/recentfilesmodel.cpp
welcomeview/savedsessionsmodel.cpp
)
if(BUILD_TESTING)
......
......@@ -13,7 +13,7 @@
#include "katemainwindow.h"
#include "kateupdatedisabler.h"
#include "kateviewspace.h"
#include "welcomeview.h"
#include "welcomeview/welcomeview.h"
#include <KTextEditor/Attribute>
#include <KTextEditor/Document>
......@@ -1583,36 +1583,7 @@ void KateViewManager::showWelcomeViewIfNeeded()
void KateViewManager::showWelcomeView()
{
auto welcomeView = new WelcomeView(this);
auto recentFilesAction = mainWindow()->recentFilesAction();
connect(recentFilesAction, &KRecentFilesAction::recentListCleared, this, &KateViewManager::refreshRecentsOnWelcomeView);
connect(welcomeView, &WelcomeView::forgetAllRecents, recentFilesAction, &KRecentFilesAction::clear);
connect(this, &KateViewManager::loadRecentFiles, welcomeView, &WelcomeView::loadRecents);
mainWindow()->addWidget(welcomeView);
refreshRecentsOnWelcomeView();
}
void KateViewManager::refreshRecentsOnWelcomeView()
{
saveRecents();
// ensure welcome view updates
Q_EMIT loadRecentFiles();
}
void KateViewManager::forgetRecentItem(QUrl const &url)
{
auto recentFilesAction = mainWindow()->recentFilesAction();
if (recentFilesAction != nullptr) {
recentFilesAction->removeUrl(url);
saveRecents();
refreshRecentsOnWelcomeView();
}
}
void KateViewManager::saveRecents()
{
mainWindow()->recentFilesAction()->saveEntries(KSharedConfig::openConfig()->group("Recent Files"));
}
void KateViewManager::triggerActiveViewFocus()
......
......@@ -121,8 +121,6 @@ Q_SIGNALS:
void showUrlNavBarChanged(bool);
void loadRecentFiles();
public:
/**
* create and activate a new view for doc, if doc == 0, then
......@@ -150,8 +148,6 @@ private:
void removeViewSpace(KateViewSpace *viewspace);
void saveRecents();
public:
KTextEditor::View *activeView();
KateViewSpace *activeViewSpace();
......@@ -321,8 +317,6 @@ public Q_SLOTS:
void hideWelcomeView(KateViewSpace *vs);
void showWelcomeViewIfNeeded();
void showWelcomeView();
void refreshRecentsOnWelcomeView();
void forgetRecentItem(QUrl const &url);
private:
KateMainWindow *m_mainWindow;
......
/*
SPDX-FileCopyrightText: 2021 Jiří Wolker <woljiri@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "recentitemsmodel.h"
#include "kateconfigdialog.h"
#include <QFile>
#include <QFileInfo>
#include <QIcon>
#include <KConfigGroup>
RecentItemsModel::RecentItemsModel()
{
m_maxItems = KateConfigDialog::recentFilesMaxCount();
}
RecentItemsModel::~RecentItemsModel()
{
}
void RecentItemsModel::loadEntries(const KConfigGroup &cg)
{
clearEntries();
// Based on implementation of KRecentFilesAction::loadEntries.
QString key;
QString value;
QString nameKey;
QString nameValue;
QUrl url;
// read file list
for (int i = 1; i <= maxItems(); i++) {
key = QStringLiteral("File%1").arg(i);
value = cg.readPathEntry(key, QString());
if (value.isEmpty()) {
continue;
}
url = QUrl::fromUserInput(value);
// Don't restore if file doesn't exist anymore
if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) {
continue;
}
nameKey = QStringLiteral("Name%1").arg(i);
nameValue = cg.readPathEntry(nameKey, url.fileName());
m_recentItems.append(RecentItem{nameValue, url});
}
Q_EMIT layoutChanged();
}
void RecentItemsModel::clearEntries()
{
m_recentItems.clear();
Q_EMIT layoutChanged();
}
int RecentItemsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_recentItems.size();
}
QVariant RecentItemsModel::data(const QModelIndex &index, int role) const
{
const RecentItemsModel::RecentItem *item = getItem(index);
if (item != nullptr) {
switch (role) {
case Qt::ItemDataRole::DisplayRole:
if (item->name.isEmpty()) {
if (item->url.isLocalFile()) {
return item->url.toLocalFile();
} else {
return item->url.toString();
}
} else {
return item->name;
}
case Qt::ItemDataRole::ToolTipRole:
if (item->url.isLocalFile()) {
return item->url.toLocalFile();
} else {
return item->url.toString();
}
case Qt::ItemDataRole::DecorationRole:
if (item->url.isLocalFile()) {
return m_iconProvider.icon(QFileInfo(item->url.toLocalFile()));
} else {
// Fallback icon for remote files.
return QIcon::fromTheme(QStringLiteral("network-server"));
}
default:
return QVariant();
}
} else {
return QVariant();
}
}
RecentItemsModel::RecentItem const *RecentItemsModel::getItem(int index) const
{
if (m_recentItems.size() > index && index >= 0) {
// We reverse the order of items.
return &m_recentItems[m_recentItems.size() - index - 1];
} else {
return nullptr;
}
}
RecentItemsModel::RecentItem const *RecentItemsModel::getItem(const QModelIndex &index) const
{
return getItem(index.row());
}
\ No newline at end of file
/*
SPDX-FileCopyrightText: 2021 Jiří Wolker <woljiri@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef RECENTITEMSMODEL_H
#define RECENTITEMSMODEL_H
#include <QAbstractListModel>
#include <QFileIconProvider>
#include <QList>
#include <QModelIndex>
#include <QString>
#include <QUrl>
class KConfigGroup;
/**
* @todo write docs
*/
class RecentItemsModel : public QAbstractListModel
{
Q_OBJECT
public:
struct RecentItem {
QString name;
QUrl url;
};
/**
* Default constructor
*/
RecentItemsModel();
/**
* Destructor
*/
~RecentItemsModel() override;
void loadEntries(const KConfigGroup &cg);
void clearEntries();
int maxItems() const
{
return m_maxItems;
}
RecentItemsModel::RecentItem const *getItem(const QModelIndex &) const;
RecentItemsModel::RecentItem const *getItem(int index) const;
// Model implementation:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::ItemDataRole::DisplayRole) const override;
private:
QList<RecentItemsModel::RecentItem> m_recentItems;
int m_maxItems;
QFileIconProvider m_iconProvider;
};
#endif // RECENTITEMSMODEL_H
\ No newline at end of file
/*
SPDX-FileCopyrightText: 2021 Jiří Wolker <woljiri@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "welcomeview.h"
#include <KIconLoader>
#include <KSharedConfig>
#include <KIO/OpenFileManagerWindowJob>
#include <QAction>
#include <QClipboard>
#include <QDir>
#include <QGraphicsOpacityEffect>
#include <QGuiApplication>
#include <QMenu>
#include <QStyledItemDelegate>
#include <QVBoxLayout>
#include <klocalizedstring.h>
#include "kateapp.h"
#include "kateviewmanager.h"
#include "kateviewspace.h"
#include "recentitemsmodel.h"
class RecentsListItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit RecentsListItemDelegate(WelcomeView *welcomeView)
: m_welcomeView(welcomeView)
{
}
WelcomeView *welcomeView() const
{
return m_welcomeView;
}
bool editorEvent(QEvent *event, QAbstractItemModel *aModel, const QStyleOptionViewItem &styleOptionViewItem, const QModelIndex &index) override
{
const RecentItemsModel *model = static_cast<RecentItemsModel *>(aModel);
const RecentItemsModel::RecentItem *item = model->getItem(index);
bool willOpenMenu = false;
QPoint menuPosition;
if (item != nullptr) {
if (event->type() == QEvent::ContextMenu) {
willOpenMenu = true;
menuPosition = static_cast<QContextMenuEvent *>(event)->globalPos();
}
if (event->type() == QEvent::MouseButtonPress) {
if (static_cast<QMouseEvent *>(event)->button() == Qt::MouseButton::RightButton) {
willOpenMenu = true;
menuPosition = static_cast<QMouseEvent *>(event)->globalPos();
}
}
if (willOpenMenu) {
event->accept();
QMenu menu;
QAction *copyPathAction = new QAction(i18n("&Copy Location"));
copyPathAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy-path")));
connect(copyPathAction, &QAction::triggered, this, [item]() {
QString path;
if (item->url.isLocalFile()) {
path = item->url.toLocalFile();
} else {
path = item->url.toString();
}
QGuiApplication::clipboard()->setText(path);
});
menu.addAction(copyPathAction);
QAction *showDirectoryAction = new QAction(i18n("&Open Containing Folder"));
showDirectoryAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open-folder")));
connect(showDirectoryAction, &QAction::triggered, this, [item]() {
if (item->url.isLocalFile()) {
KIO::highlightInFileManager({ item->url });
}
});
menu.addAction(showDirectoryAction);
if (!item->url.isLocalFile()) {
showDirectoryAction->setEnabled(false);
}
QAction *forgetItemAction = new QAction(i18nc("recent items context menu", "&Forget This Item"));
forgetItemAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history")));
connect(forgetItemAction, &QAction::triggered, this, [this, item]() {
Q_EMIT welcomeView()->forgetRecentItem(item->url);
});
menu.addAction(forgetItemAction);
menu.exec(menuPosition);
return true;
}
}
return QStyledItemDelegate::editorEvent(event, aModel, styleOptionViewItem, index);
}
private:
WelcomeView *m_welcomeView;
};
WelcomeView::WelcomeView(QWidget *parent)
: QWidget(parent)
, m_recentsModel(new RecentItemsModel)
, m_recentsItemDelegate(new RecentsListItemDelegate(this))
{
Q_ASSERT(parent);
setupUi(this);
setWindowTitle(i18n("Welcome"));
// ensure proper title and icon for both applications
welcomeLabel->setText(KateApp::isKate() ? i18n("Welcome to Kate") : i18n("Welcome to KWrite"));
appIcon->setPixmap(qApp->windowIcon().pixmap(KIconLoader::SizeEnormous, KIconLoader::SizeEnormous));
auto viewManager = qobject_cast<KateViewManager *>(parent);
connect(this, &WelcomeView::recentItemClicked, viewManager, [viewManager](const QUrl &url) {
viewManager->openUrl(url);
});
connect(this, &WelcomeView::forgetRecentItem, viewManager, &KateViewManager::forgetRecentItem);
connect(openDocumentButton, &QPushButton::clicked, viewManager, &KateViewManager::slotDocumentOpen);
connect(newDocumentButton, &QPushButton::clicked, viewManager, &KateViewManager::slotDocumentNew);
auto pluginViewChanged = [this, viewManager](const QString &name = QString()) {
static const QString projectPluginName = QStringLiteral("kateprojectplugin");
if (name.isEmpty() || name == projectPluginName) {
QObject *projectPluginView = viewManager->mainWindow()->pluginView(projectPluginName);
if (projectPluginView) {
connect(openFolderButton, &QPushButton::clicked, projectPluginView, [projectPluginView]() {
QMetaObject::invokeMethod(projectPluginView, "openDirectoryOrProject");
});
openFolderButton->show();
} else {
openFolderButton->hide();
}
}
};
pluginViewChanged();
const KTextEditor::MainWindow *mainWindow = viewManager->mainWindow()->wrapper();
connect(mainWindow, &KTextEditor::MainWindow::pluginViewCreated, this, pluginViewChanged);
connect(mainWindow, &KTextEditor::MainWindow::pluginViewDeleted, this, pluginViewChanged);
recentsListView->setContextMenuPolicy(Qt::DefaultContextMenu);
recentsListView->setModel(m_recentsModel);
recentsListView->setItemDelegate(m_recentsItemDelegate);
connect(recentsListView, &QListView::activated, this, &WelcomeView::recentsItemActivated);
connect(m_recentsModel, &RecentItemsModel::layoutChanged, this, &WelcomeView::recentListChanged);
QVBoxLayout *noRecentsLayout = new QVBoxLayout(recentsListView);
recentsListView->setLayout(noRecentsLayout);
m_noRecentsLabel = new QLabel(recentsListView);
QFont placeholderLabelFont;
// To match the size of a level 2 Heading/KTitleWidget
placeholderLabelFont.setPointSize(qRound(placeholderLabelFont.pointSize() * 1.3));
noRecentsLayout->addWidget(m_noRecentsLabel);
m_noRecentsLabel->setFont(placeholderLabelFont);
m_noRecentsLabel->setTextInteractionFlags(Qt::NoTextInteraction);
m_noRecentsLabel->setWordWrap(true);
m_noRecentsLabel->setAlignment(Qt::AlignCenter);
m_noRecentsLabel->setText(i18nc("on welcome screen", "No recent documents"));
// Match opacity of QML placeholder label component
auto *effect = new QGraphicsOpacityEffect(m_noRecentsLabel);
effect->setOpacity(0.5);
m_noRecentsLabel->setGraphicsEffect(effect);
connect(forgetAllButton, &QToolButton::clicked, this, &WelcomeView::forgetAllRecents);
}
WelcomeView::~WelcomeView()
{
delete m_recentsModel;
delete m_recentsItemDelegate;
}
void WelcomeView::loadRecents()
{
m_recentsModel->loadEntries(KSharedConfig::openConfig()->group("Recent Files"));
}
int WelcomeView::recentsCount()
{
return m_recentsModel->rowCount();
}
void WelcomeView::recentsItemActivated(const QModelIndex &index)
{
const RecentItemsModel::RecentItem *item = m_recentsModel->getItem(index);
if (item != nullptr) {
Q_EMIT recentItemClicked(item->url);
}
}
void WelcomeView::recentListChanged()
{
if (recentsCount() == 0) {
m_noRecentsLabel->show();
forgetAllButton->setEnabled(false);
} else {
m_noRecentsLabel->hide();
forgetAllButton->setEnabled(true);
}
}
#include "welcomeview.moc"
/*
SPDX-FileCopyrightText: 2021 Jiří Wolker <woljiri@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef WELCOMEVIEW_H
#define WELCOMEVIEW_H
#include "ui_welcomeview.h"
#include <QUrl>
class QListWidgetItem;
class QLabel;
class KRecentFilesAction;
class KateViewSpace;
class RecentItemsModel;
class RecentsListItemDelegate;
class WelcomeView : public QWidget, Ui::WelcomeView
{
Q_OBJECT
public:
explicit WelcomeView(QWidget *parent = nullptr);
~WelcomeView() override;
public Q_SLOTS:
void loadRecents();
// ensure we can always close this view
bool shouldClose()
{
return true;
}
Q_SIGNALS:
void recentItemClicked(QUrl const &url);
void forgetAllRecents();
void forgetRecentItem(QUrl const &url);
private Q_SLOTS:
void recentsItemActivated(QModelIndex const &index);
void recentListChanged();
private:
int recentsCount();
RecentItemsModel *m_recentsModel;
RecentsListItemDelegate *m_recentsItemDelegate;
QLabel *m_noRecentsLabel;
};
#endif // WELCOMEVIEW_H
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WelcomeView</class>
<widget class="QWidget" name="WelcomeView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1423</width>
<height>850</height>
</rect>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout" rowstretch="3,5,0,3" columnstretch="1,4,1">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="1" column="0" rowspan="3">
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>48</width>
<height>20</height>
</size>
</property>
</spacer>
</item>