From b842761ea3afbbcaf114257de84b103809b8be1f Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 3 Feb 2017 23:07:26 +0300 Subject: [PATCH] Closed beta 1000006001: Built in theme and color palette editor. --- Telegram/Resources/basic.style | 2 + .../Resources/icons/color_slider_arrow.png | Bin 0 -> 152 bytes .../Resources/icons/color_slider_arrow@2x.png | Bin 0 -> 193 bytes .../icons/color_slider_arrow_vertical.png | Bin 0 -> 175 bytes .../icons/color_slider_arrow_vertical@2x.png | Bin 0 -> 236 bytes Telegram/Resources/langs/lang.strings | 16 + Telegram/Resources/uwp/AppX/AppxManifest.xml | 4 +- Telegram/Resources/winrc/Telegram.rc | 8 +- Telegram/Resources/winrc/Updater.rc | 8 +- Telegram/SourceFiles/apiwrap.cpp | 2 +- Telegram/SourceFiles/app.cpp | 2 +- Telegram/SourceFiles/application.cpp | 2 +- Telegram/SourceFiles/boxes/abstractbox.cpp | 18 +- Telegram/SourceFiles/boxes/abstractbox.h | 6 - Telegram/SourceFiles/boxes/addcontactbox.cpp | 5 +- Telegram/SourceFiles/boxes/backgroundbox.cpp | 2 +- Telegram/SourceFiles/boxes/boxes.style | 20 + Telegram/SourceFiles/boxes/confirmbox.cpp | 5 +- Telegram/SourceFiles/boxes/contactsbox.cpp | 2 +- Telegram/SourceFiles/boxes/editcolorbox.cpp | 908 ++++++++++++++++++ Telegram/SourceFiles/boxes/editcolorbox.h | 115 +++ Telegram/SourceFiles/boxes/sharebox.cpp | 11 +- Telegram/SourceFiles/boxes/usernamebox.cpp | 5 +- .../codegen/common/basic_tokenized_file.cpp | 18 +- .../codegen/common/basic_tokenized_file.h | 9 +- .../SourceFiles/codegen/common/clean_file.cpp | 32 +- .../SourceFiles/codegen/common/clean_file.h | 8 +- .../codegen/common/clean_file_reader.h | 4 + .../SourceFiles/codegen/style/generator.cpp | 188 ++-- .../SourceFiles/codegen/style/generator.h | 1 - .../SourceFiles/codegen/style/parsed_file.cpp | 1 + .../SourceFiles/codegen/style/processor.cpp | 4 - .../codegen/style/structure_types.h | 1 + Telegram/SourceFiles/core/utils.cpp | 12 +- Telegram/SourceFiles/core/utils.h | 1 + Telegram/SourceFiles/core/version.h | 2 +- Telegram/SourceFiles/dialogswidget.cpp | 2 +- Telegram/SourceFiles/historywidget.cpp | 8 +- Telegram/SourceFiles/layerwidget.h | 6 + Telegram/SourceFiles/localstorage.cpp | 32 +- Telegram/SourceFiles/localstorage.h | 2 + Telegram/SourceFiles/mainwidget.cpp | 2 +- Telegram/SourceFiles/mainwindow.cpp | 50 +- Telegram/SourceFiles/mainwindow.h | 7 +- .../SourceFiles/media/view/mediaview.style | 2 - Telegram/SourceFiles/mediaview.cpp | 19 +- Telegram/SourceFiles/mediaview.h | 2 - Telegram/SourceFiles/overviewwidget.cpp | 2 +- .../platform/win/main_window_win.cpp | 2 +- .../profile/profile_block_invite_link.cpp | 4 +- .../settings/settings_advanced_widget.cpp | 2 +- .../settings/settings_background_widget.cpp | 7 +- .../settings/settings_fixed_bar.cpp | 8 +- .../SourceFiles/settings/settings_fixed_bar.h | 5 + .../settings/settings_general_widget.cpp | 28 +- .../settings/settings_general_widget.h | 4 +- .../settings/settings_inner_widget.cpp | 2 +- .../settings/settings_inner_widget.h | 6 +- .../SourceFiles/settings/settings_layer.cpp | 134 +++ .../SourceFiles/settings/settings_layer.h | 94 ++ .../SourceFiles/settings/settings_widget.cpp | 146 +-- .../SourceFiles/settings/settings_widget.h | 31 +- Telegram/SourceFiles/stickers/stickers.cpp | 2 +- Telegram/SourceFiles/structs.cpp | 2 +- Telegram/SourceFiles/structs.h | 2 +- Telegram/SourceFiles/ui/filedialog.cpp | 65 ++ Telegram/SourceFiles/ui/filedialog.h | 9 + Telegram/SourceFiles/ui/style/style_core.cpp | 14 + Telegram/SourceFiles/ui/style/style_core.h | 2 + Telegram/SourceFiles/ui/toast/toast.cpp | 13 + Telegram/SourceFiles/ui/toast/toast.h | 2 + .../SourceFiles/ui/toast/toast_manager.cpp | 4 + Telegram/SourceFiles/ui/widgets/buttons.cpp | 13 +- Telegram/SourceFiles/ui/widgets/buttons.h | 1 + .../SourceFiles/ui/widgets/input_fields.cpp | 2 +- Telegram/SourceFiles/ui/widgets/widgets.style | 2 +- Telegram/SourceFiles/window/main_window.cpp | 40 +- Telegram/SourceFiles/window/main_window.h | 6 +- .../window/notifications_manager_default.cpp | 2 +- .../window/{ => themes}/window_theme.cpp | 239 +++-- .../window/{ => themes}/window_theme.h | 7 + .../window/themes/window_theme_editor.cpp | 817 ++++++++++++++++ .../window/themes/window_theme_editor.h | 66 ++ .../themes/window_theme_editor_block.cpp | 778 +++++++++++++++ .../window/themes/window_theme_editor_block.h | 171 ++++ .../{ => themes}/window_theme_preview.cpp | 4 +- .../{ => themes}/window_theme_preview.h | 2 +- .../{ => themes}/window_theme_warning.cpp | 4 +- .../{ => themes}/window_theme_warning.h | 0 Telegram/SourceFiles/window/window.style | 6 + Telegram/build/set_version.bat | 2 +- Telegram/build/set_version.sh | 3 + Telegram/build/version | 2 +- Telegram/{prepare.bat => create.bat} | 16 +- Telegram/gyp/Telegram.gyp | 20 +- 95 files changed, 3870 insertions(+), 477 deletions(-) create mode 100644 Telegram/Resources/icons/color_slider_arrow.png create mode 100644 Telegram/Resources/icons/color_slider_arrow@2x.png create mode 100644 Telegram/Resources/icons/color_slider_arrow_vertical.png create mode 100644 Telegram/Resources/icons/color_slider_arrow_vertical@2x.png create mode 100644 Telegram/SourceFiles/boxes/editcolorbox.cpp create mode 100644 Telegram/SourceFiles/boxes/editcolorbox.h create mode 100644 Telegram/SourceFiles/settings/settings_layer.cpp create mode 100644 Telegram/SourceFiles/settings/settings_layer.h rename Telegram/SourceFiles/window/{ => themes}/window_theme.cpp (75%) rename Telegram/SourceFiles/window/{ => themes}/window_theme.h (90%) create mode 100644 Telegram/SourceFiles/window/themes/window_theme_editor.cpp create mode 100644 Telegram/SourceFiles/window/themes/window_theme_editor.h create mode 100644 Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp create mode 100644 Telegram/SourceFiles/window/themes/window_theme_editor_block.h rename Telegram/SourceFiles/window/{ => themes}/window_theme_preview.cpp (99%) rename Telegram/SourceFiles/window/{ => themes}/window_theme_preview.h (96%) rename Telegram/SourceFiles/window/{ => themes}/window_theme_warning.cpp (97%) rename Telegram/SourceFiles/window/{ => themes}/window_theme_warning.h (100%) rename Telegram/{prepare.bat => create.bat} (92%) diff --git a/Telegram/Resources/basic.style b/Telegram/Resources/basic.style index 85a9e6edff..dfe5849ae8 100644 --- a/Telegram/Resources/basic.style +++ b/Telegram/Resources/basic.style @@ -275,3 +275,5 @@ notifyFadeRight: icon {{ "fade_horizontal", notificationBg }}; stickerIconLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; stickerIconRight: icon {{ "fade_horizontal", emojiPanCategories }}; + +transparentPlaceholderSize: 4px; diff --git a/Telegram/Resources/icons/color_slider_arrow.png b/Telegram/Resources/icons/color_slider_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..f874974a3b871b86f1ca83b6b7a44f329f6de3c7 GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1|*LJg9Pme=)I}Jajc%C@(V(;c_N#{H@pT<6$WBL3; z!TY6K-Mlz=T36@Wt=#(0@NAFavF~T&^PM^NFeEZ&9SDuk6bD+x;OXk;vd$@?2>{ea BIU)c6 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/color_slider_arrow@2x.png b/Telegram/Resources/icons/color_slider_arrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cf8a23dc7b7b2a90c653963b1fcf3746ba0a8ba5 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0y_4o?@y5RHjjCpmI4EAqJ3ckPJ5<`cy3(J%0+mHQz9o0N0k(gFcKEWlM zcNG^yd?qu~f|(yD-?R4CJY!k>CHB?V85g4#uUglAy+klln0;%Q-0_2NSN{9H`trk^ s^~y_Kj2G`dA9QX~;(7t68~d-Si{7&1Nci=s5a=cbPgg&ebxsLQ06uI>W&i*H literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/color_slider_arrow_vertical.png b/Telegram/Resources/icons/color_slider_arrow_vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..1de79ef0887fe74c6b2f75487f861a05198616f3 GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1|*LJg%;A`(=8&z+I7uZcdrt9;;-Yg4uXU-2B&YNo zoNdTv=yvB`^xpElFZX7@|Cq*$ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/color_slider_arrow_vertical@2x.png b/Telegram/Resources/icons/color_slider_arrow_vertical@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..001b70d184c0f336d6be7176e864588c7a4be122 GIT binary patch literal 236 zcmVP mAb{gInumFw0ob;!yP6j<-XPs|sw6Q00000 + Version="1.0.6.1" /> Telegram Desktop Telegram Messenger LLP @@ -31,7 +31,7 @@ Square44x44Logo="Assets\logo44\logo44.png" Description="Telegram Desktop official messenger" /> - + diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 4bbd03c925..92b3a800f6 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -34,8 +34,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,0,6,0 - PRODUCTVERSION 1,0,6,0 + FILEVERSION 1,0,6,1 + PRODUCTVERSION 1,0,6,1 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -51,10 +51,10 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "Telegram Messenger LLP" - VALUE "FileVersion", "1.0.6.0" + VALUE "FileVersion", "1.0.6.1" VALUE "LegalCopyright", "Copyright (C) 2014-2017" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "1.0.6.0" + VALUE "ProductVersion", "1.0.6.1" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 70ad75da7e..9ac2f0990f 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,0,6,0 - PRODUCTVERSION 1,0,6,0 + FILEVERSION 1,0,6,1 + PRODUCTVERSION 1,0,6,1 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -43,10 +43,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram Messenger LLP" VALUE "FileDescription", "Telegram Updater" - VALUE "FileVersion", "1.0.6.0" + VALUE "FileVersion", "1.0.6.1" VALUE "LegalCopyright", "Copyright (C) 2014-2017" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "1.0.6.0" + VALUE "ProductVersion", "1.0.6.1" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 19facf748c..cf291c359e 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -30,7 +30,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "historywidget.h" #include "localstorage.h" #include "boxes/confirmbox.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" ApiWrap::ApiWrap(QObject *parent) : QObject(parent) , _messageDataResolveDelayed(new SingleDelayedCall(this, "resolveMessageDatas")) { diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index a4b6afa911..b88a506f04 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -44,7 +44,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "apiwrap.h" #include "numbers.h" #include "observer_peer.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "window/notifications_manager.h" #include "platform/platform_notifications_manager.h" diff --git a/Telegram/SourceFiles/application.cpp b/Telegram/SourceFiles/application.cpp index 6e207326c3..fca7dd3036 100644 --- a/Telegram/SourceFiles/application.cpp +++ b/Telegram/SourceFiles/application.cpp @@ -34,7 +34,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "autoupdater.h" #include "core/observer.h" #include "observer_peer.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "media/player/media_player_instance.h" #include "window/notifications_manager.h" #include "history/history_location_manager.h" diff --git a/Telegram/SourceFiles/boxes/abstractbox.cpp b/Telegram/SourceFiles/boxes/abstractbox.cpp index f62aa0502a..4b07004bb9 100644 --- a/Telegram/SourceFiles/boxes/abstractbox.cpp +++ b/Telegram/SourceFiles/boxes/abstractbox.cpp @@ -252,16 +252,14 @@ void AbstractBox::paintEvent(QPaintEvent *e) { void AbstractBox::paintTitle(Painter &p, const QString &title, const QString &additional) { p.setFont(st::boxTitleFont); p.setPen(st::boxTitleFg); - if (_layerType) { - auto titleWidth = st::boxTitleFont->width(title); - p.drawTextLeft(st::boxLayerTitlePosition.x(), st::boxLayerTitlePosition.y(), width(), title, titleWidth); - if (!additional.isEmpty()) { - p.setFont(st::boxLayerTitleAdditionalFont); - p.setPen(st::boxTitleAdditionalFg); - p.drawTextLeft(st::boxLayerTitlePosition.x() + titleWidth + st::boxLayerTitleAdditionalSkip, st::boxLayerTitlePosition.y() + st::boxTitleFont->ascent - st::boxLayerTitleAdditionalFont->ascent, width(), additional); - } - } else { - p.drawTextLeft(st::boxTitlePosition.x(), st::boxTitlePosition.y(), width(), title); + auto titleWidth = st::boxTitleFont->width(title); + auto titleLeft = _layerType ? st::boxLayerTitlePosition.x() : st::boxTitlePosition.x(); + auto titleTop = _layerType ? st::boxLayerTitlePosition.y() : st::boxTitlePosition.y(); + p.drawTextLeft(titleLeft, titleTop, width(), title, titleWidth); + if (!additional.isEmpty()) { + p.setFont(st::boxLayerTitleAdditionalFont); + p.setPen(st::boxTitleAdditionalFg); + p.drawTextLeft(titleLeft + titleWidth + st::boxLayerTitleAdditionalSkip, titleTop + st::boxTitleFont->ascent - st::boxLayerTitleAdditionalFont->ascent, width(), additional); } } diff --git a/Telegram/SourceFiles/boxes/abstractbox.h b/Telegram/SourceFiles/boxes/abstractbox.h index 6c3b681baf..0fc187186d 100644 --- a/Telegram/SourceFiles/boxes/abstractbox.h +++ b/Telegram/SourceFiles/boxes/abstractbox.h @@ -246,12 +246,6 @@ private: }; -template -inline object_ptr Box(Args&&... args) { - auto parent = static_cast(nullptr); - return object_ptr(parent, std_::forward(args)...); -} - enum CreatingGroupType { CreatingGroupNone, CreatingGroupGroup, diff --git a/Telegram/SourceFiles/boxes/addcontactbox.cpp b/Telegram/SourceFiles/boxes/addcontactbox.cpp index 7e14687365..e04685ccee 100644 --- a/Telegram/SourceFiles/boxes/addcontactbox.cpp +++ b/Telegram/SourceFiles/boxes/addcontactbox.cpp @@ -542,10 +542,7 @@ void SetupChannelBox::mouseMoveEvent(QMouseEvent *e) { void SetupChannelBox::mousePressEvent(QMouseEvent *e) { if (_linkOver) { Application::clipboard()->setText(_channel->inviteLink()); - - Ui::Toast::Config toast; - toast.text = lang(lng_create_channel_link_copied); - Ui::Toast::Show(App::wnd(), toast); + Ui::Toast::Show(lang(lng_create_channel_link_copied)); } } diff --git a/Telegram/SourceFiles/boxes/backgroundbox.cpp b/Telegram/SourceFiles/boxes/backgroundbox.cpp index f606372490..ba93c0f90c 100644 --- a/Telegram/SourceFiles/boxes/backgroundbox.cpp +++ b/Telegram/SourceFiles/boxes/backgroundbox.cpp @@ -24,7 +24,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "lang.h" #include "mainwidget.h" #include "mainwindow.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "styles/style_overview.h" #include "styles/style_boxes.h" #include "ui/effects/round_checkbox.h" diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 96806ca150..877b33a117 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -530,3 +530,23 @@ usernameTextStyle: TextStyle(passcodeTextStyle) { usernameDefaultFg: windowSubTextFg; downloadPathSkip: 10px; + +colorEditWidth: 390px; +colorEditSkip: 10px; +colorPickerSize: 256px; +colorPickerMarkRadius: 6px; +colorPickerMarkLine: 1px; +colorSliderSkip: 8px; +colorSliderArrowLeft: icon {{ "color_slider_arrow", sliderBgActive }}; +colorSliderArrowRight: icon {{ "color_slider_arrow-flip_horizontal", sliderBgActive }}; +colorSliderArrowTop: icon {{ "color_slider_arrow_vertical", sliderBgActive }}; +colorSliderArrowBottom: icon {{ "color_slider_arrow_vertical-flip_vertical", sliderBgActive }}; +colorSliderWidth: 19px; +colorSampleSize: size(60px, 34px); +colorFieldSkip: 13px; +colorValueInput: InputField(defaultInputField) { + textMargins: margins(16px, 3px, 0px, 2px); + heightMin: 27px; +} +colorResultInput: InputField(colorValueInput) { +} diff --git a/Telegram/SourceFiles/boxes/confirmbox.cpp b/Telegram/SourceFiles/boxes/confirmbox.cpp index 12605935fc..f8b28d47af 100644 --- a/Telegram/SourceFiles/boxes/confirmbox.cpp +++ b/Telegram/SourceFiles/boxes/confirmbox.cpp @@ -241,10 +241,7 @@ void MaxInviteBox::mousePressEvent(QMouseEvent *e) { mouseMoveEvent(e); if (_linkOver) { Application::clipboard()->setText(_link); - - Ui::Toast::Config toast; - toast.text = lang(lng_create_channel_link_copied); - Ui::Toast::Show(App::wnd(), toast); + Ui::Toast::Show(lang(lng_create_channel_link_copied)); } } diff --git a/Telegram/SourceFiles/boxes/contactsbox.cpp b/Telegram/SourceFiles/boxes/contactsbox.cpp index 4ba5449c03..812bc5eae6 100644 --- a/Telegram/SourceFiles/boxes/contactsbox.cpp +++ b/Telegram/SourceFiles/boxes/contactsbox.cpp @@ -40,7 +40,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "ui/effects/ripple_animation.h" #include "boxes/photocropbox.h" #include "boxes/confirmbox.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "observer_peer.h" #include "apiwrap.h" diff --git a/Telegram/SourceFiles/boxes/editcolorbox.cpp b/Telegram/SourceFiles/boxes/editcolorbox.cpp new file mode 100644 index 0000000000..c44992b5b7 --- /dev/null +++ b/Telegram/SourceFiles/boxes/editcolorbox.cpp @@ -0,0 +1,908 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop 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. + +It 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. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#include "stdafx.h" +#include "boxes/editcolorbox.h" + +#include "lang.h" +#include "styles/style_boxes.h" +#include "ui/widgets/shadow.h" +#include "styles/style_mediaview.h" +#include "ui/widgets/input_fields.h" + +class EditColorBox::Picker : public TWidget { +public: + Picker(QWidget *parent, QColor color); + + float64 valueX() const { + return _x; + } + float64 valueY() const { + return _y; + } + + base::Observable &changed() { + return _changed; + } + void setHSV(int hue, int saturation, int brightness); + void setRGB(int red, int green, int blue); + +protected: + void paintEvent(QPaintEvent *e); + + void mousePressEvent(QMouseEvent *e); + void mouseMoveEvent(QMouseEvent *e); + void mouseReleaseEvent(QMouseEvent *e); + +private: + void setFromColor(QColor color); + QCursor generateCursor(); + + void preparePalette(); + void updateCurrentPoint(QPoint localPosition); + + QColor _topleft; + QColor _topright; + QColor _bottomleft; + QColor _bottomright; + + QImage _palette; + bool _paletteInvalidated = false; + float64 _x = 0.; + float64 _y = 0.; + + bool _choosing = false; + base::Observable _changed; + +}; + +QCursor EditColorBox::Picker::generateCursor() { + auto diameter = convertScale(16); + auto line = convertScale(1); + auto size = ((diameter + 2 * line) >= 32) ? 64 : 32; + auto cursor = QImage(QSize(size, size) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); + cursor.setDevicePixelRatio(cRetinaFactor()); + cursor.fill(Qt::transparent); + { + Painter p(&cursor); + PainterHighQualityEnabler hq(p); + + p.setBrush(Qt::NoBrush); + auto pen = QPen(Qt::white); + pen.setWidth(3 * line); + p.setPen(pen); + p.drawEllipse((size - diameter) / 2, (size - diameter) / 2, diameter, diameter); + pen = QPen(Qt::black); + pen.setWidth(line); + p.setPen(pen); + p.drawEllipse((size - diameter) / 2, (size - diameter) / 2, diameter, diameter); + } + return QCursor(QPixmap::fromImage(cursor)); +} + +EditColorBox::Picker::Picker(QWidget *parent, QColor color) : TWidget(parent) { + setCursor(generateCursor()); + + auto size = QSize(st::colorPickerSize, st::colorPickerSize); + resize(size); + + _palette = QImage(size * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); + + setFromColor(color); +} + +void EditColorBox::Picker::paintEvent(QPaintEvent *e) { + Painter p(this); + + preparePalette(); + + p.drawImage(0, 0, _palette); + + auto left = anim::color(_topleft, _bottomleft, _y); + auto right = anim::color(_topright, _bottomright, _y); + auto color = anim::color(left, right, _x); + auto lightness = 0.2989 * color.redF() + 0.5870 * color.greenF() + 0.1140 * color.blueF(); + auto pen = QPen((lightness > 0.6) ? QColor(0, 0, 0) : QColor(255, 255, 255)); + pen.setWidth(st::colorPickerMarkLine); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + + auto x = anim::interpolate(0, width() - 1, _x); + auto y = anim::interpolate(0, height() - 1, _y); + PainterHighQualityEnabler hq(p); + + p.drawEllipse(QRect(x - st::colorPickerMarkRadius, y - st::colorPickerMarkRadius, 2 * st::colorPickerMarkRadius, 2 * st::colorPickerMarkRadius)); +} + +void EditColorBox::Picker::mousePressEvent(QMouseEvent *e) { + _choosing = true; + updateCurrentPoint(e->pos()); +} + +void EditColorBox::Picker::mouseMoveEvent(QMouseEvent *e) { + if (_choosing) { + updateCurrentPoint(e->pos()); + } +} + +void EditColorBox::Picker::mouseReleaseEvent(QMouseEvent *e) { + _choosing = false; +} + +void EditColorBox::Picker::preparePalette() { + if (!_paletteInvalidated) return; + _paletteInvalidated = false; + + auto size = _palette.width(); + auto ints = reinterpret_cast(_palette.bits()); + auto intsAddPerLine = (_palette.bytesPerLine() - size * sizeof(uint32)) / sizeof(uint32); + + constexpr auto Large = 1024 * 1024; + constexpr auto LargeBit = 20; // n / Large == (n >> LargeBit) + auto part = Large / size; + + auto topleft = anim::shifted(_topleft); + auto topright = anim::shifted(_topright); + auto bottomleft = anim::shifted(_bottomleft); + auto bottomright = anim::shifted(_bottomright); + + auto y_accumulated = 0; + for (auto y = 0; y != size; ++y, y_accumulated += part) { + auto y_ratio = y_accumulated >> (LargeBit - 8); // (y_accumulated * 256) / Large; + // 0 <= y_accumulated < Large + // 0 <= y_ratio < 256 + + auto top_ratio = 255 - y_ratio; + auto bottom_ratio = y_ratio; + + auto left = anim::reshifted(bottomleft * bottom_ratio + topleft * top_ratio); + auto right = anim::reshifted(bottomright * bottom_ratio + topright * top_ratio); + + auto x_accumulated = 0; + for (auto x = 0; x != size; ++x, x_accumulated += part) { + auto x_ratio = x_accumulated >> (LargeBit - 8); // (x_accumulated * 256) / Large; + // 0 <= x_accumulated < Large + // 0 <= x_ratio < 256 + + auto left_ratio = 255 - x_ratio; + auto right_ratio = x_ratio; + + *ints++ = anim::unshifted(left * left_ratio + right * right_ratio); + } + ints += intsAddPerLine; + } +} + +void EditColorBox::Picker::updateCurrentPoint(QPoint localPosition) { + auto x = snap(localPosition.x(), 0, width()) / float64(width()); + auto y = snap(localPosition.y(), 0, height()) / float64(height()); + if (_x != x || _y != y) { + _x = x; + _y = y; + update(); + _changed.notify(); + } +} + +void EditColorBox::Picker::setHSV(int hue, int saturation, int brightness) { + _topleft = QColor(255, 255, 255); + _topright.setHsv(qMax(0, hue), 255, 255); + _topright = _topright.toRgb(); + _bottomleft = _bottomright = QColor(0, 0, 0); + + _paletteInvalidated = true; + update(); + + _x = snap(saturation / 255., 0., 1.); + _y = 1. - snap(brightness / 255., 0., 1.); +} + +void EditColorBox::Picker::setRGB(int red, int green, int blue) { + setFromColor(QColor(red, green, blue)); +} + +void EditColorBox::Picker::setFromColor(QColor color) { + setHSV(color.hsvHue(), color.hsvSaturation(), color.value()); +} + +class EditColorBox::Slider : public TWidget { +public: + enum class Direction { + Horizontal, + Vertical, + }; + enum class Type { + Hue, + Opacity, + }; + Slider(QWidget *parent, Direction direction, Type type, QColor color); + + base::Observable &changed() { + return _changed; + } + float64 value() const { + return _value; + } + void setValue(float64 value) { + _value = snap(value, 0., 1.); + update(); + } + void setHSV(int hue, int saturation, int brightness); + void setRGB(int red, int green, int blue); + void setAlpha(int alpha); + +protected: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + +private: + float64 valueFromColor(QColor color) const; + float64 valueFromHue(int hue) const; + bool isHorizontal() const { + return (_direction == Direction::Horizontal); + } + void colorUpdated(); + void prepareMinSize(); + void generatePixmap(); + void updatePixmapFromMask(); + void updateCurrentPoint(QPoint localPosition); + + Direction _direction = Direction::Horizontal; + Type _type = Type::Hue; + + QColor _color; + float64 _value = 0; + + QImage _mask; + QPixmap _pixmap; + QBrush _transparent; + + bool _choosing = false; + base::Observable _changed; + +}; + +EditColorBox::Slider::Slider(QWidget *parent, Direction direction, Type type, QColor color) : TWidget(parent) +, _direction(direction) +, _type(type) +, _color(color.red(), color.green(), color.blue()) +, _value(valueFromColor(color)) +, _transparent((_type == Type::Hue) ? QBrush() : style::transparentPlaceholderBrush()) { + prepareMinSize(); +} + +void EditColorBox::Slider::prepareMinSize() { + auto minSize = st::colorSliderSkip + st::colorSliderWidth + st::colorSliderSkip; + resize(minSize, minSize); +} + +void EditColorBox::Slider::paintEvent(QPaintEvent *e) { + Painter p(this); + auto to = rect().marginsRemoved(QMargins(st::colorSliderSkip, st::colorSliderSkip, st::colorSliderSkip, st::colorSliderSkip)); + Ui::Shadow::paint(p, to, width(), st::defaultRoundShadow); + if (_type == Type::Opacity) { + p.fillRect(to, _transparent); + } + p.drawPixmap(to, _pixmap, _pixmap.rect()); + if (isHorizontal()) { + auto x = st::colorSliderSkip + qRound(_value * to.width()); + st::colorSliderArrowTop.paint(p, x - st::colorSliderArrowTop.width() / 2, 0, width()); + st::colorSliderArrowBottom.paint(p, x - st::colorSliderArrowBottom.width() / 2, height() - st::colorSliderArrowBottom.height(), width()); + } else { + auto y = st::colorSliderSkip + qRound(_value * to.height()); + st::colorSliderArrowLeft.paint(p, 0, y - st::colorSliderArrowLeft.height() / 2, width()); + st::colorSliderArrowRight.paint(p, width() - st::colorSliderArrowRight.width(), y - st::colorSliderArrowRight.height() / 2, width()); + } +} + +void EditColorBox::Slider::resizeEvent(QResizeEvent *e) { + generatePixmap(); + update(); +} + +void EditColorBox::Slider::mousePressEvent(QMouseEvent *e) { + _choosing = true; + updateCurrentPoint(e->pos()); +} + +void EditColorBox::Slider::mouseMoveEvent(QMouseEvent *e) { + if (_choosing) { + updateCurrentPoint(e->pos()); + } +} + +void EditColorBox::Slider::mouseReleaseEvent(QMouseEvent *e) { + _choosing = false; +} + +void EditColorBox::Slider::generatePixmap() { + auto size = (isHorizontal() ? width() : height()) * cIntRetinaFactor(); + auto image = QImage(size, cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(cRetinaFactor()); + auto ints = reinterpret_cast(image.bits()); + auto intsPerLine = image.bytesPerLine() / sizeof(uint32); + auto intsPerLineAdded = intsPerLine - size; + + constexpr auto Large = 1024 * 1024; + constexpr auto LargeBit = 20; // n / Large == (n >> LargeBit) + auto part = Large / size; + + if (_type == Type::Hue) { + QColor color; + for (auto x = 0; x != size; ++x) { + color.setHsv(x * 360 / size, 255, 255); + auto value = anim::getPremultiplied(color.toRgb()); + for (auto y = 0; y != cIntRetinaFactor(); ++y) { + ints[y * intsPerLine] = value; + } + ++ints; + } + if (!isHorizontal()) { + image = std_::move(image).transformed(QTransform(0, -1, 1, 0, 0, 0)); + } + _pixmap = App::pixmapFromImageInPlace(std_::move(image)); + } else { + auto color = anim::shifted(QColor(255, 255, 255, 255)); + auto transparent = anim::shifted(QColor(255, 255, 255, 0)); + for (auto y = 0; y != cIntRetinaFactor(); ++y) { + auto x_accumulated = 0; + for (auto x = 0; x != size; ++x, x_accumulated += part) { + auto x_ratio = x_accumulated >> (LargeBit - 8); + // 0 <= x_accumulated < Large + // 0 <= x_ratio < 256 + + *ints++ = anim::unshifted(color * x_ratio + transparent * (255 - x_ratio)); + } + ints += intsPerLineAdded; + } + if (!isHorizontal()) { + image = std_::move(image).transformed(QTransform(0, -1, 1, 0, 0, 0)); + } + _mask = std_::move(image); + updatePixmapFromMask(); + } +} + +void EditColorBox::Slider::setHSV(int hue, int saturation, int brightness) { + if (_type == Type::Hue) { + // hue == 360 converts to 0 if done in general way + _value = valueFromHue(hue); + update(); + } else { + _color.setHsv(hue, saturation, brightness); + colorUpdated(); + } +} + +void EditColorBox::Slider::setRGB(int red, int green, int blue) { + _color.setRgb(red, green, blue); + colorUpdated(); +} + +void EditColorBox::Slider::colorUpdated() { + if (_type == Type::Hue) { + _value = valueFromColor(_color); + } else if (!_mask.isNull()) { + updatePixmapFromMask(); + } + update(); +} + +float64 EditColorBox::Slider::valueFromColor(QColor color) const { + return (_type == Type::Hue) ? valueFromHue(color.hsvHue()) : color.alphaF(); +} + +float64 EditColorBox::Slider::valueFromHue(int hue) const { + return (1. - snap(hue, 0, 360) / 360.); +} + +void EditColorBox::Slider::setAlpha(int alpha) { + if (_type == Type::Opacity) { + _value = snap(alpha, 0, 255) / 255.; + update(); + } +} + +void EditColorBox::Slider::updatePixmapFromMask() { + _pixmap = App::pixmapFromImageInPlace(style::colorizeImage(_mask, _color)); +} + +void EditColorBox::Slider::updateCurrentPoint(QPoint localPosition) { + auto coord = (isHorizontal() ? localPosition.x() : localPosition.y()) - st::colorSliderSkip; + auto maximum = (isHorizontal() ? width() : height()) - 2 * st::colorSliderSkip; + auto value = snap(coord, 0, maximum) / float64(maximum); + if (_value != value) { + _value = value; + update(); + _changed.notify(); + } +} + +class EditColorBox::Field : public Ui::MaskedInputField { +public: + Field(QWidget *parent, const style::InputField &st, const QString &placeholder, int limit, const QString &units = QString()); + + int value() const { + return getLastText().toInt(); + } + + void setTextWithFocus(const QString &text) { + setText(text); + if (hasFocus()) { + selectAll(); + } + } + +protected: + void correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) override; + void paintAdditionalPlaceholder(Painter &p, TimeMs ms) override; + + void wheelEvent(QWheelEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + +private: + void changeValue(int delta); + + QString _placeholder, _units; + int _limit = 0; + int _digitLimit = 1; + int _wheelDelta = 0; + +}; + +EditColorBox::Field::Field(QWidget *parent, const style::InputField &st, const QString &placeholder, int limit, const QString &units) : Ui::MaskedInputField(parent, st) +, _placeholder(placeholder) +, _units(units) +, _limit(limit) +, _digitLimit(QString::number(_limit).size()) { +} + +void EditColorBox::Field::correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) { + QString newText; + int oldPos(nowCursor), newPos(-1), oldLen(now.length()); + + newText.reserve(oldLen); + for (int i = 0; i < oldLen; ++i) { + if (i == oldPos) { + newPos = newText.length(); + } + + QChar ch(now[i]); + if (ch.isDigit()) { + newText += ch; + } + if (newText.size() >= _digitLimit) { + break; + } + } + if (newPos < 0 || newPos > newText.size()) { + newPos = newText.size(); + } + if (newText.toInt() > _limit) { + newText = QString::number(_limit); + newPos = newText.size(); + } + if (newText != now) { + now = newText; + setText(now); + startPlaceholderAnimation(); + nowCursor = -1; + } + if (newPos != nowCursor) { + nowCursor = newPos; + setCursorPosition(nowCursor); + } +} + +void EditColorBox::Field::paintAdditionalPlaceholder(Painter &p, TimeMs ms) { + p.setFont(_st.font); + p.setPen(_st.placeholderFg); + auto inner = QRect(_st.textMargins.right(), _st.textMargins.top(), width() - 2 * _st.textMargins.right(), height() - _st.textMargins.top() - _st.textMargins.bottom()); + p.drawText(inner, _placeholder, style::al_topleft); + if (!_units.isEmpty()) { + p.drawText(inner, _units, style::al_topright); + } +} + +void EditColorBox::Field::wheelEvent(QWheelEvent *e) { + if (!hasFocus()) { + return; + } + + auto deltaX = e->angleDelta().x(), deltaY = e->angleDelta().y(); + if (cPlatform() == dbipMac || cPlatform() == dbipMacOld) { + deltaY *= -1; + } else { + deltaX *= -1; + } + _wheelDelta += (qAbs(deltaX) > qAbs(deltaY)) ? deltaX : deltaY; + + constexpr auto step = 5; + if (auto delta = _wheelDelta / step) { + _wheelDelta -= delta * step; + changeValue(delta); + } +} + +void EditColorBox::Field::changeValue(int delta) { + auto currentValue = value(); + auto newValue = snap(currentValue + delta, 0, _limit); + if (newValue != currentValue) { + setText(QString::number(newValue)); + setFocus(); + selectAll(); + emit changed(); + } +} + +void EditColorBox::Field::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Up) { + changeValue(1); + } else if (e->key() == Qt::Key_Down) { + changeValue(-1); + } else { + MaskedInputField::keyPressEvent(e); + } +} + +class EditColorBox::ResultField : public Ui::MaskedInputField { +public: + ResultField(QWidget *parent, const style::InputField &st); + + void setTextWithFocus(const QString &text) { + setText(text); + if (hasFocus()) { + selectAll(); + } + } + +protected: + void correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) override; + void paintAdditionalPlaceholder(Painter &p, TimeMs ms) override; + +}; + +EditColorBox::ResultField::ResultField(QWidget *parent, const style::InputField &st) : Ui::MaskedInputField(parent, st) { +} + +void EditColorBox::ResultField::correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) { + QString newText; + int oldPos(nowCursor), newPos(-1), oldLen(now.length()); + + newText.reserve(oldLen); + for (int i = 0; i < oldLen; ++i) { + if (i == oldPos) { + newPos = newText.length(); + } + + QChar ch(now[i]); + auto code = ch.unicode(); + if ((code >= '0' && code <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) { + newText += ch; + } + if (newText.size() >= 8) { + break; + } + } + if (newPos < 0 || newPos > newText.size()) { + newPos = newText.size(); + } + if (newText != now) { + now = newText; + setText(now); + startPlaceholderAnimation(); + nowCursor = -1; + } + if (newPos != nowCursor) { + nowCursor = newPos; + setCursorPosition(nowCursor); + } +} + +void EditColorBox::ResultField::paintAdditionalPlaceholder(Painter &p, TimeMs ms) { + p.setFont(_st.font); + p.setPen(_st.placeholderFg); + p.drawText(QRect(_st.textMargins.right(), _st.textMargins.top(), width(), height() - _st.textMargins.top() - _st.textMargins.bottom()), "#", style::al_topleft); +} + +EditColorBox::EditColorBox(QWidget*, const QString &title, QColor current) : BoxContent() +, _title(title) +, _picker(this, current) +, _hueSlider(this, Slider::Direction::Vertical, Slider::Type::Hue, current) +, _opacitySlider(this, Slider::Direction::Horizontal, Slider::Type::Opacity, current) +, _hueField(this, st::colorValueInput, "H", 360, QString() + QChar(176)) // degree character +, _saturationField(this, st::colorValueInput, "S", 100, "%") +, _brightnessField(this, st::colorValueInput, "B", 100, "%") +, _redField(this, st::colorValueInput, "R", 255) +, _greenField(this, st::colorValueInput, "G", 255) +, _blueField(this, st::colorValueInput, "B", 255) +, _result(this, st::colorResultInput) +, _transparent(style::transparentPlaceholderBrush()) +, _current(current) +, _new(current) { +} + +void EditColorBox::prepare() { + setTitle(_title); + + connect(_hueField, SIGNAL(changed()), this, SLOT(onFieldChanged())); + connect(_saturationField, SIGNAL(changed()), this, SLOT(onFieldChanged())); + connect(_brightnessField, SIGNAL(changed()), this, SLOT(onFieldChanged())); + connect(_redField, SIGNAL(changed()), this, SLOT(onFieldChanged())); + connect(_greenField, SIGNAL(changed()), this, SLOT(onFieldChanged())); + connect(_blueField, SIGNAL(changed()), this, SLOT(onFieldChanged())); + connect(_result, SIGNAL(changed()), this, SLOT(onFieldChanged())); + + connect(_hueField, SIGNAL(submitted(bool)), this, SLOT(onFieldSubmitted())); + connect(_saturationField, SIGNAL(submitted(bool)), this, SLOT(onFieldSubmitted())); + connect(_brightnessField, SIGNAL(submitted(bool)), this, SLOT(onFieldSubmitted())); + connect(_redField, SIGNAL(submitted(bool)), this, SLOT(onFieldSubmitted())); + connect(_greenField, SIGNAL(submitted(bool)), this, SLOT(onFieldSubmitted())); + connect(_blueField, SIGNAL(submitted(bool)), this, SLOT(onFieldSubmitted())); + connect(_result, SIGNAL(submitted(bool)), this, SLOT(onFieldSubmitted())); + + addButton(lang(lng_settings_save), [this] { saveColor(); }); + addButton(lang(lng_cancel), [this] { closeBox(); }); + + auto height = st::colorEditSkip + st::colorPickerSize + st::colorEditSkip + st::colorSliderWidth + st::colorEditSkip; + setDimensions(st::colorEditWidth, height); + + subscribe(_picker->changed(), [this] { updateFromControls(); }); + subscribe(_hueSlider->changed(), [this] { updateFromControls(); }); + subscribe(_opacitySlider->changed(), [this] { updateFromControls(); }); + updateFromControls(); +} + +void EditColorBox::setInnerFocus() { + _result->setFocus(); + _result->selectAll(); +} + +void EditColorBox::onFieldChanged() { + auto emitter = sender(); + auto checkHSVSender = [this, emitter](QObject *field) { + if (emitter == field) { + updateFromHSVFields(); + } + }; + auto checkRGBSender = [this, emitter](QObject *field) { + if (emitter == field) { + updateFromRGBFields(); + } + }; + checkHSVSender(_hueField); + checkHSVSender(_saturationField); + checkHSVSender(_brightnessField); + checkRGBSender(_redField); + checkRGBSender(_greenField); + checkRGBSender(_blueField); + if (emitter == _result) { + updateFromResultField(); + } +} + +void EditColorBox::onFieldSubmitted() { + Ui::MaskedInputField *fields[] = { + _hueField, + _saturationField, + _brightnessField, + _redField, + _greenField, + _blueField, + _result + }; + for (auto i = 0, count = int(base::array_size(fields)); i + 1 != count; ++i) { + if (fields[i]->hasFocus()) { + fields[i + 1]->setFocus(); + fields[i + 1]->selectAll(); + return; + } + } + if (_result->hasFocus()) { + saveColor(); + } +} + +void EditColorBox::saveColor() { + _cancelCallback = base::lambda(); + if (_saveCallback) { + _saveCallback(_new.toRgb()); + } + closeBox(); +} + +void EditColorBox::updateHSVFields() { + auto hue = qRound((1. - _hueSlider->value()) * 360); + auto saturation = qRound(_picker->valueX() * 255); + auto brightness = qRound((1. - _picker->valueY()) * 255); + auto alpha = qRound(_opacitySlider->value() * 255); + _hueField->setTextWithFocus(QString::number(hue)); + _saturationField->setTextWithFocus(QString::number(percentFromByte(saturation))); + _brightnessField->setTextWithFocus(QString::number(percentFromByte(brightness))); +} + +void EditColorBox::updateRGBFields() { + _redField->setTextWithFocus(QString::number(_new.red())); + _greenField->setTextWithFocus(QString::number(_new.green())); + _blueField->setTextWithFocus(QString::number(_new.blue())); +} + +void EditColorBox::updateResultField() { + auto text = QString(); + auto addHex = [&text](int value) { + if (value >= 0 && value <= 9) { + text.append('0' + value); + } else if (value >= 10 && value <= 15) { + text.append('a' + (value - 10)); + } + }; + auto addValue = [addHex](int value) { + addHex(value / 16); + addHex(value % 16); + }; + addValue(_new.red()); + addValue(_new.green()); + addValue(_new.blue()); + if (_new.alpha() != 255) { + addValue(_new.alpha()); + } + _result->setTextWithFocus(text); +} + +void EditColorBox::resizeEvent(QResizeEvent *e) { + auto fullwidth = _picker->width() + 2 * (st::colorEditSkip - st::colorSliderSkip) + _hueSlider->width() + st::colorSampleSize.width(); + auto left = (width() - fullwidth) / 2; + _picker->moveToLeft(left, st::colorEditSkip); + _hueSlider->setGeometryToLeft(_picker->x() + _picker->width() + st::colorEditSkip - st::colorSliderSkip, st::colorEditSkip - st::colorSliderSkip, _hueSlider->width(), st::colorPickerSize + 2 * st::colorSliderSkip); + _opacitySlider->setGeometryToLeft(_picker->x() - st::colorSliderSkip, _picker->y() + _picker->height() + st::colorEditSkip - st::colorSliderSkip, _picker->width() + 2 * st::colorSliderSkip, _opacitySlider->height()); + auto fieldLeft = _hueSlider->x() + _hueSlider->width() - st::colorSliderSkip + st::colorEditSkip; + auto fieldWidth = st::colorSampleSize.width(); + auto fieldHeight = _hueField->height(); + _newRect = QRect(fieldLeft, st::colorEditSkip, fieldWidth, st::colorSampleSize.height()); + _currentRect = _newRect.translated(0, st::colorSampleSize.height()); + _hueField->setGeometryToLeft(fieldLeft, _currentRect.y() + _currentRect.height() + st::colorFieldSkip, fieldWidth, fieldHeight); + _saturationField->setGeometryToLeft(fieldLeft, _hueField->y() + _hueField->height(), fieldWidth, fieldHeight); + _brightnessField->setGeometryToLeft(fieldLeft, _saturationField->y() + _saturationField->height(), fieldWidth, fieldHeight); + _redField->setGeometryToLeft(fieldLeft, _brightnessField->y() + _brightnessField->height() + st::colorFieldSkip, fieldWidth, fieldHeight); + _greenField->setGeometryToLeft(fieldLeft, _redField->y() + _redField->height(), fieldWidth, fieldHeight); + _blueField->setGeometryToLeft(fieldLeft, _greenField->y() + _greenField->height(), fieldWidth, fieldHeight); + _result->setGeometryToLeft(fieldLeft - (st::colorEditSkip + st::colorSliderWidth), _opacitySlider->y() + _opacitySlider->height() - st::colorSliderSkip - _result->height(), fieldWidth + (st::colorEditSkip + st::colorSliderWidth), fieldHeight); +} + +void EditColorBox::paintEvent(QPaintEvent *e) { + BoxContent::paintEvent(e); + + Painter p(this); + Ui::Shadow::paint(p, _picker->geometry(), width(), st::defaultRoundShadow); + + Ui::Shadow::paint(p, QRect(_newRect.x(), _newRect.y(), _newRect.width(), _newRect.height() + _currentRect.height()), width(), st::defaultRoundShadow); + if (_new.alphaF() < 1.) { + p.fillRect(myrtlrect(_newRect), _transparent); + } + p.fillRect(myrtlrect(_newRect), _new); + if (_current.alphaF() < 1.) { + p.fillRect(myrtlrect(_currentRect), _transparent); + } + p.fillRect(myrtlrect(_currentRect), _current); +} + +void EditColorBox::mousePressEvent(QMouseEvent *e) { + if (myrtlrect(_currentRect).contains(e->pos())) { + updateFromColor(_current); + } +} + +void EditColorBox::updateFromColor(QColor color) { + _new = color; + updateControlsFromColor(); + updateRGBFields(); + updateHSVFields(); + updateResultField(); + update(); +} + +void EditColorBox::updateFromControls() { + auto hue = qRound((1. - _hueSlider->value()) * 360); + auto saturation = qRound(_picker->valueX() * 255); + auto brightness = qRound((1. - _picker->valueY()) * 255); + auto alpha = qRound(_opacitySlider->value() * 255); + setHSV(hue, saturation, brightness, alpha); + updateHSVFields(); + updateControlsFromHSV(hue, saturation, brightness); +} + +void EditColorBox::updateFromHSVFields() { + auto hue = _hueField->value(); + auto saturation = percentToByte(_saturationField->value()); + auto brightness = percentToByte(_brightnessField->value()); + auto alpha = qRound(_opacitySlider->value() * 255); + setHSV(hue, saturation, brightness, alpha); + updateControlsFromHSV(hue, saturation, brightness); +} + +void EditColorBox::updateFromRGBFields() { + auto red = _redField->value(); + auto blue = _blueField->value(); + auto green = _greenField->value(); + auto alpha = qRound(_opacitySlider->value() * 255); + setRGB(red, green, blue, alpha); + updateResultField(); +} + +void EditColorBox::updateFromResultField() { + auto text = _result->getLastText(); + if (text.size() != 6 && text.size() != 8) { + return; + } + + auto fromHex = [](QChar hex) { + auto code = hex.unicode(); + if (code >= 'A' && code <= 'F') { + return (code - 'A' + 10); + } else if (code >= 'a' && code <= 'f') { + return (code - 'a' + 10); + } + return code - '0'; + }; + auto fromChars = [fromHex](QChar a, QChar b) { + return fromHex(a) * 0x10 + fromHex(b); + }; + auto red = fromChars(text[0], text[1]); + auto green = fromChars(text[2], text[3]); + auto blue = fromChars(text[4], text[5]); + auto alpha = (text.size() == 8) ? fromChars(text[6], text[7]) : 255; + setRGB(red, green, blue, alpha); + updateRGBFields(); +} + +void EditColorBox::updateControlsFromHSV(int hue, int saturation, int brightness) { + _picker->setHSV(hue, saturation, brightness); + _hueSlider->setHSV(hue, saturation, brightness); + _opacitySlider->setHSV(hue, saturation, brightness); +} + +void EditColorBox::updateControlsFromColor() { + auto red = _new.red(); + auto green = _new.green(); + auto blue = _new.blue(); + auto alpha = _new.alpha(); + _picker->setRGB(red, green, blue); + _hueSlider->setRGB(red, green, blue); + _opacitySlider->setRGB(red, green, blue); + _opacitySlider->setAlpha(alpha); +} + +void EditColorBox::setHSV(int hue, int saturation, int value, int alpha) { + _new.setHsv(hue, saturation, value, alpha); + updateRGBFields(); + updateResultField(); + update(); +} + +void EditColorBox::setRGB(int red, int green, int blue, int alpha) { + _new.setRgb(red, green, blue, alpha); + updateControlsFromColor(); + updateHSVFields(); + update(); +} \ No newline at end of file diff --git a/Telegram/SourceFiles/boxes/editcolorbox.h b/Telegram/SourceFiles/boxes/editcolorbox.h new file mode 100644 index 0000000000..42e92512ac --- /dev/null +++ b/Telegram/SourceFiles/boxes/editcolorbox.h @@ -0,0 +1,115 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop 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. + +It 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. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include "boxes/abstractbox.h" + +class EditColorBox : public BoxContent { + Q_OBJECT + +public: + EditColorBox(QWidget*, const QString &title, QColor current = QColor(255, 255, 255)); + + void setSaveCallback(base::lambda &&callback) { + _saveCallback = std_::move(callback); + } + + void setCancelCallback(base::lambda &&callback) { + _cancelCallback = std_::move(callback); + } + + void showColor(QColor color) { + updateFromColor(color); + } + + void closeHook() override { + if (_cancelCallback) { + _cancelCallback(); + } + } + +protected: + void prepare() override; + + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + + void mousePressEvent(QMouseEvent *e) override; + + void setInnerFocus() override; + +private slots: + void onFieldChanged(); + void onFieldSubmitted(); + +private: + void saveColor(); + + void updateFromColor(QColor color); + void updateControlsFromColor(); + void updateControlsFromHSV(int hue, int saturation, int brightness); + void updateHSVFields(); + void updateRGBFields(); + void updateResultField(); + void updateFromControls(); + void updateFromHSVFields(); + void updateFromRGBFields(); + void updateFromResultField(); + void setHSV(int hue, int saturation, int brightness, int alpha); + void setRGB(int red, int green, int blue, int alpha); + + int percentFromByte(int byte) { + return snap(qRound(byte * 100 / 255.), 0, 100); + } + int percentToByte(int percent) { + return snap(qRound(percent * 255 / 100.), 0, 255); + } + + class Picker; + class Slider; + class Field; + class ResultField; + + QString _title; + + object_ptr _picker; + object_ptr _hueSlider; + object_ptr _opacitySlider; + + object_ptr _hueField; + object_ptr _saturationField; + object_ptr _brightnessField; + object_ptr _redField; + object_ptr _greenField; + object_ptr _blueField; + object_ptr _result; + + QBrush _transparent; + QColor _current; + QColor _new; + + QRect _currentRect; + QRect _newRect; + + base::lambda _saveCallback; + base::lambda _cancelCallback; + +}; diff --git a/Telegram/SourceFiles/boxes/sharebox.cpp b/Telegram/SourceFiles/boxes/sharebox.cpp index 06ba468f64..2ba88b3c44 100644 --- a/Telegram/SourceFiles/boxes/sharebox.cpp +++ b/Telegram/SourceFiles/boxes/sharebox.cpp @@ -37,7 +37,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "history/history_media_types.h" #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "boxes/contactsbox.h" ShareBox::ShareBox(QWidget*, CopyCallback &©Callback, SubmitCallback &&submitCallback, FilterCallback &&filterCallback) @@ -872,9 +872,7 @@ void shareGameScoreFromItem(HistoryItem *item) { QApplication::clipboard()->setText(CreateInternalLinkHttps(bot->username + qsl("?game=") + shortName)); - Ui::Toast::Config toast; - toast.text = lang(lng_share_game_link_copied); - Ui::Toast::Show(App::wnd(), toast); + Ui::Toast::Show(lang(lng_share_game_link_copied)); } } } @@ -892,10 +890,7 @@ void shareGameScoreFromItem(HistoryItem *item) { } data->requests.remove(requestId); if (data->requests.empty()) { - Ui::Toast::Config toast; - toast.text = lang(lng_share_done); - Ui::Toast::Show(App::wnd(), toast); - + Ui::Toast::Show(lang(lng_share_done)); Ui::hideLayer(); } }; diff --git a/Telegram/SourceFiles/boxes/usernamebox.cpp b/Telegram/SourceFiles/boxes/usernamebox.cpp index 4853b29c39..1e4da63298 100644 --- a/Telegram/SourceFiles/boxes/usernamebox.cpp +++ b/Telegram/SourceFiles/boxes/usernamebox.cpp @@ -161,10 +161,7 @@ void UsernameBox::onChanged() { void UsernameBox::onLinkClick() { Application::clipboard()->setText(CreateInternalLinkHttps(getName())); - - Ui::Toast::Config toast; - toast.text = lang(lng_username_copied); - Ui::Toast::Show(App::wnd(), toast); + Ui::Toast::Show(lang(lng_username_copied)); } void UsernameBox::onUpdateDone(const MTPUser &user) { diff --git a/Telegram/SourceFiles/codegen/common/basic_tokenized_file.cpp b/Telegram/SourceFiles/codegen/common/basic_tokenized_file.cpp index a84cb99a5f..03b8320e8a 100644 --- a/Telegram/SourceFiles/codegen/common/basic_tokenized_file.cpp +++ b/Telegram/SourceFiles/codegen/common/basic_tokenized_file.cpp @@ -52,8 +52,6 @@ Token invalidToken() { return { Type::Invalid, QString(), ConstUtf8String(nullptr, 0), false }; } - - } // namespace BasicTokenizedFile::BasicTokenizedFile(const QString &filepath) : reader_(filepath) { @@ -152,6 +150,22 @@ Type BasicTokenizedFile::uniteLastTokens(Type type) { return type; } +QString BasicTokenizedFile::getCurrentLineComment() { + if (lineNumber_ > singleLineComments_.size()) { + reader_.logError(kErrorInternal, lineNumber_) << "internal tokenizer error (line number larger than comments list size)."; + failed_ = true; + return QString(); + } + auto commentBytes = singleLineComments_[lineNumber_ - 1].mid(2); // Skip "//" + CheckedUtf8String comment(commentBytes); + if (!comment.isValid()) { + reader_.logError(kErrorIncorrectUtf8String, lineNumber_) << "incorrect UTF-8 string in the comment."; + failed_ = true; + return QString(); + } + return comment.toString().trimmed(); +} + Type BasicTokenizedFile::readNameOrNumber() { while (!reader_.atEnd()) { if (!isDigitChar(reader_.currentChar())) { diff --git a/Telegram/SourceFiles/codegen/common/basic_tokenized_file.h b/Telegram/SourceFiles/codegen/common/basic_tokenized_file.h index 5dd201e312..fca8c59923 100644 --- a/Telegram/SourceFiles/codegen/common/basic_tokenized_file.h +++ b/Telegram/SourceFiles/codegen/common/basic_tokenized_file.h @@ -77,7 +77,11 @@ public: }; bool read() { - return reader_.read(); + if (reader_.read()) { + singleLineComments_ = reader_.singleLineComments(); + return true; + } + return false; } bool atEnd() const { return reader_.atEnd(); @@ -90,6 +94,8 @@ public: return failed_; } + QString getCurrentLineComment(); + // Log error to std::cerr with 'code' at the current position in file. LogStream logError(int code) const; LogStream logErrorUnexpectedToken() const; @@ -124,6 +130,7 @@ private: int currentToken_ = 0; int lineNumber_ = 1; bool failed_ = false; + QVector singleLineComments_; // Where the last (currently read) token has started. const char *tokenStart_ = nullptr; diff --git a/Telegram/SourceFiles/codegen/common/clean_file.cpp b/Telegram/SourceFiles/codegen/common/clean_file.cpp index 414512f2ef..b93a3880e9 100644 --- a/Telegram/SourceFiles/codegen/common/clean_file.cpp +++ b/Telegram/SourceFiles/codegen/common/clean_file.cpp @@ -86,10 +86,17 @@ bool CleanFile::read() { offset = ch; } }; - auto feedComment = [this, &offset, end](const char *ch) { + + auto lineNumber = 0; + auto feedComment = [this, &offset, end, &lineNumber](const char *ch, bool save = false) { if (ch > offset) { -// comments_.push_back({ content_.size(), QByteArray(offset, ch - offset) }); - if (result_.isEmpty()) result_.reserve(end - offset - 2); + if (save) { + singleLineComments_.resize(lineNumber + 1); + singleLineComments_[lineNumber] = QByteArray(offset, ch - offset); + } + if (result_.isEmpty()) { + result_.reserve(end - offset - 2); + } result_.append(' '); offset = ch; } @@ -105,6 +112,9 @@ bool CleanFile::read() { } } if (insideString) { + if (currentChar == '\n') { + ++lineNumber; + } ++ch; continue; } @@ -114,12 +124,14 @@ bool CleanFile::read() { insideComment = InsideComment::SingleLine; ch += 2; } else if (insideComment == InsideComment::SingleLine && currentChar == '\r' && nextChar == '\n') { - feedComment(ch); + feedComment(ch, true); ch += 2; + ++lineNumber; insideComment = InsideComment::None; } else if (insideComment == InsideComment::SingleLine && currentChar == '\n') { - feedComment(ch); + feedComment(ch, true); ++ch; + ++lineNumber; insideComment = InsideComment::None; } else if (insideComment == InsideComment::None && currentChar == '/' && nextChar == '*') { feedContent(ch); @@ -132,15 +144,21 @@ bool CleanFile::read() { } else if (insideComment == InsideComment::MultiLine && currentChar == '\r' && nextChar == '\n') { feedComment(ch); ch += 2; + ++lineNumber; feedContent(ch); } else if (insideComment == InsideComment::MultiLine && currentChar == '\n') { feedComment(ch); ++ch; + ++lineNumber; feedContent(ch); } else { + if (currentChar == '\n') { + ++lineNumber; + } ++ch; } } + singleLineComments_.resize(lineNumber + 1); if (insideComment == InsideComment::MultiLine) { common::logError(kErrorUnexpectedEndOfFile, filepath_); @@ -156,6 +174,10 @@ bool CleanFile::read() { return true; } +QVector CleanFile::singleLineComments() const { + return singleLineComments_; +} + LogStream CleanFile::logError(int code, int line) const { return common::logError(code, filepath_, line); } diff --git a/Telegram/SourceFiles/codegen/common/clean_file.h b/Telegram/SourceFiles/codegen/common/clean_file.h index 4afd8dd8f9..51f13dcb36 100644 --- a/Telegram/SourceFiles/codegen/common/clean_file.h +++ b/Telegram/SourceFiles/codegen/common/clean_file.h @@ -38,6 +38,7 @@ public: CleanFile &operator=(const CleanFile &other) = delete; bool read(); + QVector singleLineComments() const; const char *data() const { return result_.constData(); @@ -55,11 +56,8 @@ private: QString filepath_; QByteArray content_, result_; bool read_; - //struct Comment { - // int offset; - // QByteArray content; - //}; - //QVector comments_; + + QVector singleLineComments_; }; diff --git a/Telegram/SourceFiles/codegen/common/clean_file_reader.h b/Telegram/SourceFiles/codegen/common/clean_file_reader.h index add48c038e..f43a5e43c0 100644 --- a/Telegram/SourceFiles/codegen/common/clean_file_reader.h +++ b/Telegram/SourceFiles/codegen/common/clean_file_reader.h @@ -63,6 +63,10 @@ public: return (end_ - pos_); } + QVector singleLineComments() const { + return file_.singleLineComments(); + } + // Log error to std::cerr with 'code' at line number 'line' in data(). LogStream logError(int code, int line) const { return std::forward(file_.logError(code, line)); diff --git a/Telegram/SourceFiles/codegen/style/generator.cpp b/Telegram/SourceFiles/codegen/style/generator.cpp index 1ed229b767..60799f10a9 100644 --- a/Telegram/SourceFiles/codegen/style/generator.cpp +++ b/Telegram/SourceFiles/codegen/style/generator.cpp @@ -106,12 +106,13 @@ char hexFirstChar(char ch) { return hexChar((*reinterpret_cast(&ch)) >> 4); } -QString stringToEncodedString(const std::string &str) { +QString stringToEncodedString(const QString &str) { QString result, lineBreak = "\\\n"; result.reserve(str.size() * 8); bool writingHexEscapedCharacters = false, startOnNewLine = false; int lastCutSize = 0; - for (uchar ch : str) { + auto utf = str.toUtf8(); + for (auto ch : utf) { if (result.size() - lastCutSize > 80) { startOnNewLine = true; result.append(lineBreak); @@ -140,6 +141,10 @@ QString stringToEncodedString(const std::string &str) { return '"' + (startOnNewLine ? lineBreak : QString()) + result + '"'; } +QString stringToEncodedString(const std::string &str) { + return stringToEncodedString(QString::fromStdString(str)); +} + QString stringToBinaryArray(const std::string &str) { QStringList rows, chars; chars.reserve(13); @@ -334,7 +339,7 @@ QString Generator::valueAssignmentCode(structure::Value value) const { case Tag::Int: return QString("%1").arg(value.Int()); case Tag::Double: return QString("%1").arg(value.Double()); case Tag::Pixels: return pxValueName(value.Int()); - case Tag::String: return QString("qsl(%1)").arg(stringToEncodedString(value.String())); + case Tag::String: return QString("QString::fromUtf8(%1)").arg(stringToEncodedString(value.String())); case Tag::Color: { auto v(value.Color()); if (v.red == v.green && v.red == v.blue && v.red == 0 && v.alpha == 255) { @@ -441,8 +446,15 @@ public:\n\ \n\ QByteArray save() const;\n\ bool load(const QByteArray &cache);\n\ - bool setColor(QLatin1String name, uchar r, uchar g, uchar b, uchar a);\n\ - bool setColor(QLatin1String name, QLatin1String from);\n\ +\n\ + enum class SetResult {\n\ + Ok,\n\ + KeyNotFound,\n\ + ValueNotFound,\n\ + Duplicate,\n\ + };\n\ + SetResult setColor(QLatin1String name, uchar r, uchar g, uchar b, uchar a);\n\ + SetResult setColor(QLatin1String name, QLatin1String from);\n\ void reset() {\n\ clear();\n\ finalize();\n\ @@ -564,12 +576,20 @@ namespace main_palette {\n\ \n\ QByteArray save();\n\ bool load(const QByteArray &cache);\n\ -bool setColor(QLatin1String name, uchar r, uchar g, uchar b, uchar a);\n\ -bool setColor(QLatin1String name, QLatin1String from);\n\ +palette::SetResult setColor(QLatin1String name, uchar r, uchar g, uchar b, uchar a);\n\ +palette::SetResult setColor(QLatin1String name, QLatin1String from);\n\ void apply(const palette &other);\n\ void reset();\n\ int indexOfColor(color c);\n\ \n\ +struct row {\n\ +\tQLatin1String name;\n\ +\tQLatin1String value;\n\ +\tQLatin1String fallback;\n\ +\tQLatin1String description;\n\ +};\n\ +QList data();\n\ +\n\ } // namespace main_palette\n"; return true; @@ -739,10 +759,17 @@ void palette::finalize() {\n\ \n\ compute(0, -1, { 255, 255, 255, 0}); // special color\n"; + QList names; + module_.enumVariables([this, &names](const Variable &variable) -> bool { + names.push_back(variable.name); + return true; + }); + + QString dataRows; int indexInPalette = 1; QByteArray checksumString; checksumString.append("&transparent:{ 255, 255, 255, 0 }"); - bool result = module_.enumVariables([this, &indexInPalette, &checksumString](const Variable &variable) -> bool { + auto result = module_.enumVariables([this, &indexInPalette, &checksumString, &dataRows, &names](const Variable &variable) -> bool { auto name = variable.name.back(); auto index = indexInPalette++; paletteIndices_[name] = index; @@ -754,8 +781,27 @@ void palette::finalize() {\n\ auto assignment = QString("{ %1, %2, %3, %4 }").arg(color.red).arg(color.green).arg(color.blue).arg(color.alpha); source_->stream() << "\tcompute(" << index << ", " << fallbackIndex << ", " << assignment << ");\n"; checksumString.append('&' + name + ':' + assignment); + + auto isCopy = !variable.value.copyOf().isEmpty(); + auto colorString = paletteColorValue(color); + auto fallbackName = QString(); + if (fallbackIndex > 0) { + auto fallbackVariable = module_.findVariableInModule(names[fallbackIndex - 1], module_); + if (fallbackVariable && fallbackVariable->value.type().tag == structure::TypeTag::Color) { + fallbackName = fallbackVariable->name.back(); + } + } + auto value = isCopy ? fallbackName : '#' + colorString; + if (value.isEmpty()) { + return false; + } + + dataRows.append("\tresult.push_back({ qstr(\"" + name + "\"), qstr(\"" + value + "\"), qstr(\"" + (isCopy ? QString() : fallbackName) + "\"), qstr(" + stringToEncodedString(variable.description.toStdString()) + ") });\n"); return true; }); + if (!result) { + return false; + } auto count = indexInPalette; auto checksum = hashCrc32(checksumString.constData(), checksumString.size()); @@ -854,23 +900,25 @@ bool palette::load(const QByteArray &cache) {\n\ return true;\n\ }\n\ \n\ -bool palette::setColor(QLatin1String name, uchar r, uchar g, uchar b, uchar a) {\n\ - auto index = getPaletteIndex(name);\n\ - if (index >= 0) {\n\ - setData(index, { r, g, b, a });\n\ - return true;\n\ - }\n\ - return false;\n\ +palette::SetResult palette::setColor(QLatin1String name, uchar r, uchar g, uchar b, uchar a) {\n\ + auto nameIndex = getPaletteIndex(name);\n\ + if (nameIndex < 0) return SetResult::KeyNotFound;\n\ + auto duplicate = (_status[nameIndex] != Status::Initial);\n\ +\n\ + setData(nameIndex, { r, g, b, a });\n\ + return duplicate ? SetResult::Duplicate : SetResult::Ok;\n\ }\n\ \n\ -bool palette::setColor(QLatin1String name, QLatin1String from) {\n\ +palette::SetResult palette::setColor(QLatin1String name, QLatin1String from) {\n\ auto nameIndex = getPaletteIndex(name);\n\ + if (nameIndex < 0) return SetResult::KeyNotFound;\n\ + auto duplicate = (_status[nameIndex] != Status::Initial);\n\ +\n\ auto fromIndex = getPaletteIndex(from);\n\ - if (nameIndex >= 0 && fromIndex >= 0 && _status[fromIndex] == Status::Loaded) {\n\ - setData(nameIndex, *data(fromIndex));\n\ - return true;\n\ - }\n\ - return false;\n\ + if (fromIndex < 0 || _status[fromIndex] != Status::Loaded) return SetResult::ValueNotFound;\n\ +\n\ + setData(nameIndex, *data(fromIndex));\n\ + return duplicate ? SetResult::Duplicate : SetResult::Ok;\n\ }\n\ \n\ namespace main_palette {\n\ @@ -887,11 +935,11 @@ bool load(const QByteArray &cache) {\n\ return false;\n\ }\n\ \n\ -bool setColor(QLatin1String name, uchar r, uchar g, uchar b, uchar a) {\n\ +palette::SetResult setColor(QLatin1String name, uchar r, uchar g, uchar b, uchar a) {\n\ return _palette.setColor(name, r, g, b, a);\n\ }\n\ \n\ -bool setColor(QLatin1String name, QLatin1String from) {\n\ +palette::SetResult setColor(QLatin1String name, QLatin1String from) {\n\ return _palette.setColor(name, from);\n\ }\n\ \n\ @@ -909,6 +957,14 @@ int indexOfColor(color c) {\n\ return _palette.indexOfColor(c);\n\ }\n\ \n\ +QList data() {\n\ + auto result = QList();\n\ + result.reserve(" << count << ");\n\ +\n\ +" << dataRows << "\n\ + return result;\n\ +}\n\ +\n\ } // namespace main_palette\n\ \n"; @@ -1208,91 +1264,5 @@ bool Generator::collectUniqueValues() { return module_.enumVariables(collector); } -bool Generator::writeSampleTheme(const QString &filepath) { - QByteArray content; - QTextStream stream(&content); - - stream << "\ -//\n\ -// This is a sample Telegram Desktop theme file.\n\ -// It was generated from the 'colors.palette' style file.\n\ -//\n\ -// To create a theme with a background image included you should\n\ -// put two files in a .zip archive:\n\ -//\n\ -// First one is the color scheme like the one you're viewing\n\ -// right now, this file should be named 'colors.tdesktop-theme'.\n\ -//\n\ -// Second one should be the background image and it can be named\n\ -// 'background.jpg', 'background.png', 'tiled.jpg' or 'tiled.png'.\n\ -// You should name it 'background' (if you'd like it not to be tiled),\n\ -// or it can be named 'tiled' (if you'd like it to be tiled).\n\ -//\n\ -// After that you need to change the extension of your .zip archive\n\ -// to 'tdesktop-theme', so you'll have:\n\ -//\n\ -// mytheme.tdesktop-theme\n\ -// |-colors.tdesktop-theme\n\ -// |-background.jpg (or tiled.jpg, background.png, tiled.png)\n\ -//\n\n"; - - QList names; - module_.enumVariables([this, &names](const Variable &variable) -> bool { - names.push_back(variable.name); - return true; - }); - bool result = module_.enumVariables([this, &names, &stream](const Variable &variable) -> bool { - auto name = variable.name.back(); - if (variable.value.type().tag != structure::TypeTag::Color) { - return false; - } - auto color = variable.value.Color(); - //color.red = uchar(rand() % 256); - //color.green = uchar(rand() % 256); - //color.blue = uchar(rand() % 256); - //auto fallbackIndex = -1; - auto fallbackIndex = paletteIndices_.value(colorFallbackName(variable.value), -1); - auto colorString = paletteColorValue(color); - if (fallbackIndex >= 0) { - auto fallbackVariable = module_.findVariableInModule(names[fallbackIndex - 1], module_); - if (!fallbackVariable || fallbackVariable->value.type().tag != structure::TypeTag::Color) { - return false; - } - auto fallbackName = fallbackVariable->name.back(); - auto fallbackColor = fallbackVariable->value.Color(); - if (colorString == paletteColorValue(fallbackColor)) { - stream << name << ": " << fallbackName << ";\n"; - } else { - stream << name << ": #" << colorString << "; // " << fallbackName << ";\n"; - } - } else { - stream << name << ": #" << colorString << ";\n"; - } - return true; - }); - if (!result) { - return result; - } - - stream.flush(); - - QFile file(filepath); - if (file.open(QIODevice::ReadOnly)) { - if (file.readAll() == content) { - file.close(); - return true; - } - file.close(); - } - - if (!file.open(QIODevice::WriteOnly)) { - return false; - } - if (file.write(content) != content.size()) { - return false; - } - return true; -} - } // namespace style } // namespace codegen diff --git a/Telegram/SourceFiles/codegen/style/generator.h b/Telegram/SourceFiles/codegen/style/generator.h index 3d80fdb30f..fae531120d 100644 --- a/Telegram/SourceFiles/codegen/style/generator.h +++ b/Telegram/SourceFiles/codegen/style/generator.h @@ -40,7 +40,6 @@ public: bool writeHeader(); bool writeSource(); - bool writeSampleTheme(const QString &filepath); private: QString typeToString(structure::Type type) const; diff --git a/Telegram/SourceFiles/codegen/style/parsed_file.cpp b/Telegram/SourceFiles/codegen/style/parsed_file.cpp index 5b6cec8e59..726fb5a80f 100644 --- a/Telegram/SourceFiles/codegen/style/parsed_file.cpp +++ b/Telegram/SourceFiles/codegen/style/parsed_file.cpp @@ -261,6 +261,7 @@ structure::Variable ParsedFile::readVariable(const QString &name) { } if (value.type().tag != structure::TypeTag::Struct || !value.copyOf().empty()) { assertNextToken(BasicType::Semicolon); + result.description = file_.getCurrentLineComment(); } } return result; diff --git a/Telegram/SourceFiles/codegen/style/processor.cpp b/Telegram/SourceFiles/codegen/style/processor.cpp index 3cd25227f9..683ec00002 100644 --- a/Telegram/SourceFiles/codegen/style/processor.cpp +++ b/Telegram/SourceFiles/codegen/style/processor.cpp @@ -81,10 +81,6 @@ bool Processor::write(const structure::Module &module) const { if (!generator.writeSource()) { return false; } - auto themePath = srcFile.absoluteDir().absolutePath() + "/default.tdesktop-theme"; - if (options_.isPalette && !generator.writeSampleTheme(themePath)) { - return false; - } return true; } diff --git a/Telegram/SourceFiles/codegen/style/structure_types.h b/Telegram/SourceFiles/codegen/style/structure_types.h index a77812b308..8de7b8499e 100644 --- a/Telegram/SourceFiles/codegen/style/structure_types.h +++ b/Telegram/SourceFiles/codegen/style/structure_types.h @@ -198,6 +198,7 @@ private: struct Variable { FullName name; Value value; + QString description; explicit operator bool() const { return !name.isEmpty(); diff --git a/Telegram/SourceFiles/core/utils.cpp b/Telegram/SourceFiles/core/utils.cpp index 1775497449..5448e3caa4 100644 --- a/Telegram/SourceFiles/core/utils.cpp +++ b/Telegram/SourceFiles/core/utils.cpp @@ -940,6 +940,7 @@ QStringList MimeType::globPatterns() const { switch (_type) { case Known::WebP: return QStringList(qsl("*.webp")); case Known::TDesktopTheme: return QStringList(qsl("*.tdesktop-theme")); + case Known::TDesktopPalette: return QStringList(qsl("*.tdesktop-palette")); default: break; } return _typeStruct.globPatterns(); @@ -948,6 +949,7 @@ QString MimeType::filterString() const { switch (_type) { case Known::WebP: return qsl("WebP image (*.webp)"); case Known::TDesktopTheme: return qsl("Theme files (*.tdesktop-theme)"); + case Known::TDesktopPalette: return qsl("Palette files (*.tdesktop-palette)"); default: break; } return _typeStruct.filterString(); @@ -956,6 +958,7 @@ QString MimeType::name() const { switch (_type) { case Known::WebP: return qsl("image/webp"); case Known::TDesktopTheme: return qsl("application/x-tdesktop-theme"); + case Known::TDesktopPalette: return qsl("application/x-tdesktop-palette"); default: break; } return _typeStruct.name(); @@ -966,17 +969,22 @@ MimeType mimeTypeForName(const QString &mime) { return MimeType(MimeType::Known::WebP); } else if (mime == qsl("application/x-tdesktop-theme")) { return MimeType(MimeType::Known::TDesktopTheme); + } else if (mime == qsl("application/x-tdesktop-palette")) { + return MimeType(MimeType::Known::TDesktopPalette); } return MimeType(QMimeDatabase().mimeTypeForName(mime)); } MimeType mimeTypeForFile(const QFileInfo &file) { QString path = file.absoluteFilePath(); - if (path.endsWith(qsl(".webp"), Qt::CaseInsensitive)) { + if (path.endsWith(qstr(".webp"), Qt::CaseInsensitive)) { return MimeType(MimeType::Known::WebP); - } else if (path.endsWith(qsl(".tdesktop-theme"), Qt::CaseInsensitive)) { + } else if (path.endsWith(qstr(".tdesktop-theme"), Qt::CaseInsensitive)) { return MimeType(MimeType::Known::TDesktopTheme); + } else if (path.endsWith(qstr(".tdesktop-palette"), Qt::CaseInsensitive)) { + return MimeType(MimeType::Known::TDesktopPalette); } + { QFile f(path); if (f.open(QIODevice::ReadOnly)) { diff --git a/Telegram/SourceFiles/core/utils.h b/Telegram/SourceFiles/core/utils.h index c74aa37375..cc6d444a50 100644 --- a/Telegram/SourceFiles/core/utils.h +++ b/Telegram/SourceFiles/core/utils.h @@ -432,6 +432,7 @@ public: enum class Known { Unknown, TDesktopTheme, + TDesktopPalette, WebP, }; diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 1d0b95ebfa..24f553790a 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "core/utils.h" -#define BETA_VERSION_MACRO (0ULL) +#define BETA_VERSION_MACRO (1000006001ULL) constexpr int AppVersion = 1000006; constexpr str_const AppVersionStr = "1.0.6"; diff --git a/Telegram/SourceFiles/dialogswidget.cpp b/Telegram/SourceFiles/dialogswidget.cpp index ab9e585345..d2a9653696 100644 --- a/Telegram/SourceFiles/dialogswidget.cpp +++ b/Telegram/SourceFiles/dialogswidget.cpp @@ -42,7 +42,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "apiwrap.h" #include "ui/widgets/dropdown_menu.h" #include "ui/widgets/input_fields.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "autoupdater.h" #include "observer_peer.h" diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index c28d64b79a..4384f19db7 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -56,7 +56,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "localstorage.h" #include "apiwrap.h" #include "window/top_bar_widget.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "observer_peer.h" #include "core/qthelp_regex.h" #include "ui/widgets/popup_menu.h" @@ -5802,10 +5802,8 @@ void HistoryWidget::botCallbackDone(BotCallbackInfo info, const MTPmessages_BotC if (answerData.has_message()) { if (answerData.is_alert()) { Ui::show(Box(qs(answerData.vmessage))); - } else if (App::wnd()) { - Ui::Toast::Config toast; - toast.text = qs(answerData.vmessage); - Ui::Toast::Show(App::wnd(), toast); + } else { + Ui::Toast::Show(qs(answerData.vmessage)); } } else if (answerData.has_url()) { auto url = qs(answerData.vurl); diff --git a/Telegram/SourceFiles/layerwidget.h b/Telegram/SourceFiles/layerwidget.h index 00a235dc2d..7b784f27f5 100644 --- a/Telegram/SourceFiles/layerwidget.h +++ b/Telegram/SourceFiles/layerwidget.h @@ -204,3 +204,9 @@ private: mutable QSize _cachedSize; }; + +template +inline object_ptr Box(Args&&... args) { + auto parent = static_cast(nullptr); + return object_ptr(parent, std_::forward(args)...); +} diff --git a/Telegram/SourceFiles/localstorage.cpp b/Telegram/SourceFiles/localstorage.cpp index 928ffed1db..88be5f06d8 100644 --- a/Telegram/SourceFiles/localstorage.cpp +++ b/Telegram/SourceFiles/localstorage.cpp @@ -26,7 +26,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "serialize/serialize_document.h" #include "serialize/serialize_common.h" #include "data/data_drafts.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "observer_peer.h" #include "mainwidget.h" #include "mainwindow.h" @@ -601,6 +601,7 @@ bool _backgroundWasRead = false; bool _backgroundCanWrite = true; FileKey _themeKey = 0; +QString _themePaletteAbsolutePath; bool _readingUserSettings = false; FileKey _userSettingsKey = 0; @@ -3717,6 +3718,9 @@ bool readThemeUsingKey(FileKey key) { if (theme.stream.status() != QDataStream::Ok) { return false; } + + _themePaletteAbsolutePath = Window::Theme::IsPaletteTestingPath(pathAbsolute) ? pathAbsolute : QString(); + QFile file(pathRelative); if (pathRelative.isEmpty() || !file.exists()) { file.setFileName(pathAbsolute); @@ -3748,6 +3752,7 @@ bool readThemeUsingKey(FileKey key) { void writeTheme(const QString &pathRelative, const QString &pathAbsolute, const QByteArray &content, const Window::Theme::Cached &cache) { if (content.isEmpty()) { + _themePaletteAbsolutePath = QString(); if (_themeKey) { clearKey(_themeKey); _themeKey = 0; @@ -3755,6 +3760,8 @@ void writeTheme(const QString &pathRelative, const QString &pathAbsolute, const } return; } + + _themePaletteAbsolutePath = Window::Theme::IsPaletteTestingPath(pathAbsolute) ? pathAbsolute : QString(); if (!_themeKey) { _themeKey = genKey(); writeSettings(); @@ -3787,6 +3794,29 @@ bool hasTheme() { return (_themeKey != 0); } +QString themePaletteAbsolutePath() { + return _themePaletteAbsolutePath; +} + +bool copyThemeColorsToPalette(const QString &path) { + if (!_themeKey) { + return false; + } + + FileReadDescriptor theme; + if (!readEncryptedFile(theme, _themeKey, FileOption::Safe, _settingsKey)) { + return false; + } + + QByteArray themeContent; + theme.stream >> themeContent; + if (theme.stream.status() != QDataStream::Ok) { + return false; + } + + return Window::Theme::CopyColorsToPalette(path, themeContent); +} + uint32 _peerSize(PeerData *peer) { uint32 result = sizeof(quint64) + sizeof(quint64) + Serialize::storageImageLocationSize(); if (peer->isUser()) { diff --git a/Telegram/SourceFiles/localstorage.h b/Telegram/SourceFiles/localstorage.h index 3c8d48415c..9a13bcfba3 100644 --- a/Telegram/SourceFiles/localstorage.h +++ b/Telegram/SourceFiles/localstorage.h @@ -153,6 +153,8 @@ bool readBackground(); void writeTheme(const QString &pathRelative, const QString &pathAbsolute, const QByteArray &content, const Window::Theme::Cached &cache); void clearTheme(); bool hasTheme(); +QString themePaletteAbsolutePath(); +bool copyThemeColorsToPalette(const QString &file); void writeRecentHashtagsAndBots(); void readRecentHashtagsAndBots(); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index a506810eb1..7a23b2b217 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -57,7 +57,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "media/player/media_player_instance.h" #include "core/qthelp_regex.h" #include "core/qthelp_url.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "window/player_wrap_widget.h" #include "styles/style_boxes.h" diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp index deb38beed1..764d2d36cd 100644 --- a/Telegram/SourceFiles/mainwindow.cpp +++ b/Telegram/SourceFiles/mainwindow.cpp @@ -47,9 +47,10 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "settings/settings_widget.h" #include "platform/platform_notifications_manager.h" #include "window/notifications_manager.h" -#include "window/window_theme.h" -#include "window/window_theme_warning.h" +#include "window/themes/window_theme.h" +#include "window/themes/window_theme_warning.h" #include "window/window_main_menu.h" +#include "core/task_queue.h" ConnectingWidget::ConnectingWidget(QWidget *parent, const QString &text, const QString &reconnect) : TWidget(parent) , _reconnect(this, QString()) { @@ -379,14 +380,18 @@ void MainWindow::setupMain(const MTPUser *self) { } void MainWindow::showSettings() { - if (_passcode) return; - if (isHidden()) showFromTray(); + showSpecialLayer(Box()); +} + +void MainWindow::showSpecialLayer(object_ptr layer) { + if (_passcode) return; + if (!_layerBg) { _layerBg.create(bodyWidget()); } - _layerBg->showSpecialLayer(Box()); + _layerBg->showSpecialLayer(std_::move(layer)); } void MainWindow::showMainMenu() { @@ -527,17 +532,37 @@ void MainWindow::hideConnecting() { void MainWindow::themeUpdated(const Window::Theme::BackgroundUpdate &data) { using Type = Window::Theme::BackgroundUpdate::Type; + + // We delay animating theme warning because we want all other + // subscribers to receive paltte changed notification before any + // animations (that include pixmap caches with old palette values). if (data.type == Type::TestingTheme) { if (!_testingThemeWarning) { _testingThemeWarning.create(bodyWidget()); + _testingThemeWarning->hide(); _testingThemeWarning->setGeometry(rect()); _testingThemeWarning->setHiddenCallback([this] { _testingThemeWarning.destroyDelayed(); }); } - _testingThemeWarning->showAnimated(); + + base::TaskQueue::Main().Put(base::lambda_guarded(this, [this] { + if (_testingThemeWarning) { + _testingThemeWarning->showAnimated(); + } + })); } else if (data.type == Type::RevertingTheme || data.type == Type::ApplyingTheme) { - _testingThemeWarning->hideAnimated(); - _testingThemeWarning = nullptr; - setInnerFocus(); + if (_testingThemeWarning) { + if (_testingThemeWarning->isHidden()) { + _testingThemeWarning.destroy(); + } else { + base::TaskQueue::Main().Put(base::lambda_guarded(this, [this] { + if (_testingThemeWarning) { + _testingThemeWarning->hideAnimated(); + _testingThemeWarning = nullptr; + } + setInnerFocus(); + })); + } + } } } @@ -834,12 +859,9 @@ void MainWindow::closeEvent(QCloseEvent *e) { } } -void MainWindow::resizeEvent(QResizeEvent *e) { - Platform::MainWindow::resizeEvent(e); - updateControlsGeometry(); -} - void MainWindow::updateControlsGeometry() { + Platform::MainWindow::updateControlsGeometry(); + auto body = bodyWidget()->rect(); if (_passcode) _passcode->setGeometry(body); if (_main) _main->setGeometry(body); diff --git a/Telegram/SourceFiles/mainwindow.h b/Telegram/SourceFiles/mainwindow.h index 4b8ea93ad6..2841a87f32 100644 --- a/Telegram/SourceFiles/mainwindow.h +++ b/Telegram/SourceFiles/mainwindow.h @@ -145,6 +145,8 @@ public: void showMainMenu(); void updateTrayMenu(bool force = false) override; + void showSpecialLayer(object_ptr layer); + void ui_showBox(object_ptr box, ShowLayerOptions options); void ui_hideSettingsAndLayer(ShowLayerOptions options); bool ui_isLayerShown(); @@ -156,12 +158,13 @@ public: protected: bool eventFilter(QObject *o, QEvent *e) override; void closeEvent(QCloseEvent *e) override; - void resizeEvent(QResizeEvent *e) override; void initHook() override; void updateIsActiveHook() override; void clearWidgetsHook() override; + void updateControlsGeometry() override; + public slots: void checkAutoLock(); @@ -205,8 +208,6 @@ private: void themeUpdated(const Window::Theme::BackgroundUpdate &data); - void updateControlsGeometry(); - QPixmap grabInner(); void placeSmallCounter(QImage &img, int size, int count, style::color bg, const QPoint &shift, style::color color) override; diff --git a/Telegram/SourceFiles/media/view/mediaview.style b/Telegram/SourceFiles/media/view/mediaview.style index 34d568eb33..491e3fd886 100644 --- a/Telegram/SourceFiles/media/view/mediaview.style +++ b/Telegram/SourceFiles/media/view/mediaview.style @@ -111,8 +111,6 @@ mediaviewFileIconSize: 80px; mediaviewFileLink: defaultLinkButton; -mediaviewTransparentSize: 4px; - mediaviewMenu: Menu(defaultMenu) { itemBg: mediaviewMenuBg; itemBgOver: mediaviewMenuBgOver; diff --git a/Telegram/SourceFiles/mediaview.cpp b/Telegram/SourceFiles/mediaview.cpp index ce1f22b477..fc0be7cd3f 100644 --- a/Telegram/SourceFiles/mediaview.cpp +++ b/Telegram/SourceFiles/mediaview.cpp @@ -34,7 +34,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "styles/style_history.h" #include "media/media_audio.h" #include "history/history_media_types.h" -#include "window/window_theme_preview.h" +#include "window/themes/window_theme_preview.h" #include "core/task_queue.h" #include "observer_peer.h" @@ -69,6 +69,7 @@ bool typeHasMediaOverview(MediaOverviewType type) { } // namespace MediaView::MediaView(QWidget*) : TWidget(nullptr) +, _transparentBrush(style::transparentPlaceholderBrush()) , _animStarted(getms()) , _docDownload(this, lang(lng_media_download), st::mediaviewFileLink) , _docSaveAs(this, lang(lng_mediaview_save_as), st::mediaviewFileLink) @@ -96,8 +97,6 @@ MediaView::MediaView(QWidget*) : TWidget(nullptr) mediaOverviewUpdated(update); })); - generateTransparentBrush(); - setWindowFlags(Qt::FramelessWindowHint | Qt::BypassWindowManagerHint | Qt::Tool | Qt::NoDropShadowWindowHint); moveToScreen(); setAttribute(Qt::WA_NoSystemBackground, true); @@ -1186,6 +1185,7 @@ void MediaView::displayPhoto(PhotoData *photo, HistoryItem *item) { } void MediaView::destroyThemePreview() { + _themePreviewId = 0; _themePreviewShown = false; _themePreview.reset(); _themeApply.destroy(); @@ -2767,19 +2767,6 @@ void MediaView::loadBack() { } } -void MediaView::generateTransparentBrush() { - auto size = st::mediaviewTransparentSize * cIntRetinaFactor(); - auto transparent = QImage(2 * size, 2 * size, QImage::Format_ARGB32_Premultiplied); - transparent.fill(st::mediaviewTransparentBg->c); - { - Painter p(&transparent); - p.fillRect(rtlrect(0, size, size, size, 2 * size), st::mediaviewTransparentFg); - p.fillRect(rtlrect(size, 0, size, size, 2 * size), st::mediaviewTransparentFg); - } - transparent.setDevicePixelRatio(cRetinaFactor()); - _transparentBrush = QBrush(transparent); -} - MediaView::LastChatPhoto MediaView::computeLastOverviewChatPhoto() { LastChatPhoto emptyResult = { nullptr, nullptr }; auto lastPhotoInOverview = [&emptyResult](auto history, auto list) -> LastChatPhoto { diff --git a/Telegram/SourceFiles/mediaview.h b/Telegram/SourceFiles/mediaview.h index 4700d0e8ec..167c5d81aa 100644 --- a/Telegram/SourceFiles/mediaview.h +++ b/Telegram/SourceFiles/mediaview.h @@ -166,8 +166,6 @@ private: void findCurrent(); void loadBack(); - void generateTransparentBrush(); - void updateCursor(); void setZoomLevel(int newZoom); diff --git a/Telegram/SourceFiles/overviewwidget.cpp b/Telegram/SourceFiles/overviewwidget.cpp index ea7327b857..1963d7c9e7 100644 --- a/Telegram/SourceFiles/overviewwidget.cpp +++ b/Telegram/SourceFiles/overviewwidget.cpp @@ -33,7 +33,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "ui/widgets/buttons.h" #include "ui/widgets/input_fields.h" #include "window/top_bar_widget.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "lang.h" #include "mainwindow.h" #include "mainwidget.h" diff --git a/Telegram/SourceFiles/platform/win/main_window_win.cpp b/Telegram/SourceFiles/platform/win/main_window_win.cpp index 3524c95745..85a18d75da 100644 --- a/Telegram/SourceFiles/platform/win/main_window_win.cpp +++ b/Telegram/SourceFiles/platform/win/main_window_win.cpp @@ -30,7 +30,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "lang.h" #include "localstorage.h" #include "ui/widgets/popup_menu.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include diff --git a/Telegram/SourceFiles/profile/profile_block_invite_link.cpp b/Telegram/SourceFiles/profile/profile_block_invite_link.cpp index 02ce6137be..5cdaa255a5 100644 --- a/Telegram/SourceFiles/profile/profile_block_invite_link.cpp +++ b/Telegram/SourceFiles/profile/profile_block_invite_link.cpp @@ -110,9 +110,7 @@ void InviteLinkWidget::refreshLink() { } QApplication::clipboard()->setText(link); - Ui::Toast::Config toast; - toast.text = lang(lng_group_invite_copied); - Ui::Toast::Show(App::wnd(), toast); + Ui::Toast::Show(lang(lng_group_invite_copied)); return false; }); } diff --git a/Telegram/SourceFiles/settings/settings_advanced_widget.cpp b/Telegram/SourceFiles/settings/settings_advanced_widget.cpp index 23b6dcc722..740bf2a867 100644 --- a/Telegram/SourceFiles/settings/settings_advanced_widget.cpp +++ b/Telegram/SourceFiles/settings/settings_advanced_widget.cpp @@ -31,7 +31,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "ui/widgets/buttons.h" #include "ui/effects/widget_slide_wrap.h" #include "localstorage.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" namespace Settings { diff --git a/Telegram/SourceFiles/settings/settings_background_widget.cpp b/Telegram/SourceFiles/settings/settings_background_widget.cpp index a0e606d351..8cfdfaa551 100644 --- a/Telegram/SourceFiles/settings/settings_background_widget.cpp +++ b/Telegram/SourceFiles/settings/settings_background_widget.cpp @@ -30,7 +30,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "ui/widgets/buttons.h" #include "localstorage.h" #include "mainwindow.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" namespace Settings { @@ -240,7 +240,7 @@ void BackgroundWidget::needBackgroundUpdate(bool tile) { void BackgroundWidget::onChooseFromFile() { auto imgExtensions = cImgExtensions(); - auto filters = QStringList(qsl("Theme files (*.tdesktop-theme *") + imgExtensions.join(qsl(" *")) + qsl(")")); + auto filters = QStringList(qsl("Theme files (*.tdesktop-theme *.tdesktop-palette *") + imgExtensions.join(qsl(" *")) + qsl(")")); filters.push_back(filedialogAllFilesFilter()); _chooseFromFileQueryId = FileDialog::queryReadFile(lang(lng_choose_image), filters.join(qsl(";;"))); @@ -261,7 +261,8 @@ void BackgroundWidget::notifyFileQueryUpdated(const FileDialog::QueryUpdate &upd } auto filePath = update.filePaths.front(); - if (filePath.endsWith(qstr(".tdesktop-theme"), Qt::CaseInsensitive)) { + if (filePath.endsWith(qstr(".tdesktop-theme"), Qt::CaseInsensitive) + || filePath.endsWith(qstr(".tdesktop-palette"), Qt::CaseInsensitive)) { Window::Theme::Apply(filePath); return; } diff --git a/Telegram/SourceFiles/settings/settings_fixed_bar.cpp b/Telegram/SourceFiles/settings/settings_fixed_bar.cpp index 16fa6ec8d8..a4d860bfe4 100644 --- a/Telegram/SourceFiles/settings/settings_fixed_bar.cpp +++ b/Telegram/SourceFiles/settings/settings_fixed_bar.cpp @@ -24,7 +24,6 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "styles/style_settings.h" #include "styles/style_boxes.h" #include "mainwindow.h" -#include "lang.h" namespace Settings { @@ -32,6 +31,11 @@ FixedBar::FixedBar(QWidget *parent) : TWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent); } +void FixedBar::setText(const QString &text) { + _text = text; + update(); +} + int FixedBar::resizeGetHeight(int newWidth) { return st::settingsFixedBarHeight - st::boxRadius; } @@ -43,7 +47,7 @@ void FixedBar::paintEvent(QPaintEvent *e) { p.setFont(st::settingsFixedBarFont); p.setPen(st::windowFg); - p.drawTextLeft(st::settingsFixedBarTextPosition.x(), st::settingsFixedBarTextPosition.y() - st::boxRadius, width(), lang(lng_menu_settings)); + p.drawTextLeft(st::settingsFixedBarTextPosition.x(), st::settingsFixedBarTextPosition.y() - st::boxRadius, width(), _text); } } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_fixed_bar.h b/Telegram/SourceFiles/settings/settings_fixed_bar.h index 170b76144f..89dabeb033 100644 --- a/Telegram/SourceFiles/settings/settings_fixed_bar.h +++ b/Telegram/SourceFiles/settings/settings_fixed_bar.h @@ -30,11 +30,16 @@ class FixedBar : public TWidget { public: FixedBar(QWidget *parent); + void setText(const QString &text); + protected: void paintEvent(QPaintEvent *e) override; int resizeGetHeight(int newWidth) override; +private: + QString _text; + }; } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_general_widget.cpp b/Telegram/SourceFiles/settings/settings_general_widget.cpp index 5614d42bbf..f947910b14 100644 --- a/Telegram/SourceFiles/settings/settings_general_widget.cpp +++ b/Telegram/SourceFiles/settings/settings_general_widget.cpp @@ -189,21 +189,21 @@ void GeneralWidget::refreshControls() { if (cPlatform() == dbipWindows || cSupportTray()) { addChildRow(_enableTrayIcon, marginSmall, lang(lng_settings_workmode_tray), SLOT(onEnableTrayIcon()), (cWorkMode() == dbiwmTrayOnly || cWorkMode() == dbiwmWindowAndTray)); -#ifdef Q_OS_WIN - addChildRow(_enableTaskbarIcon, marginLarge, lang(lng_settings_workmode_window), SLOT(onEnableTaskbarIcon()), (cWorkMode() == dbiwmWindowOnly || cWorkMode() == dbiwmWindowAndTray)); + if (cPlatform() == dbipWindows) { + addChildRow(_enableTaskbarIcon, marginLarge, lang(lng_settings_workmode_window), SLOT(onEnableTaskbarIcon()), (cWorkMode() == dbiwmWindowOnly || cWorkMode() == dbiwmWindowAndTray)); #ifndef OS_WIN_STORE - addChildRow(_autoStart, marginSmall, lang(lng_settings_auto_start), SLOT(onAutoStart()), cAutoStart()); - addChildRow(_startMinimized, marginLarge, slidedPadding, lang(lng_settings_start_min), SLOT(onStartMinimized()), (cStartMinimized() && !Global::LocalPasscode())); - subscribe(Global::RefLocalPasscodeChanged(), [this] { - _startMinimized->entity()->setChecked(cStartMinimized() && !Global::LocalPasscode()); - }); - if (!cAutoStart()) { - _startMinimized->hideFast(); - } - addChildRow(_addInSendTo, marginSmall, lang(lng_settings_add_sendto), SLOT(onAddInSendTo()), cSendToMenu()); + addChildRow(_autoStart, marginSmall, lang(lng_settings_auto_start), SLOT(onAutoStart()), cAutoStart()); + addChildRow(_startMinimized, marginLarge, slidedPadding, lang(lng_settings_start_min), SLOT(onStartMinimized()), (cStartMinimized() && !Global::LocalPasscode())); + subscribe(Global::RefLocalPasscodeChanged(), [this] { + _startMinimized->entity()->setChecked(cStartMinimized() && !Global::LocalPasscode()); + }); + if (!cAutoStart()) { + _startMinimized->hideFast(); + } + addChildRow(_addInSendTo, marginSmall, lang(lng_settings_add_sendto), SLOT(onAddInSendTo()), cSendToMenu()); #endif // OS_WIN_STORE -#endif // Q_OS_WIN + } } } @@ -297,7 +297,7 @@ void GeneralWidget::updateWorkmode() { Local::writeSettings(); } -#if defined Q_OS_WIN && !defined OS_WIN_STORE +#if !defined OS_WIN_STORE void GeneralWidget::onAutoStart() { cSetAutoStart(_autoStart->checked()); if (cAutoStart()) { @@ -335,6 +335,6 @@ void GeneralWidget::onAddInSendTo() { psSendToMenu(_addInSendTo->checked()); Local::writeSettings(); } -#endif // Q_OS_WIN && !OS_WIN_STORE +#endif // !OS_WIN_STORE } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_general_widget.h b/Telegram/SourceFiles/settings/settings_general_widget.h index 7b9579724d..833b3857c9 100644 --- a/Telegram/SourceFiles/settings/settings_general_widget.h +++ b/Telegram/SourceFiles/settings/settings_general_widget.h @@ -94,11 +94,11 @@ private slots: void onEnableTrayIcon(); void onEnableTaskbarIcon(); -#if defined Q_OS_WIN && !defined OS_WIN_STORE +#ifndef OS_WIN_STORE void onAutoStart(); void onStartMinimized(); void onAddInSendTo(); -#endif // Q_OS_WIN && !OS_WIN_STORE +#endif // !OS_WIN_STORE void onRestart(); diff --git a/Telegram/SourceFiles/settings/settings_inner_widget.cpp b/Telegram/SourceFiles/settings/settings_inner_widget.cpp index aa8b93988f..d2c972eeb3 100644 --- a/Telegram/SourceFiles/settings/settings_inner_widget.cpp +++ b/Telegram/SourceFiles/settings/settings_inner_widget.cpp @@ -35,7 +35,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org namespace Settings { -InnerWidget::InnerWidget(QWidget *parent) : TWidget(parent) +InnerWidget::InnerWidget(QWidget *parent) : LayerInner(parent) , _self(App::self()) { refreshBlocks(); subscribe(Global::RefSelfChanged(), [this]() { selfUpdated(); }); diff --git a/Telegram/SourceFiles/settings/settings_inner_widget.h b/Telegram/SourceFiles/settings/settings_inner_widget.h index 5bf68e8307..ecc7f5fa10 100644 --- a/Telegram/SourceFiles/settings/settings_inner_widget.h +++ b/Telegram/SourceFiles/settings/settings_inner_widget.h @@ -20,19 +20,21 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #pragma once +#include "settings/settings_layer.h" + namespace Settings { class CoverWidget; class BlockWidget; -class InnerWidget : public TWidget, private base::Subscriber { +class InnerWidget : public LayerInner, private base::Subscriber { Q_OBJECT public: InnerWidget(QWidget *parent); // Count new height for width=newWidth and resize to it. - void resizeToWidth(int newWidth, int contentLeft) { + void resizeToWidth(int newWidth, int contentLeft) override { _contentLeft = contentLeft; return TWidget::resizeToWidth(newWidth); } diff --git a/Telegram/SourceFiles/settings/settings_layer.cpp b/Telegram/SourceFiles/settings/settings_layer.cpp new file mode 100644 index 0000000000..6cbd2cba3e --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_layer.cpp @@ -0,0 +1,134 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop 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. + +It 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. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#include "stdafx.h" +#include "settings/settings_layer.h" + +#include "settings/settings_inner_widget.h" +#include "settings/settings_fixed_bar.h" +#include "styles/style_settings.h" +#include "styles/style_window.h" +#include "styles/style_boxes.h" +#include "ui/effects/widget_fade_wrap.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/buttons.h" +#include "mainwindow.h" +#include "mainwidget.h" +#include "localstorage.h" +#include "boxes/confirmbox.h" +#include "application.h" +#include "ui/filedialog.h" +#include "window/themes/window_theme.h" + +namespace Settings { + +Layer::Layer() +: _scroll(this, st::settingsScroll) +, _fixedBar(this) +, _fixedBarClose(this, st::settingsFixedBarClose) +, _fixedBarShadow(this, object_ptr(this)) { + _fixedBar->moveToLeft(0, st::boxRadius); + _fixedBarClose->moveToRight(0, 0); + _fixedBarShadow->entity()->resize(width(), st::lineWidth); + _fixedBarShadow->moveToLeft(0, _fixedBar->y() + _fixedBar->height()); + _fixedBarShadow->hideFast(); + _scroll->moveToLeft(0, st::settingsFixedBarHeight); + + connect(_scroll, SIGNAL(scrolled()), this, SLOT(onScroll())); +} + +void Layer::setCloseClickHandler(base::lambda &&callback) { + _fixedBarClose->setClickedCallback(std_::move(callback)); +} + +void Layer::onScroll() { + if (_scroll->scrollTop() > 0) { + _fixedBarShadow->showAnimated(); + } else { + _fixedBarShadow->hideAnimated(); + } +} + +void Layer::resizeToWidth(int newWidth, int newContentLeft) { + // Widget height depends on InnerWidget height, so we + // resize it here, not in the resizeEvent() handler. + _inner->resizeToWidth(newWidth, newContentLeft); + + resizeUsingInnerHeight(newWidth, _inner->height()); +} + +void Layer::onInnerHeightUpdated() { + resizeUsingInnerHeight(width(), _inner->height()); +} + +void Layer::doSetInnerWidget(object_ptr widget) { + _inner = _scroll->setOwnedWidget(std_::move(widget)); + connect(_inner, SIGNAL(heightUpdated()), this, SLOT(onInnerHeightUpdated())); +} + +void Layer::paintEvent(QPaintEvent *e) { + Painter p(this); + auto clip = e->rect(); + if (_roundedCorners) { + auto paintTopRounded = clip.intersects(QRect(0, 0, width(), st::boxRadius)); + auto paintBottomRounded = clip.intersects(QRect(0, height() - st::boxRadius, width(), st::boxRadius)); + if (paintTopRounded || paintBottomRounded) { + auto parts = qFlags(App::RectPart::None); + if (paintTopRounded) parts |= App::RectPart::TopFull; + if (paintBottomRounded) parts |= App::RectPart::BottomFull; + App::roundRect(p, rect(), st::boxBg, BoxCorners, nullptr, parts); + } + auto other = clip.intersected(QRect(0, st::boxRadius, width(), height() - 2 * st::boxRadius)); + if (!other.isEmpty()) { + p.fillRect(other, st::boxBg); + } + } else { + p.fillRect(e->rect(), st::boxBg); + } +} + +void Layer::resizeEvent(QResizeEvent *e) { + LayerWidget::resizeEvent(e); + if (!width() || !height()) { + return; + } + + _fixedBar->resizeToWidth(width()); + _fixedBar->moveToLeft(0, st::boxRadius); + _fixedBarClose->moveToRight(0, 0); + auto shadowTop = _fixedBar->y() + _fixedBar->height(); + _fixedBarShadow->entity()->resize(width(), st::lineWidth); + _fixedBarShadow->moveToLeft(0, shadowTop); + + auto scrollSize = QSize(width(), height() - shadowTop - (_roundedCorners ? st::boxRadius : 0)); + if (_scroll->size() != scrollSize) { + _scroll->resize(scrollSize); + } + if (!_scroll->isHidden()) { + auto scrollTop = _scroll->scrollTop(); + _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); + } +} + +void Layer::setTitle(const QString &title) { + _fixedBar->setText(title); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_layer.h b/Telegram/SourceFiles/settings/settings_layer.h new file mode 100644 index 0000000000..3f097b2a51 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_layer.h @@ -0,0 +1,94 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop 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. + +It 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. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include "layerwidget.h" + +class BoxLayerTitleShadow; + +namespace Ui { +class ScrollArea; +class IconButton; +template +class WidgetFadeWrap; +} // namespace Ui + +namespace Settings { + +class FixedBar; +class LayerInner : public TWidget { +public: + LayerInner(QWidget *parent) : TWidget(parent) { + } + + virtual void resizeToWidth(int newWidth, int contentLeft) { + TWidget::resizeToWidth(newWidth); + } + +}; + +class Layer : public LayerWidget { + Q_OBJECT + +public: + Layer(); + + void setCloseClickHandler(base::lambda &&callback); + void resizeToWidth(int newWidth, int newContentLeft); + +protected: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + + template + QPointer setInnerWidget(object_ptr widget) { + auto result = QPointer(widget); + doSetInnerWidget(std_::move(widget)); + return result; + } + + void setTitle(const QString &title); + void setRoundedCorners(bool roundedCorners) { + _roundedCorners = roundedCorners; + } + +private slots: + void onInnerHeightUpdated(); + void onScroll(); + +private: + void doSetInnerWidget(object_ptr widget); + + virtual void resizeUsingInnerHeight(int newWidth, int innerHeight) { + resize(newWidth, height()); + } + + object_ptr _scroll; + QPointer _inner; + object_ptr _fixedBar; + object_ptr _fixedBarClose; + object_ptr> _fixedBarShadow; + + bool _roundedCorners = false; + +}; + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_widget.cpp b/Telegram/SourceFiles/settings/settings_widget.cpp index 6db50f88e2..b6f2aa5e7f 100644 --- a/Telegram/SourceFiles/settings/settings_widget.cpp +++ b/Telegram/SourceFiles/settings/settings_widget.cpp @@ -33,7 +33,11 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "mainwidget.h" #include "localstorage.h" #include "boxes/confirmbox.h" +#include "lang.h" #include "application.h" +#include "ui/filedialog.h" +#include "window/themes/window_theme.h" +#include "window/themes/window_theme_editor.h" namespace Settings { namespace { @@ -88,6 +92,23 @@ void fillCodes() { main->getDifference(); } }); + Codes.insert(qsl("loadcolors"), []() { + FileDialog::askOpenPath("Open palette file", "Palette (*.tdesktop-palette)", [](const FileDialog::OpenResult &result) { + if (!result.paths.isEmpty()) { + Window::Theme::Apply(result.paths.front()); + } + }); + }); + Codes.insert(qsl("edittheme"), []() { + auto palettePath = Local::themePaletteAbsolutePath(); + if (palettePath.isEmpty()) { + FileDialog::askWritePath(lang(lng_theme_editor_save_palette), "Palette (*.tdesktop-palette)", "colors.tdesktop-palette", [](const QString &path) { + Window::Theme::Editor::StartFromCurrentTheme(path); + }); + } else { + Window::Theme::Editor::Start(palettePath); + } + }); } void codesFeedString(const QString &text) { @@ -123,42 +144,28 @@ void codesFeedString(const QString &text) { } // namespace -Widget::Widget(QWidget *parent) : LayerWidget(parent) -, _scroll(this, st::settingsScroll) -, _fixedBar(this) -, _fixedBarClose(this, st::settingsFixedBarClose) -, _fixedBarShadow(this, object_ptr(this)) { - _inner = _scroll->setOwnedWidget(object_ptr(this)); - - _fixedBar->moveToLeft(0, st::boxRadius); - _fixedBarClose->moveToRight(0, 0); - _fixedBarShadow->entity()->resize(width(), st::lineWidth); - _fixedBarShadow->moveToLeft(0, _fixedBar->y() + _fixedBar->height()); - _fixedBarShadow->hideFast(); - _scroll->moveToLeft(0, st::settingsFixedBarHeight); - - connect(_scroll, SIGNAL(scrolled()), this, SLOT(onScroll())); - - _fixedBarClose->setClickedCallback([]() { +Widget::Widget(QWidget *parent) { + setTitle(lang(lng_menu_settings)); + _inner = setInnerWidget(object_ptr(this)); + setCloseClickHandler([]() { Ui::hideSettingsAndLayer(); }); - - connect(_inner, SIGNAL(heightUpdated()), this, SLOT(onInnerHeightUpdated())); } -void Widget::onScroll() { - if (_scroll->scrollTop() > 0) { - _fixedBarShadow->showAnimated(); - } else { - _fixedBarShadow->hideAnimated(); - } +void Widget::showFinished() { + _inner->showFinished(); +} + +void Widget::keyPressEvent(QKeyEvent *e) { + codesFeedString(e->text()); + return LayerWidget::keyPressEvent(e); } void Widget::parentResized() { auto parentSize = parentWidget()->size(); - int windowWidth = parentSize.width(); - int newWidth = st::settingsMaxWidth; - int newContentLeft = st::settingsMaxPadding; + auto windowWidth = parentSize.width(); + auto newWidth = st::settingsMaxWidth; + auto newContentLeft = st::settingsMaxPadding; if (windowWidth <= st::settingsMaxWidth) { newWidth = windowWidth; newContentLeft = st::settingsMinPadding; @@ -176,92 +183,27 @@ void Widget::parentResized() { newContentLeft += ((newWidth - st::windowMinWidth) * (st::settingsMaxPadding - st::settingsMinPadding)) / (st::settingsMaxWidth - st::windowMinWidth); } } - - // Widget height depends on InnerWidget height, so we - // resize it here, not in the resizeEvent() handler. - _inner->resizeToWidth(newWidth, newContentLeft); - - resizeUsingInnerHeight(newWidth, newContentLeft); + resizeToWidth(newWidth, newContentLeft); } -void Widget::onInnerHeightUpdated() { - resizeUsingInnerHeight(width(), _contentLeft); -} - -void Widget::resizeUsingInnerHeight(int newWidth, int newContentLeft) { +void Widget::resizeUsingInnerHeight(int newWidth, int innerHeight) { if (!App::wnd()) return; auto parentSize = parentWidget()->size(); - int windowWidth = parentSize.width(); - int windowHeight = parentSize.height(); - int maxHeight = st::settingsFixedBarHeight + _inner->height(); - int newHeight = maxHeight + st::boxRadius; + auto windowWidth = parentSize.width(); + auto windowHeight = parentSize.height(); + auto maxHeight = st::settingsFixedBarHeight + innerHeight; + auto newHeight = maxHeight + st::boxRadius; if (newHeight > windowHeight || newWidth >= windowWidth) { newHeight = windowHeight; } - if (_contentLeft != newContentLeft) { - _contentLeft = newContentLeft; - } - - _roundedCorners = (newHeight < windowHeight); - setAttribute(Qt::WA_OpaquePaintEvent, !_roundedCorners); + auto roundedCorners = newHeight < windowHeight; + setRoundedCorners(roundedCorners); + setAttribute(Qt::WA_OpaquePaintEvent, !roundedCorners); setGeometry((windowWidth - newWidth) / 2, (windowHeight - newHeight) / 2, newWidth, newHeight); update(); } -void Widget::showFinished() { - _inner->showFinished(); -} - -void Widget::paintEvent(QPaintEvent *e) { - Painter p(this); - auto clip = e->rect(); - if (_roundedCorners) { - auto paintTopRounded = clip.intersects(QRect(0, 0, width(), st::boxRadius)); - auto paintBottomRounded = clip.intersects(QRect(0, height() - st::boxRadius, width(), st::boxRadius)); - if (paintTopRounded || paintBottomRounded) { - auto parts = qFlags(App::RectPart::None); - if (paintTopRounded) parts |= App::RectPart::TopFull; - if (paintBottomRounded) parts |= App::RectPart::BottomFull; - App::roundRect(p, rect(), st::boxBg, BoxCorners, nullptr, parts); - } - auto other = clip.intersected(QRect(0, st::boxRadius, width(), height() - 2 * st::boxRadius)); - if (!other.isEmpty()) { - p.fillRect(other, st::boxBg); - } - } else { - p.fillRect(e->rect(), st::boxBg); - } -} - -void Widget::resizeEvent(QResizeEvent *e) { - LayerWidget::resizeEvent(e); - if (!width() || !height()) { - return; - } - - _fixedBar->resizeToWidth(width()); - _fixedBar->moveToLeft(0, st::boxRadius); - _fixedBarClose->moveToRight(0, 0); - auto shadowTop = _fixedBar->y() + _fixedBar->height(); - _fixedBarShadow->entity()->resize(width(), st::lineWidth); - _fixedBarShadow->moveToLeft(0, shadowTop); - - auto scrollSize = QSize(width(), height() - shadowTop - (_roundedCorners ? st::boxRadius : 0)); - if (_scroll->size() != scrollSize) { - _scroll->resize(scrollSize); - } - if (!_scroll->isHidden()) { - auto scrollTop = _scroll->scrollTop(); - _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); - } -} - -void Widget::keyPressEvent(QKeyEvent *e) { - codesFeedString(e->text()); - return LayerWidget::keyPressEvent(e); -} - } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_widget.h b/Telegram/SourceFiles/settings/settings_widget.h index 0abebdcaea..6a5c760ea9 100644 --- a/Telegram/SourceFiles/settings/settings_widget.h +++ b/Telegram/SourceFiles/settings/settings_widget.h @@ -20,51 +20,30 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #pragma once -#include "layerwidget.h" - -class BoxLayerTitleShadow; - -namespace Ui { -class ScrollArea; -class IconButton; -template -class WidgetFadeWrap; -} // namespace Ui +#include "settings/settings_layer.h" namespace Settings { class InnerWidget; -class FixedBar; -class Widget : public LayerWidget { +class Widget : public Layer { Q_OBJECT public: - Widget(QWidget *parent); + Widget(QWidget*); - void parentResized() override; void showFinished() override; + void parentResized() override; protected: - void paintEvent(QPaintEvent *e) override; - void resizeEvent(QResizeEvent *e) override; void keyPressEvent(QKeyEvent *e) override; -private slots: - void onInnerHeightUpdated(); - void onScroll(); - private: - void resizeUsingInnerHeight(int newWidth, int newContentLeft); + void resizeUsingInnerHeight(int newWidth, int innerHeight) override; - object_ptr _scroll; QPointer _inner; - object_ptr _fixedBar; - object_ptr _fixedBarClose; - object_ptr> _fixedBarShadow; int _contentLeft = 0; - bool _roundedCorners = false; }; diff --git a/Telegram/SourceFiles/stickers/stickers.cpp b/Telegram/SourceFiles/stickers/stickers.cpp index ea9b576146..0bfff3c228 100644 --- a/Telegram/SourceFiles/stickers/stickers.cpp +++ b/Telegram/SourceFiles/stickers/stickers.cpp @@ -86,7 +86,7 @@ void applyArchivedResult(const MTPDmessages_stickerSetInstallResultArchive &d) { toast.text = lang(lng_stickers_packs_archived); toast.maxWidth = st::stickersToastMaxWidth; toast.padding = st::stickersToastPadding; - Ui::Toast::Show(App::wnd(), toast); + Ui::Toast::Show(toast); // Ui::show(Box(archived), KeepOtherLayers); emit App::main()->stickersUpdated(); diff --git a/Telegram/SourceFiles/structs.cpp b/Telegram/SourceFiles/structs.cpp index 277b3b2599..d22275395b 100644 --- a/Telegram/SourceFiles/structs.cpp +++ b/Telegram/SourceFiles/structs.cpp @@ -35,7 +35,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "localstorage.h" #include "history/history_media_types.h" #include "styles/style_history.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" namespace { diff --git a/Telegram/SourceFiles/structs.h b/Telegram/SourceFiles/structs.h index 4451413766..d918320f51 100644 --- a/Telegram/SourceFiles/structs.h +++ b/Telegram/SourceFiles/structs.h @@ -1169,7 +1169,7 @@ public: return (type == AnimatedDocument) && !mime.compare(qstr("video/mp4"), Qt::CaseInsensitive); } bool isTheme() const { - return name.endsWith(qstr(".tdesktop-theme"), Qt::CaseInsensitive); + return name.endsWith(qstr(".tdesktop-theme"), Qt::CaseInsensitive) || name.endsWith(qstr(".tdesktop-palette"), Qt::CaseInsensitive); } bool isMusic() const { if (auto s = song()) { diff --git a/Telegram/SourceFiles/ui/filedialog.cpp b/Telegram/SourceFiles/ui/filedialog.cpp index e85b6a89d5..324a9ff8b2 100644 --- a/Telegram/SourceFiles/ui/filedialog.cpp +++ b/Telegram/SourceFiles/ui/filedialog.cpp @@ -25,6 +25,8 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "localstorage.h" #include "platform/platform_file_dialog.h" +#include "core/task_queue.h" + void filedialogInit() { if (cDialogLastPath().isEmpty()) { #ifdef Q_OS_WIN @@ -384,4 +386,67 @@ base::Observable &QueryDone() { return QueryDoneObservable; } +void askOpenPath(const QString &caption, const QString &filter, base::lambda &&callback, base::lambda &&failed) { + base::TaskQueue::Main().Put([caption, filter, callback = std_::move(callback), failed = std_::move(failed)] { + auto file = QString(); + auto remoteContent = QByteArray(); + if (filedialogGetOpenFile(file, remoteContent, caption, filter) && (!file.isEmpty() || !remoteContent.isEmpty())) { + if (callback) { + auto result = OpenResult(); + if (!file.isEmpty()) { + result.paths.push_back(file); + } + result.remoteContent = remoteContent; + callback(result); + } + } else if (failed) { + failed(); + } + }); +} + +void askOpenPaths(const QString &caption, const QString &filter, base::lambda &&callback, base::lambda &&failed) { + base::TaskQueue::Main().Put([caption, filter, callback = std_::move(callback), failed = std_::move(failed)] { + auto files = QStringList(); + auto remoteContent = QByteArray(); + if (filedialogGetOpenFiles(files, remoteContent, caption, filter) && (!files.isEmpty() || !remoteContent.isEmpty())) { + if (callback) { + auto result = OpenResult(); + result.paths = files; + result.remoteContent = remoteContent; + callback(result); + } + } else if (failed) { + failed(); + } + }); + +} + +void askWritePath(const QString &caption, const QString &filter, const QString &initialPath, base::lambda &&callback, base::lambda &&failed) { + base::TaskQueue::Main().Put([caption, filter, initialPath, callback = std_::move(callback), failed = std_::move(failed)] { + auto file = QString(); + if (filedialogGetSaveFile(file, caption, filter, initialPath)) { + if (callback) { + callback(file); + } + } else if (failed) { + failed(); + } + }); +} + +void askFolder(const QString &caption, base::lambda &&callback, base::lambda &&failed) { + base::TaskQueue::Main().Put([caption, callback = std_::move(callback), failed = std_::move(failed)] { + auto folder = QString(); + if (filedialogGetDir(folder, caption) && !folder.isEmpty()) { + if (callback) { + callback(folder); + } + } else if (failed) { + failed(); + } + }); +} + } // namespace FileDialog diff --git a/Telegram/SourceFiles/ui/filedialog.h b/Telegram/SourceFiles/ui/filedialog.h index 1479b9fc7e..045266cb7a 100644 --- a/Telegram/SourceFiles/ui/filedialog.h +++ b/Telegram/SourceFiles/ui/filedialog.h @@ -65,4 +65,13 @@ bool processQuery(); base::Observable &QueryDone(); +struct OpenResult { + QStringList paths; + QByteArray remoteContent; +}; +void askOpenPath(const QString &caption, const QString &filter, base::lambda &&callback, base::lambda &&failed = base::lambda()); +void askOpenPaths(const QString &caption, const QString &filter, base::lambda &&callback, base::lambda &&failed = base::lambda()); +void askWritePath(const QString &caption, const QString &filter, const QString &initialPath, base::lambda &&callback, base::lambda &&failed = base::lambda()); +void askFolder(const QString &caption, base::lambda &&callback, base::lambda &&failed = base::lambda()); + } // namespace FileDialog diff --git a/Telegram/SourceFiles/ui/style/style_core.cpp b/Telegram/SourceFiles/ui/style/style_core.cpp index 59b69ef6ee..4bb663eb8f 100644 --- a/Telegram/SourceFiles/ui/style/style_core.cpp +++ b/Telegram/SourceFiles/ui/style/style_core.cpp @@ -115,6 +115,20 @@ void colorizeImage(const QImage &src, QColor c, QImage *outResult, QRect srcRect outResult->setDevicePixelRatio(src.devicePixelRatio()); } +QBrush transparentPlaceholderBrush() { + auto size = st::transparentPlaceholderSize * cIntRetinaFactor(); + auto transparent = QImage(2 * size, 2 * size, QImage::Format_ARGB32_Premultiplied); + transparent.fill(st::mediaviewTransparentBg->c); + { + Painter p(&transparent); + p.fillRect(rtlrect(0, size, size, size, 2 * size), st::mediaviewTransparentFg); + p.fillRect(rtlrect(size, 0, size, size, 2 * size), st::mediaviewTransparentFg); + } + transparent.setDevicePixelRatio(cRetinaFactor()); + return QBrush(transparent); + +} + namespace internal { QImage createCircleMask(int size, QColor bg, QColor fg) { diff --git a/Telegram/SourceFiles/ui/style/style_core.h b/Telegram/SourceFiles/ui/style/style_core.h index 73520210c1..dc1c4144e8 100644 --- a/Telegram/SourceFiles/ui/style/style_core.h +++ b/Telegram/SourceFiles/ui/style/style_core.h @@ -78,6 +78,8 @@ inline QImage colorizeImage(const QImage &src, const color &c, QRect srcRect = Q return colorizeImage(src, c->c, srcRect); } +QBrush transparentPlaceholderBrush(); + namespace internal { QImage createCircleMask(int size, QColor bg, QColor fg); diff --git a/Telegram/SourceFiles/ui/toast/toast.cpp b/Telegram/SourceFiles/ui/toast/toast.cpp index 3dcb66b4a5..393a4dc7c1 100644 --- a/Telegram/SourceFiles/ui/toast/toast.cpp +++ b/Telegram/SourceFiles/ui/toast/toast.cpp @@ -23,6 +23,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "ui/toast/toast_manager.h" #include "ui/toast/toast_widget.h" +#include "mainwindow.h" namespace Ui { namespace Toast { @@ -40,6 +41,18 @@ void Show(QWidget *parent, const Config &config) { } } +void Show(const Config &config) { + if (auto window = App::wnd()) { + Show(window->bodyWidget(), config); + } +} + +void Show(const QString &text) { + Config toast; + toast.text = text; + Show(toast); +} + void Instance::opacityAnimationCallback() { _widget->setShownLevel(_a_opacity.current(_hiding ? 0. : 1.)); _widget->update(); diff --git a/Telegram/SourceFiles/ui/toast/toast.h b/Telegram/SourceFiles/ui/toast/toast.h index 729b1d2aeb..4a389515c2 100644 --- a/Telegram/SourceFiles/ui/toast/toast.h +++ b/Telegram/SourceFiles/ui/toast/toast.h @@ -36,6 +36,8 @@ struct Config { QMargins padding; }; void Show(QWidget *parent, const Config &config); +void Show(const Config &config); +void Show(const QString &text); class Instance { struct Private { diff --git a/Telegram/SourceFiles/ui/toast/toast_manager.cpp b/Telegram/SourceFiles/ui/toast/toast_manager.cpp index cfd42b0475..60440c4111 100644 --- a/Telegram/SourceFiles/ui/toast/toast_manager.cpp +++ b/Telegram/SourceFiles/ui/toast/toast_manager.cpp @@ -40,6 +40,10 @@ Manager::Manager(QWidget *parent) : QObject(parent) { } Manager *Manager::instance(QWidget *parent) { + if (!parent) { + return nullptr; + } + _managers.createIfNull(); auto i = _managers->constFind(parent); if (i == _managers->cend()) { diff --git a/Telegram/SourceFiles/ui/widgets/buttons.cpp b/Telegram/SourceFiles/ui/widgets/buttons.cpp index d90d12b99d..97f95fc089 100644 --- a/Telegram/SourceFiles/ui/widgets/buttons.cpp +++ b/Telegram/SourceFiles/ui/widgets/buttons.cpp @@ -632,14 +632,19 @@ CrossButton::CrossButton(QWidget *parent, const style::CrossButton &st) : Ripple hide(); } -void CrossButton::hideAnimated() { - startAnimation(false); -} - void CrossButton::showAnimated() { startAnimation(true); } +void CrossButton::showFast() { + showAnimated(); + _a_show.finish(); +} + +void CrossButton::hideAnimated() { + startAnimation(false); +} + void CrossButton::hideFast() { hideAnimated(); _a_show.finish(); diff --git a/Telegram/SourceFiles/ui/widgets/buttons.h b/Telegram/SourceFiles/ui/widgets/buttons.h index 9e9b1fc391..45b156cba0 100644 --- a/Telegram/SourceFiles/ui/widgets/buttons.h +++ b/Telegram/SourceFiles/ui/widgets/buttons.h @@ -213,6 +213,7 @@ public: CrossButton(QWidget *parent, const style::CrossButton &st); void showAnimated(); + void showFast(); void hideAnimated(); void hideFast(); diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index ffdda652d4..d7be4cb818 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -24,7 +24,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "ui/widgets/popup_menu.h" #include "mainwindow.h" #include "ui/countryinput.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "lang.h" #include "numbers.h" diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index dec96a5ccc..5606f1dbcc 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -586,7 +586,7 @@ attentionLeftOutlineButton: OutlineButton(defaultLeftOutlineButton) { textBgOver: attentionButtonBgOver; textFg: attentionButtonFg; - textFgOver: attentionButtonFg; + textFgOver: attentionButtonFgOver; ripple: RippleAnimation(defaultRippleAnimation) { color: attentionButtonBgRipple; diff --git a/Telegram/SourceFiles/window/main_window.cpp b/Telegram/SourceFiles/window/main_window.cpp index 11a64eed3f..c6b5cd43c9 100644 --- a/Telegram/SourceFiles/window/main_window.cpp +++ b/Telegram/SourceFiles/window/main_window.cpp @@ -24,7 +24,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "localstorage.h" #include "styles/style_window.h" #include "platform/platform_window_title.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "mediaview.h" #include "mainwindow.h" @@ -248,11 +248,16 @@ void MainWindow::resizeEvent(QResizeEvent *e) { void MainWindow::updateControlsGeometry() { auto bodyTop = 0; + auto bodyWidth = width(); if (_title && !_title->isHidden()) { _title->setGeometry(0, bodyTop, width(), _title->height()); bodyTop += _title->height(); } - _body->setGeometry(0, bodyTop, width(), height() - bodyTop); + if (_rightColumn) { + bodyWidth -= _rightColumn->width(); + _rightColumn->setGeometry(bodyWidth, bodyTop, width() - bodyWidth, height() - bodyTop); + } + _body->setGeometry(0, bodyTop, bodyWidth, height() - bodyTop); } void MainWindow::updateUnreadCounter() { @@ -276,7 +281,7 @@ void MainWindow::savePosition(Qt::WindowState state) { auto r = geometry(); curPos.x = r.x(); curPos.y = r.y(); - curPos.w = r.width(); + curPos.w = r.width() - (_rightColumn ? _rightColumn->width() : 0); curPos.h = r.height(); curPos.maximized = 0; } @@ -317,6 +322,35 @@ bool MainWindow::minimizeToTray() { return true; } +void MainWindow::showRightColumn(object_ptr widget) { + auto wasWidth = width(); + auto wasRightWidth = _rightColumn ? _rightColumn->width() : 0; + _rightColumn = std_::move(widget); + if (_rightColumn) { + _rightColumn->setParent(this); + _rightColumn->show(); + _rightColumn->setFocus(); + } else if (App::wnd()) { + App::wnd()->setInnerFocus(); + } + auto nowRightWidth = _rightColumn ? _rightColumn->width() : 0; + setMinimumWidth(st::windowMinWidth + nowRightWidth); + auto nowWidth = width(); + + if (!isMaximized()) { + auto desktop = QDesktopWidget().availableGeometry(this); + auto newWidth = qMin(wasWidth + nowRightWidth - wasRightWidth, desktop.width()); + auto newLeft = qMin(x(), desktop.x() + desktop.width() - newWidth); + if (x() != newLeft || width() != newWidth) { + setGeometry(newLeft, y(), newWidth, height()); + } else { + updateControlsGeometry(); + } + } else { + updateControlsGeometry(); + } +} + void MainWindow::documentUpdated(DocumentData *doc) { if (!_mediaView || _mediaView->isHidden()) return; _mediaView->documentUpdated(doc); diff --git a/Telegram/SourceFiles/window/main_window.h b/Telegram/SourceFiles/window/main_window.h index 70c1961212..cf7c19456c 100644 --- a/Telegram/SourceFiles/window/main_window.h +++ b/Telegram/SourceFiles/window/main_window.h @@ -68,6 +68,8 @@ public: QWidget *filedialogParent(); + void showRightColumn(object_ptr widget); + virtual void updateTrayMenu(bool force = false) { } @@ -125,6 +127,8 @@ protected: virtual void showTrayTooltip() { } + virtual void updateControlsGeometry(); + // This one is overriden in Windows for historical reasons. virtual int32 screenNameChecksum(const QString &name) const; @@ -143,7 +147,6 @@ private slots: private: void updatePalette(); - void updateControlsGeometry(); void updateUnreadCounter(); void initSize(); @@ -154,6 +157,7 @@ private: object_ptr _title = { nullptr }; object_ptr _body; + object_ptr _rightColumn = { nullptr }; QString _titleText; diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index bbcefdaade..b1f7ea6810 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -28,7 +28,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "ui/widgets/buttons.h" #include "ui/widgets/input_fields.h" #include "dialogs/dialogs_layout.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "styles/style_dialogs.h" #include "styles/style_boxes.h" #include "styles/style_window.h" diff --git a/Telegram/SourceFiles/window/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp similarity index 75% rename from Telegram/SourceFiles/window/window_theme.cpp rename to Telegram/SourceFiles/window/themes/window_theme.cpp index ef8e04f304..95bc5cb43e 100644 --- a/Telegram/SourceFiles/window/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -19,7 +19,7 @@ Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "stdafx.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "mainwidget.h" #include "localstorage.h" @@ -27,6 +27,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "core/zlib_help.h" #include "styles/style_widgets.h" #include "styles/style_history.h" +#include "boxes/backgroundbox.h" namespace Window { namespace Theme { @@ -51,6 +52,13 @@ struct Data { }; NeverFreedPointer instance; +inline bool AreTestingTheme() { + if (instance) { + return !instance->applying.paletteForRevert.isEmpty(); + } + return false; +}; + QByteArray readThemeContent(const QString &path) { QFile file(path); if (!file.exists()) { @@ -63,7 +71,7 @@ QByteArray readThemeContent(const QString &path) { return QByteArray(); } if (!file.open(QIODevice::ReadOnly)) { - LOG(("Theme Warning: could not open theme file: %1").arg(path)); + LOG(("Theme Error: could not open theme file: %1").arg(path)); return QByteArray(); } @@ -102,7 +110,7 @@ bool readNameAndValue(const char *&from, const char *end, QLatin1String *outName return false; } if (*from != ':') { - LOG(("Theme Error: Expected ':' between each name and value in the color scheme.")); + LOG(("Theme Error: Expected ':' between each name and value in the color scheme (while reading key '%1')").arg(*outName)); return false; } if (!skipWhitespaces(++from, end)) { @@ -113,7 +121,7 @@ bool readNameAndValue(const char *&from, const char *end, QLatin1String *outName if (*from == '#') ++from; if (readName(from, end).size() == 0) { - LOG(("Theme Error: Expected a color value in #rrggbb or #rrggbbaa format in the color scheme.")); + LOG(("Theme Error: Expected a color value in #rrggbb or #rrggbbaa format in the color scheme (while reading key '%1')").arg(*outName)); return false; } *outValue = QLatin1String(valueStart, from - valueStart); @@ -123,7 +131,7 @@ bool readNameAndValue(const char *&from, const char *end, QLatin1String *outName return false; } if (*from != ';') { - LOG(("Theme Error: Expected ';' after each value in the color scheme.")); + LOG(("Theme Error: Expected ';' after each value in the color scheme (while reading key '%1')").arg(*outName)); return false; } ++from; @@ -136,7 +144,7 @@ enum class SetResult { NotFound, }; SetResult setColorSchemeValue(QLatin1String name, QLatin1String value, Instance *out) { - auto found = false; + auto result = style::palette::SetResult::Ok; auto size = value.size(); auto data = value.data(); if (data[0] == '#' && (size == 7 || size == 9)) { @@ -146,41 +154,39 @@ SetResult setColorSchemeValue(QLatin1String name, QLatin1String value, Instance auto b = readHexUchar(data[5], data[6], error); auto a = (size == 9) ? readHexUchar(data[7], data[8], error) : uchar(255); if (error) { - LOG(("Theme Error: Expected a color value in #rrggbb or #rrggbbaa format in the color scheme (while applying '%1: %2')").arg(QLatin1String(name)).arg(QLatin1String(value))); - return SetResult::Bad; + LOG(("Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)").arg(name).arg(value)); + return SetResult::Ok; } else if (out) { - found = out->palette.setColor(name, r, g, b, a); + result = out->palette.setColor(name, r, g, b, a); } else { - found = style::main_palette::setColor(name, r, g, b, a); + result = style::main_palette::setColor(name, r, g, b, a); } } else { if (out) { - found = out->palette.setColor(name, value); + result = out->palette.setColor(name, value); } else { - found = style::main_palette::setColor(name, value); + result = style::main_palette::setColor(name, value); } } - return found ? SetResult::Ok : SetResult::NotFound; + if (result == style::palette::SetResult::Ok) { + return SetResult::Ok; + } else if (result == style::palette::SetResult::KeyNotFound) { + return SetResult::NotFound; + } else if (result == style::palette::SetResult::ValueNotFound) { + LOG(("Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)").arg(name).arg(value)); + return SetResult::Ok; + } else if (result == style::palette::SetResult::Duplicate) { + LOG(("Theme Warning: Color value appears more than once in the color scheme (while applying '%1: %2')").arg(name).arg(value)); + return SetResult::Ok; + } else { + LOG(("Theme Error: Unexpected internal error.")); + } + return SetResult::Bad; } -bool loadColorScheme(const QByteArray &content, Instance *out = nullptr) { - if (content.size() > kThemeSchemeSizeLimit) { - LOG(("Theme Error: color scheme file too large (should be less than 1 MB, got %2)").arg(content.size())); - return false; - } - - QMap unsupported; - auto data = base::parse::stripComments(content); - auto from = data.constData(), end = from + data.size(); - while (from != end) { - QLatin1String name(""), value(""); - if (!readNameAndValue(from, end, &name, &value)) { - return false; - } - if (name.size() == 0) { // End of content reached. - return true; - } - +bool loadColorScheme(const QByteArray &content, Instance *out) { + auto unsupported = QMap(); + return ReadPaletteValues(content, [&unsupported, out](QLatin1String name, QLatin1String value) { // Find the named value in the already read unsupported list. value = unsupported.value(value, value); @@ -188,11 +194,10 @@ bool loadColorScheme(const QByteArray &content, Instance *out = nullptr) { if (result == SetResult::Bad) { return false; } else if (result == SetResult::NotFound) { - LOG(("Theme Warning: unexpected name or value in the color scheme (while applying '%1: %2')").arg(name).arg(value)); unsupported.insert(name, value); } - } - return true; + return true; + }); } void applyBackground(QImage &&background, bool tiled, Instance *out) { @@ -276,8 +281,12 @@ bool loadTheme(const QByteArray &content, Cached &cache, Instance *out = nullptr file.getGlobalInfo(&globalInfo); if (file.error() == UNZ_OK) { auto schemeContent = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit); + if (file.error() == UNZ_END_OF_LIST_OF_FILE) { + file.clearError(); + schemeContent = file.readFileContent("colors.tdesktop-palette", zlib::kCaseInsensitive, kThemeSchemeSizeLimit); + } if (file.error() != UNZ_OK) { - LOG(("Theme Error: could not read 'colors.tdesktop-theme' in the theme file.")); + LOG(("Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file.")); return false; } if (!loadColorScheme(schemeContent, out)) { @@ -330,13 +339,13 @@ QImage prepareBackgroundImage(QImage &&image) { return std_::move(image); } -void initColor(style::color color, float64 hue, float64 saturation) { +void adjustColor(style::color color, float64 hue, float64 saturation) { auto original = color->c; original.setHslF(hue, saturation, original.lightnessF(), original.alphaF()); color.set(original.red(), original.green(), original.blue(), original.alpha()); } -void initColorsFromBackground(const QImage &img) { +void adjustColorsUsingBackground(const QImage &img) { t_assert(img.format() == QImage::Format_ARGB32_Premultiplied); uint64 components[3] = { 0 }; @@ -361,12 +370,12 @@ void initColorsFromBackground(const QImage &img) { auto bgColor = QColor(components[0], components[1], components[2]); auto hue = bgColor.hslHueF(); auto saturation = bgColor.hslSaturationF(); - initColor(st::msgServiceBg, hue, saturation); - initColor(st::msgServiceBgSelected, hue, saturation); - initColor(st::historyScroll.bg, hue, saturation); - initColor(st::historyScroll.bgOver, hue, saturation); - initColor(st::historyScroll.barBg, hue, saturation); - initColor(st::historyScroll.barBgOver, hue, saturation); + adjustColor(st::msgServiceBg, hue, saturation); + adjustColor(st::msgServiceBgSelected, hue, saturation); + adjustColor(st::historyScroll.bg, hue, saturation); + adjustColor(st::historyScroll.bgOver, hue, saturation); + adjustColor(st::historyScroll.barBg, hue, saturation); + adjustColor(st::historyScroll.barBgOver, hue, saturation); } } // namespace @@ -392,7 +401,9 @@ void ChatBackground::setImage(int32 id, QImage &&image) { if (_id == kThemeBackground) { _tile = _themeTile; setPreparedImage(QImage(_themeImage)); - } else if (_id == internal::kTestingThemeBackground || _id == internal::kTestingDefaultBackground) { + } else if (_id == internal::kTestingThemeBackground + || _id == internal::kTestingDefaultBackground + || _id == internal::kTestingEditorBackground) { if (_id == internal::kTestingDefaultBackground || image.isNull()) { image.load(qsl(":/gui/art/bg.jpg")); _id = internal::kTestingDefaultBackground; @@ -420,14 +431,35 @@ void ChatBackground::setImage(int32 id, QImage &&image) { void ChatBackground::setPreparedImage(QImage &&image) { image = std_::move(image).convertToFormat(QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(cRetinaFactor()); - if (_id != kThemeBackground && _id != internal::kTestingThemeBackground) { - auto colorsFromSomeTheme = Local::hasTheme(); - if (instance && !instance->applying.paletteForRevert.isEmpty()) { - colorsFromSomeTheme = !instance->applying.path.isEmpty(); - } - if (colorsFromSomeTheme || (_id != kDefaultBackground && _id != internal::kTestingDefaultBackground)) { - initColorsFromBackground(image); + + auto adjustColors = [this] { + auto someCustomThemeApplied = [] { + if (AreTestingTheme()) { + return !instance->applying.path.isEmpty(); + } + return Local::hasTheme(); + }; + auto usingThemeBackground = [this] { + return (_id == kThemeBackground || _id == internal::kTestingThemeBackground); + }; + auto usingDefaultBackground = [this] { + return (_id == kDefaultBackground || _id == internal::kTestingDefaultBackground); + }; + auto testingPalette = [] { + if (AreTestingTheme()) { + return IsPaletteTestingPath(instance->applying.path); + } + return !Local::themePaletteAbsolutePath().isEmpty(); + }; + + if (someCustomThemeApplied()) { + return !usingThemeBackground() && !testingPalette(); } + return !usingDefaultBackground(); + }; + + if (adjustColors()) { + adjustColorsUsingBackground(image); } auto width = image.width(); @@ -522,7 +554,13 @@ void ChatBackground::saveForRevert() { void ChatBackground::setTestingTheme(Instance &&theme) { style::main_palette::apply(theme.palette); - if (!theme.background.isNull() || _id == kThemeBackground) { + if (AreTestingTheme() && IsPaletteTestingPath(instance->applying.path)) { + // Grab current background image if it is not already custom + if (_id != kCustomBackground) { + saveForRevert(); + setImage(internal::kTestingEditorBackground, std_::move(_pixmap).toImage()); + } + } else if (!theme.background.isNull() || _id == kThemeBackground) { saveForRevert(); setImage(internal::kTestingThemeBackground, std_::move(theme.background)); setTile(theme.tiled); @@ -547,7 +585,12 @@ void ChatBackground::setTestingDefaultTheme() { } void ChatBackground::keepApplied() { - if (_id == internal::kTestingThemeBackground) { + if (_id == internal::kTestingEditorBackground) { + _id = kCustomBackground; + _themeImage = QImage(); + _themeTile = false; + writeNewBackgroundSettings(); + } else if (_id == internal::kTestingThemeBackground) { _id = kThemeBackground; _themeImage = _pixmap.toImage(); _themeTile = _tile; @@ -565,11 +608,13 @@ void ChatBackground::writeNewBackgroundSettings() { if (_tile != _tileForRevert) { Local::writeUserSettings(); } - Local::writeBackground(_id, QImage()); + Local::writeBackground(_id, (_id == kThemeBackground || _id == kDefaultBackground) ? QImage() : _pixmap.toImage()); } void ChatBackground::revert() { - if (_id == internal::kTestingThemeBackground || _id == internal::kTestingDefaultBackground) { + if (_id == internal::kTestingThemeBackground + || _id == internal::kTestingDefaultBackground + || _id == internal::kTestingEditorBackground) { setTile(_tileForRevert); setImage(_idForRevert, std_::move(_imageForRevert)); } else { @@ -639,6 +684,27 @@ void ApplyDefault() { Background()->setTestingDefaultTheme(); } +bool ApplyEditedPalette(const QString &path, const QByteArray &content) { + Instance out; + if (!loadColorScheme(content, &out)) { + return false; + } + out.cached.colors = out.palette.save(); + out.cached.paletteChecksum = style::palette::Checksum(); + out.cached.contentChecksum = hashCrc32(content.constData(), content.size()); + + instance.createIfNull(); + instance->applying.path = path; + instance->applying.content = content; + instance->applying.cached = out.cached; + if (instance->applying.paletteForRevert.isEmpty()) { + instance->applying.paletteForRevert = style::main_palette::save(); + } + Background()->setTestingTheme(std_::move(out)); + KeepApplied(); + return true; +} + void KeepApplied() { if (!instance) { return; @@ -669,6 +735,13 @@ bool LoadFromFile(const QString &path, Instance *out, QByteArray *outContent) { return loadTheme(*outContent, out->cached, out); } +bool IsPaletteTestingPath(const QString &path) { + if (path.endsWith(qstr(".tdesktop-palette"), Qt::CaseInsensitive)) { + return QFileInfo(path).exists(); + } + return false; +} + void ComputeBackgroundRects(QRect wholeFill, QSize imageSize, QRect &to, QRect &from) { if (uint64(imageSize.width()) * wholeFill.height() > uint64(imageSize.height()) * wholeFill.width()) { float64 pxsize = wholeFill.height() / float64(imageSize.height()); @@ -693,5 +766,61 @@ void ComputeBackgroundRects(QRect wholeFill, QSize imageSize, QRect &to, QRect & } } +bool CopyColorsToPalette(const QString &path, const QByteArray &themeContent) { + auto paletteContent = themeContent; + + zlib::FileToRead file(themeContent); + + unz_global_info globalInfo = { 0 }; + file.getGlobalInfo(&globalInfo); + if (file.error() == UNZ_OK) { + paletteContent = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit); + if (file.error() == UNZ_END_OF_LIST_OF_FILE) { + file.clearError(); + paletteContent = file.readFileContent("colors.tdesktop-palette", zlib::kCaseInsensitive, kThemeSchemeSizeLimit); + } + if (file.error() != UNZ_OK) { + LOG(("Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file, while copying to '%1'.").arg(path)); + return false; + } + } + + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) { + LOG(("Theme Error: could not open file for write '%1'").arg(path)); + return false; + } + + if (f.write(paletteContent) != paletteContent.size()) { + LOG(("Theme Error: could not write palette to '%1'").arg(path)); + return false; + } + return true; +} + +bool ReadPaletteValues(const QByteArray &content, base::lambda &&callback) { + if (content.size() > kThemeSchemeSizeLimit) { + LOG(("Theme Error: color scheme file too large (should be less than 1 MB, got %2)").arg(content.size())); + return false; + } + + auto data = base::parse::stripComments(content); + auto from = data.constData(), end = from + data.size(); + while (from != end) { + auto name = QLatin1String(""); + auto value = QLatin1String(""); + if (!readNameAndValue(from, end, &name, &value)) { + return false; + } + if (name.size() == 0) { // End of content reached. + return true; + } + if (!callback(name, value)) { + return false; + } + } + return true; +} + } // namespace Theme } // namespace Window diff --git a/Telegram/SourceFiles/window/window_theme.h b/Telegram/SourceFiles/window/themes/window_theme.h similarity index 90% rename from Telegram/SourceFiles/window/window_theme.h rename to Telegram/SourceFiles/window/themes/window_theme.h index 1e7ad3b733..8c6c717c28 100644 --- a/Telegram/SourceFiles/window/window_theme.h +++ b/Telegram/SourceFiles/window/themes/window_theme.h @@ -27,6 +27,7 @@ namespace internal { constexpr int32 kUninitializedBackground = -999; constexpr int32 kTestingThemeBackground = -666; constexpr int32 kTestingDefaultBackground = -665; +constexpr int32 kTestingEditorBackground = -664; } // namespace internal @@ -62,10 +63,12 @@ struct Preview { bool Apply(const QString &filepath); bool Apply(std_::unique_ptr preview); void ApplyDefault(); +bool ApplyEditedPalette(const QString &path, const QByteArray &content); void KeepApplied(); void Revert(); bool LoadFromFile(const QString &file, Instance *out, QByteArray *outContent); +bool IsPaletteTestingPath(const QString &path); struct BackgroundUpdate { enum class Type { @@ -136,5 +139,9 @@ ChatBackground *Background(); void ComputeBackgroundRects(QRect wholeFill, QSize imageSize, QRect &to, QRect &from); +bool CopyColorsToPalette(const QString &path, const QByteArray &themeContent); + +bool ReadPaletteValues(const QByteArray &content, base::lambda &&callback); + } // namespace Theme } // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor.cpp b/Telegram/SourceFiles/window/themes/window_theme_editor.cpp new file mode 100644 index 0000000000..51e2e490be --- /dev/null +++ b/Telegram/SourceFiles/window/themes/window_theme_editor.cpp @@ -0,0 +1,817 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop 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. + +It 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. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#include "stdafx.h" +#include "window/themes/window_theme_editor.h" + +#include "window/themes/window_theme.h" +#include "window/themes/window_theme_editor_block.h" +#include "mainwindow.h" +#include "localstorage.h" +#include "boxes/confirmbox.h" +#include "styles/style_window.h" +#include "styles/style_settings.h" +#include "styles/style_dialogs.h" +#include "styles/style_boxes.h" +#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 "core/parse_helper.h" +#include "core/task_queue.h" +#include "core/zlib_help.h" +#include "ui/toast/toast.h" +#include "ui/filedialog.h" +#include "boxes/editcolorbox.h" +#include "lang.h" + +namespace Window { +namespace Theme { +namespace { + +struct ReadColorResult { + ReadColorResult(QColor color, bool error = false) : color(color), error(error) { + } + QColor color; + bool error = false; +}; + +ReadColorResult colorError(const QString &name) { + return { QColor(), true }; +} + +ReadColorResult readColor(const QString &name, const char *data, int size) { + if (size != 6 && size != 8) { + return colorError(name); + } + auto readHex = [](char ch) { + if (ch >= '0' && ch <= '9') { + return (ch - '0'); + } else if (ch >= 'a' && ch <= 'f') { + return (ch - 'a' + 10); + } else if (ch >= 'A' && ch <= 'F') { + return (ch - 'A' + 10); + } + return -1; + }; + auto readValue = [readHex](const char *data) { + auto high = readHex(data[0]); + auto low = readHex(data[1]); + return (high >= 0 && low >= 0) ? (high * 0x10 + low) : -1; + }; + auto r = readValue(data); + auto g = readValue(data + 2); + auto b = readValue(data + 4); + auto a = (size == 8) ? readValue(data + 6) : 255; + if (r < 0 || g < 0 || b < 0 || a < 0) { + return colorError(name); + } + return { QColor(r, g, b, a) }; +} + +bool skipComment(const char *&data, const char *end) { + if (data == end) return false; + if (*data == '/' && data + 1 != end) { + if (*(data + 1) == '/') { + data += 2; + while (data != end && *data != '\n') { + ++data; + } + return true; + } else if (*(data + 1) == '*') { + data += 2; + while (true) { + while (data != end && *data != '*') { + ++data; + } + if (data != end) ++data; + if (data == end || *data == '/') { + break; + } + } + return true; + } + } + return false; +} + +void skipWhitespacesAndComments(const char *&data, const char *end) { + while (data != end) { + if (!base::parse::skipWhitespaces(data, end)) return; + if (!skipComment(data, end)) return; + } +} + +QLatin1String readValue(const char *&data, const char *end) { + auto start = data; + if (data != end && *data == '#') { + ++data; + } + base::parse::readName(data, end); + return QLatin1String(start, data - start); +} + +bool isValidColorValue(QLatin1String value) { + auto isValidHexChar = [](char ch) { + return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); + }; + auto data = value.data(); + auto size = value.size(); + if ((size != 7 && size != 9) || data[0] != '#') { + return false; + } + for (auto i = 1; i != size; ++i) { + if (!isValidHexChar(data[i])) { + return false; + } + } + return true; +} + +QByteArray replaceValueInContent(const QByteArray &content, const QByteArray &name, const QByteArray &value) { + auto validNames = OrderedSet(); + auto start = content.constBegin(), data = start, end = data + content.size(); + auto lastValidValueStart = end, lastValidValueEnd = end; + while (data != end) { + skipWhitespacesAndComments(data, end); + if (data == end) break; + + auto foundName = base::parse::readName(data, end); + skipWhitespacesAndComments(data, end); + if (data == end || *data != ':') { + return "error"; + } + ++data; + skipWhitespacesAndComments(data, end); + auto valueStart = data; + auto value = readValue(data, end); + auto valueEnd = data; + if (value.size() == 0) { + return "error"; + } + auto validValue = validNames.contains(value) || isValidColorValue(value); + if (validValue) { + validNames.insert(foundName); + if (foundName == name) { + lastValidValueStart = valueStart; + lastValidValueEnd = valueEnd; + } + } + skipWhitespacesAndComments(data, end); + if (data == end || *data != ';') { + return "error"; + } + ++data; + } + if (lastValidValueStart != end) { + auto result = QByteArray(); + result.reserve((lastValidValueStart - start) + value.size() + (end - lastValidValueEnd)); + result.append(start, lastValidValueStart - start); + result.append(value); + if (end - lastValidValueEnd > 0) result.append(lastValidValueEnd, end - lastValidValueEnd); + return result; + } + return QByteArray(); +} + +QString bytesToUtf8(QLatin1String bytes) { + return QString::fromUtf8(bytes.data(), bytes.size()); +} + +} // namespace + +class Editor::Inner : public TWidget, private base::Subscriber { +public: + Inner(QWidget *parent, const QString &path); + + void setErrorCallback(base::lambda &&callback) { + _errorCallback = std_::move(callback); + } + void setFocusCallback(base::lambda &&callback) { + _focusCallback = std_::move(callback); + } + void setScrollCallback(base::lambda &&callback) { + _scrollCallback = std_::move(callback); + } + + void prepare(); + + base::lambda exportCallback(); + + void filterRows(const QString &query); + void chooseRow(); + + void selectSkip(int direction); + void selectSkipPage(int delta, int direction); + + ~Inner() { + if (_context.box) _context.box->closeBox(); + } + +protected: + void paintEvent(QPaintEvent *e) override; + int resizeGetHeight(int newWidth) override; + +private: + bool readData(); + bool readExistingRows(); + bool feedExistingRow(const QString &name, QLatin1String value); + + void error() { + if (_errorCallback) { + _errorCallback(); + } + } + void applyEditing(const QString &name, const QString ©Of, QColor value); + + EditorBlock::Context _context; + + QString _path; + QByteArray _paletteContent; + base::lambda _errorCallback; + base::lambda _focusCallback; + base::lambda _scrollCallback; + + object_ptr _existingRows; + object_ptr _newRows; + + bool _applyingUpdate = false; + +}; + +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 _chooseFromFile; + object_ptr _tileBackground; + +}; + +Editor::Inner::Inner(QWidget *parent, const QString &path) : TWidget(parent) +, _path(path) +, _existingRows(this, EditorBlock::Type::Existing, &_context) +, _newRows(this, EditorBlock::Type::New, &_context) { + resize(st::windowMinWidth, st::windowMinHeight); + subscribe(_context.resized, [this] { + resizeToWidth(width()); + }); + subscribe(_context.pending, [this](const EditorBlock::Context::EditionData &data) { + applyEditing(data.name, data.copyOf, data.value); + }); + subscribe(_context.updated, [this] { + if (_context.name.isEmpty() && _focusCallback) { + _focusCallback(); + } + }); + subscribe(_context.scroll, [this](const EditorBlock::Context::ScrollData &data) { + if (_scrollCallback) { + auto top = (data.type == EditorBlock::Type::Existing ? _existingRows : _newRows)->y(); + top += data.position; + _scrollCallback(top, top + data.height); + } + }); + subscribe(Background(), [this](const BackgroundUpdate &update) { + if (_applyingUpdate) return; + + if (update.type == BackgroundUpdate::Type::TestingTheme) { + Revert(); + App::CallDelayed(st::slideDuration, this, [] { + Ui::show(Box(lang(lng_theme_editor_cant_change_theme))); + }); + } + }); +} + +void Editor::Inner::prepare() { + if (!readData()) { + error(); + } +} + +base::lambda Editor::Inner::exportCallback() { + return App::LambdaDelayed(st::defaultRippleAnimation.hideDuration, this, [this] { + auto background = Background()->pixmap().toImage(); + auto backgroundContent = QByteArray(); + auto tiled = Background()->tile(); + { + QBuffer buffer(&backgroundContent); + background.save(&buffer, "JPG", 87); + } + Ui::show(Box(_paletteContent, background, backgroundContent, tiled)); + }); +} + +void Editor::Inner::filterRows(const QString &query) { + _existingRows->filterRows(query); + _newRows->filterRows(query); +} + +void Editor::Inner::chooseRow() { + if (!_existingRows->hasSelected() && !_newRows->hasSelected()) { + selectSkip(1); + } + if (_existingRows->hasSelected()) { + _existingRows->chooseRow(); + } else if (_newRows->hasSelected()) { + _newRows->chooseRow(); + } +} + +// Block::selectSkip(-1) removes the selection if it can't select anything +// Block::selectSkip(1) leaves the selection if it can't select anything +void Editor::Inner::selectSkip(int direction) { + if (direction > 0) { + if (_newRows->hasSelected()) { + _existingRows->clearSelected(); + _newRows->selectSkip(direction); + } else if (_existingRows->hasSelected()) { + if (!_existingRows->selectSkip(direction)) { + if (_newRows->selectSkip(direction)) { + _existingRows->clearSelected(); + } + } + } else { + if (!_existingRows->selectSkip(direction)) { + _newRows->selectSkip(direction); + } + } + } else { + if (_existingRows->hasSelected()) { + _newRows->clearSelected(); + _existingRows->selectSkip(direction); + } else if (_newRows->hasSelected()) { + if (!_newRows->selectSkip(direction)) { + _existingRows->selectSkip(direction); + } + } + } +} + +void Editor::Inner::selectSkipPage(int delta, int direction) { + auto defaultRowHeight = st::themeEditorMargin.top() + + st::themeEditorSampleSize.height() + + st::themeEditorDescriptionSkip + + st::defaultTextStyle.font->height + + st::themeEditorMargin.bottom(); + for (auto i = 0, count = ceilclamp(delta, defaultRowHeight, 1, delta); i != count; ++i) { + selectSkip(direction); + } +} + +void Editor::Inner::paintEvent(QPaintEvent *e) { + Painter p(this); + + p.setFont(st::settingsFixedBarFont); + p.setPen(st::windowFg); + if (!_newRows->isHidden()) { + p.drawTextLeft(st::themeEditorMargin.left(), _existingRows->y() + _existingRows->height() + st::settingsFixedBarTextPosition.y(), width(), lang(lng_theme_editor_new_keys)); + } +} + +int Editor::Inner::resizeGetHeight(int newWidth) { + auto rowsWidth = newWidth; + _existingRows->resizeToWidth(rowsWidth); + _newRows->resizeToWidth(rowsWidth); + + _existingRows->moveToLeft(0, 0); + _newRows->moveToLeft(0, _existingRows->height() + st::settingsFixedBarHeight); + + auto lowest = (_newRows->isHidden() ? _existingRows : _newRows).data(); + + return lowest->y() + lowest->height(); +} + +bool Editor::Inner::readData() { + if (!readExistingRows()) { + return false; + } + + auto rows = style::main_palette::data(); + for_const (auto &row, rows) { + auto name = bytesToUtf8(row.name); + auto description = bytesToUtf8(row.description); + if (!_existingRows->feedDescription(name, description)) { + if (row.value.data()[0] == '#') { + auto result = readColor(name, row.value.data() + 1, row.value.size() - 1); + t_assert(!result.error); + _newRows->feed(name, result.color); + //if (!_newRows->feedFallbackName(name, str_const_toString(row.fallback))) { + // t_assert(!"Row for fallback not found"); + //} + } else { + auto copyOf = bytesToUtf8(row.value); + if (auto result = _existingRows->find(copyOf)) { + _newRows->feed(name, *result, copyOf); + } else if (!_newRows->feedCopy(name, copyOf)) { + t_assert(!"Copy of unknown value in the default palette"); + } + t_assert(row.fallback.size() == 0); + } + if (!_newRows->feedDescription(name, description)) { + t_assert(!"Row for description not found"); + } + } + } + return true; +} + +bool Editor::Inner::readExistingRows() { + QFile f(_path); + if (!f.open(QIODevice::ReadOnly)) { + LOG(("Theme Error: could not open color palette file '%1'").arg(_path)); + return false; + } + + _paletteContent = f.readAll(); + if (f.error() != QFileDevice::NoError) { + LOG(("Theme Error: could not read content from palette file '%1'").arg(_path)); + return false; + } + f.close(); + + return ReadPaletteValues(_paletteContent, [this](QLatin1String name, QLatin1String value) { + return feedExistingRow(name, value); + }); +} + +bool Editor::Inner::feedExistingRow(const QString &name, QLatin1String value) { + auto data = value.data(); + auto size = value.size(); + if (data[0] != '#') { + return _existingRows->feedCopy(name, QString(value)); + } + auto result = readColor(name, data + 1, size - 1); + if (result.error) { + LOG(("Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)").arg(name).arg(value)); + } else { + _existingRows->feed(name, result.color); + } + return true; +} + +QString colorString(QColor color) { + auto result = QString(); + result.reserve(9); + result.append('#'); + auto addHex = [&result](int code) { + if (code >= 0 && code < 10) { + result.append('0' + code); + } else if (code >= 10 && code < 16) { + result.append('a' + (code - 10)); + } + }; + auto addValue = [addHex](int code) { + addHex(code / 16); + addHex(code % 16); + }; + addValue(color.red()); + addValue(color.green()); + addValue(color.blue()); + if (color.alpha() != 255) { + addValue(color.alpha()); + } + return result; +} + +void Editor::Inner::applyEditing(const QString &name, const QString ©Of, QColor value) { + auto plainName = name.toLatin1(); + auto plainValue = (copyOf.isEmpty() ? colorString(value) : copyOf).toLatin1(); + auto newContent = replaceValueInContent(_paletteContent, plainName, plainValue); + if (newContent == "error") { + LOG(("Theme Error: could not replace '%1: %2' in content").arg(name).arg(copyOf.isEmpty() ? colorString(value) : copyOf)); + error(); + return; + } + if (newContent.isEmpty()) { + auto newline = (_paletteContent.indexOf("\r\n") >= 0 ? "\r\n" : "\n"); + auto addedline = (_paletteContent.endsWith('\n') ? "" : newline); + newContent = _paletteContent + addedline + plainName + ": " + plainValue + ";" + newline; + } + QFile f(_path); + if (!f.open(QIODevice::WriteOnly)) { + LOG(("Theme Error: could not open '%1' for writing a palette update.").arg(_path)); + error(); + return; + } + if (f.write(newContent) != newContent.size()) { + LOG(("Theme Error: could not write all content to '%1' while writing a palette update.").arg(_path)); + error(); + return; + } + f.close(); + + _applyingUpdate = true; + if (!ApplyEditedPalette(_path, newContent)) { + LOG(("Theme Error: could not apply newly composed content :(")); + error(); + return; + } + _applyingUpdate = false; + + _paletteContent = newContent; +} + +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, rows) { + stream << bytesToUtf8(row.name) << ": " << bytesToUtf8(row.value) << "; // " << bytesToUtf8(row.description).replace('\n', ' ').replace('\r', ' ') << "\n"; + } +} + +ThemeExportBox::ThemeExportBox(QWidget*, const QByteArray &paletteContent, const QImage &background, const QByteArray &backgroundContent, bool tileBackground) : BoxContent() +, _paletteContent(paletteContent) +, _background(background) +, _backgroundContent(backgroundContent) +, _chooseFromFile(this, lang(lng_settings_bg_from_file), st::boxLinkButton) +, _tileBackground(this, lang(lng_settings_bg_tile), tileBackground, st::defaultBoxCheckbox) { + _imageText = lng_theme_editor_saved_to_jpg(lt_size, formatSizeText(_backgroundContent.size())); + _chooseFromFile->setClickedCallback([this] { chooseBackgroundFromFile(); }); +} + +void ThemeExportBox::prepare() { + setTitle(lang(lng_theme_editor_background_image)); + + addButton(lang(lng_theme_editor_export), [this] { exportTheme(); }); + addButton(lang(lng_cancel), [this] { closeBox(); }); + + auto height = st::settingsSmallSkip + st::settingsBackgroundSize + st::settingsSmallSkip + _tileBackground->height(); + + setDimensions(st::boxWideWidth, height); + + updateThumbnail(); +} + +void ThemeExportBox::paintEvent(QPaintEvent *e) { + BoxContent::paintEvent(e); + + Painter p(this); + + auto linkLeft = st::boxPadding.left() + st::settingsBackgroundSize + st::settingsSmallSkip; + + p.setPen(st::boxTextFg); + p.setFont(st::boxTextFont); + p.drawTextLeft(linkLeft, st::settingsSmallSkip, width(), _imageText); + + p.drawPixmapLeft(st::boxPadding.left(), st::settingsSmallSkip, width(), _thumbnail); +} + +void ThemeExportBox::resizeEvent(QResizeEvent *e) { + auto linkLeft = st::boxPadding.left() + st::settingsBackgroundSize + st::settingsSmallSkip; + _chooseFromFile->moveToLeft(linkLeft, st::settingsSmallSkip + st::boxTextFont->height + st::settingsSmallSkip); + _tileBackground->moveToLeft(st::boxPadding.left(), st::settingsSmallSkip + st::settingsBackgroundSize + 2 * st::settingsSmallSkip); +} + +void ThemeExportBox::updateThumbnail() { + int32 size = st::settingsBackgroundSize * 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::settingsBackgroundSize, st::settingsBackgroundSize), 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::askOpenPath(lang(lng_theme_editor_choose_image), "Image files (*.jpeg *.jpg *.png)", base::lambda_guarded(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"); + _imageText = (_isPng ? lng_theme_editor_read_from_png : lng_theme_editor_read_from_jpg)(lt_size, formatSizeText(_backgroundContent.size())); + _tileBackground->setChecked(false); + updateThumbnail(); + } + } + })); +} + +void ThemeExportBox::exportTheme() { + App::CallDelayed(st::defaultRippleAnimation.hideDuration, this, [this] { + auto caption = lang(lng_theme_editor_choose_name); + auto filter = "Themes (*.tdesktop-theme)"; + auto name = "awesome.tdesktop-theme"; + FileDialog::askWritePath(caption, filter, name, base::lambda_guarded(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(lang(lng_theme_editor_error))); + 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(lang(lng_theme_editor_error))); + return; + } + if (f.write(result) != result.size()) { + LOG(("Theme Error: could not write zip-ed theme to file '%1'").arg(path)); + Ui::show(Box(lang(lng_theme_editor_error))); + return; + } + Ui::hideLayer(); + Ui::Toast::Show(lang(lng_theme_editor_done)); + })); + }); +} + +Editor::Editor(QWidget*, const QString &path) +: _scroll(this, st::settingsScroll) +, _close(this, st::contactsMultiSelect.fieldCancel) +, _select(this, st::contactsMultiSelect, lang(lng_country_ph)) +, _leftShadow(this) +, _topShadow(this) +, _export(this, lang(lng_theme_editor_export_button).toUpper(), st::dialogsUpdateButton) { + _inner = _scroll->setOwnedWidget(object_ptr(this, path)); + + _export->setClickedCallback(_inner->exportCallback()); + + _inner->setErrorCallback([this] { + Ui::show(Box(lang(lng_theme_editor_error))); + + // This could be from inner->_context observable notification. + // We should not destroy it while iterating in subscribers. + base::TaskQueue::Main().Put(base::lambda_guarded(this, [this] { closeEditor(); })); + }); + _inner->setFocusCallback([this] { + App::CallDelayed(2 * st::boxDuration, this, [this] { _select->setInnerFocus(); }); + }); + _inner->setScrollCallback([this](int top, int bottom) { + _scroll->scrollToY(top, bottom); + }); + _close->setClickedCallback([this] { closeEditor(); }); + _close->showFast(); + + _select->resizeToWidth(st::windowMinWidth); + _select->setQueryChangedCallback([this](const QString &query) { _inner->filterRows(query); _scroll->scrollToY(0); }); + _select->setSubmittedCallback([this](bool) { _inner->chooseRow(); }); + + _inner->prepare(); + resizeToWidth(st::windowMinWidth); +} + +void Editor::StartFromCurrentTheme(const QString &path) { + if (!Local::copyThemeColorsToPalette(path)) { + writeDefaultPalette(path); + } + if (!Apply(path)) { + Ui::show(Box(lang(lng_theme_editor_error))); + return; + } + KeepApplied(); + Start(path); +} + +void Editor::resizeEvent(QResizeEvent *e) { + _export->resizeToWidth(width()); + _close->moveToRight(0, 0); + + _select->resizeToWidth(width()); + _select->moveToLeft(0, _close->height()); + + auto shadowTop = _select->y() + _select->height(); + + _topShadow->resize(width() - st::lineWidth, st::lineWidth); + _topShadow->moveToLeft(st::lineWidth, shadowTop); + _leftShadow->resize(st::lineWidth, height()); + _leftShadow->moveToLeft(0, 0); + auto scrollSize = QSize(width(), height() - shadowTop - _export->height()); + if (_scroll->size() != scrollSize) { + _scroll->resize(scrollSize); + } + _inner->resizeToWidth(width()); + _scroll->moveToLeft(0, shadowTop); + if (!_scroll->isHidden()) { + auto scrollTop = _scroll->scrollTop(); + _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); + } + _export->moveToLeft(0, _scroll->y() + _scroll->height()); +} + +void Editor::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Escape) { + if (!_select->getQuery().isEmpty()) { + _select->clearQuery(); + } else if (auto window = App::wnd()) { + window->setInnerFocus(); + } + } else if (e->key() == Qt::Key_Down) { + _inner->selectSkip(1); + } else if (e->key() == Qt::Key_Up) { + _inner->selectSkip(-1); + } else if (e->key() == Qt::Key_PageDown) { + _inner->selectSkipPage(_scroll->height(), 1); + } else if (e->key() == Qt::Key_PageUp) { + _inner->selectSkipPage(_scroll->height(), -1); + } +} + +void Editor::focusInEvent(QFocusEvent *e) { + _select->setInnerFocus(); +} + +void Editor::paintEvent(QPaintEvent *e) { + Painter p(this); + + p.fillRect(e->rect(), st::dialogsBg); + + p.setFont(st::settingsFixedBarFont); + p.setPen(st::windowFg); + p.drawTextLeft(st::themeEditorMargin.left(), st::themeEditorMargin.top(), width(), lang(lng_theme_editor_title)); +} + +void Editor::Start(const QString &path) { + if (auto window = App::wnd()) { + window->showRightColumn(Box(path)); + } +} + +void Editor::closeEditor() { + if (auto window = App::wnd()) { + window->showRightColumn(nullptr); + } +} + +} // namespace Theme +} // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor.h b/Telegram/SourceFiles/window/themes/window_theme_editor.h new file mode 100644 index 0000000000..8a27ab6608 --- /dev/null +++ b/Telegram/SourceFiles/window/themes/window_theme_editor.h @@ -0,0 +1,66 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop 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. + +It 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. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +class BoxLayerTitleShadow; + +namespace Ui { +class FlatButton; +class ScrollArea; +class CrossButton; +class MultiSelect; +} // namespace Ui + +namespace Window { +namespace Theme { + +class Editor : public TWidget { + Q_OBJECT + +public: + Editor(QWidget*, const QString &path); + + static void StartFromCurrentTheme(const QString &path); + static void Start(const QString &path); + +protected: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + + void focusInEvent(QFocusEvent *e) override; + +private: + void closeEditor(); + + object_ptr _scroll; + class Inner; + QPointer _inner; + object_ptr _close; + object_ptr _select; + object_ptr _leftShadow; + object_ptr _topShadow; + object_ptr _export; + +}; + +} // namespace Theme +} // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp b/Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp new file mode 100644 index 0000000000..7c1cf701de --- /dev/null +++ b/Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp @@ -0,0 +1,778 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop 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. + +It 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. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#include "stdafx.h" +#include "window/themes/window_theme_editor_block.h" + +#include "styles/style_window.h" +#include "ui/effects/ripple_animation.h" +#include "boxes/editcolorbox.h" +#include "lang.h" + +namespace Window { +namespace Theme { +namespace { + +auto SearchSplitter = QRegularExpression(qsl("[\\@\\s\\-\\+\\(\\)\\[\\]\\{\\}\\<\\>\\,\\.\\:\\!\\_\\;\\\"\\'\\x0\\#]")); + +} // namespace + +class EditorBlock::Row { +public: + Row(const QString &name, const QString ©Of, QColor value); + + QString name() const { + return _name; + } + + void setCopyOf(const QString ©Of) { + _copyOf = copyOf; + fillSearchIndex(); + } + QString copyOf() const { + return _copyOf; + } + + void setValue(QColor value); + const QColor &value() const { + return _value; + } + + QString description() const { + return _description.originalText(); + } + const Text &descriptionText() const { + return _description; + } + void setDescription(const QString &description) { + _description.setText(st::defaultTextStyle, description); + fillSearchIndex(); + } + + const OrderedSet &searchWords() const { + return _searchWords; + } + bool searchWordsContain(const QString &needle) const { + for_const (auto &word, _searchWords) { + if (word.startsWith(needle)) { + return true; + } + } + return false; + } + + const OrderedSet &searchStartChars() const { + return _searchStartChars; + } + + void setTop(int top) { + _top = top; + } + int top() const { + return _top; + } + + void setHeight(int height) { + _height = height; + } + int height() const { + return _height; + } + + Ui::RippleAnimation *ripple() const { + return _ripple.get(); + } + Ui::RippleAnimation *setRipple(std_::unique_ptr ripple) const { + _ripple = std_::move(ripple); + return _ripple.get(); + } + void resetRipple() const { + _ripple = nullptr; + } + +private: + void fillValueString(); + void fillSearchIndex(); + + QString _name; + QString _copyOf; + QColor _value; + QString _valueString; + Text _description = { st::windowMinWidth / 2 }; + + OrderedSet _searchWords; + OrderedSet _searchStartChars; + + int _top = 0; + int _height = 0; + + mutable std_::unique_ptr _ripple; + +}; + +EditorBlock::Row::Row(const QString &name, const QString ©Of, QColor value) +: _name(name) +, _copyOf(copyOf) { + setValue(value); +} + +void EditorBlock::Row::setValue(QColor value) { + _value = value; + fillValueString(); + fillSearchIndex(); +} + +void EditorBlock::Row::fillValueString() { + auto addHex = [this](int code) { + if (code >= 0 && code < 10) { + _valueString.append('0' + code); + } else if (code >= 10 && code < 16) { + _valueString.append('a' + (code - 10)); + } + }; + auto addCode = [this, addHex](int code) { + addHex(code / 16); + addHex(code % 16); + }; + _valueString.resize(0); + _valueString.reserve(9); + _valueString.append('#'); + addCode(_value.red()); + addCode(_value.green()); + addCode(_value.blue()); + if (_value.alpha() != 255) { + addCode(_value.alpha()); + } +} + +void EditorBlock::Row::fillSearchIndex() { + _searchWords.clear(); + _searchStartChars.clear(); + auto toIndex = _name + ' ' + _copyOf + ' ' + textAccentFold(_description.originalText()) + ' ' + _valueString; + auto words = toIndex.toLower().split(SearchSplitter, QString::SkipEmptyParts); + for_const (auto &word, words) { + _searchWords.insert(word); + _searchStartChars.insert(word[0]); + } +} + +EditorBlock::EditorBlock(QWidget *parent, Type type, Context *context) : TWidget(parent) +, _type(type) +, _context(context) +, _transparent(style::transparentPlaceholderBrush()) { + setMouseTracking(true); + subscribe(_context->updated, [this] { + if (_mouseSelection) { + _lastGlobalPos = QCursor::pos(); + updateSelected(mapFromGlobal(_lastGlobalPos)); + } + update(); + }); + if (_type == Type::Existing) { + subscribe(_context->appended, [this](const Context::AppendData &added) { + auto name = added.name; + auto value = added.value; + feed(name, value); + feedDescription(name, added.description); + + auto row = findRow(name); + t_assert(row != nullptr); + auto possibleCopyOf = added.possibleCopyOf; + auto copyOf = checkCopyOf(findRowIndex(row), possibleCopyOf) ? possibleCopyOf : QString(); + removeFromSearch(*row); + row->setCopyOf(copyOf); + addToSearch(*row); + + _context->changed.notify({ QStringList(name), value }, true); + _context->resized.notify(); + _context->pending.notify({ name, copyOf, value }, true); + }); + } else { + subscribe(_context->changed, [this](const Context::ChangeData &data) { + checkCopiesChanged(0, data.names, data.value); + }); + } +} + +void EditorBlock::feed(const QString &name, QColor value, const QString ©OfExisting) { + if (findRow(name)) { + // Remove the existing row and mark all its copies as unique keys. + LOG(("Theme Warning: Color value '%1' appears more than once in the color scheme.").arg(name)); + removeRow(name); + } + addRow(name, copyOfExisting, value); +} + +bool EditorBlock::feedCopy(const QString &name, const QString ©Of) { + if (auto row = findRow(copyOf)) { + if (findRow(name)) { + // Remove the existing row and mark all its copies as unique keys. + LOG(("Theme Warning: Color value '%1' appears more than once in the color scheme.").arg(name)); + removeRow(name); + + // row was invalidated by removeRow() call. + row = findRow(copyOf); + } + addRow(name, copyOf, row->value()); + } else { + LOG(("Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)").arg(name).arg(copyOf)); + } + return true; +} + +void EditorBlock::removeRow(const QString &name, bool removeCopyReferences) { + auto it = _indices.find(name); + t_assert(it != _indices.cend()); + + auto index = it.value(); + for (auto i = index + 1, count = _data.size(); i != count; ++i) { + auto &row = _data[i]; + removeFromSearch(row); + _indices[row.name()] = i - 1; + if (removeCopyReferences && row.copyOf() == name) { + row.setCopyOf(QString()); + } + } + _data.erase(_data.begin() + index); + _indices.erase(it); + for (auto i = index, count = _data.size(); i != count; ++i) { + addToSearch(_data[i]); + } +} + +void EditorBlock::addToSearch(const Row &row) { + auto query = _searchQuery; + if (!query.isEmpty()) resetSearch(); + + auto index = findRowIndex(&row); + for_const (auto ch, row.searchStartChars()) { + _searchIndex[ch].insert(index); + } + + if (!query.isEmpty()) searchByQuery(query); +} + +void EditorBlock::removeFromSearch(const Row &row) { + auto query = _searchQuery; + if (!query.isEmpty()) resetSearch(); + + auto index = findRowIndex(&row); + for_const (auto ch, row.searchStartChars()) { + auto it = _searchIndex.find(ch); + if (it != _searchIndex.cend()) { + it->remove(index); + if (it->isEmpty()) { + _searchIndex.erase(it); + } + } + } + + if (!query.isEmpty()) searchByQuery(query); +} + +void EditorBlock::filterRows(const QString &query) { + searchByQuery(query); +} + +void EditorBlock::chooseRow() { + if (_selected < 0) { + return; + } + activateRow(rowAtIndex(_selected)); +} + +void EditorBlock::activateRow(const Row &row) { + if (_context->box) { + if (_type == Type::Existing) { + _context->possibleCopyOf = row.name(); + _context->box->showColor(row.value()); + } + } else { + _editing = findRowIndex(&row); + if (auto box = Ui::show(Box(row.name(), row.value()))) { + box->setSaveCallback(base::lambda_guarded(this, [this](QColor value) { + saveEditing(value); + })); + box->setCancelCallback(base::lambda_guarded(this, [this] { + cancelEditing(); + })); + _context->box = box; + _context->name = row.name(); + _context->updated.notify(); + } + } +} + +bool EditorBlock::selectSkip(int direction) { + _mouseSelection = false; + + auto maxSelected = (isSearch() ? _searchResults.size() : _data.size()) - 1; + auto newSelected = _selected + direction; + if (newSelected < -1 || newSelected > maxSelected) { + newSelected = maxSelected; + } + if (auto changed = (newSelected != _selected)) { + setSelected(newSelected); + scrollToSelected(); + return (newSelected >= 0); + } + return false; +} + +void EditorBlock::scrollToSelected() { + if (_selected >= 0) { + Context::ScrollData update; + update.type = _type; + update.position = rowAtIndex(_selected).top(); + update.height = rowAtIndex(_selected).height(); + _context->scroll.notify(update, true); + } +} + +void EditorBlock::searchByQuery(QString query) { + auto searchWords = QStringList(); + if (!query.isEmpty()) { + searchWords = textAccentFold(query.trimmed().toLower()).split(SearchSplitter, QString::SkipEmptyParts); + query = searchWords.join(' '); + } + if (_searchQuery != query) { + setSelected(-1); + setPressed(-1); + + _searchQuery = query; + _searchResults.clear(); + + auto toFilter = OrderedSet(); + for_const (auto &word, searchWords) { + if (word.isEmpty()) continue; + + auto testToFilter = _searchIndex.value(word[0]); + if (testToFilter.isEmpty()) { + toFilter.clear(); + break; + } else if (toFilter.isEmpty() || testToFilter.size() < toFilter.size()) { + toFilter = testToFilter; + } + } + if (!toFilter.isEmpty()) { + auto allWordsFound = [&searchWords](const Row &row) { + for_const (auto &word, searchWords) { + if (!row.searchWordsContain(word)) { + return false; + } + } + return true; + }; + for_const (auto index, toFilter) { + if (allWordsFound(_data[index])) { + _searchResults.push_back(index); + } + } + } + + _context->resized.notify(true); + } +} + +const QColor *EditorBlock::find(const QString &name) { + if (auto row = findRow(name)) { + return &row->value(); + } + return nullptr; +} + +bool EditorBlock::feedDescription(const QString &name, const QString &description) { + if (auto row = findRow(name)) { + removeFromSearch(*row); + row->setDescription(description); + addToSearch(*row); + return true; + } + return false; +} + +template +void EditorBlock::enumerateRows(Callback callback) { + if (isSearch()) { + for_const (auto index, _searchResults) { + if (!callback(_data[index])) { + break; + } + } + } else { + for_const (auto &row, _data) { + if (!callback(row)) { + break; + } + } + } +} + +template +void EditorBlock::enumerateRows(Callback callback) const { + if (isSearch()) { + for_const (auto index, _searchResults) { + if (!callback(_data[index])) { + break; + } + } + } else { + for_const (auto &row, _data) { + if (!callback(row)) { + break; + } + } + } +} + +template +void EditorBlock::enumerateRowsFrom(int top, Callback callback) { + auto started = false; + auto index = 0; + enumerateRows([top, callback, &started, &index](Row &row) { + if (!started) { + if (row.top() + row.height() <= top) { + ++index; + return true; + } + started = true; + } + return callback(index++, row); + }); +} + +template +void EditorBlock::enumerateRowsFrom(int top, Callback callback) const { + auto started = false; + enumerateRows([top, callback, &started](const Row &row) { + if (!started) { + if (row.top() + row.height() <= top) { + return true; + } + started = true; + } + return callback(row); + }); +} + +int EditorBlock::resizeGetHeight(int newWidth) { + auto result = 0; + auto descriptionWidth = newWidth - st::themeEditorMargin.left() - st::themeEditorMargin.right(); + enumerateRows([this, &result, descriptionWidth](Row &row) { + row.setTop(result); + + auto height = row.height(); + if (!height) { + height = st::themeEditorMargin.top() + st::themeEditorSampleSize.height(); + if (!row.descriptionText().isEmpty()) { + height += st::themeEditorDescriptionSkip + row.descriptionText().countHeight(descriptionWidth); + } + height += st::themeEditorMargin.bottom(); + row.setHeight(height); + } + result += row.height(); + return true; + }); + + if (_type == Type::New) { + setHidden(!result); + } + if (_type == Type::Existing && !result && !isSearch()) { + return st::noContactsHeight; + } + return result; +} + +void EditorBlock::mousePressEvent(QMouseEvent *e) { + updateSelected(e->pos()); + setPressed(_selected); +} + +void EditorBlock::mouseReleaseEvent(QMouseEvent *e) { + auto pressed = _pressed; + setPressed(-1); + if (pressed == _selected) { + if (_context->box) { + chooseRow(); + } else if (_selected >= 0) { + App::CallDelayed(st::defaultRippleAnimation.hideDuration, this, [this, index = findRowIndex(&rowAtIndex(_selected))] { + if (index >= 0 && index < _data.size()) { + activateRow(_data[index]); + } + }); + } + } +} + +void EditorBlock::saveEditing(QColor value) { + if (_editing < 0) { + return; + } + auto &row = _data[_editing]; + auto name = row.name(); + if (_type == Type::New) { + auto removing = base::take(_editing, -1); + setSelected(-1); + setPressed(-1); + + auto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf; + auto color = value; + auto description = row.description(); + + removeRow(name, false); + + _context->appended.notify({ name, possibleCopyOf, color, description }, true); + } else if (_type == Type::Existing) { + removeFromSearch(row); + + auto valueChanged = (row.value() != value); + if (valueChanged) { + row.setValue(value); + } + + auto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf; + auto copyOf = checkCopyOf(_editing, possibleCopyOf) ? possibleCopyOf : QString(); + auto copyOfChanged = (row.copyOf() != copyOf); + if (copyOfChanged) { + row.setCopyOf(copyOf); + } + + addToSearch(row); + + if (valueChanged || copyOfChanged) { + checkCopiesChanged(_editing + 1, QStringList(name), value); + _context->pending.notify({ name, copyOf, value }, true); + } + } + cancelEditing(); +} + +void EditorBlock::checkCopiesChanged(int startIndex, QStringList names, QColor value) { + for (auto i = startIndex, count = _data.size(); i != count; ++i) { + auto &checkIfIsCopy = _data[i]; + if (names.contains(checkIfIsCopy.copyOf())) { + removeFromSearch(checkIfIsCopy); + checkIfIsCopy.setValue(value); + names.push_back(checkIfIsCopy.name()); + addToSearch(checkIfIsCopy); + } + } + if (_type == Type::Existing) { + _context->changed.notify({ names, value }, true); + } +} + +void EditorBlock::cancelEditing() { + if (_editing >= 0) { + updateRow(_data[_editing]); + } + _editing = -1; + if (auto box = base::take(_context->box)) { + box->closeBox(); + } + _context->possibleCopyOf = QString(); + if (!_context->name.isEmpty()) { + _context->name = QString(); + _context->updated.notify(); + } +} + +bool EditorBlock::checkCopyOf(int index, const QString &possibleCopyOf) { + auto copyOfIndex = findRowIndex(possibleCopyOf); + return (copyOfIndex >= 0 + && index > copyOfIndex + && _data[copyOfIndex].value().toRgb() == _data[index].value().toRgb()); +} + +void EditorBlock::mouseMoveEvent(QMouseEvent *e) { + if (_lastGlobalPos != e->globalPos() || _mouseSelection) { + _lastGlobalPos = e->globalPos(); + updateSelected(e->pos()); + } +} + +void EditorBlock::updateSelected(QPoint localPosition) { + _mouseSelection = true; + auto top = localPosition.y(); + auto underMouseIndex = -1; + enumerateRowsFrom(top, [&underMouseIndex, top](int index, const Row &row) { + if (row.top() <= top) { + underMouseIndex = index; + } + return false; + }); + setSelected(underMouseIndex); +} + +void EditorBlock::leaveEvent(QEvent *e) { + _mouseSelection = false; + setSelected(-1); +} + +void EditorBlock::paintEvent(QPaintEvent *e) { + Painter p(this); + + auto clip = e->rect(); + if (_data.isEmpty()) { + p.fillRect(clip, st::dialogsBg); + p.setFont(st::noContactsFont); + p.setPen(st::noContactsColor); + p.drawText(QRect(0, 0, width(), st::noContactsHeight), lang(lng_theme_editor_no_keys)); + } + + auto ms = getms(); + auto cliptop = clip.y(); + auto clipbottom = cliptop + clip.height(); + enumerateRowsFrom(cliptop, [this, &p, clipbottom, ms](int index, const Row &row) { + if (row.top() >= clipbottom) { + return false; + } + paintRow(p, index, row, ms); + return true; + }); +} + +void EditorBlock::paintRow(Painter &p, int index, const Row &row, TimeMs ms) { + auto rowTop = row.top() + st::themeEditorMargin.top(); + + auto rect = QRect(0, row.top(), width(), row.height()); + auto selected = (_pressed >= 0) ? (index == _pressed) : (index == _selected); + auto active = (findRowIndex(&row) == _editing); + p.fillRect(rect, active ? st::dialogsBgActive : selected ? st::dialogsBgOver : st::dialogsBg); + if (auto ripple = row.ripple()) { + ripple->paint(p, 0, row.top(), width(), ms, &(active ? st::activeButtonBgRipple : st::windowBgRipple)->c); + if (ripple->empty()) { + row.resetRipple(); + } + } + + auto sample = QRect(width() - st::themeEditorMargin.right() - st::themeEditorSampleSize.width(), rowTop, st::themeEditorSampleSize.width(), st::themeEditorSampleSize.height()); + Ui::Shadow::paint(p, sample, width(), st::defaultRoundShadow); + if (row.value().alpha() != 255) { + p.fillRect(myrtlrect(sample), _transparent); + } + p.fillRect(myrtlrect(sample), row.value()); + + auto rowWidth = width() - st::themeEditorMargin.left() - st::themeEditorMargin.right(); + auto nameWidth = rowWidth - st::themeEditorSampleSize.width() - st::themeEditorDescriptionSkip; + + p.setFont(st::themeEditorNameFont); + p.setPen(active ? st::dialogsNameFgActive : selected ? st::dialogsNameFgOver : st::dialogsNameFg); + p.drawTextLeft(st::themeEditorMargin.left(), rowTop, width(), st::themeEditorNameFont->elided(row.name(), nameWidth)); + + if (!row.copyOf().isEmpty()) { + auto copyTop = rowTop + st::themeEditorNameFont->height; + p.setFont(st::themeEditorCopyNameFont); + p.drawTextLeft(st::themeEditorMargin.left(), copyTop, width(), st::themeEditorCopyNameFont->elided("= " + row.copyOf(), nameWidth)); + } + + if (!row.descriptionText().isEmpty()) { + auto descriptionTop = rowTop + st::themeEditorSampleSize.height() + st::themeEditorDescriptionSkip; + p.setPen(active ? st::dialogsTextFgActive : selected ? st::dialogsTextFgOver : st::dialogsTextFg); + row.descriptionText().drawLeft(p, st::themeEditorMargin.left(), descriptionTop, rowWidth, width()); + } + + if (isEditing() && !active && (_type == Type::New || (_editing >= 0 && findRowIndex(&row) >= _editing))) { + p.fillRect(rect, st::layerBg); + } +} + +void EditorBlock::setSelected(int selected) { + if (isEditing()) { + if (_type == Type::New) { + selected = -1; + } else if (_editing >= 0 && selected >= 0 && findRowIndex(&rowAtIndex(selected)) >= _editing) { + selected = -1; + } + } + if (_selected != selected) { + if (_selected >= 0) updateRow(rowAtIndex(_selected)); + _selected = selected; + if (_selected >= 0) updateRow(rowAtIndex(_selected)); + setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default); + } +} + +void EditorBlock::setPressed(int pressed) { + if (_pressed != pressed) { + if (_pressed >= 0) { + updateRow(rowAtIndex(_pressed)); + stopLastRipple(_pressed); + } + _pressed = pressed; + if (_pressed >= 0) { + addRowRipple(_pressed); + updateRow(rowAtIndex(_pressed)); + } + } +} + +void EditorBlock::addRowRipple(int index) { + auto &row = rowAtIndex(index); + auto ripple = row.ripple(); + if (!ripple) { + auto mask = Ui::RippleAnimation::rectMask(QSize(width(), row.height())); + ripple = row.setRipple(std_::make_unique(st::defaultRippleAnimation, std_::move(mask), [this, index] { + updateRow(rowAtIndex(index)); + })); + } + auto origin = mapFromGlobal(QCursor::pos()) - QPoint(0, row.top()); + ripple->add(origin); +} + +void EditorBlock::stopLastRipple(int index) { + auto &row = rowAtIndex(index); + if (row.ripple()) { + row.ripple()->lastStop(); + } +} + +void EditorBlock::updateRow(const Row &row) { + update(0, row.top(), width(), row.height()); +} + +void EditorBlock::addRow(const QString &name, const QString ©Of, QColor value) { + _data.push_back({ name, copyOf, value }); + _indices.insert(name, _data.size() - 1); + addToSearch(_data.back()); +} + +EditorBlock::Row &EditorBlock::rowAtIndex(int index) { + if (isSearch()) { + return _data[_searchResults[index]]; + } + return _data[index]; +} + +int EditorBlock::findRowIndex(const QString &name) const { + return _indices.value(name, -1);; +} + +EditorBlock::Row *EditorBlock::findRow(const QString &name) { + auto index = findRowIndex(name); + return (index >= 0) ? &_data[index] : nullptr; +} + +int EditorBlock::findRowIndex(const Row *row) { + return row ? (row - &_data[0]) : -1; +} + +} // namespace Theme +} // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor_block.h b/Telegram/SourceFiles/window/themes/window_theme_editor_block.h new file mode 100644 index 0000000000..5730e1ac85 --- /dev/null +++ b/Telegram/SourceFiles/window/themes/window_theme_editor_block.h @@ -0,0 +1,171 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop 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. + +It 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. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +class EditColorBox; + +namespace Window { +namespace Theme { + +class EditorBlock : public TWidget, private base::Subscriber { +public: + enum class Type { + Existing, + New, + }; + struct Context { + QPointer box; + QString name; + QString possibleCopyOf; + + base::Observable updated; + base::Observable resized; + + struct AppendData { + QString name; + QString possibleCopyOf; + QColor value; + QString description; + }; + base::Observable appended; + + struct ChangeData { + QStringList names; + QColor value; + }; + base::Observable changed; + + struct EditionData { + QString name; + QString copyOf; + QColor value; + }; + base::Observable pending; + + struct ScrollData { + Type type; + int position; + int height; + }; + base::Observable scroll; + }; + EditorBlock(QWidget *parent, Type type, Context *context); + + void filterRows(const QString &query); + void chooseRow(); + bool hasSelected() const { + return (_selected >= 0); + } + void clearSelected() { + setSelected(-1); + } + bool selectSkip(int direction); + + void feed(const QString &name, QColor value, const QString ©OfExisting = QString()); + bool feedCopy(const QString &name, const QString ©Of); + const QColor *find(const QString &name); + + bool feedDescription(const QString &name, const QString &description); + +protected: + void paintEvent(QPaintEvent *e) override; + int resizeGetHeight(int newWidth) override; + + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void leaveEvent(QEvent *e) override; + +private: + class Row; + + void addRow(const QString &name, const QString ©Of, QColor value); + void removeRow(const QString &name, bool removeCopyReferences = true); + + void addToSearch(const Row &row); + void removeFromSearch(const Row &row); + + template + void enumerateRows(Callback callback); + + template + void enumerateRows(Callback callback) const; + + template + void enumerateRowsFrom(int top, Callback callback); + + template + void enumerateRowsFrom(int top, Callback callback) const; + + Row &rowAtIndex(int index); + int findRowIndex(const QString &name) const; + Row *findRow(const QString &name); + int findRowIndex(const Row *row); + void updateRow(const Row &row); + void paintRow(Painter &p, int index, const Row &row, TimeMs ms); + + void updateSelected(QPoint localPosition); + void setSelected(int selected); + void setPressed(int pressed); + void addRowRipple(int index); + void stopLastRipple(int index); + void scrollToSelected(); + + bool isEditing() const { + return !_context->name.isEmpty(); + } + void saveEditing(QColor value); + void cancelEditing(); + bool checkCopyOf(int index, const QString &possibleCopyOf); + void checkCopiesChanged(int startIndex, QStringList names, QColor value); + void activateRow(const Row &row); + + bool isSearch() const { + return !_searchQuery.isEmpty(); + } + void searchByQuery(QString query); + void resetSearch() { + searchByQuery(QString()); + } + + Type _type = Type::Existing; + Context *_context = nullptr; + + std_::vector_of_moveable _data; + QMap _indices; + + QString _searchQuery; + QVector _searchResults; + QMap> _searchIndex; + + int _selected = -1; + int _pressed = -1; + int _editing = -1; + + QPoint _lastGlobalPos; + bool _mouseSelection = false; + + QBrush _transparent; + +}; + +} // namespace Theme +} // namespace Window diff --git a/Telegram/SourceFiles/window/window_theme_preview.cpp b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp similarity index 99% rename from Telegram/SourceFiles/window/window_theme_preview.cpp rename to Telegram/SourceFiles/window/themes/window_theme_preview.cpp index 82c49ac8ad..abcaaa17c9 100644 --- a/Telegram/SourceFiles/window/window_theme_preview.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp @@ -19,9 +19,9 @@ Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "stdafx.h" -#include "window/window_theme_preview.h" +#include "window/themes/window_theme_preview.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "lang.h" #include "platform/platform_window_title.h" #include "styles/style_widgets.h" diff --git a/Telegram/SourceFiles/window/window_theme_preview.h b/Telegram/SourceFiles/window/themes/window_theme_preview.h similarity index 96% rename from Telegram/SourceFiles/window/window_theme_preview.h rename to Telegram/SourceFiles/window/themes/window_theme_preview.h index 5b93d21491..9366ac763d 100644 --- a/Telegram/SourceFiles/window/window_theme_preview.h +++ b/Telegram/SourceFiles/window/themes/window_theme_preview.h @@ -20,7 +20,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #pragma once -#include "window/window_theme.h" +#include "window/themes/window_theme.h" namespace Window { namespace Theme { diff --git a/Telegram/SourceFiles/window/window_theme_warning.cpp b/Telegram/SourceFiles/window/themes/window_theme_warning.cpp similarity index 97% rename from Telegram/SourceFiles/window/window_theme_warning.cpp rename to Telegram/SourceFiles/window/themes/window_theme_warning.cpp index e689216c8e..19fab5e796 100644 --- a/Telegram/SourceFiles/window/window_theme_warning.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_warning.cpp @@ -19,12 +19,12 @@ Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "stdafx.h" -#include "window/window_theme_warning.h" +#include "window/themes/window_theme_warning.h" #include "styles/style_boxes.h" #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" -#include "window/window_theme.h" +#include "window/themes/window_theme.h" #include "lang.h" namespace Window { diff --git a/Telegram/SourceFiles/window/window_theme_warning.h b/Telegram/SourceFiles/window/themes/window_theme_warning.h similarity index 100% rename from Telegram/SourceFiles/window/window_theme_warning.h rename to Telegram/SourceFiles/window/themes/window_theme_warning.h diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 4fdc48e5fe..4259573456 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -267,6 +267,12 @@ topBarInfoButton: PeerAvatarButton { } topBarSlideDuration: 200; +themeEditorSampleSize: size(90px, 51px); +themeEditorMargin: margins(17px, 10px, 17px, 10px); +themeEditorDescriptionSkip: 10px; +themeEditorNameFont: font(15px semibold); +themeEditorCopyNameFont: font(fsize semibold); + // Mac specific macAccessoryWidth: 450.; diff --git a/Telegram/build/set_version.bat b/Telegram/build/set_version.bat index 35f4d4f103..01f2f25644 100644 --- a/Telegram/build/set_version.bat +++ b/Telegram/build/set_version.bat @@ -104,7 +104,7 @@ call :repl "Replace=("FileVersion",) (\s*)"\d+.\d+.\d+.\d+"/ call :repl "Replace=("ProductVersion",) (\s*)"\d+.\d+.\d+.\d+"/$1$2 "%VersionMajor%.%VersionMinor%.%VersionPatch%.%VersionBeta%"" "Filename=%ResourcePath%" || goto :error echo Patching appxmanifest.xml... -set "ResourcePath=%FullScriptPath%..\Resources\uwp\appxmanifest.xml" +set "ResourcePath=%FullScriptPath%..\Resources\uwp\AppX\AppxManifest.xml" call :repl "Replace= (Version=)"\d+.\d+.\d+.\d+"/ $1"%VersionMajor%.%VersionMinor%.%VersionPatch%.%VersionBeta%"" "Filename=%ResourcePath%" || goto :error exit /b diff --git a/Telegram/build/set_version.sh b/Telegram/build/set_version.sh index ba667a7572..1580682aff 100755 --- a/Telegram/build/set_version.sh +++ b/Telegram/build/set_version.sh @@ -120,3 +120,6 @@ repl "\(PRODUCTVERSION\) \([ ]*\)[0-9][0-9]*,[0-9][0-9]*,[0-9][0-9]*,[0-9][0-9]* repl "\(\"FileVersion\",\) \([ ]*\)\"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\"" "\1\2 \"$VersionMajor.$VersionMinor.$VersionPatch.$VersionBeta\"" "$ResourcePath" repl "\(\"ProductVersion\",\) \([ ]*\)\"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\"" "\1\2 \"$VersionMajor.$VersionMinor.$VersionPatch.$VersionBeta\"" "$ResourcePath" +echo "Patching appxmanifest.xml..." +ResourcePath="$FullScriptPath/../Resources/uwp/AppX/AppxManifest.xml" +repl " \(Version=\)\"[0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*\"" " \1\"$VersionMajor.$VersionMinor.$VersionPatch.$VersionBeta\"" "$ResourcePath" diff --git a/Telegram/build/version b/Telegram/build/version index cdafbd7aaf..b930f657bf 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -3,4 +3,4 @@ AppVersionStrMajor 1.0 AppVersionStrSmall 1.0.6 AppVersionStr 1.0.6 AlphaChannel 0 -BetaVersion 0 +BetaVersion 1000006001 diff --git a/Telegram/prepare.bat b/Telegram/create.bat similarity index 92% rename from Telegram/prepare.bat rename to Telegram/create.bat index 950f24863b..363748109a 100644 --- a/Telegram/prepare.bat +++ b/Telegram/create.bat @@ -4,10 +4,7 @@ set "FullScriptPath=%~dp0" set "FullExecPath=%cd%" set "Command=%1" -if "%Command%" == "module" ( - call :write_module %2 - exit /b %errorlevel% -) else if "%Command%" == "header" ( +if "%Command%" == "header" ( call :write_header %2 exit /b %errorlevel% ) else if "%Command%" == "source" ( @@ -15,11 +12,8 @@ if "%Command%" == "module" ( exit /b %errorlevel% ) -cd gyp -call refresh.bat -cd .. - -exit /b +call :write_module %Command% +exit /b %errorlevel% :write_module ( @@ -30,8 +24,8 @@ exit /b exit /b 1 ) echo Generating module !CommandPathUnix!.. - call prepare.bat header !CommandPathUnix! - call prepare.bat source !CommandPathUnix! + call create.bat header !CommandPathUnix! + call create.bat source !CommandPathUnix! exit /b ) diff --git a/Telegram/gyp/Telegram.gyp b/Telegram/gyp/Telegram.gyp index a1e632ae91..45ab8cd27f 100644 --- a/Telegram/gyp/Telegram.gyp +++ b/Telegram/gyp/Telegram.gyp @@ -173,6 +173,8 @@ '<(src_loc)/boxes/contactsbox.h', '<(src_loc)/boxes/downloadpathbox.cpp', '<(src_loc)/boxes/downloadpathbox.h', + '<(src_loc)/boxes/editcolorbox.cpp', + '<(src_loc)/boxes/editcolorbox.h', '<(src_loc)/boxes/emojibox.cpp', '<(src_loc)/boxes/emojibox.h', '<(src_loc)/boxes/languagebox.cpp', @@ -457,6 +459,8 @@ '<(src_loc)/settings/settings_info_widget.h', '<(src_loc)/settings/settings_inner_widget.cpp', '<(src_loc)/settings/settings_inner_widget.h', + '<(src_loc)/settings/settings_layer.cpp', + '<(src_loc)/settings/settings_layer.h', '<(src_loc)/settings/settings_notifications_widget.cpp', '<(src_loc)/settings/settings_notifications_widget.h', '<(src_loc)/settings/settings_privacy_widget.cpp', @@ -574,12 +578,16 @@ '<(src_loc)/window/top_bar_widget.h', '<(src_loc)/window/window_main_menu.cpp', '<(src_loc)/window/window_main_menu.h', - '<(src_loc)/window/window_theme.cpp', - '<(src_loc)/window/window_theme.h', - '<(src_loc)/window/window_theme_preview.cpp', - '<(src_loc)/window/window_theme_preview.h', - '<(src_loc)/window/window_theme_warning.cpp', - '<(src_loc)/window/window_theme_warning.h', + '<(src_loc)/window/themes/window_theme.cpp', + '<(src_loc)/window/themes/window_theme.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_preview.cpp', + '<(src_loc)/window/themes/window_theme_preview.h', + '<(src_loc)/window/themes/window_theme_warning.cpp', + '<(src_loc)/window/themes/window_theme_warning.h', '<(src_loc)/window/window_title.h', '<(sp_media_key_tap_loc)/SPMediaKeyTap.m',