diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index 31e1e159..f042c3e1 100644 --- a/src/core/configmgr.cpp +++ b/src/core/configmgr.cpp @@ -315,7 +315,9 @@ QString ConfigMgr::getAppThemeFolder() const QString ConfigMgr::getUserThemeFolder() const { - return PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("themes")); + auto folderPath = PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("themes")); + QDir().mkpath(folderPath); + return folderPath; } QString ConfigMgr::getAppDocsFolder() const @@ -325,7 +327,9 @@ QString ConfigMgr::getAppDocsFolder() const QString ConfigMgr::getUserDocsFolder() const { - return PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("docs")); + auto folderPath = PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("docs")); + QDir().mkpath(folderPath); + return folderPath; } QString ConfigMgr::getAppSyntaxHighlightingFolder() const diff --git a/src/core/theme.cpp b/src/core/theme.cpp index e2b34fc4..0b5b4bc6 100644 --- a/src/core/theme.cpp +++ b/src/core/theme.cpp @@ -41,12 +41,35 @@ bool Theme::isValidThemeFolder(const QString &p_folder) return true; } +QString Theme::getDisplayName(const QString &p_folder, const QString &p_locale) +{ + auto obj = readPaletteFile(p_folder); + const auto metaObj = obj[QStringLiteral("metadata")].toObject(); + QString prefix("display_name"); + + if (!p_locale.isEmpty()) { + // Check full locale. + auto fullLocale = QString("%1_%2").arg(prefix, p_locale); + if (metaObj.contains(fullLocale)) { + return metaObj.value(fullLocale).toString(); + } + + auto shortLocale = QString("%1_%2").arg(prefix, p_locale.split('_')[0]); + if (metaObj.contains(shortLocale)) { + return metaObj.value(shortLocale).toString(); + } + } + + if (metaObj.contains(prefix)) { + return metaObj.value(prefix).toString(); + } + return PathUtils::dirName(p_folder); +} + Theme *Theme::fromFolder(const QString &p_folder) { Q_ASSERT(!p_folder.isEmpty()); - QDir dir(p_folder); - - auto obj = readJsonFile(QDir(p_folder).filePath(getFileName(File::Palette))); + auto obj = readPaletteFile(p_folder); auto metadata = readMetadata(obj); auto paletteObj = translatePalette(obj); return new Theme(p_folder, @@ -312,6 +335,12 @@ QJsonObject Theme::readJsonFile(const QString &p_filePath) return QJsonDocument::fromJson(bytes).object(); } +QJsonObject Theme::readPaletteFile(const QString &p_folder) +{ + auto obj = readJsonFile(QDir(p_folder).filePath(getFileName(File::Palette))); + return obj; +} + QJsonValue Theme::findValueByKeyPath(const Palette &p_palette, const QString &p_keyPath) { auto keys = p_keyPath.split('#'); @@ -366,6 +395,8 @@ QString Theme::getFileName(File p_fileType) return QStringLiteral("editor-highlight.theme"); case File::MarkdownEditorHighlightStyle: return QStringLiteral("markdown-editor-highlight.theme"); + case File::Cover: + return QStringLiteral("cover.png"); default: Q_ASSERT(false); return ""; @@ -395,3 +426,18 @@ QString Theme::getMarkdownEditorHighlightTheme() const return getEditorHighlightTheme(); } + +QString Theme::name() const +{ + return PathUtils::dirName(m_themeFolderPath); +} + +QPixmap Theme::getCover(const QString &p_folder) +{ + QDir dir(p_folder); + if (dir.exists(getFileName(File::Cover))) { + const auto coverFile = dir.filePath(getFileName(File::Cover)); + return QPixmap(coverFile); + } + return QPixmap(); +} diff --git a/src/core/theme.h b/src/core/theme.h index 47d798f8..ea5534bf 100644 --- a/src/core/theme.h +++ b/src/core/theme.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace tests { @@ -26,6 +27,7 @@ namespace vnotex MarkdownEditorStyle, EditorHighlightStyle, MarkdownEditorHighlightStyle, + Cover, Max }; @@ -42,10 +44,16 @@ namespace vnotex // Return the file path of the theme or just the theme name. QString getMarkdownEditorHighlightTheme() const; + QString name() const; + static bool isValidThemeFolder(const QString &p_folder); static Theme *fromFolder(const QString &p_folder); + static QString getDisplayName(const QString &p_folder, const QString &p_locale); + + static QPixmap getCover(const QString &p_folder); + private: struct Metadata { @@ -100,6 +108,8 @@ namespace vnotex static QJsonObject readJsonFile(const QString &p_filePath); + static QJsonObject readPaletteFile(const QString &p_folder); + // Whether @p_str is a reference definition like "@xxxx". static bool isRef(const QString &p_str); diff --git a/src/core/thememgr.cpp b/src/core/thememgr.cpp index 5fc8d894..8c083259 100644 --- a/src/core/thememgr.cpp +++ b/src/core/thememgr.cpp @@ -8,6 +8,8 @@ #include "exception.h" #include #include +#include "configmgr.h" +#include "coreconfig.h" using namespace vnotex; @@ -37,11 +39,13 @@ QString ThemeMgr::getIconFile(const QString &p_icon) const void ThemeMgr::loadAvailableThemes() { + m_themes.clear(); + for (const auto &pa : s_searchPaths) { loadThemes(pa); } - if (m_availableThemes.isEmpty()) { + if (m_themes.isEmpty()) { Exception::throwOne(Exception::Type::EssentialFileMissing, QString("no available themes found in paths: %1").arg(s_searchPaths.join(QLatin1Char(';')))); } @@ -53,17 +57,21 @@ void ThemeMgr::loadThemes(const QString &p_path) QDir dir(p_path); dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); auto themeFolders = dir.entryList(); + const auto localeStr = ConfigMgr::getInst().getCoreConfig().getLocaleToUse(); for (auto &folder : themeFolders) { - checkAndAddThemeFolder(PathUtils::concatenateFilePath(p_path, folder)); + checkAndAddThemeFolder(PathUtils::concatenateFilePath(p_path, folder), localeStr); } } -void ThemeMgr::checkAndAddThemeFolder(const QString &p_folder) +void ThemeMgr::checkAndAddThemeFolder(const QString &p_folder, const QString &p_locale) { if (Theme::isValidThemeFolder(p_folder)) { - QString themeName = PathUtils::dirName(p_folder); - m_availableThemes.insert(themeName, p_folder); - qDebug() << "add theme" << themeName << p_folder; + ThemeInfo info; + info.m_name = PathUtils::dirName(p_folder); + info.m_displayName = Theme::getDisplayName(p_folder, p_locale); + info.m_folderPath = p_folder; + m_themes.push_back(info); + qDebug() << "add theme" << info.m_name << info.m_displayName << info.m_folderPath; } } @@ -76,14 +84,14 @@ void ThemeMgr::loadCurrentTheme(const QString &p_themeName) { auto themeFolder = findThemeFolder(p_themeName); if (themeFolder.isNull()) { - qCritical() << "fail to locate theme" << p_themeName; + qWarning() << "failed to locate theme" << p_themeName; } else { m_currentTheme.reset(loadTheme(themeFolder)); } if (!m_currentTheme) { const QString defaultTheme("native"); - qInfo() << "fall back to default theme" << defaultTheme; + qWarning() << "fall back to default theme" << defaultTheme; m_currentTheme.reset(loadTheme(findThemeFolder(defaultTheme))); } } @@ -91,28 +99,38 @@ void ThemeMgr::loadCurrentTheme(const QString &p_themeName) Theme *ThemeMgr::loadTheme(const QString &p_themeFolder) { if (p_themeFolder.isEmpty()) { - qCritical("fail to load theme from empty folder"); + qWarning("failed to load theme from empty folder"); return nullptr; } try { return Theme::fromFolder(p_themeFolder); } catch (Exception &p_e) { - qCritical("fail to load theme from folder %s (%s)", - p_themeFolder.toStdString().c_str(), - p_e.what()); + qWarning("failed to load theme from folder %s (%s)", + p_themeFolder.toStdString().c_str(), + p_e.what()); return nullptr; } } QString ThemeMgr::findThemeFolder(const QString &p_name) const { - auto it = m_availableThemes.find(p_name); - if (it != m_availableThemes.end()) { - return it.value(); + auto theme = findTheme(p_name); + if (theme) { + return theme->m_folderPath; + } + return QString(); +} + +const ThemeMgr::ThemeInfo *ThemeMgr::findTheme(const QString &p_name) const +{ + for (const auto &info : m_themes) { + if (info.m_name == p_name) { + return &info; + } } - return QString(); + return nullptr; } QString ThemeMgr::fetchQtStyleSheet() const @@ -165,3 +183,22 @@ void ThemeMgr::setBaseBackground(const QColor &p_bg) { m_baseBackground = p_bg; } + +const QVector &ThemeMgr::getAllThemes() const +{ + return m_themes; +} + +QPixmap ThemeMgr::getThemePreview(const QString &p_name) const +{ + auto theme = findTheme(p_name); + if (theme) { + return Theme::getCover(theme->m_folderPath); + } + return QPixmap(); +} + +void ThemeMgr::refresh() +{ + loadAvailableThemes(); +} diff --git a/src/core/thememgr.h b/src/core/thememgr.h index 18c3fe9f..21ba1f3d 100644 --- a/src/core/thememgr.h +++ b/src/core/thememgr.h @@ -4,10 +4,11 @@ #include #include -#include #include #include +#include #include +#include #include "theme.h" @@ -17,6 +18,17 @@ namespace vnotex { Q_OBJECT public: + struct ThemeInfo + { + // Id. + QString m_name; + + // Locale supported. + QString m_displayName; + + QString m_folderPath; + }; + ThemeMgr(const QString &p_currentThemeName, QObject *p_parent = nullptr); // @p_icon: file path or file name of the icon. @@ -40,6 +52,18 @@ namespace vnotex const QColor &getBaseBackground() const; void setBaseBackground(const QColor &p_bg); + const QVector &getAllThemes() const; + + const Theme &getCurrentTheme() const; + + QPixmap getThemePreview(const QString &p_name) const; + + const ThemeInfo *findTheme(const QString &p_name) const; + + // Refresh the themes list. + // Won't affect current theme since we do not support changing theme real time for now. + void refresh(); + static void addSearchPath(const QString &p_path); static void addSyntaxHighlightingSearchPaths(const QStringList &p_paths); @@ -49,9 +73,7 @@ namespace vnotex void loadThemes(const QString &p_path); - void checkAndAddThemeFolder(const QString &p_folder); - - const Theme &getCurrentTheme() const; + void checkAndAddThemeFolder(const QString &p_folder, const QString &p_locale); void loadCurrentTheme(const QString &p_themeName); @@ -59,8 +81,7 @@ namespace vnotex QString findThemeFolder(const QString &p_name) const; - // Theme name to folder path mapping. - QHash m_availableThemes; + QVector m_themes; QScopedPointer m_currentTheme; diff --git a/src/data/extra/extra.qrc b/src/data/extra/extra.qrc index 4cc140f3..35b852c1 100644 --- a/src/data/extra/extra.qrc +++ b/src/data/extra/extra.qrc @@ -5,6 +5,7 @@ themes/native/interface.qss themes/native/web.css themes/native/palette.json + themes/native/cover.png docs/en/get_started.txt docs/en/about_vnotex.txt docs/en/shortcuts.md diff --git a/src/data/extra/themes/native/cover.png b/src/data/extra/themes/native/cover.png new file mode 100644 index 00000000..9864a975 Binary files /dev/null and b/src/data/extra/themes/native/cover.png differ diff --git a/src/data/extra/themes/native/palette.json b/src/data/extra/themes/native/palette.json index 3c851b32..ff9c9efa 100644 --- a/src/data/extra/themes/native/palette.json +++ b/src/data/extra/themes/native/palette.json @@ -7,7 +7,10 @@ "//comment" : "If there is a file named 'markdown-editor-highlight.theme' under theme folder, this value will be ignored.", "//comment" : "Otherwise, this value specify the theme name to use.", "//comment" : "If empty, editor-highlight-theme will be used.", - "markdown-editor-highlight-theme" : "Markdown Default" + "markdown-editor-highlight-theme" : "Markdown Default", + "display_name" : "Native", + "//comment" : "Display name for different locales", + "display_name_zh_CN" : "原素" }, "base" : { "fg1" : "#31373c", diff --git a/src/utils/widgetutils.cpp b/src/utils/widgetutils.cpp index 3d0199f0..dd2899e1 100644 --- a/src/utils/widgetutils.cpp +++ b/src/utils/widgetutils.cpp @@ -20,6 +20,7 @@ #include #include #include +#include using namespace vnotex; @@ -188,29 +189,62 @@ void WidgetUtils::resizeToHideScrollBarLater(QScrollArea *p_scroll, bool p_verti void WidgetUtils::resizeToHideScrollBar(QScrollArea *p_scroll, bool p_vertical, bool p_horizontal) { - p_scroll->adjustSize(); + bool changed = false; + auto parentWidget = p_scroll->parentWidget(); if (p_horizontal && WidgetUtils::isScrollBarVisible(p_scroll, true)) { auto scrollBar = p_scroll->horizontalScrollBar(); auto delta = scrollBar->maximum() - scrollBar->minimum(); - int newWidth = p_scroll->width() + delta; auto availableSize = WidgetUtils::availableScreenSize(p_scroll); - if (newWidth <= availableSize.width()) { - p_scroll->resize(newWidth, p_scroll->height()); + + if (parentWidget) { + int newWidth = parentWidget->width() + delta; + if (newWidth <= availableSize.width()) { + changed = true; + p_scroll->resize(p_scroll->width() + delta, p_scroll->height()); + auto geo = parentWidget->geometry(); + parentWidget->setGeometry(geo.x() - delta / 2, + geo.y(), + newWidth, + geo.height()); + } + } else { + int newWidth = p_scroll->width() + delta; + if (newWidth <= availableSize.width()) { + changed = true; + p_scroll->resize(newWidth, p_scroll->height()); + } } } if (p_vertical && WidgetUtils::isScrollBarVisible(p_scroll, false)) { auto scrollBar = p_scroll->verticalScrollBar(); auto delta = scrollBar->maximum() - scrollBar->minimum(); - int newHeight = p_scroll->height() + delta; auto availableSize = WidgetUtils::availableScreenSize(p_scroll); - if (newHeight <= availableSize.height()) { - p_scroll->resize(p_scroll->width(), newHeight); + + if (parentWidget) { + int newHeight = parentWidget->height() + delta; + if (newHeight <= availableSize.height()) { + changed = true; + p_scroll->resize(p_scroll->width(), p_scroll->height() + delta); + auto geo = parentWidget->geometry(); + parentWidget->setGeometry(geo.x(), + geo.y() - delta / 2, + geo.width(), + newHeight); + } + } else { + int newHeight = p_scroll->height() + delta; + if (newHeight <= availableSize.height()) { + changed = true; + p_scroll->resize(p_scroll->width(), newHeight); + } } } - p_scroll->updateGeometry(); + if (changed) { + p_scroll->updateGeometry(); + } } QShortcut *WidgetUtils::createShortcut(const QString &p_shortcut, diff --git a/src/widgets/dialogs/settings/settingsdialog.cpp b/src/widgets/dialogs/settings/settingsdialog.cpp index 4b25eeed..912b1548 100644 --- a/src/widgets/dialogs/settings/settingsdialog.cpp +++ b/src/widgets/dialogs/settings/settingsdialog.cpp @@ -35,7 +35,7 @@ void SettingsDialog::setupUI() setupPageExplorer(mainLayout, widget); m_pageLayout = new QStackedLayout(); - mainLayout->addLayout(m_pageLayout, 3); + mainLayout->addLayout(m_pageLayout, 5); setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Apply @@ -56,6 +56,7 @@ void SettingsDialog::setupPageExplorer(QBoxLayout *p_layout, QWidget *p_parent) m_pageExplorer = new TreeWidget(TreeWidget::None, p_parent); TreeWidget::setupSingleColumnHeaderlessTree(m_pageExplorer, false, false); TreeWidget::showHorizontalScrollbar(m_pageExplorer); + m_pageExplorer->setMinimumWidth(128); layout->addWidget(m_pageExplorer); connect(m_pageExplorer, &QTreeWidget::currentItemChanged, @@ -65,7 +66,7 @@ void SettingsDialog::setupPageExplorer(QBoxLayout *p_layout, QWidget *p_parent) m_pageLayout->setCurrentWidget(page); }); - p_layout->addLayout(layout, 1); + p_layout->addLayout(layout, 2); } void SettingsDialog::setupPages() diff --git a/src/widgets/dialogs/settings/themepage.cpp b/src/widgets/dialogs/settings/themepage.cpp index d9506b93..7af09bfb 100644 --- a/src/widgets/dialogs/settings/themepage.cpp +++ b/src/widgets/dialogs/settings/themepage.cpp @@ -2,8 +2,22 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include using namespace vnotex; @@ -16,10 +30,71 @@ ThemePage::ThemePage(QWidget *p_parent) void ThemePage::setupUI() { auto mainLayout = new QVBoxLayout(this); + + // Theme. + { + auto layout = new QGridLayout(); + mainLayout->addLayout(layout); + + m_themeListWidget = new ListWidget(this); + layout->addWidget(m_themeListWidget, 0, 0, 3, 2); + connect(m_themeListWidget, &QListWidget::currentItemChanged, + this, [this](QListWidgetItem *p_current, QListWidgetItem *p_previous) { + Q_UNUSED(p_previous); + loadThemePreview(p_current ? p_current->data(Qt::UserRole).toString() : QString()); + pageIsChanged(); + }); + + auto refreshBtn = new QPushButton(tr("Refresh"), this); + layout->addWidget(refreshBtn, 3, 0, 1, 1); + connect(refreshBtn, &QPushButton::clicked, + this, [this]() { + VNoteX::getInst().getThemeMgr().refresh(); + loadThemes(); + }); + + auto addBtn = new QPushButton(tr("Add/Delete"), this); + layout->addWidget(addBtn, 3, 1, 1, 1); + // TODO: open an editor to edit the theme list. + connect(addBtn, &QPushButton::clicked, + this, []() { + WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(ConfigMgr::getInst().getUserThemeFolder())); + }); + + auto updateBtn = new QPushButton(tr("Update"), this); + layout->addWidget(updateBtn, 4, 0, 1, 1); + + auto openLocationBtn = new QPushButton(tr("Open Location"), this); + layout->addWidget(openLocationBtn, 4, 1, 1, 1); + connect(openLocationBtn, &QPushButton::clicked, + this, [this]() { + auto theme = VNoteX::getInst().getThemeMgr().findTheme(currentTheme()); + if (theme) { + WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(theme->m_folderPath)); + } + }); + + m_noPreviewText = tr("No Preview Available"); + m_previewLabel = new QLabel(m_noPreviewText, this); + m_previewLabel->setScaledContents(true); + m_previewLabel->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); + auto scrollArea = new QScrollArea(this); + scrollArea->setBackgroundRole(QPalette::Dark); + scrollArea->setWidget(m_previewLabel); + scrollArea->setMinimumSize(256, 256); + layout->addWidget(scrollArea, 0, 2, 5, 1); + } + + // Override. + { + auto box = new QGroupBox(tr("Style Override"), this); + mainLayout->addWidget(box); + } } void ThemePage::loadInternal() { + loadThemes(); } void ThemePage::saveInternal() @@ -30,3 +105,56 @@ QString ThemePage::title() const { return tr("Theme"); } + +void ThemePage::loadThemes() +{ + const auto &themeMgr = VNoteX::getInst().getThemeMgr(); + const auto &themes = themeMgr.getAllThemes(); + + m_themeListWidget->clear(); + for (const auto &info : themes) { + auto item = new QListWidgetItem(info.m_displayName, m_themeListWidget); + item->setData(Qt::UserRole, info.m_name); + item->setToolTip(info.m_folderPath); + } + + // Set current theme. + bool found = false; + const auto curThemeName = themeMgr.getCurrentTheme().name(); + for (int i = 0; i < m_themeListWidget->count(); ++i) { + if (m_themeListWidget->item(i)->data(Qt::UserRole).toString() == curThemeName) { + m_themeListWidget->setCurrentRow(i); + found = true; + break; + } + } + + if (!found && m_themeListWidget->count() > 0) { + m_themeListWidget->setCurrentRow(0); + } +} + +void ThemePage::loadThemePreview(const QString &p_name) +{ + if (p_name.isEmpty()) { + m_previewLabel->setText(m_noPreviewText); + } + + auto pixmap = VNoteX::getInst().getThemeMgr().getThemePreview(p_name); + if (pixmap.isNull()) { + m_previewLabel->setText(m_noPreviewText); + } else { + const int pwidth = 512; + m_previewLabel->setPixmap(pixmap.scaledToWidth(pwidth, Qt::SmoothTransformation)); + } + m_previewLabel->adjustSize(); +} + +QString ThemePage::currentTheme() const +{ + auto item = m_themeListWidget->currentItem(); + if (item) { + return item->data(Qt::UserRole).toString(); + } + return QString(); +} diff --git a/src/widgets/dialogs/settings/themepage.h b/src/widgets/dialogs/settings/themepage.h index 7b33d086..4be41ce0 100644 --- a/src/widgets/dialogs/settings/themepage.h +++ b/src/widgets/dialogs/settings/themepage.h @@ -3,6 +3,9 @@ #include "settingspage.h" +class QListWidget; +class QLabel; + namespace vnotex { class ThemePage : public SettingsPage @@ -20,6 +23,18 @@ namespace vnotex private: void setupUI(); + + void loadThemes(); + + void loadThemePreview(const QString &p_name); + + QString currentTheme() const; + + QListWidget *m_themeListWidget = nullptr; + + QLabel *m_previewLabel = nullptr; + + QString m_noPreviewText; }; }