Upload saved theme to the cloud.

This commit is contained in:
John Preston 2019-09-03 11:25:19 +03:00
parent 4b045a602c
commit 229bc56cc8
15 changed files with 834 additions and 421 deletions

View File

@ -1603,6 +1603,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_theme_editor_done" = "Theme exported successfully!";
"lng_theme_editor_title" = "Edit color palette";
"lng_theme_editor_export_button" = "Export theme";
"lng_theme_editor_save_button" = "Save theme";
"lng_theme_editor_create_title" = "Create theme";
"lng_theme_editor_name" = "Theme name";

View File

@ -27,10 +27,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/image/image.h"
#include "ui/image/image_source.h"
#include "lang/lang_keys.h"
#include "window/themes/window_theme_editor.h"
#include "window/themes/window_theme.h"
#include "window/themes/window_themes_embedded.h"
#include "window/themes/window_theme_create_box.h"
#include "window/themes/window_theme_editor_box.h"
#include "window/window_session_controller.h"
#include "info/profile/info_profile_button.h"
#include "storage/localstorage.h"
@ -43,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "support/support_templates.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "styles/style_settings.h"
#include "styles/style_boxes.h"
@ -1264,7 +1264,9 @@ void SetupThemeOptions(
&st::settingsIconThemes,
st::settingsChatIconLeft
)->addClickHandler([=] {
Ui::show(Box(Window::Theme::CreateBox, &controller->session()));
Ui::show(Box(
Window::Theme::CreateBox,
&controller->window()->controller()));
});
AddSkip(container);

View File

