Le Tan 5 лет назад
Родитель
Сommit
40a3df305b

+ 6 - 2
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

+ 49 - 3
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();
+}

+ 10 - 0
src/core/theme.h

@@ -5,6 +5,7 @@
 #include <QHash>
 #include <QJsonObject>
 #include <QPair>
+#include <QPixmap>
 
 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);
 

+ 53 - 16
src/core/thememgr.cpp

@@ -8,6 +8,8 @@
 #include "exception.h"
 #include <utils/iconutils.h>
 #include <vtextedit/vtexteditor.h>
+#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,30 +99,40 @@ 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 nullptr;
+}
+
 QString ThemeMgr::fetchQtStyleSheet() const
 {
     Q_ASSERT(m_currentTheme);
@@ -165,3 +183,22 @@ void ThemeMgr::setBaseBackground(const QColor &p_bg)
 {
     m_baseBackground = p_bg;
 }
+
+const QVector<ThemeMgr::ThemeInfo> &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();
+}

+ 27 - 6
src/core/thememgr.h

@@ -4,10 +4,11 @@
 #include <QObject>
 
 #include <QString>
-#include <QHash>
 #include <QScopedPointer>
 #include <QStringList>
+#include <QVector>
 #include <QColor>
+#include <QPixmap>
 
 #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<ThemeInfo> &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<QString, QString> m_availableThemes;
+        QVector<ThemeInfo> m_themes;
 
         QScopedPointer<Theme> m_currentTheme;
 

+ 1 - 0
src/data/extra/extra.qrc

@@ -5,6 +5,7 @@
         <file>themes/native/interface.qss</file>
         <file>themes/native/web.css</file>
         <file>themes/native/palette.json</file>
+        <file>themes/native/cover.png</file>
         <file>docs/en/get_started.txt</file>
         <file>docs/en/about_vnotex.txt</file>
         <file>docs/en/shortcuts.md</file>

BIN
src/data/extra/themes/native/cover.png


+ 4 - 1
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",

+ 42 - 8
src/utils/widgetutils.cpp

@@ -20,6 +20,7 @@
 #include <QModelIndex>
 #include <QFontDatabase>
 #include <QMenu>
+#include <QDebug>
 
 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,

+ 3 - 2
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()

+ 128 - 0
src/widgets/dialogs/settings/themepage.cpp

@@ -2,8 +2,22 @@
 
 #include <QComboBox>
 #include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QGridLayout>
+#include <QLabel>
+#include <QGroupBox>
+#include <QDebug>
+#include <QUrl>
+#include <QScrollArea>
 
+#include <widgets/listwidget.h>
+#include <QPushButton>
+#include <QListWidgetItem>
 #include <widgets/widgetsfactory.h>
+#include <core/thememgr.h>
+#include <core/vnotex.h>
+#include <core/configmgr.h>
+#include <utils/widgetutils.h>
 
 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();
+}

+ 15 - 0
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;
     };
 }