/*********************************************************************** * * Copyright (C) 2014-2018 wereturtle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ***********************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ThemeFactory.h" #include "AppSettings.h" #define GW_EDITOR_ASPECT "Editor/aspect" #define GW_EDITOR_BACKGROUND_COLOR "Editor/backgroundColor" #define GW_EDITOR_BACKGROUND_OPACITY "Editor/backgroundOpacity" #define GW_SPELLING_ERROR_COLOR "Editor/spellingErrorColor" #define GW_DEFAULT_TEXT_COLOR "Markdown/defaultTextColor" #define GW_MARKUP_COLOR "Markdown/markupColor" #define GW_LINK_COLOR "Markdown/linkColor" #define GW_HEADING_COLOR "Markdown/headingColor" #define GW_EMPHASIS_COLOR "Markdown/emphasisColor" #define GW_BLOCKQUOTE_COLOR "Markdown/blockquoteColor" #define GW_CODE_COLOR "Markdown/codeColor" #define GW_IMAGE_ASPECT "Background/imageAspect" #define GW_IMAGE_URL "Background/imageUrl" #define GW_BACKGROUND_COLOR "Background/color" #define GW_HUD_FOREGROUND_COLOR "HUD/foregroundColor" #define GW_HUD_BACKGROUND_COLOR "HUD/backgroundColor" const QString ThemeFactory::CLASSIC_LIGHT_THEME_NAME = QString("Classic Light"); const QString ThemeFactory::CLASSIC_DARK_THEME_NAME = QString("Classic Dark"); const QString ThemeFactory::PLAINSTRACTION_LIGHT_THEME_NAME = QString("Plainstraction Light"); const QString ThemeFactory::PLAINSTRACTION_DARK_THEME_NAME = QString("Plainstraction Dark"); ThemeFactory* ThemeFactory::instance = NULL; ThemeFactory* ThemeFactory::getInstance() { if (NULL == instance) { instance = new ThemeFactory(); } return instance; } ThemeFactory::~ThemeFactory() { ; } void ThemeFactory::loadClassicLightTheme() { Theme theme(CLASSIC_LIGHT_THEME_NAME); theme.setBackgroundColor("#eff0f1"); theme.setBackgroundImageAspect(PictureAspectNone); theme.setHudBackgroundColor("#31363b"); theme.setHudForegroundColor("#eff0f1"); theme.setEditorAspect(EditorAspectStretch); theme.setEditorBackgroundColor("#eff0f1"); theme.setDefaultTextColor("#31363b"); theme.setMarkupColor("#bdc3c7"); theme.setLinkColor("#2980b9"); theme.setHeadingColor("#31363b"); theme.setEmphasisColor("#31363b"); theme.setBlockquoteColor("#31363b"); theme.setCodeColor("#31363b"); theme.setSpellingErrorColor("#da4453"); theme.setBuiltIn(true); builtInThemes.append(theme); } void ThemeFactory::loadClassicDarkTheme() { Theme theme(CLASSIC_DARK_THEME_NAME); theme.setBackgroundColor("#151719"); theme.setBackgroundImageAspect(PictureAspectNone); theme.setHudBackgroundColor("#152F3D"); theme.setHudForegroundColor("#bdc3c7"); theme.setEditorAspect(EditorAspectStretch); theme.setEditorBackgroundColor("#151719"); theme.setDefaultTextColor("#bdc3c7"); theme.setMarkupColor("#575b5f"); theme.setLinkColor("#5f8eb1"); theme.setHeadingColor("#bdc3c7"); theme.setEmphasisColor("#bdc3c7"); theme.setBlockquoteColor("#bdc3c7"); theme.setCodeColor("#bdc3c7"); theme.setSpellingErrorColor("#da4453"); theme.setBuiltIn(true); builtInThemes.append(theme); } void ThemeFactory::loadPlainstractionLightTheme() { Theme theme(PLAINSTRACTION_LIGHT_THEME_NAME); theme.setBackgroundColor("#ffffff"); theme.setBackgroundImageAspect(PictureAspectNone); theme.setHudBackgroundColor("#eff0f1"); theme.setHudForegroundColor("#009bc8"); theme.setEditorAspect(EditorAspectStretch); theme.setEditorBackgroundColor("#ffffff"); theme.setDefaultTextColor("#1d2023"); theme.setMarkupColor("#c4e4f1"); theme.setLinkColor("#c23184"); theme.setHeadingColor("#009bc8"); theme.setEmphasisColor("#009bc8"); theme.setBlockquoteColor("#009bc8"); theme.setCodeColor("#009bc8"); theme.setSpellingErrorColor("#c23184"); theme.setBuiltIn(true); builtInThemes.append(theme); } void ThemeFactory::loadPlainstractionDarkTheme() { Theme theme(PLAINSTRACTION_DARK_THEME_NAME); theme.setBackgroundColor("#141414"); theme.setBackgroundImageAspect(PictureAspectNone); theme.setHudBackgroundColor("#1e262d"); theme.setHudForegroundColor("#009bc8"); theme.setEditorAspect(EditorAspectStretch); theme.setEditorBackgroundColor("#141414"); theme.setDefaultTextColor("#c7cfd0"); theme.setMarkupColor("#074051"); theme.setLinkColor("#b31771"); theme.setHeadingColor("#009bc8"); theme.setEmphasisColor("#009bc8"); theme.setBlockquoteColor("#009bc8"); theme.setCodeColor("#009bc8"); theme.setSpellingErrorColor("#b31771"); theme.setBuiltIn(true); builtInThemes.append(theme); } QStringList ThemeFactory::getAvailableThemes() const { QStringList themeNames; foreach (Theme theme, builtInThemes) { themeNames.append(theme.getName()); } themeNames << customThemeNames; return themeNames; } Theme ThemeFactory::getDefaultTheme() const { QString err; return loadTheme(CLASSIC_LIGHT_THEME_NAME, err); } Theme ThemeFactory::loadTheme(const QString& name, QString& err) const { err = QString(); for (int i = 0; i < builtInThemes.size(); i++) { if (builtInThemes[i].getName() == name) { return builtInThemes[i]; } } if (customThemeNames.contains(name)) { QString themeFilePath = themeDirectoryPath + QString("/") + name + QString("/theme.cfg"); QFileInfo themeFileInfo(themeFilePath); if (!themeFileInfo.exists()) { err = QObject::tr("The specified theme does not exist in the file system: ") + themeFilePath; return builtInThemes[0]; } QSettings themeSettings(themeFilePath, QSettings::IniFormat); int editorBackgroundAlpha; int editorAspect; int backgroundImageAspect; QString backgroundImageUrl; QColor editorBackgroundColor; QColor spellingErrorColor; QColor backgroundColor; QColor hudForegroundColor; QColor hudBackgroundColor; QColor defaultTextColor; QColor markupColor; QColor linkColor; QColor headingColor; QColor emphasisColor; QColor blockquoteColor; QColor codeColor; // Load and validate the theme contents. if ( !extractIntSetting(themeSettings, GW_EDITOR_ASPECT, editorAspect, EditorAspectFirst, EditorAspectLast, err) || !extractColorSetting(themeSettings, GW_EDITOR_BACKGROUND_COLOR, editorBackgroundColor, err) || !extractIntSetting(themeSettings, GW_EDITOR_BACKGROUND_OPACITY, editorBackgroundAlpha, 0, 255, err) || !extractColorSetting(themeSettings, GW_SPELLING_ERROR_COLOR, spellingErrorColor, err) || !extractIntSetting(themeSettings, GW_IMAGE_ASPECT, backgroundImageAspect, PictureAspectFirst, PictureAspectLast, err) || !extractColorSetting(themeSettings, GW_BACKGROUND_COLOR, backgroundColor, err) || !extractColorSetting(themeSettings, GW_HUD_FOREGROUND_COLOR, hudForegroundColor, err) || !extractColorSetting(themeSettings, GW_HUD_BACKGROUND_COLOR, hudBackgroundColor, err) || !extractColorSetting(themeSettings, GW_DEFAULT_TEXT_COLOR, defaultTextColor, err) || !extractColorSetting(themeSettings, GW_MARKUP_COLOR, markupColor, err) || !extractColorSetting(themeSettings, GW_LINK_COLOR, linkColor, err) ) { // Return error. return builtInThemes[0]; } // If theme colors added after 1.5.0 are missing, clear the error message and set // default colors for backwards compatiblity with older themes. // if ( !extractColorSetting(themeSettings, GW_HEADING_COLOR, headingColor, err) || !extractColorSetting(themeSettings, GW_EMPHASIS_COLOR, emphasisColor, err) || !extractColorSetting(themeSettings, GW_BLOCKQUOTE_COLOR, blockquoteColor, err) || !extractColorSetting(themeSettings, GW_CODE_COLOR, codeColor, err) ) { err = QString(); headingColor = defaultTextColor; emphasisColor = defaultTextColor; blockquoteColor = defaultTextColor; codeColor = defaultTextColor; } Theme theme; theme.setEditorAspect((EditorAspect) editorAspect); editorBackgroundColor.setAlpha(editorBackgroundAlpha); theme.setEditorBackgroundColor(editorBackgroundColor); theme.setSpellingErrorColor(spellingErrorColor); theme.setBackgroundImageAspect((PictureAspect) backgroundImageAspect); theme.setBackgroundColor(backgroundColor); theme.setHudForegroundColor(hudForegroundColor); theme.setHudBackgroundColor(hudBackgroundColor); theme.setDefaultTextColor(defaultTextColor); theme.setMarkupColor(markupColor); theme.setLinkColor(linkColor); theme.setHeadingColor(headingColor); theme.setEmphasisColor(emphasisColor); theme.setBlockquoteColor(blockquoteColor); theme.setCodeColor(codeColor); // Check if the picture aspect is set to be a background image (not None), that // the background image URL is set. // if ( (PictureAspectNone != theme.getBackgroundImageAspect()) && !extractStringSetting(themeSettings, GW_IMAGE_URL, backgroundImageUrl, err) ) { // Return error. return builtInThemes[0]; } theme.setBackgroundImageUrl(backgroundImageUrl); theme.setName(name); QFileInfo imgFileInfo(theme.getBackgroundImageUrl()); // If the background image file path is relative and located in the same directory // as the theme file, then fill out the full path for the the background image URL. // if ( !theme.getBackgroundImageUrl().isNull() && !theme.getBackgroundImageUrl().isEmpty() && imgFileInfo.isRelative() ) { theme.setBackgroundImageUrl(getDirectoryForTheme(name).path() + QString("/") + theme.getBackgroundImageUrl()); } // Return success. err = QString(); return theme; } else { err = QObject::tr("The specified theme is not available. Try restarting the application. " "If problem persists, please file a bug report."); return builtInThemes[0]; } } void ThemeFactory::deleteTheme(const QString& name, QString& err) { err = QString(); QDir themeDir = this->getDirectoryForTheme(name); if (themeDir.exists()) { // Traverse the the directory structure depth-first, and slowly build a stack of // files to delete. The reason we build a stack of these files is because // files in a directory must be deleted before their parent directory is // deleted. // QStack dirTraverseStack; QStack fileDeleteStack; dirTraverseStack.push(QFileInfo(themeDir.path())); fileDeleteStack.push(QFileInfo(themeDir.path())); // Traverse the directory structure. while (!dirTraverseStack.isEmpty()) { QFileInfo fileInfo = dirTraverseStack.pop(); if (fileInfo.isDir()) { QDir dir(fileInfo.filePath()); QFileInfoList fileList = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst); // Add files in the current directory to the file deletion stack. for (QFileInfoList::iterator fileIter = fileList.begin(); fileIter != fileList.end(); fileIter++) { fileDeleteStack.push(*fileIter); // If file is a directory, add it to the traversal stack so that we // visit it. // if (fileIter->isDir()) { dirTraverseStack.push(*fileIter); } } } } // Pop files from the file deletion stack and delete them. while (!fileDeleteStack.isEmpty()) { bool result; QFileInfo fileInfo = fileDeleteStack.pop(); if (fileInfo.isDir()) { QDir dir(fileInfo.absoluteFilePath()); result = dir.rmdir(fileInfo.absoluteFilePath()); } else { result = QFile::remove(fileInfo.absoluteFilePath()); } if (!result) { err = QObject::tr("Could not delete %1 from theme. Please try setting the theme file permissions to be writeable.") .arg(fileInfo.filePath()); return; } } } // Finally, remove the theme from the available themes list. customThemeNames.removeOne(name); } void ThemeFactory::saveTheme(const QString& name, Theme& theme, QString& err) { err = QString(); // Check if theme was renamed. If so, rename the theme's directory. if (theme.getName() != name) { // Check for rename to default name. for (int i = 0; i < builtInThemes.size(); i++) { if (theme.getName() == builtInThemes[i].getName()) { err = QObject::tr("'%1' already exists. Please choose another name.").arg(theme.getName()); return; } } QDir oldThemeDir = this->getDirectoryForTheme(name); QDir newThemeDir = this->getDirectoryForTheme(theme.getName()); if (newThemeDir.exists()) { err = QObject::tr("'%1' theme already exists. Please choose another name.").arg(theme.getName()); return; } if (oldThemeDir.exists()) { QString backgroundImageUrl = theme.getBackgroundImageUrl(); // If there's a background image, set the image URL to be the in the // new theme directory to which we are renaming the theme. // if ( (PictureAspectNone != theme.getBackgroundImageAspect()) && !theme.getBackgroundImageUrl().isNull() && !theme.getBackgroundImageUrl().isEmpty() ) { QFileInfo oldImgFileInfo(theme.getBackgroundImageUrl()); if (oldImgFileInfo.dir() == oldThemeDir) { backgroundImageUrl = this->getDirectoryForTheme(theme.getName()).path() + QString("/") + oldImgFileInfo.fileName(); } } // Rename the theme directory. QDir themeLocDir(this->themeDirectoryPath); bool success = themeLocDir.rename(name, theme.getName()); if (!success) { err = QObject::tr("Failed to rename theme. Please check file permissions."); return; } theme.setBackgroundImageUrl(backgroundImageUrl); // Now update the available themes list with the new theme name. for (int i = 0; i < customThemeNames.size(); i++) { if (name == customThemeNames[i]) { customThemeNames[i] = theme.getName(); qSort(customThemeNames.begin(), customThemeNames.end()); break; } } } } bool isNewTheme = false; if (!this->getDirectoryForTheme(theme.getName()).exists()) { isNewTheme = true; } QString themeFilePath = themeDirectoryPath + QString("/") + theme.getName() + QString("/theme.cfg"); // Load original theme settings so we can clean up the old background image // later, if any. Theme oldTheme; bool oldThemeExists = true; oldTheme = this->loadTheme(theme.getName(), err); if (!err.isNull()) { oldThemeExists = false; err = QString(); } QSettings settings(themeFilePath, QSettings::IniFormat); if (!settings.isWritable()) { err = QObject::tr("Theme is read-only. Please try renaming the theme, or setting the theme file to be writable."); return; } settings.setValue(GW_EDITOR_ASPECT, QVariant((int) theme.getEditorAspect())); settings.setValue(GW_EDITOR_BACKGROUND_COLOR, QVariant(theme.getEditorBackgroundColor().name())); settings.setValue(GW_EDITOR_BACKGROUND_OPACITY, QVariant(theme.getEditorBackgroundColor().alpha())); settings.setValue(GW_SPELLING_ERROR_COLOR, QVariant(theme.getSpellingErrorColor().name())); settings.setValue(GW_DEFAULT_TEXT_COLOR, QVariant(theme.getDefaultTextColor().name())); settings.setValue(GW_MARKUP_COLOR, QVariant(theme.getMarkupColor().name())); settings.setValue(GW_LINK_COLOR, QVariant(theme.getLinkColor().name())); settings.setValue(GW_HEADING_COLOR, QVariant(theme.getHeadingColor().name())); settings.setValue(GW_EMPHASIS_COLOR, QVariant(theme.getEmphasisColor().name())); settings.setValue(GW_BLOCKQUOTE_COLOR, QVariant(theme.getBlockquoteColor().name())); settings.setValue(GW_CODE_COLOR, QVariant(theme.getCodeColor().name())); settings.setValue(GW_IMAGE_ASPECT, QVariant((int) theme.getBackgroundImageAspect())); settings.setValue(GW_BACKGROUND_COLOR, QVariant(theme.getBackgroundColor().name())); settings.setValue(GW_HUD_FOREGROUND_COLOR, QVariant(theme.getHudForegroundColor().name())); settings.setValue(GW_HUD_BACKGROUND_COLOR, QVariant(theme.getHudBackgroundColor().name())); QDir themeDir = this->getDirectoryForTheme(theme.getName()); QFileInfo imgFileInfo(theme.getBackgroundImageUrl()); // Delete any old background images. if (oldThemeExists && (PictureAspectNone != oldTheme.getBackgroundImageAspect())) { QFileInfo oldImgFileInfo(oldTheme.getBackgroundImageUrl()); if ( oldImgFileInfo.exists() && (oldImgFileInfo.dir() == themeDir) && (imgFileInfo != oldImgFileInfo) ) { QFile oldImgFile(oldTheme.getBackgroundImageUrl()); bool result = oldImgFile.remove(); if (!result) { err = QObject::tr("Failed to remove old theme image. Please check file permissions."); return; } } } // If there's a background image, copy the image to the theme's directory, and set the background image URL // to point to it. // if ( (PictureAspectNone != theme.getBackgroundImageAspect()) && !theme.getBackgroundImageUrl().isNull() && !theme.getBackgroundImageUrl().isEmpty() ) { QFileInfo imgCopyFileInfo(themeDir.path() + QString("/") + imgFileInfo.fileName()); // Copy the image to the theme directory if it's not already there. if (themeDir != imgFileInfo.dir()) { // If there's an image in the theme folder that has the same file name as the // source image that is being copied, then delete that image first. // if (imgCopyFileInfo.exists()) { QFile imgCopyFile(imgCopyFileInfo.filePath()); bool result = imgCopyFile.remove(); if (!result) { err = QObject::tr("The old theme image file could not be removed from the theme directory. Please check file permissions."); settings.setValue("Background/imageAspect", QVariant((int) PictureAspectNone)); return; } } // Now copy the new image file to the theme directory. QFile imgFile(theme.getBackgroundImageUrl()); bool result = imgFile.copy(imgCopyFileInfo.filePath()); if (!result) { err = QObject::tr("Theme image file could not be copied to the theme directory. Please check file permissions."); settings.setValue(GW_IMAGE_ASPECT, QVariant((int) PictureAspectNone)); return; } else { settings.setValue(GW_IMAGE_URL, QVariant(imgCopyFileInfo.fileName())); } } else { settings.setValue(GW_IMAGE_URL, QVariant(imgCopyFileInfo.fileName())); } } settings.sync(); if (QSettings::NoError != settings.status()) { err = QObject::tr("Theme could not be saved to disk. Please check file permissions or try renaming the theme."); return; } if (isNewTheme) { customThemeNames.append(theme.getName()); qSort(customThemeNames.begin(), customThemeNames.end()); } } QDir ThemeFactory::getThemeDirectory() const { return themeDirectory; } QDir ThemeFactory::getDirectoryForTheme(const QString& name) const { QDir themeDir(this->themeDirectoryPath + QString("/") + name); return themeDir.path(); } QString ThemeFactory::generateUntitledThemeName() const { QString name = QObject::tr("Untitled 1"); int count = 1; bool availableNameFound = false; while (!availableNameFound) { bool tryAgain = false; for (int i = 0; i < this->customThemeNames.size(); i++) { if (name == this->customThemeNames[i]) { count++; name = QObject::tr("Untitled %1").arg(count); tryAgain = true; break; } } if (!tryAgain) { availableNameFound = true; } } return name; } ThemeFactory::ThemeFactory() { themeDirectoryPath = AppSettings::getInstance()->getThemeDirectoryPath(); themeDirectory = QDir(themeDirectoryPath); if (!themeDirectory.exists()) { themeDirectory.mkpath(themeDirectory.path()); } themeDirectory.setFilter(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Writable); themeDirectory.setSorting(QDir::Name); QFileInfoList themeFiles = themeDirectory.entryInfoList(); QMap sortedThemes; for (int i = 0; i < themeFiles.size(); i++) { QString baseName = themeFiles[i].fileName(); sortedThemes.insertMulti(baseName.toLower(), baseName); } customThemeNames = sortedThemes.values(); // Set up built-in themes. loadClassicLightTheme(); loadClassicDarkTheme(); loadPlainstractionLightTheme(); loadPlainstractionDarkTheme(); } bool ThemeFactory::extractColorSetting ( const QSettings& settings, const QString& key, QColor& value, QString& err ) const { QVariant variantValue = settings.value(key); if (variantValue.isNull() || !variantValue.isValid() || !variantValue.canConvert(QVariant::Color)) { err = QObject::tr("Invalid or missing value for %1 provided.").arg(key); return false; } value = variantValue.value(); return true; } bool ThemeFactory::extractIntSetting ( const QSettings& settings, const QString& key, int& value, int min, int max, QString& err ) const { QVariant variantValue = settings.value(key); if (variantValue.isNull() || !variantValue.isValid() || !variantValue.canConvert(QVariant::Int)) { err = QObject::tr("Invalid or missing value for %1 provided.").arg(key); return false; } value = variantValue.toInt(); if ((value < min) || (value > max)) { err = QObject::tr("Value for %1 is out of range. Valid values are between %2 and %3, inclusive."); err = err.arg(key).arg(min).arg(max); return false; } return true; } bool ThemeFactory::extractStringSetting ( const QSettings& settings, const QString& key, QString& value, QString& err ) const { QVariant variantValue = settings.value(key); if (variantValue.isNull() || !variantValue.isValid() || !variantValue.canConvert(QVariant::String)) { err = QObject::tr("Invalid or missing value for %1 provided.").arg(key); return false; } value = variantValue.toString(); return true; }