@ -76,7 +76,7 @@ struct Uploader::File {
Uploader::File::File(const SendMediaReady &media) : media(media) {
partsCount = media.parts.size();
if (type() == SendMediaType::File
|| type() == SendMediaType::WallPaper
|| type() == SendMediaType::ThemeFile
|| type() == SendMediaType::Audio) {
setDocSize(media.file.isEmpty()
? media.data.size()
@ -92,7 +92,7 @@ Uploader::File::File(const std::shared_ptr<FileLoadResult> &file)
? file->fileparts.size()
: file->thumbparts.size();
if (type() == SendMediaType::File
|| type() == SendMediaType::WallPaper
|| type() == SendMediaType::ThemeFile
|| type() == SendMediaType::Audio) {
setDocSize(file->filesize);
} else {
@ -154,7 +154,7 @@ void Uploader::uploadMedia(
if (media.type == SendMediaType::Photo) {
Auth().data().processPhoto(media.photo, media.photoThumbs);
} else if (media.type == SendMediaType::File
|| media.type == SendMediaType::WallPaper
|| media.type == SendMediaType::ThemeFile
|| media.type == SendMediaType::Audio) {
const auto document = media.photoThumbs.empty()
? Auth().data().processDocument(media.document)
@ -163,7 +163,7 @@ void Uploader::uploadMedia(
base::duplicate(media.photoThumbs.front().second));
if (!media.data.isEmpty()) {
document->setData(media.data);
if (media.type == SendMediaType::WallPaper) {
if (media.type == SendMediaType::ThemeFile) {
document->checkWallPaperProperties();
}
if (document->saveToCache()
@ -193,7 +193,7 @@ void Uploader::upload(
photo->uploadingData = std::make_unique<Data::UploadState>(
file->partssize);
} else if (file->type == SendMediaType::File
|| file->type == SendMediaType::WallPaper
|| file->type == SendMediaType::ThemeFile
|| file->type == SendMediaType::Audio) {
const auto document = file->thumb.isNull()
? Auth().data().processDocument(file->document)
@ -207,7 +207,7 @@ void Uploader::upload(
std::move(file->goodThumbnailBytes));
if (!file->content.isEmpty()) {
document->setData(file->content);
if (file->type == SendMediaType::WallPaper) {
if (file->type == SendMediaType::ThemeFile) {
document->checkWallPaperProperties();
}
if (document->saveToCache()
@ -233,7 +233,7 @@ void Uploader::currentFailed() {
if (j->second.type() == SendMediaType::Photo) {
_photoFailed.fire_copy(j->first);
} else if (j->second.type() == SendMediaType::File
|| j->second.type() == SendMediaType::WallPaper
|| j->second.type() == SendMediaType::ThemeFile
|| j->second.type() == SendMediaType::Audio) {
const auto document = Auth().data().document(j->second.id());
if (document->uploading()) {
@ -335,7 +335,7 @@ void Uploader::sendNext() {
MTP_bytes(md5));
_photoReady.fire({ uploadingId, options, file, edit });
} else if (uploadingData.type() == SendMediaType::File
|| uploadingData.type() == SendMediaType::WallPaper
|| uploadingData.type() == SendMediaType::ThemeFile
|| uploadingData.type() == SendMediaType::Audio) {
QByteArray docMd5(32, Qt::Uninitialized);
hashMd5Hex(uploadingData.md5Hash.result(), docMd5.data());
@ -412,7 +412,7 @@ void Uploader::sendNext() {
* uploadingData.docPartSize;
toSend = content.mid(offset, uploadingData.docPartSize);
if ((uploadingData.type() == SendMediaType::File
|| uploadingData.type() == SendMediaType::WallPaper
|| uploadingData.type() == SendMediaType::ThemeFile
|| uploadingData.type() == SendMediaType::Audio)
&& uploadingData.docSentParts <= kUseBigFilesFrom) {
uploadingData.md5Hash.feed(toSend.constData(), toSend.size());
@ -554,7 +554,7 @@ void Uploader::partLoaded(const MTPBool &result, mtpRequestId requestId) {
}
_photoProgress.fire_copy(fullId);
} else if (file.type() == SendMediaType::File
|| file.type() == SendMediaType::WallPaper
|| file.type() == SendMediaType::ThemeFile
|| file.type() == SendMediaType::Audio) {
const auto document = Auth().data().document(file.id());
if (document->uploading()) {

View File

@ -243,68 +243,6 @@ SendMediaReady PreparePeerPhoto(PeerId peerId, QImage &&image) {
0);
}
SendMediaReady PrepareWallPaper(const QImage &image) {
PreparedPhotoThumbs thumbnails;
QVector<MTPPhotoSize> sizes;
QByteArray jpeg;
QBuffer jpegBuffer(&jpeg);
image.save(&jpegBuffer, "JPG", 87);
const auto scaled = [&](int size) {
return image.scaled(
size,
size,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
};
const auto push = [&](const char *type, QImage &&image) {
sizes.push_back(MTP_photoSize(
MTP_string(type),
MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)),
MTP_int(image.width()),
MTP_int(image.height()), MTP_int(0)));
thumbnails.emplace(type[0], std::move(image));
};
push("s", scaled(320));
const auto filename = qsl("wallpaper.jpg");
auto attributes = QVector<MTPDocumentAttribute>(
1,
MTP_documentAttributeFilename(MTP_string(filename)));
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(image.width()),
MTP_int(image.height())));
const auto id = rand_value<DocumentId>();
const auto document = MTP_document(
MTP_flags(0),
MTP_long(id),
MTP_long(0),
MTP_bytes(),
MTP_int(base::unixtime::now()),
MTP_string("image/jpeg"),
MTP_int(jpeg.size()),
MTP_vector<MTPPhotoSize>(sizes),
MTP_int(MTP::maindc()),
MTP_vector<MTPDocumentAttribute>(attributes));
return SendMediaReady(
SendMediaType::WallPaper,
QString(), // filepath
filename,
jpeg.size(),
jpeg,
id,
0,
QString(),
PeerId(),
MTP_photoEmpty(MTP_long(0)),
thumbnails,
document,
QByteArray(),
0);
}
TaskQueue::TaskQueue(crl::time stopTimeoutMs) {
if (stopTimeoutMs > 0) {
_stopTimer = new QTimer(this);

View File

@ -21,7 +21,7 @@ enum class SendMediaType {
Photo,
Audio,
File,
WallPaper,
ThemeFile,
Secure,
};
@ -85,7 +85,6 @@ struct SendMediaReady {
};
SendMediaReady PreparePeerPhoto(PeerId peerId, QImage &&image);
SendMediaReady PrepareWallPaper(const QImage &image);
using TaskId = void*; // no interface, just id

View File

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/file_upload.h"
#include "base/parse_helper.h"
#include "base/zlib_help.h"
#include "base/unixtime.h"
#include "data/data_session.h"
#include "main/main_account.h" // Account::sessionValue.
#include "ui/image/image.h"
@ -395,6 +396,68 @@ void ClearApplying() {
GlobalApplying = Applying();
}
SendMediaReady PrepareWallPaper(const QImage &image) {
PreparedPhotoThumbs thumbnails;
QVector<MTPPhotoSize> sizes;
QByteArray jpeg;
QBuffer jpegBuffer(&jpeg);
image.save(&jpegBuffer, "JPG", 87);
const auto scaled = [&](int size) {
return image.scaled(
size,
size,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
};
const auto push = [&](const char *type, QImage &&image) {
sizes.push_back(MTP_photoSize(
MTP_string(type),
MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)),
MTP_int(image.width()),
MTP_int(image.height()), MTP_int(0)));
thumbnails.emplace(type[0], std::move(image));
};
push("s", scaled(320));
const auto filename = qsl("wallpaper.jpg");
auto attributes = QVector<MTPDocumentAttribute>(
1,
MTP_documentAttributeFilename(MTP_string(filename)));
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(image.width()),
MTP_int(image.height())));
const auto id = rand_value<DocumentId>();
const auto document = MTP_document(
MTP_flags(0),
MTP_long(id),
MTP_long(0),
MTP_bytes(),
MTP_int(base::unixtime::now()),
MTP_string("image/jpeg"),
MTP_int(jpeg.size()),
MTP_vector<MTPPhotoSize>(sizes),
MTP_int(MTP::maindc()),
MTP_vector<MTPDocumentAttribute>(attributes));
return SendMediaReady(
SendMediaType::ThemeFile,
QString(), // filepath
filename,
jpeg.size(),
jpeg,
id,
0,
QString(),
PeerId(),
MTP_photoEmpty(MTP_long(0)),
thumbnails,
document,
QByteArray(),
0);
}
} // namespace
ChatBackground::AdjustableColor::AdjustableColor(style::color data)

View File

@ -1,148 +0,0 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "window/themes/window_theme_create_box.h"
#include "window/themes/window_theme.h"
#include "window/themes/window_theme_editor.h"
#include "boxes/generic_box.h"
#include "boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/labels.h"
#include "info/profile/info_profile_button.h"
#include "main/main_session.h"
#include "core/file_utilities.h"
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "mainwindow.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
#include "styles/style_boxes.h"
namespace Window {
namespace Theme {
namespace {
void ImportFromFile(
not_null<Main::Session*> session,
not_null<QWidget*> parent) {
const auto &imgExtensions = cImgExtensions();
auto filters = QStringList(
qsl("Theme files (*.tdesktop-theme *.tdesktop-palette)"));
filters.push_back(FileDialog::AllFilesFilter());
const auto callback = crl::guard(session, [=](
const FileDialog::OpenResult &result) {
if (result.paths.isEmpty()) {
return;
}
Window::Theme::Apply(result.paths.front());
});
FileDialog::GetOpenPath(
parent.get(),
tr::lng_choose_image(tr::now),
filters.join(qsl(";;")),
crl::guard(parent, callback));
}
QString BytesToUTF8(QLatin1String string) {
return QString::fromUtf8(string.data(), string.size());
}
void WriteDefaultPalette(const QString &path) {
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
LOG(("Theme Error: could not open '%1' for writing.").arg(path));
return;
}
QTextStream stream(&f);
stream.setCodec("UTF-8");
auto rows = style::main_palette::data();
for (const auto &row : std::as_const(rows)) {
stream
<< BytesToUTF8(row.name)
<< ": "
<< BytesToUTF8(row.value)
<< "; // "
<< BytesToUTF8(
row.description
).replace(
'\n',
' '
).replace(
'\r',
' ')
<< "\n";
}
}
void StartEditor(
not_null<Main::Session*> session,
const QString &title) {
const auto path = EditingPalettePath();
if (!Local::copyThemeColorsToPalette(path)) {
WriteDefaultPalette(path);
}
if (!Apply(path)) {
Ui::show(Box<InformBox>(tr::lng_theme_editor_error(tr::now)));
return;
}
KeepApplied();
if (const auto window = App::wnd()) {
window->showRightColumn(Box<Editor>());
}
}
} // namespace
void CreateBox(not_null<GenericBox*> box, not_null<Main::Session*> session) {
box->setTitle(tr::lng_theme_editor_create_title(Ui::Text::WithEntities));
const auto name = box->addRow(object_ptr<Ui::InputField>(
box,
st::defaultInputField,
tr::lng_theme_editor_name()));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_theme_editor_create_description(),
st::boxDividerLabel),
style::margins(
st::boxRowPadding.left(),
st::boxRowPadding.left(),
st::boxRowPadding.right(),
st::boxRowPadding.right()));
box->addRow(
object_ptr<Info::Profile::Button>(
box,
tr::lng_theme_editor_import_existing() | Ui::Text::ToUpper(),
st::createThemeImportButton),
style::margins()
)->addClickHandler([=] {
ImportFromFile(session, box);
});
box->setFocusCallback([=] { name->setFocusFast(); });
box->addButton(tr::lng_box_done(), [=] {
const auto title = name->getLastText().trimmed();
if (title.isEmpty()) {
name->showError();
return;
}
box->closeBox();
StartEditor(session, title);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace Theme
} // namespace Window

View File

@ -9,9 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/themes/window_theme.h"
#include "window/themes/window_theme_editor_block.h"
#include "window/themes/window_theme_editor_box.h"
#include "window/themes/window_themes_embedded.h"
#include "window/window_controller.h"
#include "main/main_account.h"
#include "mainwindow.h"
#include "layout.h"
#include "storage/localstorage.h"
#include "boxes/confirm_box.h"
#include "styles/style_window.h"
@ -20,9 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/multi_select.h"
#include "ui/image/image_prepare.h"
#include "ui/toast/toast.h"
#include "base/parse_helper.h"
#include "base/zlib_help.h"
@ -247,8 +247,9 @@ public:
}
void prepare();
Fn<void()> exportCallback();
[[nodiscard]] QByteArray paletteContent() const {
return _paletteContent;
}
void filterRows(const QString &query);
void chooseRow();
@ -293,34 +294,6 @@ private:
};
class ThemeExportBox : public BoxContent {
public:
ThemeExportBox(QWidget*, const QByteArray &paletteContent, const QImage &background, const QByteArray &backgroundContent, bool tileBackground);
protected:
void prepare() override;
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void updateThumbnail();
void chooseBackgroundFromFile();
void exportTheme();
QByteArray _paletteContent;
QImage _background;
QByteArray _backgroundContent;
bool _isPng = false;
QString _imageText;
QPixmap _thumbnail;
object_ptr<Ui::LinkButton> _chooseFromFile;
object_ptr<Ui::Checkbox> _tileBackground;
};
bool CopyColorsToPalette(
const QString &destination,
const QString &themePath,
@ -402,19 +375,6 @@ void Editor::Inner::prepare() {
}
}
Fn<void()> Editor::Inner::exportCallback() {
return App::LambdaDelayed(st::defaultRippleAnimation.hideDuration, this, [=] {
auto background = Background()->createCurrentImage();
auto backgroundContent = QByteArray();
auto tiled = Background()->tile();
{
QBuffer buffer(&backgroundContent);
background.save(&buffer, "JPG", 87);
}
Ui::show(Box<ThemeExportBox>(_paletteContent, background, backgroundContent, tiled));
});
}
void Editor::Inner::filterRows(const QString &query) {
if (query == ":sort-for-accent") {
sortByAccentDistance();
@ -637,150 +597,45 @@ void Editor::Inner::applyEditing(const QString &name, const QString &copyOf, QCo
_paletteContent = newContent;
}
ThemeExportBox::ThemeExportBox(QWidget*, const QByteArray &paletteContent, const QImage &background, const QByteArray &backgroundContent, bool tileBackground) : BoxContent()
, _paletteContent(paletteContent)
, _background(background)
, _backgroundContent(backgroundContent)
, _chooseFromFile(this, tr::lng_settings_bg_from_file(tr::now), st::boxLinkButton)
, _tileBackground(this, tr::lng_settings_bg_tile(tr::now), tileBackground, st::defaultBoxCheckbox) {
_imageText = tr::lng_theme_editor_saved_to_jpg(tr::now, lt_size, formatSizeText(_backgroundContent.size()));
_chooseFromFile->setClickedCallback([this] { chooseBackgroundFromFile(); });
}
//void ThemeExportBox::exportTheme() {
// App::CallDelayed(st::defaultRippleAnimation.hideDuration, this, [this] {
// auto caption = tr::lng_theme_editor_choose_name(tr::now);
// auto filter = "Themes (*.tdesktop-theme)";
// auto name = "awesome.tdesktop-theme";
// FileDialog::GetWritePath(this, caption, filter, name, crl::guard(this, [this](const QString &path) {
// QFile f(path);
// if (!f.open(QIODevice::WriteOnly)) {
// LOG(("Theme Error: could not open zip-ed theme file '%1' for writing").arg(path));
// Ui::show(Box<InformBox>(tr::lng_theme_editor_error(tr::now)));
// return;
// }
// if (f.write(result) != result.size()) {
// LOG(("Theme Error: could not write zip-ed theme to file '%1'").arg(path));
// Ui::show(Box<InformBox>(tr::lng_theme_editor_error(tr::now)));
// return;
// }
// Ui::hideLayer();
// Ui::Toast::Show(tr::lng_theme_editor_done(tr::now));
// }));
// });
//}
void ThemeExportBox::prepare() {
setTitle(tr::lng_theme_editor_background_image());
addButton(tr::lng_theme_editor_export(), [this] { exportTheme(); });
addButton(tr::lng_cancel(), [this] { closeBox(); });
auto height = st::themesSmallSkip + st::themesBackgroundSize + st::themesSmallSkip + _tileBackground->height();
setDimensions(st::boxWideWidth, height);
updateThumbnail();
}
void ThemeExportBox::paintEvent(QPaintEvent *e) {
BoxContent::paintEvent(e);
Painter p(this);
auto linkLeft = st::boxPadding.left() + st::themesBackgroundSize + st::themesSmallSkip;
p.setPen(st::boxTextFg);
p.setFont(st::boxTextFont);
p.drawTextLeft(linkLeft, st::themesSmallSkip, width(), _imageText);
p.drawPixmapLeft(st::boxPadding.left(), st::themesSmallSkip, width(), _thumbnail);
}
void ThemeExportBox::resizeEvent(QResizeEvent *e) {
auto linkLeft = st::boxPadding.left() + st::themesBackgroundSize + st::themesSmallSkip;
_chooseFromFile->moveToLeft(linkLeft, st::themesSmallSkip + st::boxTextFont->height + st::themesSmallSkip);
_tileBackground->moveToLeft(st::boxPadding.left(), st::themesSmallSkip + st::themesBackgroundSize + 2 * st::themesSmallSkip);
}
void ThemeExportBox::updateThumbnail() {
int32 size = st::themesBackgroundSize * cIntRetinaFactor();
QImage back(size, size, QImage::Format_ARGB32_Premultiplied);
back.setDevicePixelRatio(cRetinaFactor());
{
Painter p(&back);
PainterHighQualityEnabler hq(p);
auto &pix = _background;
int sx = (pix.width() > pix.height()) ? ((pix.width() - pix.height()) / 2) : 0;
int sy = (pix.height() > pix.width()) ? ((pix.height() - pix.width()) / 2) : 0;
int s = (pix.width() > pix.height()) ? pix.height() : pix.width();
p.drawImage(QRect(0, 0, st::themesBackgroundSize, st::themesBackgroundSize), pix, QRect(sx, sy, s, s));
}
Images::prepareRound(back, ImageRoundRadius::Small);
_thumbnail = App::pixmapFromImageInPlace(std::move(back));
_thumbnail.setDevicePixelRatio(cRetinaFactor());
update();
}
void ThemeExportBox::chooseBackgroundFromFile() {
FileDialog::GetOpenPath(this, tr::lng_theme_editor_choose_image(tr::now), "Image files (*.jpeg *.jpg *.png)", crl::guard(this, [this](const FileDialog::OpenResult &result) {
auto content = result.remoteContent;
if (!result.paths.isEmpty()) {
QFile f(result.paths.front());
if (f.open(QIODevice::ReadOnly)) {
content = f.readAll();
f.close();
}
}
if (!content.isEmpty()) {
auto format = QByteArray();
auto image = App::readImage(content, &format);
if (!image.isNull() && (format == "jpeg" || format == "jpg" || format == "png")) {
_background = image;
_backgroundContent = content;
_isPng = (format == "png");
auto sizeText = formatSizeText(_backgroundContent.size());
_imageText = _isPng ? tr::lng_theme_editor_read_from_png(tr::now, lt_size, sizeText) : tr::lng_theme_editor_read_from_jpg(tr::now, lt_size, sizeText);
_tileBackground->setChecked(false);
updateThumbnail();
}
}
}));
}
void ThemeExportBox::exportTheme() {
App::CallDelayed(st::defaultRippleAnimation.hideDuration, this, [this] {
auto caption = tr::lng_theme_editor_choose_name(tr::now);
auto filter = "Themes (*.tdesktop-theme)";
auto name = "awesome.tdesktop-theme";
FileDialog::GetWritePath(this, caption, filter, name, crl::guard(this, [this](const QString &path) {
zlib::FileToWrite zip;
zip_fileinfo zfi = { { 0, 0, 0, 0, 0, 0 }, 0, 0, 0 };
auto background = std::string(_tileBackground->checked() ? "tiled" : "background") + (_isPng ? ".png" : ".jpg");
zip.openNewFile(background.c_str(), &zfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION);
zip.writeInFile(_backgroundContent.constData(), _backgroundContent.size());
zip.closeFile();
auto scheme = "colors.tdesktop-theme";
zip.openNewFile(scheme, &zfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION);
zip.writeInFile(_paletteContent.constData(), _paletteContent.size());
zip.closeFile();
zip.close();
if (zip.error() != ZIP_OK) {
LOG(("Theme Error: could not export zip-ed theme, status: %1").arg(zip.error()));
Ui::show(Box<InformBox>(tr::lng_theme_editor_error(tr::now)));
return;
}
auto result = zip.result();
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
LOG(("Theme Error: could not open zip-ed theme file '%1' for writing").arg(path));
Ui::show(Box<InformBox>(tr::lng_theme_editor_error(tr::now)));
return;
}
if (f.write(result) != result.size()) {
LOG(("Theme Error: could not write zip-ed theme to file '%1'").arg(path));
Ui::show(Box<InformBox>(tr::lng_theme_editor_error(tr::now)));
return;
}
Ui::hideLayer();
Ui::Toast::Show(tr::lng_theme_editor_done(tr::now));
}));
});
}
Editor::Editor(QWidget*)
: _scroll(this, st::themesScroll)
Editor::Editor(QWidget*, not_null<Window::Controller*> window)
: _window(window)
, _scroll(this, st::themesScroll)
, _close(this, st::contactsMultiSelect.fieldCancel)
, _select(this, st::contactsMultiSelect, tr::lng_country_ph())
, _leftShadow(this)
, _topShadow(this)
, _export(this, tr::lng_theme_editor_export_button(tr::now).toUpper(), st::dialogsUpdateButton) {
, _save(this, tr::lng_theme_editor_save_button(tr::now).toUpper(), st::dialogsUpdateButton) {
const auto path = EditingPalettePath();
_inner = _scroll->setOwnedWidget(object_ptr<Inner>(this, path));
_export->setClickedCallback(_inner->exportCallback());
_save->setClickedCallback(App::LambdaDelayed(
st::defaultRippleAnimation.hideDuration,
this,
[=] { save(); }));
_inner->setErrorCallback([this] {
Ui::show(Box<InformBox>(tr::lng_theme_editor_error(tr::now)));
@ -808,8 +663,16 @@ Editor::Editor(QWidget*)
resizeToWidth(st::windowMinWidth);
}
void Editor::save() {
if (!_window->account().sessionExists()) {
//_window->show(Box<InformBox>())
return;
}
Ui::show(Box(SaveThemeBox, _window, _inner->paletteContent()));
}
void Editor::resizeEvent(QResizeEvent *e) {
_export->resizeToWidth(width());
_save->resizeToWidth(width());
_close->moveToRight(0, 0);
_select->resizeToWidth(width());
@ -821,7 +684,7 @@ void Editor::resizeEvent(QResizeEvent *e) {
_topShadow->moveToLeft(st::lineWidth, shadowTop);
_leftShadow->resize(st::lineWidth, height());
_leftShadow->moveToLeft(0, 0);
auto scrollSize = QSize(width(), height() - shadowTop - _export->height());
auto scrollSize = QSize(width(), height() - shadowTop - _save->height());
if (_scroll->size() != scrollSize) {
_scroll->resize(scrollSize);
}
@ -831,7 +694,7 @@ void Editor::resizeEvent(QResizeEvent *e) {
auto scrollTop = _scroll->scrollTop();
_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
}
_export->moveToLeft(0, _scroll->y() + _scroll->height());
_save->moveToLeft(0, _scroll->y() + _scroll->height());
}
void Editor::keyPressEvent(QKeyEvent *e) {
@ -894,7 +757,7 @@ void Editor::paintEvent(QPaintEvent *e) {
//}
void Editor::closeEditor() {
if (auto window = App::wnd()) {
if (const auto window = App::wnd()) {
window->showRightColumn(nullptr);
}
}

View File

@ -16,6 +16,9 @@ class PlainShadow;
} // namespace Ui
namespace Window {
class Controller;
namespace Theme {
bool CopyColorsToPalette(
@ -25,7 +28,7 @@ bool CopyColorsToPalette(
class Editor : public TWidget {
public:
explicit Editor(QWidget*);
Editor(QWidget*, not_null<Window::Controller*> window);
protected:
void paintEvent(QPaintEvent *e) override;
@ -35,8 +38,10 @@ protected:
void focusInEvent(QFocusEvent *e) override;
private:
void save();
void closeEditor();
not_null<Window::Controller*> _window;
object_ptr<Ui::ScrollArea> _scroll;
class Inner;
QPointer<Inner> _inner;
@ -44,7 +49,7 @@ private:
object_ptr<Ui::MultiSelect> _select;
object_ptr<Ui::PlainShadow> _leftShadow;
object_ptr<Ui::PlainShadow> _topShadow;
object_ptr<Ui::FlatButton> _export;
object_ptr<Ui::FlatButton> _save;
};

View File

@ -0,0 +1,664 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "window/themes/window_theme_editor_box.h"
#include "window/themes/window_theme.h"
#include "window/themes/window_theme_editor.h"
#include "window/window_controller.h"
#include "boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/image/image_prepare.h"
#include "ui/toast/toast.h"
#include "info/profile/info_profile_button.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "storage/localstorage.h"
#include "core/file_utilities.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "base/zlib_help.h"
#include "base/unixtime.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "storage/file_upload.h"
#include "mainwindow.h"
#include "layout.h"
#include "apiwrap.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
#include "styles/style_settings.h"
#include "styles/style_boxes.h"
namespace Window {
namespace Theme {
namespace {
constexpr auto kRandomSlugSize = 16;
constexpr auto kMinSlugSize = 5;
constexpr auto kMaxSlugSize = 64;
enum class SaveErrorType {
Other,
Name,
Link,
};
struct PreparedBackground {
QByteArray content;
bool tile = false;
bool isPng = false;
};
class BackgroundSelector : public Ui::RpWidget {
public:
BackgroundSelector(
QWidget *parent,
const QImage &background,
const PreparedBackground &data);
[[nodiscard]] PreparedBackground result() const;
int resizeGetHeight(int newWidth) override;
protected:
void paintEvent(QPaintEvent *e) override;
private:
void updateThumbnail();
void chooseBackgroundFromFile();
object_ptr<Ui::LinkButton> _chooseFromFile;
object_ptr<Ui::Checkbox> _tileBackground;
QImage _background;
QByteArray _backgroundContent;
bool _isPng = false;
QString _imageText;
QPixmap _thumbnail;
};
BackgroundSelector::BackgroundSelector(
QWidget *parent,
const QImage &background,
const PreparedBackground &data)
: RpWidget(parent)
, _chooseFromFile(
this,
tr::lng_settings_bg_from_file(tr::now),
st::boxLinkButton)
, _tileBackground(
this,
tr::lng_settings_bg_tile(tr::now),
data.tile,
st::defaultBoxCheckbox)
, _background(background)
, _backgroundContent(data.content) {
_imageText = tr::lng_theme_editor_saved_to_jpg(
tr::now,
lt_size,
formatSizeText(_backgroundContent.size()));
_chooseFromFile->setClickedCallback([=] { chooseBackgroundFromFile(); });
const auto height = st::boxTextFont->height
+ st::themesSmallSkip
+ _chooseFromFile->heightNoMargins()
+ st::themesSmallSkip
+ _tileBackground->heightNoMargins();
resize(width(), height);
updateThumbnail();
}
void BackgroundSelector::paintEvent(QPaintEvent *e) {
Painter p(this);
const auto left = height() + st::themesSmallSkip;
p.setPen(st::boxTextFg);
p.setFont(st::boxTextFont);
p.drawTextLeft(left, 0, width(), _imageText);
p.drawPixmapLeft(0, 0, width(), _thumbnail);
}
int BackgroundSelector::resizeGetHeight(int newWidth) {
const auto left = height() + st::themesSmallSkip;
_chooseFromFile->moveToLeft(left, st::boxTextFont->height + st::themesSmallSkip);
_tileBackground->moveToLeft(left, st::boxTextFont->height + st::themesSmallSkip + _chooseFromFile->height() + st::themesSmallSkip);
return height();
}
void BackgroundSelector::updateThumbnail() {
const auto size = height();
auto back = QImage(
QSize(size, size) * cIntRetinaFactor(),
QImage::Format_ARGB32_Premultiplied);
back.setDevicePixelRatio(cRetinaFactor());
{
Painter p(&back);
PainterHighQualityEnabler hq(p);
auto &pix = _background;
int sx = (pix.width() > pix.height()) ? ((pix.width() - pix.height()) / 2) : 0;
int sy = (pix.height() > pix.width()) ? ((pix.height() - pix.width()) / 2) : 0;
int s = (pix.width() > pix.height()) ? pix.height() : pix.width();
p.drawImage(QRect(0, 0, size, size), pix, QRect(sx, sy, s, s));
}
Images::prepareRound(back, ImageRoundRadius::Small);
_thumbnail = App::pixmapFromImageInPlace(std::move(back));
_thumbnail.setDevicePixelRatio(cRetinaFactor());
update();
}
void BackgroundSelector::chooseBackgroundFromFile() {
const auto callback = [=](const FileDialog::OpenResult &result) {
auto content = result.remoteContent;
if (!result.paths.isEmpty()) {
QFile f(result.paths.front());
if (f.open(QIODevice::ReadOnly)) {
content = f.readAll();
f.close();
}
}
if (!content.isEmpty()) {
auto format = QByteArray();
auto image = App::readImage(content, &format);
if (!image.isNull()
&& (format == "jpeg"
|| format == "jpg"
|| format == "png")) {
_background = image;
_backgroundContent = content;
_isPng = (format == "png");
const auto phrase = _isPng
? tr::lng_theme_editor_read_from_png
: tr::lng_theme_editor_read_from_jpg;
_imageText = phrase(
tr::now,
lt_size,
formatSizeText(_backgroundContent.size()));
_tileBackground->setChecked(false);
updateThumbnail();
}
}
};
FileDialog::GetOpenPath(
this,
tr::lng_theme_editor_choose_image(tr::now),
"Image files (*.jpeg *.jpg *.png)",
crl::guard(this, callback));
}
PreparedBackground BackgroundSelector::result() const {
return {
_backgroundContent,
_tileBackground->checked(),
_isPng,
};
}
void ImportFromFile(
not_null<Main::Session*> session,
not_null<QWidget*> parent) {
const auto &imgExtensions = cImgExtensions();
auto filters = QStringList(
qsl("Theme files (*.tdesktop-theme *.tdesktop-palette)"));
filters.push_back(FileDialog::AllFilesFilter());
const auto callback = crl::guard(session, [=](
const FileDialog::OpenResult &result) {
if (result.paths.isEmpty()) {
return;
}
Window::Theme::Apply(result.paths.front());
});
FileDialog::GetOpenPath(
parent.get(),
tr::lng_choose_image(tr::now),
filters.join(qsl(";;")),
crl::guard(parent, callback));
}
QString BytesToUTF8(QLatin1String string) {
return QString::fromUtf8(string.data(), string.size());
}
void WriteDefaultPalette(const QString &path) {
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
LOG(("Theme Error: could not open '%1' for writing.").arg(path));
return;
}
QTextStream stream(&f);
stream.setCodec("UTF-8");
auto rows = style::main_palette::data();
for (const auto &row : std::as_const(rows)) {
stream
<< BytesToUTF8(row.name)
<< ": "
<< BytesToUTF8(row.value)
<< "; // "
<< BytesToUTF8(
row.description
).replace(
'\n',
' '
).replace(
'\r',
' ')
<< "\n";
}
}
void StartEditor(
not_null<Window::Controller*> window,
const QString &title) {
const auto path = EditingPalettePath();
if (!Local::copyThemeColorsToPalette(path)) {
WriteDefaultPalette(path);
}
if (!Apply(path)) {
window->show(Box<InformBox>(tr::lng_theme_editor_error(tr::now)));
return;
}
KeepApplied();
window->showRightColumn(Box<Editor>(window));
}
[[nodiscard]] QString GenerateSlug() {
const auto letters = uint8('Z' + 1 - 'A');
const auto digits = uint8('9' + 1 - '0');
const auto values = uint8(2 * letters + digits);
auto result = QString();
result.reserve(kRandomSlugSize);
for (auto i = 0; i != kRandomSlugSize; ++i) {
const auto value = rand_value<uint8>() % values;
if (value < letters) {
result.append(char('A' + value));
} else if (value < 2 * letters) {
result.append(char('a' + (value - letters)));
} else {
result.append(char('0' + (value - 2 * letters)));
}
}
return result;
}
[[nodiscard]] QByteArray PrepareTheme(
const QByteArray &palette,
const PreparedBackground &background) {
zlib::FileToWrite zip;
zip_fileinfo zfi = { { 0, 0, 0, 0, 0, 0 }, 0, 0, 0 };
const auto back = std::string(background.tile ? "tiled" : "background")
+ (background.isPng ? ".png" : ".jpg");
zip.openNewFile(
back.c_str(),
&zfi,
nullptr,
0,
nullptr,
0,
nullptr,
Z_DEFLATED,
Z_DEFAULT_COMPRESSION);
zip.writeInFile(
background.content.constData(),
background.content.size());
zip.closeFile();
const auto scheme = "colors.tdesktop-theme";
zip.openNewFile(
scheme,
&zfi,
nullptr,
0,
nullptr,
0,
nullptr,
Z_DEFLATED,
Z_DEFAULT_COMPRESSION);
zip.writeInFile(palette.constData(), palette.size());
zip.closeFile();
zip.close();
if (zip.error() != ZIP_OK) {
LOG(("Theme Error: could not export zip-ed theme, status: %1"
).arg(zip.error()));
return QByteArray();
}
return zip.result();
}
[[nodiscard]] bool IsGoodSlug(const QString &slug) {
if (slug.size() < kMinSlugSize || slug.size() > kMaxSlugSize) {
return false;
}
const auto i = ranges::find_if(slug, [](QChar ch) {
return (ch < 'A' || ch > 'Z')
&& (ch < 'a' || ch > 'z')
&& (ch < '0' || ch > '9')
&& (ch != '_');
});
return (i == slug.end());
}
SendMediaReady PrepareThemeMedia(
const QString &name,
const QByteArray &content) {
PreparedPhotoThumbs thumbnails;
QVector<MTPPhotoSize> sizes;
//const auto push = [&](const char *type, QImage &&image) {
// sizes.push_back(MTP_photoSize(
// MTP_string(type),
// MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)),
// MTP_int(image.width()),
// MTP_int(image.height()), MTP_int(0)));
// thumbnails.emplace(type[0], std::move(image));
//};
//push("s", scaled(320));
const auto filename = QString(name).replace(' ', '_')
+ qsl(".tdesktop-theme"); // #TODO themes
auto attributes = QVector<MTPDocumentAttribute>(
1,
MTP_documentAttributeFilename(MTP_string(filename)));
const auto id = rand_value<DocumentId>();
const auto document = MTP_document(
MTP_flags(0),
MTP_long(id),
MTP_long(0),
MTP_bytes(),
MTP_int(base::unixtime::now()),
MTP_string("application/x-tgtheme-tdesktop"),
MTP_int(content.size()),
MTP_vector<MTPPhotoSize>(sizes),
MTP_int(MTP::maindc()),
MTP_vector<MTPDocumentAttribute>(attributes));
return SendMediaReady(
SendMediaType::ThemeFile,
QString(), // filepath
filename,
content.size(),
content,
id,
0,
QString(),
PeerId(),
MTP_photoEmpty(MTP_long(0)),
thumbnails,
document,
QByteArray(),
0);
}
Fn<void()> SaveTheme(
not_null<Window::Controller*> window,
const QByteArray &palette,
const PreparedBackground &background,
const QString &name,
const QString &link,
Fn<void()> done,
Fn<void(SaveErrorType,QString)> fail) {
using Storage::UploadedDocument;
struct State {
FullMsgId id;
bool generating = false;
mtpRequestId requestId = 0;
rpl::lifetime lifetime;
};
if (name.isEmpty()) {
fail(SaveErrorType::Name, {});
return nullptr;
} else if (!IsGoodSlug(link)) {
fail(SaveErrorType::Link, {});
return nullptr;
}
const auto session = &window->account().session();
const auto api = &session->api();
const auto state = std::make_shared<State>();
state->id = FullMsgId(
0,
session->data().nextLocalMessageId());
const auto createTheme = [=](const MTPDocument &data) {
const auto document = session->data().processDocument(data);
state->requestId = api->request(MTPaccount_CreateTheme(
MTP_string(link),
MTP_string(name),
document->mtpInput()
)).done([=](const MTPTheme &result) {
done();
}).fail([=](const RPCError &error) {
fail(SaveErrorType::Other, error.type());
}).send();
};
const auto uploadTheme = [=](const UploadedDocument &data) {
state->requestId = api->request(MTPaccount_UploadTheme(
MTP_flags(0),
data.file,
MTPInputFile(), // thumb
MTP_string(name + ".tdesktop-theme"), // #TODO themes
MTP_string("application/x-tgtheme-tdesktop")
)).done([=](const MTPDocument &result) {
createTheme(result);
}).fail([=](const RPCError &error) {
fail(SaveErrorType::Other, error.type());
}).send();
};
const auto uploadFile = [=](const QByteArray &theme) {
session->uploader().documentReady(
) | rpl::filter([=](const UploadedDocument &data) {
return data.fullId == state->id;
}) | rpl::start_with_next([=](const UploadedDocument &data) {
uploadTheme(data);
}, state->lifetime);
session->uploader().uploadMedia(
state->id,
PrepareThemeMedia(name, theme));
};
state->generating = true;
crl::async([=] {
crl::on_main([=, ready = PrepareTheme(palette, background)]{
if (!state->generating) {
return;
}
state->generating = false;
uploadFile(ready);
});
});
return [=] {
if (state->generating) {
state->generating = false;
} else {
api->request(base::take(state->requestId)).cancel();
session->uploader().cancel(state->id);
state->lifetime.destroy();
}
};
}
} // namespace
void CreateBox(
not_null<GenericBox*> box,
not_null<Window::Controller*> window) {
Expects(window->account().sessionExists());
box->setTitle(tr::lng_theme_editor_create_title(Ui::Text::WithEntities));
const auto name = box->addRow(object_ptr<Ui::InputField>(
box,
st::defaultInputField,
tr::lng_theme_editor_name()));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_theme_editor_create_description(),
st::boxDividerLabel),
style::margins(
st::boxRowPadding.left(),
st::boxRowPadding.left(),
st::boxRowPadding.right(),
st::boxRowPadding.right()));
box->addRow(
object_ptr<Info::Profile::Button>(
box,
tr::lng_theme_editor_import_existing() | Ui::Text::ToUpper(),
st::createThemeImportButton),
style::margins()
)->addClickHandler([=] {
ImportFromFile(&window->account().session(), box);
});
box->setFocusCallback([=] { name->setFocusFast(); });
box->addButton(tr::lng_box_done(), [=] {
const auto title = name->getLastText().trimmed();
if (title.isEmpty()) {
name->showError();
return;
}
box->closeBox();
StartEditor(window, title);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void SaveThemeBox(
not_null<GenericBox*> box,
not_null<Window::Controller*> window,
const QByteArray &palette) {
Expects(window->account().sessionExists());
const auto background = Background()->createCurrentImage();
auto backgroundContent = QByteArray();
const auto tiled = Background()->tile();
{
QBuffer buffer(&backgroundContent);
background.save(&buffer, "JPG", 87);
}
box->setTitle(tr::lng_theme_editor_save_title(Ui::Text::WithEntities));
const auto name = box->addRow(object_ptr<Ui::InputField>(
box,
st::defaultInputField,
tr::lng_theme_editor_name()));
const auto linkWrap = box->addRow(
object_ptr<Ui::RpWidget>(box),
style::margins(
st::boxRowPadding.left(),
st::themesSmallSkip,
st::boxRowPadding.right(),
st::boxRowPadding.bottom()));
const auto link = Ui::CreateChild<Ui::UsernameInput>(
linkWrap,
st::createThemeLink,
rpl::single(qsl("link")),
GenerateSlug(),
true);
linkWrap->widthValue(
) | rpl::start_with_next([=](int width) {
link->resize(width, link->height());
link->moveToLeft(0, 0, width);
}, link->lifetime());
link->heightValue(
) | rpl::start_with_next([=](int height) {
linkWrap->resize(linkWrap->width(), height);
}, link->lifetime());
link->setLinkPlaceholder(
Core::App().createInternalLink(qsl("addtheme/")));
link->setPlaceholderHidden(false);
link->setMaxLength(kMaxSlugSize);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_theme_editor_link_about(),
st::boxDividerLabel),
style::margins(
st::boxRowPadding.left(),
st::themesSmallSkip,
st::boxRowPadding.right(),
st::boxRowPadding.bottom()));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_theme_editor_background_image(),
st::settingsSubsectionTitle),
st::settingsSubsectionTitlePadding);
const auto back = box->addRow(
object_ptr<BackgroundSelector>(
box,
background,
PreparedBackground{ backgroundContent, tiled }),
style::margins(
st::boxRowPadding.left(),
st::themesSmallSkip,
st::boxRowPadding.right(),
st::boxRowPadding.bottom()));
box->setFocusCallback([=] { name->setFocusFast(); });
box->setWidth(st::boxWideWidth);
const auto saving = box->lifetime().make_state<bool>();
const auto cancel = std::make_shared<Fn<void()>>(nullptr);
box->lifetime().add([=] { if (*cancel) (*cancel)(); });
box->addButton(tr::lng_settings_save(), [=] {
if (*saving) {
return;
}
*saving = true;
const auto done = crl::guard(box, [=] {
box->closeBox();
window->showRightColumn(nullptr);
});
const auto fail = crl::guard(box, [=](
SaveErrorType type,
const QString &text) {
if (!text.isEmpty()) {
Ui::Toast::Show(text);
}
if (type == SaveErrorType::Name) {
name->showError();
} else if (type == SaveErrorType::Link) {
link->showError();
}
});
*cancel = SaveTheme(
window,
palette,
back->result(),
name->getLastText().trimmed(),
link->getLastText().trimmed(),
done,
fail);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace Theme
} // namespace Window

View File

@ -7,16 +7,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class GenericBox;
namespace Main {
class Session;
} // namespace Main
#include "boxes/generic_box.h"
namespace Window {
class Controller;
namespace Theme {
void CreateBox(not_null<GenericBox*> box, not_null<Main::Session*> session);
void CreateBox(
not_null<GenericBox*> box,
not_null<Window::Controller*> window);
void SaveThemeBox(
not_null<GenericBox*> box,
not_null<Window::Controller*> window,
const QByteArray &palette);
} // namespace Theme
} // namespace Window

View File

@ -309,6 +309,21 @@ createThemeImportButton: InfoProfileButton {
ripple: defaultRippleAnimation;
}
createThemeLink: InputField(defaultInputField) {
textMargins: margins(0px, 7px, 0px, 0px);
textBg: boxBg;
placeholderFg: placeholderFg;
placeholderFgActive: placeholderFgActive;
placeholderFgError: placeholderFgActive;
placeholderMargins: margins(0px, 0px, 0px, 0px);
placeholderScale: 0.;
placeholderFont: boxTextFont;
heightMin: 34px;
font: boxTextFont;
}
// Mac specific

View File

@ -43,7 +43,7 @@ void Controller::firstShow() {
void Controller::checkThemeEditor() {
if (Window::Theme::Background()->isEditingTheme()) {
_widget.showRightColumn(Box<Window::Theme::Editor>());
showRightColumn(Box<Window::Theme::Editor>(this));
}
}
@ -74,6 +74,10 @@ void Controller::showBox(
_widget.ui_showBox(std::move(content), options, animated);
}
void Controller::showRightColumn(object_ptr<TWidget> widget) {
_widget.showRightColumn(std::move(widget));
}
void Controller::activate() {
_widget.activate();
}

View File

@ -52,6 +52,8 @@ public:
return result;
}
void showRightColumn(object_ptr<TWidget> widget);
void activate();
void reActivate();
void updateIsActive(int timeout);

View File

@ -889,12 +889,12 @@
<(src_loc)/window/window_top_bar_wrap.h
<(src_loc)/window/themes/window_theme.cpp
<(src_loc)/window/themes/window_theme.h
<(src_loc)/window/themes/window_theme_create_box.cpp
<(src_loc)/window/themes/window_theme_create_box.h
<(src_loc)/window/themes/window_theme_editor.cpp
<(src_loc)/window/themes/window_theme_editor.h
<(src_loc)/window/themes/window_theme_editor_block.cpp
<(src_loc)/window/themes/window_theme_editor_block.h
<(src_loc)/window/themes/window_theme_editor_box.cpp
<(src_loc)/window/themes/window_theme_editor_box.h
<(src_loc)/window/themes/window_theme_preview.cpp
<(src_loc)/window/themes/window_theme_preview.h
<(src_loc)/window/themes/window_theme_warning.cpp