diff --git a/app/src/filedialog.cpp b/app/src/filedialog.cpp index e1b21bf1ce..6fc2947394 100644 --- a/app/src/filedialog.cpp +++ b/app/src/filedialog.cpp @@ -74,7 +74,7 @@ void FileDialog::setLastSavePaths(const QString& filePath) QFileInfo filePathInfo(filePath); QDir projectPath = filePathInfo.absoluteDir(); QString baseName = filePathInfo.baseName(); - QList fileTypes = { FileType::IMAGE, FileType::IMAGE_SEQUENCE, FileType::GIF, FileType::MOVIE, FileType::SOUND, FileType::PALETTE }; + QList fileTypes = { FileType::IMAGE, FileType::IMAGE_SEQUENCE, FileType::GIF, FileType::MOVIE, FileType::SOUND, FileType::PALETTE, FileType::THEME_PALETTE }; for (FileType& fileType : fileTypes) { setLastSavePath(fileType, projectPath.absoluteFilePath(defaultFileName(fileType, baseName))); @@ -123,6 +123,7 @@ QString FileDialog::getDefaultExtensionByFileType(const FileType fileType) case FileType::PALETTE: return PFF_DEFAULT_PALETTE_EXT; case FileType::MOVIE: return PFF_DEFAULT_MOVIE_EXT; case FileType::SOUND: return PFF_DEFAULT_SOUND_EXT; + case FileType::THEME_PALETTE: return PFF_DEFAULT_THEME_PALETTE_EXT; default: Q_UNREACHABLE(); } @@ -172,6 +173,7 @@ QString FileDialog::openDialogCaption(FileType fileType) case FileType::MOVIE: return tr("Import movie"); case FileType::SOUND: return tr("Import sound"); case FileType::PALETTE: return tr("Open palette"); + case FileType::THEME_PALETTE: return tr("Import color palette"); } return ""; } @@ -188,6 +190,7 @@ QString FileDialog::saveDialogCaption(FileType fileType) case FileType::MOVIE: return tr("Export movie"); case FileType::SOUND: return ""; case FileType::PALETTE: return tr("Export palette"); + case FileType::THEME_PALETTE: return ""; } return ""; } @@ -204,6 +207,7 @@ QString FileDialog::openFileFilters(FileType fileType) case FileType::MOVIE: return PFF_MOVIE_EXT; case FileType::SOUND: return PFF_SOUND_EXT_FILTER; case FileType::PALETTE: return PFF_PALETTE_EXT_FILTER; + case FileType::THEME_PALETTE: return PFF_THEME_PALETTE_EXT_FILTER; } return ""; } @@ -220,6 +224,7 @@ QString FileDialog::saveFileFilters(FileType fileType) case FileType::MOVIE: return "MP4 (*.mp4);; AVI (*.avi);; WebM (*.webm);; APNG (*.apng)"; case FileType::SOUND: return ""; case FileType::PALETTE: return PFF_PALETTE_EXT_FILTER; + case FileType::THEME_PALETTE: return ""; } return ""; } @@ -297,6 +302,7 @@ QString FileDialog::toSettingKey(FileType fileType) case FileType::MOVIE: return "Movie"; case FileType::SOUND: return "Sound"; case FileType::PALETTE: return "Palette"; + case FileType::THEME_PALETTE: return "ThemePalette"; } return ""; } diff --git a/app/src/generalpage.cpp b/app/src/generalpage.cpp index 77044ca5ea..4997014b7b 100644 --- a/app/src/generalpage.cpp +++ b/app/src/generalpage.cpp @@ -18,12 +18,16 @@ GNU General Public License for more details. #include "generalpage.h" #include +#include #include #include #include +#include "errordialog.h" +#include "filedialog.h" #include "pencildef.h" #include "preferencemanager.h" +#include "theming.h" #include "ui_generalpage.h" @@ -33,6 +37,11 @@ GeneralPage::GeneralPage() : ui(new Ui::GeneralPage) QSettings settings(PENCIL2D, PENCIL2D); + ui->styleCombo->addItem("System Default", ""); + ui->styleCombo->addItems(Theming::availableStyles()); + + populatePaletteCombo(false); + QString languages [][3] { // translatable string, endonym, locale code @@ -77,6 +86,10 @@ GeneralPage::GeneralPage() : ui(new Ui::GeneralPage) ui->languageCombo->addItem(itemText, localeCode); } + QIcon warningIcon = style()->standardIcon(QStyle::SP_MessageBoxWarning); + ui->paletteWarningLabel->setPixmap(warningIcon.pixmap(28, 28)); + ui->paletteWarningLabel->setVisible(false); + int value = settings.value("windowOpacity").toInt(); ui->windowOpacityLevel->setValue(100 - value); @@ -109,6 +122,10 @@ GeneralPage::GeneralPage() : ui(new Ui::GeneralPage) connect(ui->languageCombo, curIndexChanged, this, &GeneralPage::languageChanged); connect(ui->windowOpacityLevel, &QSlider::valueChanged, this, &GeneralPage::windowOpacityChange); connect(ui->backgroundButtons, buttonClicked, this, &GeneralPage::backgroundChanged); + connect(ui->styleCombo, curIndexChanged, this, &GeneralPage::styleChanged); + connect(ui->paletteCombo, curIndexChanged, this, &GeneralPage::paletteChanged); + connect(ui->addPaletteButton, &QPushButton::pressed, this, &GeneralPage::addPalette); + connect(ui->removePaletteButton, &QPushButton::pressed, this, &GeneralPage::removePalette); connect(ui->shadowsBox, &QCheckBox::stateChanged, this, &GeneralPage::shadowsCheckboxStateChanged); connect(ui->toolCursorsBox, &QCheckBox::stateChanged, this, &GeneralPage::toolCursorsCheckboxStateChanged); connect(ui->antialiasingBox, &QCheckBox::stateChanged, this, &GeneralPage::antiAliasCheckboxStateChanged); @@ -150,6 +167,13 @@ void GeneralPage::updateValues() ui->curveSmoothingLevel->setValue(mManager->getInt(SETTING::CURVE_SMOOTHING)); QSignalBlocker b2(ui->windowOpacityLevel); ui->windowOpacityLevel->setValue(100 - mManager->getInt(SETTING::WINDOW_OPACITY)); + QSignalBlocker b19(ui->styleCombo); + int styleIndex = ui->styleCombo->findText(mManager->getString(SETTING::STYLE_ID), Qt::MatchFixedString); + ui->styleCombo->setCurrentIndex(qMax(0, styleIndex)); + QSignalBlocker b20(ui->styleCombo); + QString paletteKey = mManager->getString(SETTING::PALETTE_ID); + setCurrentPalette(paletteKey); + ui->removePaletteButton->setEnabled(!paletteKey.isEmpty() && !Theming::getPalette(paletteKey).isBuiltIn()); QSignalBlocker b3(ui->shadowsBox); ui->shadowsBox->setChecked(mManager->isOn(SETTING::SHADOW)); QSignalBlocker b4(ui->toolCursorsBox); @@ -249,6 +273,60 @@ void GeneralPage::highResCheckboxStateChanged(int b) mManager->set(SETTING::HIGH_RESOLUTION, b != Qt::Unchecked); } +void GeneralPage::styleChanged(int index) +{ + mManager->set(SETTING::STYLE_ID, ui->styleCombo->itemText(index)); +} + +void GeneralPage::paletteChanged(int index) +{ + QString paletteKey = ui->paletteCombo->itemData(index).toString(); + mManager->set(SETTING::PALETTE_ID, paletteKey); + ui->removePaletteButton->setEnabled(!paletteKey.isEmpty() && !Theming::getPalette(paletteKey).isBuiltIn()); + if (m_showMissingPalette) + { + ui->paletteWarningLabel->setVisible(index == 1); + } +} + +void GeneralPage::addPalette() +{ + QString filePath = FileDialog::getOpenFileName(this, FileType::THEME_PALETTE); + if (!filePath.isEmpty()) + { + QFileInfo fileInfo(filePath); + auto result = Theming::addPalette(filePath); + Status st = result.first; + ThemeColorPalette newPalette = result.second; + if (st.ok()) + { + mManager->set(SETTING::PALETTE_ID, newPalette.id()); + populatePaletteCombo(); + } + else + { + ErrorDialog errorDialog(st.title(), st.description(), st.details().html()); + errorDialog.exec(); + } + } +} + +void GeneralPage::removePalette() +{ + QString key = ui->paletteCombo->currentData().toString(); + Status st = Theming::removePalette(key); + if (st.ok()) + { + ui->paletteCombo->removeItem(ui->paletteCombo->currentIndex()); + ui->paletteCombo->setCurrentIndex(0); + } + else + { + ErrorDialog errorDialog(st.title(), st.description(), st.details().html()); + errorDialog.exec(); + } +} + void GeneralPage::shadowsCheckboxStateChanged(int b) { mManager->set(SETTING::SHADOW, b != Qt::Unchecked); @@ -408,3 +486,57 @@ void GeneralPage::undoRedoCancelButtonPressed() ui->undoRedoGroupCancelButton->setDisabled(true); ui->undoRedoGroupApplyButton->setDisabled(true); } + +void GeneralPage::populatePaletteCombo(bool usePreference) +{ + { + QSignalBlocker b(ui->paletteCombo); + + ui->paletteCombo->clear(); + m_showMissingPalette = false; + ui->paletteCombo->addItem("Default", ""); + ui->paletteCombo->insertSeparator(1); + QList palettes = Theming::availablePalettes(); + std::sort(palettes.begin(), palettes.end(), [](ThemeColorPalette a, ThemeColorPalette b) { return a.displayName().toLower() < b.displayName().toLower(); }); + for (const auto& palette : palettes) + { + if (palette.isDark()) continue; + ui->paletteCombo->addItem(palette.displayName(), palette.id()); + } + ui->paletteCombo->insertSeparator(ui->paletteCombo->count()); + for (const auto& palette : palettes) + { + if (!palette.isDark()) continue; + ui->paletteCombo->addItem(palette.displayName(), palette.id()); + } + } + + if (usePreference) + { + setCurrentPalette(mManager->getString(SETTING::PALETTE_ID)); + QString paletteKey = mManager->getString(SETTING::PALETTE_ID); + } +} + +void GeneralPage::setCurrentPalette(const QString& paletteKey) +{ + int paletteIndex = ui->paletteCombo->findData(paletteKey, Qt::UserRole, Qt::MatchFixedString); + ThemeColorPalette palette = Theming::getPalette(paletteKey); + if (paletteIndex >= 0) + { + ui->paletteCombo->setCurrentIndex(paletteIndex); + } + else if (palette.isValid()) + { + populatePaletteCombo(false); + paletteIndex = ui->paletteCombo->findData(paletteKey, Qt::UserRole, Qt::MatchFixedString); + ui->paletteCombo->setCurrentIndex(paletteIndex); + } + else + { + ui->paletteCombo->insertItem(1, palette.displayName(), paletteKey); + ui->paletteCombo->setCurrentIndex(1); + m_showMissingPalette = true; + ui->paletteWarningLabel->setVisible(true); + } +} diff --git a/app/src/generalpage.h b/app/src/generalpage.h index 2b951f7765..dec48f598e 100644 --- a/app/src/generalpage.h +++ b/app/src/generalpage.h @@ -48,6 +48,10 @@ public slots: private slots: void languageChanged(int i); + void styleChanged(int index); + void paletteChanged(int index); + void addPalette(); + void removePalette(); void shadowsCheckboxStateChanged(int b); void antiAliasCheckboxStateChanged(int b); void toolCursorsCheckboxStateChanged(int b); @@ -65,12 +69,15 @@ private slots: void undoRedoCancelButtonPressed(); private: + void populatePaletteCombo(bool usePreference = true); + void setCurrentPalette(const QString& paletteKey); bool canApplyOrCancelUndoRedoChanges() const; void updateSafeHelperTextEnabledState(); Ui::GeneralPage* ui = nullptr; PreferenceManager* mManager = nullptr; + bool m_showMissingPalette = false; }; #endif // GENERALPAGE_H diff --git a/app/src/pencil2d.cpp b/app/src/pencil2d.cpp index 74e8cba94b..259d5031e9 100644 --- a/app/src/pencil2d.cpp +++ b/app/src/pencil2d.cpp @@ -24,6 +24,7 @@ GNU General Public License for more details. #include #include #include +#include #include #include #include @@ -32,9 +33,11 @@ GNU General Public License for more details. #include "commandlineexporter.h" #include "commandlineparser.h" +#include "editor.h" #include "mainwindow2.h" #include "pencildef.h" #include "platformhandler.h" +#include "theming.h" #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) @@ -42,7 +45,8 @@ GNU General Public License for more details. #endif Pencil2D::Pencil2D(int& argc, char** argv) : - QApplication(argc, argv) + QApplication(argc, argv), + DEFAULT_STYLE(style()->objectName()) { // Set organization and application name setOrganizationName("Pencil2D"); @@ -134,6 +138,32 @@ bool Pencil2D::event(QEvent* event) return QApplication::event(event); } +void Pencil2D::setTheme(const QString styleId, const QString paletteId) +{ + QStyle* newStyle = Theming::getStyle(styleId); + if (newStyle != nullptr) + { + setStyle(newStyle); + } + else + { + newStyle = setStyle(DEFAULT_STYLE); + } + + // Palette should be set after style is set + ThemeColorPalette palette(Theming::getPalette(paletteId)); + if (palette.isValid()) + { + setPalette(palette.palette()); + } + else + { + setPalette(newStyle->standardPalette()); + } + + mainWindow->update(); +} + void Pencil2D::installTranslators() { QSettings setting(PENCIL2D, PENCIL2D); @@ -171,7 +201,21 @@ void Pencil2D::prepareGuiStartup(const QString& inputPath) PlatformHandler::configurePlatformSpecificSettings(); mainWindow.reset(new MainWindow2); + PreferenceManager* prefs = mainWindow->mEditor->preference(); + setTheme(prefs->getString(SETTING::STYLE_ID), prefs->getString(SETTING::PALETTE_ID)); + connect(this, &Pencil2D::openFileRequested, mainWindow.get(), &MainWindow2::openFile); + connect(prefs, &PreferenceManager::optionChanged, [=](SETTING setting) { + switch (setting) + { + case SETTING::STYLE_ID: + case SETTING::PALETTE_ID: + setTheme(prefs->getString(SETTING::STYLE_ID), prefs->getString(SETTING::PALETTE_ID)); + break; + default: + break; + } + }); mainWindow->show(); mainWindow->openStartupFile(inputPath); diff --git a/app/src/pencil2d.h b/app/src/pencil2d.h index 4789788371..ee49c30231 100644 --- a/app/src/pencil2d.h +++ b/app/src/pencil2d.h @@ -72,6 +72,7 @@ class Pencil2D : public QApplication bool event(QEvent* event) override; + void setTheme(const QString styleId, const QString paletteId); signals: /** * Emitted when the operating system requests that a file should be opened. @@ -93,6 +94,8 @@ class Pencil2D : public QApplication */ void prepareGuiStartup(const QString &inputPath); + const QString DEFAULT_STYLE; + std::unique_ptr mainWindow; std::unique_ptr mProcessLock; diff --git a/app/ui/generalpage.ui b/app/ui/generalpage.ui index 1d96d9e204..a5c9630d1c 100644 --- a/app/ui/generalpage.ui +++ b/app/ui/generalpage.ui @@ -26,9 +26,9 @@ 0 - -407 - 314 - 932 + 0 + 363 + 1195 @@ -53,41 +53,122 @@ - - - - Window opacity - - - - - - Opacity - - - - - - - 30 - - - 100 - - - Qt::Horizontal - - - - - - Appearance + + + + + + Window opacity + + + + + + + 30 + + + 100 + + + Qt::Horizontal + + + + + + + + + + + Window style + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Color palette + + + + + + + + + + Palette is missing or could not be loaded + + + + + + + + + + + + + + :/icons/themes/playful/misc/add-color.svg:/icons/themes/playful/misc/add-color.svg + + + + + + + + + + + :/icons/themes/playful/misc/remove-color.svg:/icons/themes/playful/misc/remove-color.svg + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -434,7 +515,7 @@ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - + 6 @@ -578,6 +659,10 @@ scrollArea languageCombo windowOpacityLevel + styleCombo + paletteCombo + addPaletteButton + removePaletteButton shadowsBox toolCursorsBox canvasCursorBox @@ -592,8 +677,21 @@ gridCheckBox gridSizeInputW gridSizeInputH + actionSafeCheckBox + actionSafeInput + titleSafeCheckBox + titleSafeInput + safeHelperTextCheckbox + invertScrollDirectionBox + framePoolSizeSpin + newUndoRedoCheckBox + undoStepsBox + undoRedoGroupApplyButton + undoRedoGroupCancelButton - + + + diff --git a/core_lib/core_lib.pro b/core_lib/core_lib.pro index 9e0e8df982..93cd3e6eaa 100644 --- a/core_lib/core_lib.pro +++ b/core_lib/core_lib.pro @@ -105,6 +105,7 @@ HEADERS += \ src/util/pencilerror.h \ src/util/pencilsettings.h \ src/util/preferencesdef.h \ + src/util/theming.h \ src/util/transform.h \ src/util/util.h \ src/util/log.h \ @@ -186,6 +187,7 @@ SOURCES += src/graphics/bitmap/bitmapimage.cpp \ src/util/pencilerror.cpp \ src/util/pencilsettings.cpp \ src/util/log.cpp \ + src/util/theming.cpp \ src/util/transform.cpp \ src/util/util.cpp \ src/util/pointerevent.cpp \ diff --git a/core_lib/data/core_lib.qrc b/core_lib/data/core_lib.qrc index 5bc67e882b..4053207cb1 100644 --- a/core_lib/data/core_lib.qrc +++ b/core_lib/data/core_lib.qrc @@ -1,5 +1,17 @@ resources/kb.ini + theme_palettes/darker.conf + theme_palettes/ia-ora.conf + theme_palettes/waves.conf + theme_palettes/rose-pine.conf + theme_palettes/arc-dark.conf + theme_palettes/arc-light.conf + theme_palettes/frappe.conf + theme_palettes/gruvbox-dark.conf + theme_palettes/gruvbox-light.conf + theme_palettes/latte.conf + theme_palettes/macchiato.conf + theme_palettes/mocha.conf diff --git a/core_lib/data/theme_palettes/arc-dark.conf b/core_lib/data/theme_palettes/arc-dark.conf new file mode 100644 index 0000000000..26529d2dc0 --- /dev/null +++ b/core_lib/data/theme_palettes/arc-dark.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Arc Dark +LightOrDark=dark + +[ColorScheme] +active_colors=#ffffff, #383C4A, #979797, #5e5c5b, #302f2e, #4a4947, #ffffff, #ffffff, #ffffff, #3d3d3d, #2F343F, #e7e4e0, #5294e2, #f9f9f9, #5294e2, #a70b06, #5c5b5a, #ffffff, #3f3f36, #ffffff +disabled_colors=#808080, #383C4A, #979797, #5e5c5b, #302f2e, #4a4947, #808080, #ffffff, #808080, #3d3d3d, #2F343F, #e7e4e0, #5294e2, #808080, #5294e2, #a70b06, #5c5b5a, #ffffff, #3f3f36, #ffffff +inactive_colors=#ffffff, #383C4A, #979797, #5e5c5b, #302f2e, #4a4947, #ffffff, #ffffff, #ffffff, #3d3d3d, #2F343F, #e7e4e0, #5294e2, #f9f9f9, #5294e2, #a70b06, #5c5b5a, #ffffff, #3f3f36, #ffffff diff --git a/core_lib/data/theme_palettes/arc-light.conf b/core_lib/data/theme_palettes/arc-light.conf new file mode 100644 index 0000000000..8231775c56 --- /dev/null +++ b/core_lib/data/theme_palettes/arc-light.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Arc Light +LightOrDark=light + +[ColorScheme] +active_colors=#000000, #F5F6F7, #ffffff, #F3F3F3, #9f9d9a, #b8b5b2, #000000, #ffffff, #000000, #ffffff, #E7E8EB, #b1aeab, #5492DF, #ffffff, #0000ff, #ff0000, #f7f5f3, #000000, #ffffdc, #000000 +disabled_colors=#bebebe, #F5F6F7, #ffffff, #F3F3F3, #9f9d9a, #b8b5b2, #bebebe, #ffffff, #bebebe, #efebe7, #E7E8EB, #b1aeab, #9f9d9a, #ffffff, #0000ff, #ff0000, #f7f5f3, #000000, #ffffdc, #000000 +inactive_colors=#000000, #F5F6F7, #ffffff, #F3F3F3, #9f9d9a, #b8b5b2, #000000, #ffffff, #000000, #ffffff, #E7E8EB, #b1aeab, #5492DF, #ffffff, #0000ff, #ff0000, #f7f5f3, #000000, #ffffdc, #000000 diff --git a/core_lib/data/theme_palettes/darker.conf b/core_lib/data/theme_palettes/darker.conf new file mode 100644 index 0000000000..a63d982180 --- /dev/null +++ b/core_lib/data/theme_palettes/darker.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Darker +LightOrDark=dark + +[ColorScheme] +active_colors=#ffffffff, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffffffff, #ffffffff, #ffffffff, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #fff9f9f9, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff +disabled_colors=#ff808080, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff808080, #ffffffff, #ff808080, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #ff808080, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff +inactive_colors=#ffffffff, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffffffff, #ffffffff, #ffffffff, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #fff9f9f9, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff diff --git a/core_lib/data/theme_palettes/frappe.conf b/core_lib/data/theme_palettes/frappe.conf new file mode 100644 index 0000000000..c2afafd5ac --- /dev/null +++ b/core_lib/data/theme_palettes/frappe.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Frappe +LightOrDark=dark + +[ColorScheme] +active_colors=#ffc6d0f5, #ff292c3c, #ffa5adce, #ff949cbb, #ff51576d, #ff737994, #ffc6d0f5, #ffc6d0f5, #ffc6d0f5, #ff303446, #ff292c3c, #ff838ba7, #ff8caaee, #ff303446, #ff8caaee, #ffe78284, #ff303446, #ffc6d0f5, #ff232634, #ffc6d0f5, #80838ba7 +disabled_colors=#ffa5adce, #ff292c3c, #ffa5adce, #ff949cbb, #ff51576d, #ff737994, #ffa5adce, #ffa5adce, #ffa5adce, #ff303446, #ff292c3c, #ff838ba7, #ff626880, #ffb5bfe2, #ff8caaee, #ffe78284, #ff303446, #ffc6d0f5, #ff232634, #ffc6d0f5, #80838ba7 +inactive_colors=#ffc6d0f5, #ff292c3c, #ffa5adce, #ff949cbb, #ff51576d, #ff737994, #ffc6d0f5, #ffc6d0f5, #ffc6d0f5, #ff303446, #ff292c3c, #ff838ba7, #ff414559, #ffa5adcb, #ff8caaee, #ffe78284, #ff303446, #ffc6d0f5, #ff232634, #ffc6d0f5, #80838ba7 diff --git a/core_lib/data/theme_palettes/gruvbox-dark.conf b/core_lib/data/theme_palettes/gruvbox-dark.conf new file mode 100644 index 0000000000..e39afdf8bb --- /dev/null +++ b/core_lib/data/theme_palettes/gruvbox-dark.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Gruvbox Dark +LightOrDark=dark + +[ColorScheme] +active_colors=#ffebdbb2, #ff1d2021, #ffbdae93, #ffa89984, #ff3c3836, #ff504945, #ffebdbb2, #ffebdbb2, #ffebdbb2, #ff282828, #ff1d2021, #ff504945, #ff458588, #ff282828, #ff458588, #ffcc241d, #ff282828, #ffebdbb2, #ff1d2021, #ffebdbb2, #ffbdae93 +disabled_colors=#ffbdae93, #ff1d2021, #ffbdae93, #ffa89984, #ff3c3836, #ff504945, #ffbdae93, #ffbdae93, #ffbdae93, #ff282828, #ff1d2021, #ff504945, #ff438184, #ff3c3836, #ff458588, #ffcc241d, #ff282828, #ffebdbb2, #ff1d2021, #ffebdbb2, #ffbdae93 +inactive_colors=#ffebdbb2, #ff1d2021, #ffbdae93, #ffa89984, #ff3c3836, #ff504945, #ffebdbb2, #ffebdbb2, #ffebdbb2, #ff282828, #ff1d2021, #ff504945, #ff438184, #ffa89984, #ff458588, #ffcc241d, #ff282828, #ffebdbb2, #ff1d2021, #ffebdbb2, #ffbdae93 diff --git a/core_lib/data/theme_palettes/gruvbox-light.conf b/core_lib/data/theme_palettes/gruvbox-light.conf new file mode 100644 index 0000000000..363cfdcef3 --- /dev/null +++ b/core_lib/data/theme_palettes/gruvbox-light.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Gruvbox Light +LightOrDark=light + +[ColorScheme] +active_colors=#ff504945, #ffebdbb2, #ff665c54, #ff7c6f64, #ffd5c4a1, #ffbdae93, #ff3c3836, #ff3c3836, #ff3c3836, #fffbf1c7, #fffbf1c7, #ffa89984, #ff458588, #fffbf1c7, #ff458588, #ffcc241d, #ffebdbb2, #ff3c3836, #ffebdbb2, #ff3c3836, #ff928374 +disabled_colors=#ff665c54, #ffebdbb2, #ff665c54, #ff7c6f64, #ffd5c4a1, #ffbdae93, #ff665c54, #ff665c54, #ff665c54, #fffbf1c7, #fffbf1c7, #ffa79883, #ffbdae93, #ff504945, #ff458588, #ffbd211b, #ffebdbb2, #ff3c3836, #ffebdbb2, #ff3c3836, #ff928374 +inactive_colors=#ff504945, #ffebdbb2, #ff665c54, #ff7c6f64, #ffd5c4a1, #ffbdae93, #ff373432, #ff3c3836, #ff3c3836, #fffbf1c7, #fffbf1c7, #ffa79883, #ffd5c4a1, #ff665c54, #ff458588, #ffbd211b, #ffebdbb2, #ff3c3836, #ffebdbb2, #ff3c3836, #ff928374 diff --git a/core_lib/data/theme_palettes/ia-ora.conf b/core_lib/data/theme_palettes/ia-ora.conf new file mode 100644 index 0000000000..989164ee34 --- /dev/null +++ b/core_lib/data/theme_palettes/ia-ora.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Ia Ora +LightOrDark=light + +[ColorScheme] +active_colors=#ff000000, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff000000, #ffffffff, #ff000000, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ffffffff, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 +disabled_colors=#ff808080, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff808080, #ffffffff, #ff808080, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ff808080, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 +inactive_colors=#ff000000, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff000000, #ffffffff, #ff000000, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ffffffff, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 diff --git a/core_lib/data/theme_palettes/latte.conf b/core_lib/data/theme_palettes/latte.conf new file mode 100644 index 0000000000..e150585dcc --- /dev/null +++ b/core_lib/data/theme_palettes/latte.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Latt\x0E9 +LightOrDark=light + +[ColorScheme] +active_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff1e66f5, #ffeff1f5, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1 +disabled_colors=#ff6c6f85, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff6c6f85, #ff6c6f85, #ff6c6f85, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff9ca0b0, #ff5c5f77, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1 +inactive_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ffccd0da, #ff6c6f85, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1 diff --git a/core_lib/data/theme_palettes/macchiato.conf b/core_lib/data/theme_palettes/macchiato.conf new file mode 100644 index 0000000000..71bc51cadc --- /dev/null +++ b/core_lib/data/theme_palettes/macchiato.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Macchiato +LightOrDark=dark + +[ColorScheme] +active_colors=#ffcad3f5, #ff1e2030, #ffa5adcb, #ff939ab7, #ff494d64, #ff6e738d, #ffcad3f5, #ffcad3f5, #ffcad3f5, #ff24273a, #ff1e2030, #ff8087a2, #ff8aadf4, #ff24273a, #ff8aadf4, #ffed8796, #ff24273a, #ffcad3f5, #ff181926, #ffcad3f5, #808087a2 +disabled_colors=#ffa5adcb, #ff1e2030, #ffa5adcb, #ff939ab7, #ff494d64, #ff6e738d, #ffa5adcb, #ffa5adcb, #ffa5adcb, #ff24273a, #ff1e2030, #ff8087a2, #ff8aadf4, #ff494d64, #ff8aadf4, #ffed8796, #ff24273a, #ffcad3f5, #ff181926, #ffcad3f5, #808087a2 +inactive_colors=#ffcdd6f4, #ff1e2030, #ffa5adcb, #ff939ab7, #ff494d64, #ff6e738d, #ffcad3f5, #ffcad3f5, #ffcad3f5, #ff24273a, #ff1e2030, #ff8087a2, #ff8aadf4, #ffa5adcb, #ff8aadf4, #ffed8796, #ff24273a, #ffcad3f5, #ff181926, #ffcad3f5, #808087a2 diff --git a/core_lib/data/theme_palettes/mocha.conf b/core_lib/data/theme_palettes/mocha.conf new file mode 100644 index 0000000000..f895a0e9f6 --- /dev/null +++ b/core_lib/data/theme_palettes/mocha.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Mocha +LightOrDark=dark + +[ColorScheme] +active_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ff1e1e2e, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c +disabled_colors=#ffa6adc8, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffa6adc8, #ffa6adc8, #ffa6adc8, #ff1e1e2e, #ff11111b, #ff7f849c, #ff89b4fa, #ff45475a, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c +inactive_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ffa6adc8, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c diff --git a/core_lib/data/theme_palettes/rose-pine.conf b/core_lib/data/theme_palettes/rose-pine.conf new file mode 100644 index 0000000000..86e9dc2b90 --- /dev/null +++ b/core_lib/data/theme_palettes/rose-pine.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Ros\x0E9 Pine +LightOrDark=dark + +[ColorScheme] +active_colors=#ffe0def4, #ff26233a, #ff524f67, #ff403d52, #ff21202e, #ff26233a, #ff6e6a86, #ffeb6f92, #ffebbcba, #ff26233a, #ff191724, #ff26233a, #ff524f67, #fff6c177, #ff9ccfd8, #ffc4a7e7, #ff1a1c1e, #ff000000, #ff21202e, #ff908caa, #809e9e9e +disabled_colors=#ffebbcba, #ff1f1d2e, #ff524f67, #ff403d52, #ff21202e, #ff26233a, #ff6e6a86, #ffeb6f92, #ffebbcba, #ff26233a, #ff191724, #ff26233a, #ff403d52, #ff524f67, #ff908caa, #ff524f67, #ff1a1c1e, #ff000000, #ff21202e, #ff9ccfd8, #809e9e9e +inactive_colors=#ffe0def4, #ff26233a, #ff524f67, #ff403d52, #ff21202e, #ff26233a, #ff6e6a86, #ffeb6f92, #ffebbcba, #ff26233a, #ff191724, #ff26233a, #ff403d52, #ffebbcba, #ff31748f, #ff6e6a86, #ff1a1c1e, #ff000000, #ff21202e, #ffc4a7e7, #809e9e9e diff --git a/core_lib/data/theme_palettes/waves.conf b/core_lib/data/theme_palettes/waves.conf new file mode 100644 index 0000000000..0d3adfce10 --- /dev/null +++ b/core_lib/data/theme_palettes/waves.conf @@ -0,0 +1,8 @@ +[Metadata] +DisplayName=Waves +LightOrDark=dark + +[ColorScheme] +active_colors=#ffb0b0b0, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffb0b0b0, #ffb0b0b0, #ffb0b0b0, #ff010b2c, #ff010b2c, #ffb0b0b0, #ff302f2e, #ffb0b0b0, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 +disabled_colors=#ff808080, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff808080, #ff808080, #ff808080, #ff00071d, #ff00071d, #ffb0b0b0, #ff00071d, #ff808080, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 +inactive_colors=#ffb0b0b0, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffb0b0b0, #ffb0b0b0, #ffb0b0b0, #ff010b2c, #ff010b2c, #ffb0b0b0, #ff302f2e, #ffb0b0b0, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 diff --git a/core_lib/src/managers/preferencemanager.cpp b/core_lib/src/managers/preferencemanager.cpp index 8cb247aa63..281c621051 100644 --- a/core_lib/src/managers/preferencemanager.cpp +++ b/core_lib/src/managers/preferencemanager.cpp @@ -75,6 +75,8 @@ void PreferenceManager::loadPrefs() set(SETTING::TOOL_CURSOR, settings.value(SETTING_TOOL_CURSOR, true).toBool()); set(SETTING::CANVAS_CURSOR, settings.value(SETTING_CANVAS_CURSOR, true).toBool()); set(SETTING::HIGH_RESOLUTION, settings.value(SETTING_HIGH_RESOLUTION, true).toBool()); + set(SETTING::STYLE_ID, settings.value(SETTING_STYLE_ID, "").toString()); + set(SETTING::PALETTE_ID, settings.value(SETTING_PALETTE_ID, "").toString()); set(SETTING::SHADOW, settings.value(SETTING_SHADOW, false).toBool()); set(SETTING::QUICK_SIZING, settings.value(SETTING_QUICK_SIZING, true).toBool()); set(SETTING::SHOW_SELECTION_INFO, settings.value(SETTING_SHOW_SELECTION_INFO, false).toBool()); @@ -197,6 +199,12 @@ void PreferenceManager::set(SETTING option, QString value) QSettings settings(PENCIL2D, PENCIL2D); switch (option) { + case SETTING::STYLE_ID: + settings.setValue(SETTING_STYLE_ID, value); + break; + case SETTING::PALETTE_ID: + settings.setValue(SETTING_PALETTE_ID, value); + break; case SETTING::BACKGROUND_STYLE: settings.setValue(SETTING_BACKGROUND_STYLE, value); break; diff --git a/core_lib/src/managers/thememanager.cpp b/core_lib/src/managers/thememanager.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core_lib/src/managers/thememanager.h b/core_lib/src/managers/thememanager.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core_lib/src/util/fileformat.h b/core_lib/src/util/fileformat.h index 6935d0aa44..7d1d6d67d5 100644 --- a/core_lib/src/util/fileformat.h +++ b/core_lib/src/util/fileformat.h @@ -57,6 +57,8 @@ GNU General Public License for more details. #define PFF_SOUND_EXT_FILTER \ QCoreApplication::translate("FileFormat", "Sound formats") + " (*.wav *.mp3 *.wma *.ogg *.flac *.opus *.aiff *.aac *.caf);;WAV (*.wav);;MP3 (*.mp3);;WMA (*.wma);;OGG (*.ogg);;FLAC (*.flac);;Opus (*.opus);;AIFF (*.aiff);;AAC (*.aac);;CAF (*.caf)" +#define PFF_THEME_PALETTE_EXT_FILTER \ + QCoreApplication::translate("FileFormat", "qt5ct Color Palette") + " (*.conf);;" #define PFF_DEFAULT_PROJECT_EXT \ QString(".pclx") @@ -79,6 +81,9 @@ GNU General Public License for more details. #define PFF_DEFAULT_SOUND_EXT \ QString(".wav") +#define PFF_DEFAULT_THEME_PALETTE_EXT \ + QString(".conf") + #define PFF_OLD_DATA_DIR "data" #define PFF_DATA_DIR "data" #define PFF_XML_FILE_NAME "main.xml" diff --git a/core_lib/src/util/filetype.h b/core_lib/src/util/filetype.h index 9a88fad1e8..8dd47b7d68 100644 --- a/core_lib/src/util/filetype.h +++ b/core_lib/src/util/filetype.h @@ -10,7 +10,8 @@ enum class FileType ANIMATED_IMAGE, MOVIE, SOUND, - PALETTE + PALETTE, + THEME_PALETTE, }; #endif // FILETYPE_H diff --git a/core_lib/src/util/pencildef.h b/core_lib/src/util/pencildef.h index a16f6f89c6..5580dd099a 100644 --- a/core_lib/src/util/pencildef.h +++ b/core_lib/src/util/pencildef.h @@ -278,6 +278,8 @@ const static int MaxFramesBound = 9999; #define SETTING_ANTIALIAS "Antialiasing" #define SETTING_SHOW_GRID "ShowGrid" #define SETTING_COUNT "Count" +#define SETTING_STYLE_ID "StyleId" +#define SETTING_PALETTE_ID "PaletteId" #define SETTING_SHADOW "Shadow" #define SETTING_PREV_ONION "PrevOnion" #define SETTING_NEXT_ONION "NextOnion" diff --git a/core_lib/src/util/preferencesdef.h b/core_lib/src/util/preferencesdef.h index 66683c2e5a..e3156b872e 100644 --- a/core_lib/src/util/preferencesdef.h +++ b/core_lib/src/util/preferencesdef.h @@ -91,6 +91,8 @@ enum class SETTING LOAD_MOST_RECENT, LOAD_DEFAULT_PRESET, DEFAULT_PRESET, + STYLE_ID, + PALETTE_ID, COUNT, // COUNT must always be the last one. }; diff --git a/core_lib/src/util/theming.cpp b/core_lib/src/util/theming.cpp new file mode 100644 index 0000000000..63da214950 --- /dev/null +++ b/core_lib/src/util/theming.cpp @@ -0,0 +1,291 @@ +#include "theming.h" + +#include +#include +#include +#include +#include +#include + +#include "pencilerror.h" + +/** + * Get a list of all valid keys that can be passed to Theming::getStyle(). + * + * The exact list will vary depending on the platform, loaded Qt plugins, + * and the Qt runtime configuration. + * + * @return A list of string keys. + * @see getStyle() + */ +QStringList Theming::availableStyles() +{ + return QStyleFactory::keys(); +} + +/** + * Get a list of all valid keys that can be passed to Theming::getPalette(). + * + * This list is generated by finding all built-in palettes, and all .conf + * files in the user's palette directory. This may include invalid palettes. + * Filter using ThemeColorPalette::isValid if only valid palettes are required. + * + * @return A list of loaded palettes. + * @see getPalette() + */ +QList Theming::availablePalettes() +{ + QList palettes; + + // Built-in palettes + QDir builtinDir(":/theme_palettes"); + for (const QString& palettePath : builtinDir.entryList({"*.conf"}, QDir::Files)) + { + palettes.append(builtinDir.filePath(palettePath)); + } + + // User palettes + QDir userDir = userPaletteDir(); + for (const QString& palettePath : userDir.entryList({"*.conf"}, QDir::Files)) + { + palettes.append(userDir.filePath(palettePath)); + } + + return palettes; +} + +/** + * Fetch a style with a given key. + * + * @param key A case-insensitive style identifier. Current defined by the Qt plugin providing the style. + * @return A QStyle instance corresponding to the given key or a null pointer if there is none. + * @see availableStyles() + */ +QStyle* Theming::getStyle(const QString& key) +{ + QStyle* style = QStyleFactory::create(key); + + return style; +} + +/** + * Fetch a palette with a given key. + * + * @param key A palette identifier. + * @return A loaded palette corresponding to the given key. If no palette with the key exists, + * or the conf file for the palette is invalid, an invalid palette will be returned. + * @see availablePalettes() + */ +ThemeColorPalette Theming::getPalette(const QString& key) +{ + if (key.startsWith("builtin-")) + { + return ThemeColorPalette(QString(":/theme_palettes/%1.conf").arg(key.mid(8))); + } + else if (key.startsWith("user-")) + { + return ThemeColorPalette(userPaletteDir().filePath(QString("%1.conf").arg(key.mid(5)))); + } + ThemeColorPalette invalidPalette; + invalidPalette.setInvalidWithId(key); + return invalidPalette; +} + +/** + * Saves a palette to the user's palette directory. + * + * @param filePath The path to a .conf palette file to load. + * @return A Status indicating if the palette was added successfully, and the new + * palette if successful. + */ +std::pair Theming::addPalette(const QString& filePath) +{ + ThemeColorPalette palette; + DebugDetails dd; + dd << QString("Raw file path: %1").arg(filePath); + QString errorTitle = QCoreApplication::translate("Theming", "Unable to load color palette"); + + // Perform some basic checks to provide more detailed errors + if (!filePath.endsWith(".conf")) return { Status(Status::INVALID_ARGUMENT, dd, errorTitle, QCoreApplication::translate("Theming", "Color palette file is the wrong format. Only .ini files are accepted.")), palette }; + QFileInfo fileInfo(filePath); + if (fileInfo.baseName().isEmpty()) return { Status(Status::INVALID_ARGUMENT, dd, errorTitle, QCoreApplication::translate("Theming", "The filename cannot be empty.")), palette }; + if (!fileInfo.isFile()) return { Status(Status::FILE_NOT_FOUND, dd, errorTitle, QCoreApplication::translate("Theming", "File not found or cannot be read.")), palette }; + if (!fileInfo.isReadable()) return { Status(Status::ERROR_FILE_CANNOT_OPEN, dd, errorTitle, QCoreApplication::translate("Theming", "File not found or cannot be read.")), palette }; + + // Attempt to construct the palette and verify that it is valid + palette.loadFromFile(filePath); + if (!palette.isValid()) return { Status(Status::FAIL, dd, errorTitle, QCoreApplication::translate("Theming", "Cannot load color palette, the file is not in the correct format or contains errors.")), palette }; + + // Copy to the user's palette directory + QFile inFile(filePath); + QDir destDir = userPaletteDir(); + QString destPath = destDir.filePath(fileInfo.fileName()); + QFileInfo destFileInfo(destPath); + int offset = 0; + while (destFileInfo.exists()) + { + destFileInfo.setFile(destDir.filePath(QString("%1-%2.conf").arg(fileInfo.baseName()).arg(++offset))); + destPath = destFileInfo.absoluteFilePath(); + } + dd << QString("Palette save path: %1").arg(destPath); + if (inFile.copy(destPath)) + { + return { Status::OK, ThemeColorPalette(destPath) }; + } + return { Status(Status::FAIL, dd, errorTitle, QCoreApplication::translate("Theming", "An internal error occurred and the palette could not be saved.")), palette }; +} + +/** + * Removes a palette from the user's palette directory. + * + * The .conf palette file will be deleted by this function and cannot be undone. + * + * @param key The palette id of the palette to remove. + * @return A Status indicated if the palette was removed successfully. + */ +Status Theming::removePalette(const QString& key) +{ + DebugDetails dd; + dd << QString("Palette key: %1").arg(key); + QString errorTitle = QCoreApplication::translate("Theming", "Unable to delete color palette"); + + if (!key.startsWith("user-")) return Status(Status::FAIL, dd, errorTitle, QCoreApplication::translate("Theming", "This palette is built-in and cannot be deleted.")); + + QString destPath = userPaletteDir().filePath(QString("%1.conf").arg(key.mid(5))); + dd << QString("Palette path: %1").arg(destPath); + QFile paletteFile(destPath); + if (!paletteFile.exists() || paletteFile.remove()) + { + return Status::OK; + } + return Status(Status::FAIL, dd, errorTitle, QCoreApplication::translate("Theming", "Unable to delete palette file.")); +} + +/** + * Convenience function to get the user palette dir. + * + * @return The path to the directory to write user added palettes to. + */ +const QDir Theming::userPaletteDir() +{ + QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + dir.cd("theme_palettes"); + + return dir; +} + +ThemeColorPalette::ThemeColorPalette(const QString& filePath) +{ + loadFromFile(filePath); +} + +/** + * Create a QPalette instance from a .conf palette file. + * + * The file format must be in the INI format. It should have a ColorScheme group with + * active_colors, disabled_colors, and inactive_colors keys. Each should have at least 20 string list values + * corresponding to colors (typically in a hexadecimal RGBA format, ex #ff424245). + * + * @param filename The path to the .conf file to load. + * @return A QPalette instance with the color set from the .conf or nullptr if the file cannot be loaded or is not valid. + */ +bool ThemeColorPalette::loadFromFile(const QString& filePath) +{ + m_valid = tryLoad(filePath); + return m_valid; +} + +/** + * Sets the id but marks it as invalid. + * + * This function can be used to create a palette with a desired id + * without a valid file path. + * + * @param id + */ +void ThemeColorPalette::setInvalidWithId(const QString& id) +{ + m_valid = false; + m_mode = Mode::Unknown; + if (id.contains('-')) + { + m_filePath = QString("%1.conf").arg(id); + m_displayName = id.mid(id.indexOf('-')); + } + else + { + m_filePath = QString("user-%1.conf").arg(id); + m_displayName = id; + } +} + +QString ThemeColorPalette::id() const +{ + QFileInfo fileInfo(m_filePath); + return fileInfo.baseName().prepend(isBuiltIn() ? "builtin-" : "user-"); +} + +bool ThemeColorPalette::isDark() const +{ + if (!m_valid) return false; + + if (m_mode == Mode::Unknown) + { + // Guess the mode based on the lightness of the background color + QColor backgroundColor = m_palette.color(QPalette::Normal, QPalette::Window); + return backgroundColor.lightnessF() < 0.5; + } + return m_mode == Mode::Dark; +} + +/** + * Private implementation of loadFromFile. + * + * @see ThemeColorPalette::loadFromFile + */ +bool ThemeColorPalette::tryLoad(const QString& filePath) +{ + m_valid = false; + m_filePath = filePath; + + QFileInfo fileInfo(filePath); + m_displayName = fileInfo.baseName(); + if (!filePath.endsWith(".conf")) return false; + if (fileInfo.baseName().isEmpty()) return false; + //if (!fileInfo.isFile()) return false; + if (!fileInfo.isReadable()) return false; + + QSettings conf(filePath, QSettings::IniFormat); + + conf.beginGroup("Metadata"); + m_displayName = conf.value("DisplayName", m_displayName).toString(); + + QString modeStr = conf.value("LightOrDark").toString().toLower(); + if (modeStr == "light") m_mode = Mode::Light; + else if (modeStr == "dark") m_mode = Mode::Dark; + else m_mode = Mode::Unknown; + + conf.endGroup(); + + conf.beginGroup("ColorScheme"); + QList> colorGroups = { + { QPalette::Active, "active_colors" }, + { QPalette::Disabled, "disabled_colors" }, + { QPalette::Inactive, "inactive_colors" } + }; + m_palette = QPalette(); + for (const auto& colorGroup : colorGroups) + { + QStringList colors = conf.value(colorGroup.second).toStringList(); + // 20 is QPalette::NColorRoles prior to Qt 5.12, and is the minimum number of colors required for this format. + if (colors.count() < 20) return false; + + for (int i = 0; i < qMin(colors.count(), static_cast(QPalette::NColorRoles)); i++) + { + m_palette.setColor(colorGroup.first, QPalette::ColorRole(i), colors[i]); + } + } + conf.endGroup(); + + return true; +} diff --git a/core_lib/src/util/theming.h b/core_lib/src/util/theming.h new file mode 100644 index 0000000000..4af6ac8f2c --- /dev/null +++ b/core_lib/src/util/theming.h @@ -0,0 +1,62 @@ +#ifndef THEMING_H +#define THEMING_H + +#include + +class QDir; +class QStyle; + +class Status; +class ThemeColorPalette; + +class Theming +{ +public: + static QStringList availableStyles(); + static QList availablePalettes(); + + static QStyle* getStyle(const QString& key); + static ThemeColorPalette getPalette(const QString& key); + + static std::pair addPalette(const QString& filePath); + static Status removePalette(const QString& key); + +private: + static QPalette* loadPaletteConf(const QString& filename); + + static const QDir userPaletteDir(); +}; + +class ThemeColorPalette +{ +public: + ThemeColorPalette() {} + ThemeColorPalette(const QString& filePath); + + bool loadFromFile(const QString& filePath); + void setInvalidWithId(const QString& id); + + bool isValid() const { return m_valid; } + QString id() const; + QString displayName() const { return m_displayName; } + QPalette palette() const { return m_valid ? m_palette : QPalette(); } + bool isDark() const; + bool isBuiltIn() const { return m_filePath.startsWith(':'); } + +private: + enum Mode { + Light, + Dark, + Unknown + }; + + bool tryLoad(const QString& filePath); + + bool m_valid = false; + QString m_filePath; + QString m_displayName; + QPalette m_palette; + Mode m_mode; +}; + +#endif // THEMING_H