/***********************************************************************
*
* 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;
}