1049 lines
27 KiB
C++
1049 lines
27 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "settings/settings_chat.h"
|
|
|
|
#include "settings/settings_common.h"
|
|
#include "boxes/connection_box.h"
|
|
#include "boxes/auto_download_box.h"
|
|
#include "boxes/stickers_box.h"
|
|
#include "boxes/background_box.h"
|
|
#include "boxes/background_preview_box.h"
|
|
#include "boxes/download_path_box.h"
|
|
#include "boxes/local_storage_box.h"
|
|
#include "ui/wrap/vertical_layout.h"
|
|
#include "ui/wrap/slide_wrap.h"
|
|
#include "ui/widgets/input_fields.h"
|
|
#include "ui/widgets/checkbox.h"
|
|
#include "ui/widgets/labels.h"
|
|
#include "ui/effects/radial_animation.h"
|
|
#include "ui/toast/toast.h"
|
|
#include "ui/image/image.h"
|
|
#include "ui/image/image_source.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "window/themes/window_theme_editor.h"
|
|
#include "window/themes/window_theme.h"
|
|
#include "info/profile/info_profile_button.h"
|
|
#include "storage/localstorage.h"
|
|
#include "core/file_utilities.h"
|
|
#include "data/data_session.h"
|
|
#include "chat_helpers/emoji_sets_manager.h"
|
|
#include "support/support_common.h"
|
|
#include "support/support_templates.h"
|
|
#include "auth_session.h"
|
|
#include "mainwidget.h"
|
|
#include "styles/style_settings.h"
|
|
#include "styles/style_boxes.h"
|
|
|
|
namespace Settings {
|
|
|
|
class BackgroundRow : public Ui::RpWidget {
|
|
public:
|
|
BackgroundRow(QWidget *parent);
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *e) override;
|
|
|
|
int resizeGetHeight(int newWidth) override;
|
|
|
|
private:
|
|
void updateImage();
|
|
|
|
float64 radialProgress() const;
|
|
bool radialLoading() const;
|
|
QRect radialRect() const;
|
|
void radialStart();
|
|
crl::time radialTimeShift() const;
|
|
void radialAnimationCallback(crl::time now);
|
|
|
|
QPixmap _background;
|
|
object_ptr<Ui::LinkButton> _chooseFromGallery;
|
|
object_ptr<Ui::LinkButton> _chooseFromFile;
|
|
|
|
Ui::RadialAnimation _radial;
|
|
|
|
};
|
|
|
|
class DefaultTheme final : public Ui::AbstractCheckView {
|
|
public:
|
|
enum class Type {
|
|
DayBlue,
|
|
Default,
|
|
Night,
|
|
NightGreen,
|
|
};
|
|
struct Scheme {
|
|
Type type = Type();
|
|
QColor background;
|
|
QColor sent;
|
|
QColor received;
|
|
QColor radiobuttonInactive;
|
|
QColor radiobuttonActive;
|
|
QString name;
|
|
QString path;
|
|
};
|
|
DefaultTheme(Scheme scheme, bool checked);
|
|
|
|
QSize getSize() const override;
|
|
void paint(
|
|
Painter &p,
|
|
int left,
|
|
int top,
|
|
int outerWidth) override;
|
|
QImage prepareRippleMask() const override;
|
|
bool checkRippleStartPosition(QPoint position) const override;
|
|
|
|
private:
|
|
void checkedChangedHook(anim::type animated) override;
|
|
|
|
Scheme _scheme;
|
|
Ui::RadioView _radio;
|
|
|
|
};
|
|
|
|
void ChooseFromFile(not_null<QWidget*> parent);
|
|
|
|
BackgroundRow::BackgroundRow(QWidget *parent) : RpWidget(parent)
|
|
, _chooseFromGallery(
|
|
this,
|
|
lang(lng_settings_bg_from_gallery),
|
|
st::settingsLink)
|
|
, _chooseFromFile(this, lang(lng_settings_bg_from_file), st::settingsLink)
|
|
, _radial([=](crl::time now) { radialAnimationCallback(now); }) {
|
|
updateImage();
|
|
|
|
_chooseFromGallery->addClickHandler([] {
|
|
Ui::show(Box<BackgroundBox>());
|
|
});
|
|
_chooseFromFile->addClickHandler([=] {
|
|
ChooseFromFile(this);
|
|
});
|
|
|
|
using Update = const Window::Theme::BackgroundUpdate;
|
|
base::ObservableViewer(
|
|
*Window::Theme::Background()
|
|
) | rpl::filter([](const Update &update) {
|
|
return (update.type == Update::Type::New
|
|
|| update.type == Update::Type::Start
|
|
|| update.type == Update::Type::Changed);
|
|
}) | rpl::start_with_next([=] {
|
|
updateImage();
|
|
}, lifetime());
|
|
}
|
|
|
|
void BackgroundRow::paintEvent(QPaintEvent *e) {
|
|
Painter p(this);
|
|
|
|
const auto radial = _radial.animating();
|
|
const auto radialOpacity = radial ? _radial.opacity() : 0.;
|
|
if (radial) {
|
|
const auto backThumb = App::main()->newBackgroundThumb();
|
|
if (!backThumb) {
|
|
p.drawPixmap(0, 0, _background);
|
|
} else {
|
|
const auto &pix = backThumb->pixBlurred(
|
|
Data::FileOrigin(),
|
|
st::settingsBackgroundThumb);
|
|
const auto factor = cIntRetinaFactor();
|
|
p.drawPixmap(
|
|
0,
|
|
0,
|
|
st::settingsBackgroundThumb,
|
|
st::settingsBackgroundThumb,
|
|
pix,
|
|
0,
|
|
(pix.height() - st::settingsBackgroundThumb * factor) / 2,
|
|
st::settingsBackgroundThumb * factor,
|
|
st::settingsBackgroundThumb * factor);
|
|
}
|
|
|
|
const auto outer = radialRect();
|
|
const auto inner = QRect(
|
|
QPoint(
|
|
outer.x() + (outer.width() - st::radialSize.width()) / 2,
|
|
outer.y() + (outer.height() - st::radialSize.height()) / 2),
|
|
st::radialSize);
|
|
p.setPen(Qt::NoPen);
|
|
p.setOpacity(radialOpacity);
|
|
p.setBrush(st::radialBg);
|
|
|
|
{
|
|
PainterHighQualityEnabler hq(p);
|
|
p.drawEllipse(inner);
|
|
}
|
|
|
|
p.setOpacity(1);
|
|
const auto arc = inner.marginsRemoved(QMargins(
|
|
st::radialLine,
|
|
st::radialLine,
|
|
st::radialLine,
|
|
st::radialLine));
|
|
_radial.draw(p, arc, st::radialLine, st::radialFg);
|
|
} else {
|
|
p.drawPixmap(0, 0, _background);
|
|
}
|
|
}
|
|
|
|
int BackgroundRow::resizeGetHeight(int newWidth) {
|
|
auto linkTop = st::settingsFromGalleryTop;
|
|
auto linkLeft = st::settingsBackgroundThumb + st::settingsThumbSkip;
|
|
auto linkWidth = newWidth - linkLeft;
|
|
_chooseFromGallery->resizeToWidth(
|
|
qMin(linkWidth, _chooseFromGallery->naturalWidth()));
|
|
_chooseFromFile->resizeToWidth(
|
|
qMin(linkWidth, _chooseFromFile->naturalWidth()));
|
|
_chooseFromGallery->moveToLeft(linkLeft, linkTop, newWidth);
|
|
linkTop += _chooseFromGallery->height() + st::settingsFromFileTop;
|
|
_chooseFromFile->moveToLeft(linkLeft, linkTop, newWidth);
|
|
return st::settingsBackgroundThumb;
|
|
}
|
|
|
|
float64 BackgroundRow::radialProgress() const {
|
|
return App::main()->chatBackgroundProgress();
|
|
}
|
|
|
|
bool BackgroundRow::radialLoading() const {
|
|
const auto main = App::main();
|
|
if (main->chatBackgroundLoading()) {
|
|
main->checkChatBackground();
|
|
if (main->chatBackgroundLoading()) {
|
|
return true;
|
|
} else {
|
|
const_cast<BackgroundRow*>(this)->updateImage();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QRect BackgroundRow::radialRect() const {
|
|
return QRect(
|
|
0,
|
|
0,
|
|
st::settingsBackgroundThumb,
|
|
st::settingsBackgroundThumb);
|
|
}
|
|
|
|
void BackgroundRow::radialStart() {
|
|
if (radialLoading() && !_radial.animating()) {
|
|
_radial.start(radialProgress());
|
|
if (const auto shift = radialTimeShift()) {
|
|
_radial.update(
|
|
radialProgress(),
|
|
!radialLoading(),
|
|
crl::now() + shift);
|
|
}
|
|
}
|
|
}
|
|
|
|
crl::time BackgroundRow::radialTimeShift() const {
|
|
return st::radialDuration;
|
|
}
|
|
|
|
void BackgroundRow::radialAnimationCallback(crl::time now) {
|
|
const auto updated = _radial.update(
|
|
radialProgress(),
|
|
!radialLoading(),
|
|
now + radialTimeShift());
|
|
if (!anim::Disabled() || updated) {
|
|
rtlupdate(radialRect());
|
|
}
|
|
}
|
|
|
|
void BackgroundRow::updateImage() {
|
|
int32 size = st::settingsBackgroundThumb * cIntRetinaFactor();
|
|
QImage back(size, size, QImage::Format_ARGB32_Premultiplied);
|
|
back.setDevicePixelRatio(cRetinaFactor());
|
|
{
|
|
Painter p(&back);
|
|
PainterHighQualityEnabler hq(p);
|
|
|
|
if (const auto color = Window::Theme::Background()->colorForFill()) {
|
|
p.fillRect(
|
|
0,
|
|
0,
|
|
st::settingsBackgroundThumb,
|
|
st::settingsBackgroundThumb,
|
|
*color);
|
|
} else {
|
|
const auto &pix = Window::Theme::Background()->pixmap();
|
|
const auto sx = (pix.width() > pix.height())
|
|
? ((pix.width() - pix.height()) / 2)
|
|
: 0;
|
|
const auto sy = (pix.height() > pix.width())
|
|
? ((pix.height() - pix.width()) / 2)
|
|
: 0;
|
|
const auto s = (pix.width() > pix.height())
|
|
? pix.height()
|
|
: pix.width();
|
|
p.drawPixmap(
|
|
0,
|
|
0,
|
|
st::settingsBackgroundThumb,
|
|
st::settingsBackgroundThumb,
|
|
pix,
|
|
sx,
|
|
sy,
|
|
s,
|
|
s);
|
|
}
|
|
}
|
|
Images::prepareRound(back, ImageRoundRadius::Small);
|
|
_background = App::pixmapFromImageInPlace(std::move(back));
|
|
_background.setDevicePixelRatio(cRetinaFactor());
|
|
|
|
rtlupdate(radialRect());
|
|
|
|
if (radialLoading()) {
|
|
radialStart();
|
|
}
|
|
}
|
|
|
|
DefaultTheme::DefaultTheme(Scheme scheme, bool checked)
|
|
: AbstractCheckView(st::defaultRadio.duration, checked, nullptr)
|
|
, _scheme(scheme)
|
|
, _radio(st::defaultRadio, checked, [=] { update(); }) {
|
|
_radio.setToggledOverride(_scheme.radiobuttonActive);
|
|
_radio.setUntoggledOverride(_scheme.radiobuttonInactive);
|
|
}
|
|
|
|
QSize DefaultTheme::getSize() const {
|
|
return st::settingsThemePreviewSize;
|
|
}
|
|
|
|
void DefaultTheme::paint(
|
|
Painter &p,
|
|
int left,
|
|
int top,
|
|
int outerWidth) {
|
|
const auto received = QRect(
|
|
st::settingsThemeBubblePosition,
|
|
st::settingsThemeBubbleSize);
|
|
const auto sent = QRect(
|
|
outerWidth - received.width() - st::settingsThemeBubblePosition.x(),
|
|
received.y() + received.height() + st::settingsThemeBubbleSkip,
|
|
received.width(),
|
|
received.height());
|
|
const auto radius = st::settingsThemeBubbleRadius;
|
|
|
|
p.fillRect(
|
|
QRect(QPoint(), st::settingsThemePreviewSize),
|
|
_scheme.background);
|
|
|
|
PainterHighQualityEnabler hq(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(_scheme.received);
|
|
p.drawRoundedRect(rtlrect(received, outerWidth), radius, radius);
|
|
p.setBrush(_scheme.sent);
|
|
p.drawRoundedRect(rtlrect(sent, outerWidth), radius, radius);
|
|
|
|
const auto radio = _radio.getSize();
|
|
_radio.paint(
|
|
p,
|
|
(outerWidth - radio.width()) / 2,
|
|
getSize().height() - radio.height() - st::settingsThemeRadioBottom,
|
|
outerWidth);
|
|
}
|
|
|
|
QImage DefaultTheme::prepareRippleMask() const {
|
|
return QImage();
|
|
}
|
|
|
|
bool DefaultTheme::checkRippleStartPosition(QPoint position) const {
|
|
return false;
|
|
}
|
|
|
|
void DefaultTheme::checkedChangedHook(anim::type animated) {
|
|
_radio.setChecked(checked(), animated);
|
|
}
|
|
|
|
void ChooseFromFile(not_null<QWidget*> parent) {
|
|
const auto &imgExtensions = cImgExtensions();
|
|
auto filters = QStringList(
|
|
qsl("Theme files (*.tdesktop-theme *.tdesktop-palette *")
|
|
+ imgExtensions.join(qsl(" *"))
|
|
+ qsl(")"));
|
|
filters.push_back(FileDialog::AllFilesFilter());
|
|
const auto callback = [=](const FileDialog::OpenResult &result) {
|
|
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (!result.paths.isEmpty()) {
|
|
const auto filePath = result.paths.front();
|
|
const auto hasExtension = [&](QLatin1String extension) {
|
|
return filePath.endsWith(extension, Qt::CaseInsensitive);
|
|
};
|
|
if (hasExtension(qstr(".tdesktop-theme"))
|
|
|| hasExtension(qstr(".tdesktop-palette"))) {
|
|
Window::Theme::Apply(filePath);
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto image = result.remoteContent.isEmpty()
|
|
? App::readImage(result.paths.front())
|
|
: App::readImage(result.remoteContent);
|
|
if (image.isNull() || image.width() <= 0 || image.height() <= 0) {
|
|
return;
|
|
}
|
|
auto local = Data::CustomWallPaper();
|
|
local.setLocalImageAsThumbnail(std::make_shared<Image>(
|
|
std::make_unique<Images::ImageSource>(
|
|
std::move(image),
|
|
"JPG")));
|
|
Ui::show(Box<BackgroundPreviewBox>(local));
|
|
};
|
|
FileDialog::GetOpenPath(
|
|
parent.get(),
|
|
lang(lng_choose_image),
|
|
filters.join(qsl(";;")),
|
|
crl::guard(parent, callback));
|
|
|
|
}
|
|
|
|
QString DownloadPathText() {
|
|
if (Global::DownloadPath().isEmpty()) {
|
|
return lang(lng_download_path_default);
|
|
} else if (Global::DownloadPath() == qsl("tmp")) {
|
|
return lang(lng_download_path_temp);
|
|
}
|
|
return QDir::toNativeSeparators(Global::DownloadPath());
|
|
}
|
|
|
|
void SetupStickersEmoji(not_null<Ui::VerticalLayout*> container) {
|
|
AddDivider(container);
|
|
AddSkip(container);
|
|
|
|
AddSubsectionTitle(container, lng_settings_stickers_emoji);
|
|
|
|
auto wrap = object_ptr<Ui::VerticalLayout>(container);
|
|
const auto inner = wrap.data();
|
|
container->add(object_ptr<Ui::OverrideMargins>(
|
|
container,
|
|
std::move(wrap),
|
|
QMargins(0, 0, 0, st::settingsCheckbox.margin.bottom())));
|
|
|
|
const auto checkbox = [&](LangKey label, bool checked) {
|
|
return object_ptr<Ui::Checkbox>(
|
|
container,
|
|
lang(label),
|
|
checked,
|
|
st::settingsCheckbox);
|
|
};
|
|
const auto add = [&](LangKey label, bool checked, auto &&handle) {
|
|
inner->add(
|
|
checkbox(label, checked),
|
|
st::settingsCheckboxPadding
|
|
)->checkedChanges(
|
|
) | rpl::start_with_next(
|
|
std::move(handle),
|
|
inner->lifetime());
|
|
};
|
|
add(
|
|
lng_settings_replace_emojis,
|
|
Global::ReplaceEmoji(),
|
|
[](bool checked) {
|
|
Global::SetReplaceEmoji(checked);
|
|
Global::RefReplaceEmojiChanged().notify();
|
|
Local::writeUserSettings();
|
|
});
|
|
|
|
add(
|
|
lng_settings_suggest_emoji,
|
|
Global::SuggestEmoji(),
|
|
[](bool checked) {
|
|
Global::SetSuggestEmoji(checked);
|
|
Local::writeUserSettings();
|
|
});
|
|
|
|
add(
|
|
lng_settings_suggest_by_emoji,
|
|
Global::SuggestStickersByEmoji(),
|
|
[](bool checked) {
|
|
Global::SetSuggestStickersByEmoji(checked);
|
|
Local::writeUserSettings();
|
|
});
|
|
|
|
AddButton(
|
|
container,
|
|
lng_stickers_you_have,
|
|
st::settingsChatButton,
|
|
&st::settingsIconStickers,
|
|
st::settingsChatIconLeft
|
|
)->addClickHandler([] {
|
|
Ui::show(Box<StickersBox>(StickersBox::Section::Installed));
|
|
});
|
|
|
|
AddButton(
|
|
container,
|
|
lng_emoji_manage_sets,
|
|
st::settingsChatButton,
|
|
&st::settingsIconEmoji,
|
|
st::settingsChatIconLeft
|
|
)->addClickHandler([] {
|
|
Ui::show(Box<Ui::Emoji::ManageSetsBox>());
|
|
});
|
|
|
|
AddSkip(container, st::settingsCheckboxesSkip);
|
|
}
|
|
|
|
void SetupMessages(not_null<Ui::VerticalLayout*> container) {
|
|
AddDivider(container);
|
|
AddSkip(container);
|
|
|
|
AddSubsectionTitle(container, lng_settings_messages);
|
|
|
|
AddSkip(container, st::settingsSendTypeSkip);
|
|
|
|
using SendByType = Ui::InputSubmitSettings;
|
|
|
|
const auto skip = st::settingsSendTypeSkip;
|
|
auto wrap = object_ptr<Ui::VerticalLayout>(container);
|
|
const auto inner = wrap.data();
|
|
container->add(
|
|
object_ptr<Ui::OverrideMargins>(
|
|
container,
|
|
std::move(wrap),
|
|
QMargins(0, skip, 0, skip)));
|
|
|
|
const auto group = std::make_shared<Ui::RadioenumGroup<SendByType>>(
|
|
Auth().settings().sendSubmitWay());
|
|
const auto add = [&](SendByType value, LangKey key) {
|
|
inner->add(
|
|
object_ptr<Ui::Radioenum<SendByType>>(
|
|
inner,
|
|
group,
|
|
value,
|
|
lang(key),
|
|
st::settingsSendType),
|
|
st::settingsSendTypePadding);
|
|
};
|
|
const auto small = st::settingsSendTypePadding;
|
|
const auto top = skip;
|
|
add(SendByType::Enter, lng_settings_send_enter);
|
|
add(
|
|
SendByType::CtrlEnter,
|
|
((cPlatform() == dbipMac || cPlatform() == dbipMacOld)
|
|
? lng_settings_send_cmdenter
|
|
: lng_settings_send_ctrlenter));
|
|
|
|
group->setChangedCallback([](SendByType value) {
|
|
Auth().settings().setSendSubmitWay(value);
|
|
if (App::main()) {
|
|
App::main()->ctrlEnterSubmitUpdated();
|
|
}
|
|
Local::writeUserSettings();
|
|
});
|
|
|
|
AddSkip(inner, st::settingsCheckboxesSkip);
|
|
}
|
|
|
|
void SetupExport(not_null<Ui::VerticalLayout*> container) {
|
|
AddButton(
|
|
container,
|
|
lng_settings_export_data,
|
|
st::settingsButton
|
|
)->addClickHandler([] {
|
|
Ui::hideSettingsAndLayer();
|
|
App::CallDelayed(
|
|
st::boxDuration,
|
|
&Auth(),
|
|
[] { Auth().data().startExport(); });
|
|
});
|
|
}
|
|
|
|
void SetupLocalStorage(not_null<Ui::VerticalLayout*> container) {
|
|
AddButton(
|
|
container,
|
|
lng_settings_manage_local_storage,
|
|
st::settingsButton
|
|
)->addClickHandler([] {
|
|
LocalStorageBox::Show(
|
|
&Auth().data().cache(),
|
|
&Auth().data().cacheBigFile());
|
|
});
|
|
}
|
|
|
|
void SetupDataStorage(not_null<Ui::VerticalLayout*> container) {
|
|
using namespace rpl::mappers;
|
|
|
|
AddDivider(container);
|
|
AddSkip(container);
|
|
|
|
AddSubsectionTitle(container, lng_settings_data_storage);
|
|
|
|
const auto ask = AddButton(
|
|
container,
|
|
lng_download_path_ask,
|
|
st::settingsButton
|
|
)->toggleOn(rpl::single(Global::AskDownloadPath()));
|
|
|
|
#ifndef OS_WIN_STORE
|
|
const auto showpath = Ui::CreateChild<rpl::event_stream<bool>>(ask);
|
|
const auto path = container->add(
|
|
object_ptr<Ui::SlideWrap<Button>>(
|
|
container,
|
|
object_ptr<Button>(
|
|
container,
|
|
Lang::Viewer(lng_download_path),
|
|
st::settingsButton)));
|
|
auto pathtext = rpl::single(
|
|
rpl::empty_value()
|
|
) | rpl::then(base::ObservableViewer(
|
|
Global::RefDownloadPathChanged()
|
|
)) | rpl::map([] {
|
|
return DownloadPathText();
|
|
});
|
|
CreateRightLabel(
|
|
path->entity(),
|
|
std::move(pathtext),
|
|
st::settingsButton,
|
|
lng_download_path);
|
|
path->entity()->addClickHandler([] {
|
|
Ui::show(Box<DownloadPathBox>());
|
|
});
|
|
path->toggleOn(ask->toggledValue() | rpl::map(!_1));
|
|
#endif // OS_WIN_STORE
|
|
|
|
ask->toggledValue(
|
|
) | rpl::filter([](bool checked) {
|
|
return (checked != Global::AskDownloadPath());
|
|
}) | rpl::start_with_next([=](bool checked) {
|
|
Global::SetAskDownloadPath(checked);
|
|
Local::writeUserSettings();
|
|
|
|
#ifndef OS_WIN_STORE
|
|
showpath->fire_copy(!checked);
|
|
#endif // OS_WIN_STORE
|
|
|
|
}, ask->lifetime());
|
|
|
|
SetupLocalStorage(container);
|
|
SetupExport(container);
|
|
|
|
AddSkip(container, st::settingsCheckboxesSkip);
|
|
}
|
|
|
|
void SetupAutoDownload(not_null<Ui::VerticalLayout*> container) {
|
|
AddDivider(container);
|
|
AddSkip(container);
|
|
|
|
AddSubsectionTitle(container, lng_media_auto_settings);
|
|
|
|
using Source = Data::AutoDownload::Source;
|
|
const auto add = [&](LangKey label, Source source) {
|
|
AddButton(
|
|
container,
|
|
label,
|
|
st::settingsButton
|
|
)->addClickHandler([=] {
|
|
Ui::show(Box<AutoDownloadBox>(source));
|
|
});
|
|
};
|
|
add(lng_media_auto_in_private, Source::User);
|
|
add(lng_media_auto_in_groups, Source::Group);
|
|
add(lng_media_auto_in_channels, Source::Channel);
|
|
|
|
AddSkip(container, st::settingsCheckboxesSkip);
|
|
}
|
|
|
|
void SetupChatBackground(not_null<Ui::VerticalLayout*> container) {
|
|
AddDivider(container);
|
|
AddSkip(container);
|
|
|
|
AddSubsectionTitle(container, lng_settings_section_background);
|
|
|
|
container->add(
|
|
object_ptr<BackgroundRow>(container),
|
|
st::settingsBackgroundPadding);
|
|
|
|
const auto skipTop = st::settingsCheckbox.margin.top();
|
|
const auto skipBottom = st::settingsCheckbox.margin.bottom();
|
|
auto wrap = object_ptr<Ui::VerticalLayout>(container);
|
|
const auto inner = wrap.data();
|
|
container->add(
|
|
object_ptr<Ui::OverrideMargins>(
|
|
container,
|
|
std::move(wrap),
|
|
QMargins(0, skipTop, 0, skipBottom)));
|
|
|
|
AddSkip(container, st::settingsTileSkip);
|
|
|
|
const auto tile = inner->add(
|
|
object_ptr<Ui::Checkbox>(
|
|
inner,
|
|
lang(lng_settings_bg_tile),
|
|
Window::Theme::Background()->tile(),
|
|
st::settingsCheckbox),
|
|
st::settingsSendTypePadding);
|
|
const auto adaptive = inner->add(
|
|
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
|
|
inner,
|
|
object_ptr<Ui::Checkbox>(
|
|
inner,
|
|
lang(lng_settings_adaptive_wide),
|
|
Global::AdaptiveForWide(),
|
|
st::settingsCheckbox),
|
|
st::settingsSendTypePadding));
|
|
|
|
tile->checkedChanges(
|
|
) | rpl::start_with_next([](bool checked) {
|
|
Window::Theme::Background()->setTile(checked);
|
|
}, tile->lifetime());
|
|
|
|
using Update = const Window::Theme::BackgroundUpdate;
|
|
base::ObservableViewer(
|
|
*Window::Theme::Background()
|
|
) | rpl::filter([](const Update &update) {
|
|
return (update.type == Update::Type::Changed);
|
|
}) | rpl::map([] {
|
|
return Window::Theme::Background()->tile();
|
|
}) | rpl::start_with_next([=](bool tiled) {
|
|
tile->setChecked(tiled);
|
|
}, tile->lifetime());
|
|
|
|
adaptive->toggleOn(rpl::single(
|
|
rpl::empty_value()
|
|
) | rpl::then(base::ObservableViewer(
|
|
Adaptive::Changed()
|
|
)) | rpl::map([] {
|
|
return (Global::AdaptiveChatLayout() == Adaptive::ChatLayout::Wide);
|
|
}));
|
|
|
|
adaptive->entity()->checkedChanges(
|
|
) | rpl::start_with_next([](bool checked) {
|
|
Global::SetAdaptiveForWide(checked);
|
|
Adaptive::Changed().notify();
|
|
Local::writeUserSettings();
|
|
}, adaptive->lifetime());
|
|
}
|
|
|
|
void SetupDefaultThemes(not_null<Ui::VerticalLayout*> container) {
|
|
using Type = DefaultTheme::Type;
|
|
using Scheme = DefaultTheme::Scheme;
|
|
const auto block = container->add(object_ptr<Ui::FixedHeightWidget>(
|
|
container));
|
|
const auto scheme = DefaultTheme::Scheme();
|
|
const auto color = [](str_const hex) {
|
|
Expects(hex.size() == 6);
|
|
|
|
const auto component = [](char a, char b) {
|
|
const auto convert = [](char ch) {
|
|
Expects((ch >= '0' && ch <= '9')
|
|
|| (ch >= 'A' && ch <= 'F')
|
|
|| (ch >= 'a' && ch <= 'f'));
|
|
|
|
return (ch >= '0' && ch <= '9')
|
|
? int(ch - '0')
|
|
: int(ch - ((ch >= 'A' && ch <= 'F') ? 'A' : 'a') + 10);
|
|
};
|
|
return convert(a) * 16 + convert(b);
|
|
};
|
|
|
|
return QColor(
|
|
component(hex[0], hex[1]),
|
|
component(hex[2], hex[3]),
|
|
component(hex[4], hex[5]));
|
|
};
|
|
static const auto schemes = {
|
|
Scheme{
|
|
Type::DayBlue,
|
|
color("7ec4ea"),
|
|
color("d7f0ff"),
|
|
color("ffffff"),
|
|
color("d7f0ff"),
|
|
color("ffffff"),
|
|
"Blue",
|
|
":/gui/day-blue.tdesktop-theme"
|
|
},
|
|
Scheme{
|
|
Type::Default,
|
|
color("90ce89"),
|
|
color("eaffdc"),
|
|
color("ffffff"),
|
|
color("eaffdc"),
|
|
color("ffffff"),
|
|
"Classic",
|
|
QString()
|
|
},
|
|
Scheme{
|
|
Type::Night,
|
|
color("485761"),
|
|
color("5ca7d4"),
|
|
color("6b808d"),
|
|
color("6b808d"),
|
|
color("5ca7d4"),
|
|
"Midnight",
|
|
":/gui/night.tdesktop-theme"
|
|
},
|
|
Scheme{
|
|
Type::NightGreen,
|
|
color("485761"),
|
|
color("74bf93"),
|
|
color("6b808d"),
|
|
color("6b808d"),
|
|
color("74bf93"),
|
|
"Matrix",
|
|
":/gui/night-green.tdesktop-theme"
|
|
},
|
|
};
|
|
const auto chosen = [&] {
|
|
if (Window::Theme::IsNonDefaultBackground()) {
|
|
return Type(-1);
|
|
}
|
|
const auto path = Window::Theme::Background()->themeAbsolutePath();
|
|
for (const auto &scheme : schemes) {
|
|
if (path == scheme.path) {
|
|
return scheme.type;
|
|
}
|
|
}
|
|
return Type(-1);
|
|
};
|
|
const auto group = std::make_shared<Ui::RadioenumGroup<Type>>(chosen());
|
|
auto buttons = ranges::view::all(
|
|
schemes
|
|
) | ranges::view::transform([&](const Scheme &scheme) {
|
|
auto check = std::make_unique<DefaultTheme>(scheme, false);
|
|
const auto weak = check.get();
|
|
const auto result = Ui::CreateChild<Ui::Radioenum<Type>>(
|
|
block,
|
|
group,
|
|
scheme.type,
|
|
scheme.name,
|
|
st::settingsTheme,
|
|
std::move(check));
|
|
weak->setUpdateCallback([=] { result->update(); });
|
|
return result;
|
|
}) | ranges::to_vector;
|
|
|
|
using Update = const Window::Theme::BackgroundUpdate;
|
|
const auto apply = [=](const Scheme &scheme) {
|
|
const auto isNight = [](const Scheme &scheme) {
|
|
const auto type = scheme.type;
|
|
return (type != Type::DayBlue) && (type != Type::Default);
|
|
};
|
|
const auto currentlyIsCustom = (chosen() == Type(-1));
|
|
if (Window::Theme::IsNightMode() == isNight(scheme)) {
|
|
Window::Theme::ApplyDefaultWithPath(scheme.path);
|
|
} else {
|
|
Window::Theme::ToggleNightMode(scheme.path);
|
|
}
|
|
if (!currentlyIsCustom) {
|
|
Window::Theme::KeepApplied();
|
|
}
|
|
};
|
|
group->setChangedCallback([=](Type type) {
|
|
const auto i = ranges::find_if(schemes, [&](const Scheme &scheme) {
|
|
return (type == scheme.type && type != chosen());
|
|
});
|
|
if (i != end(schemes)) {
|
|
apply(*i);
|
|
}
|
|
});
|
|
base::ObservableViewer(
|
|
*Window::Theme::Background()
|
|
) | rpl::filter([](const Update &update) {
|
|
return (update.type == Update::Type::ApplyingTheme
|
|
|| update.type == Update::Type::New);
|
|
}) | rpl::map([=] {
|
|
return chosen();
|
|
}) | rpl::start_with_next([=](Type type) {
|
|
group->setValue(type);
|
|
}, container->lifetime());
|
|
|
|
for (const auto button : buttons) {
|
|
button->setCheckAlignment(style::al_top);
|
|
button->resizeToWidth(button->width());
|
|
}
|
|
block->resize(block->width(), buttons[0]->height());
|
|
block->widthValue(
|
|
) | rpl::start_with_next([buttons = std::move(buttons)](int width) {
|
|
Expects(!buttons.empty());
|
|
|
|
// |------| |---------| |-------| |-------|
|
|
// pad | blue | skip | classic | 3*skip | night | skip | night | pad
|
|
// |------| |---------| |-------| |-------|
|
|
const auto padding = st::settingsButton.padding;
|
|
width -= padding.left() + padding.right();
|
|
const auto desired = st::settingsThemePreviewSize.width();
|
|
const auto count = int(buttons.size());
|
|
const auto smallSkips = (count / 2);
|
|
const auto bigSkips = ((count - 1) / 2);
|
|
const auto skipRatio = 3;
|
|
const auto skipSegments = smallSkips + bigSkips * skipRatio;
|
|
const auto minSkip = st::settingsThemeMinSkip;
|
|
const auto single = [&] {
|
|
if (width >= skipSegments * minSkip + count * desired) {
|
|
return desired;
|
|
}
|
|
return (width - skipSegments * minSkip) / count;
|
|
}();
|
|
if (single <= 0) {
|
|
return;
|
|
}
|
|
const auto fullSkips = width - count * single;
|
|
const auto segment = fullSkips / float64(skipSegments);
|
|
const auto smallSkip = segment;
|
|
const auto bigSkip = segment * skipRatio;
|
|
auto left = padding.left() + 0.;
|
|
auto index = 0;
|
|
for (const auto button : buttons) {
|
|
button->resizeToWidth(single);
|
|
button->moveToLeft(int(std::round(left)), 0);
|
|
left += button->width() + ((index++ % 2) ? bigSkip : smallSkip);
|
|
}
|
|
}, block->lifetime());
|
|
|
|
AddSkip(container);
|
|
}
|
|
|
|
void SetupThemeOptions(not_null<Ui::VerticalLayout*> container) {
|
|
AddSkip(container, st::settingsPrivacySkip);
|
|
|
|
AddSubsectionTitle(container, lng_settings_themes);
|
|
|
|
AddSkip(container, st::settingsThemesTopSkip);
|
|
SetupDefaultThemes(container);
|
|
AddSkip(container, st::settingsThemesBottomSkip);
|
|
|
|
AddButton(
|
|
container,
|
|
lng_settings_bg_edit_theme,
|
|
st::settingsChatButton,
|
|
&st::settingsIconThemes,
|
|
st::settingsChatIconLeft
|
|
)->addClickHandler(App::LambdaDelayed(
|
|
st::settingsChatButton.ripple.hideDuration,
|
|
container,
|
|
[] { Window::Theme::Editor::Start(); }));
|
|
|
|
AddSkip(container);
|
|
}
|
|
|
|
void SetupSupportSwitchSettings(not_null<Ui::VerticalLayout*> container) {
|
|
using SwitchType = Support::SwitchSettings;
|
|
const auto group = std::make_shared<Ui::RadioenumGroup<SwitchType>>(
|
|
Auth().settings().supportSwitch());
|
|
const auto add = [&](SwitchType value, const QString &label) {
|
|
container->add(
|
|
object_ptr<Ui::Radioenum<SwitchType>>(
|
|
container,
|
|
group,
|
|
value,
|
|
label,
|
|
st::settingsSendType),
|
|
st::settingsSendTypePadding);
|
|
};
|
|
add(SwitchType::None, "Just send the reply");
|
|
add(SwitchType::Next, "Send and switch to next");
|
|
add(SwitchType::Previous, "Send and switch to previous");
|
|
group->setChangedCallback([](SwitchType value) {
|
|
Auth().settings().setSupportSwitch(value);
|
|
Local::writeUserSettings();
|
|
});
|
|
}
|
|
|
|
void SetupSupportChatsLimitSlice(not_null<Ui::VerticalLayout*> container) {
|
|
constexpr auto kDayDuration = 24 * 60 * 60;
|
|
struct Option {
|
|
int days = 0;
|
|
QString label;
|
|
};
|
|
const auto options = std::vector<Option>{
|
|
{ 1, "1 day" },
|
|
{ 7, "1 week" },
|
|
{ 30, "1 month" },
|
|
{ 365, "1 year" },
|
|
{ 0, "All of them" },
|
|
};
|
|
const auto current = Auth().settings().supportChatsTimeSlice();
|
|
const auto days = current / kDayDuration;
|
|
const auto best = ranges::min_element(
|
|
options,
|
|
std::less<>(),
|
|
[&](const Option &option) { return std::abs(option.days - days); });
|
|
|
|
const auto group = std::make_shared<Ui::RadiobuttonGroup>(best->days);
|
|
for (const auto &option : options) {
|
|
container->add(
|
|
object_ptr<Ui::Radiobutton>(
|
|
container,
|
|
group,
|
|
option.days,
|
|
option.label,
|
|
st::settingsSendType),
|
|
st::settingsSendTypePadding);
|
|
}
|
|
group->setChangedCallback([=](int days) {
|
|
Auth().settings().setSupportChatsTimeSlice(days * kDayDuration);
|
|
Local::writeUserSettings();
|
|
});
|
|
}
|
|
|
|
void SetupSupport(not_null<Ui::VerticalLayout*> container) {
|
|
AddSkip(container);
|
|
|
|
AddSubsectionTitle(container, rpl::single(qsl("Support settings")));
|
|
|
|
AddSkip(container, st::settingsSendTypeSkip);
|
|
|
|
const auto skip = st::settingsSendTypeSkip;
|
|
auto wrap = object_ptr<Ui::VerticalLayout>(container);
|
|
const auto inner = wrap.data();
|
|
container->add(
|
|
object_ptr<Ui::OverrideMargins>(
|
|
container,
|
|
std::move(wrap),
|
|
QMargins(0, skip, 0, skip)));
|
|
|
|
SetupSupportSwitchSettings(inner);
|
|
|
|
AddSkip(inner, st::settingsCheckboxesSkip);
|
|
|
|
inner->add(
|
|
object_ptr<Ui::Checkbox>(
|
|
inner,
|
|
"Enable templates autocomplete",
|
|
Auth().settings().supportTemplatesAutocomplete(),
|
|
st::settingsCheckbox),
|
|
st::settingsSendTypePadding
|
|
)->checkedChanges(
|
|
) | rpl::start_with_next([=](bool checked) {
|
|
Auth().settings().setSupportTemplatesAutocomplete(checked);
|
|
Local::writeUserSettings();
|
|
}, inner->lifetime());
|
|
|
|
AddSkip(inner, st::settingsCheckboxesSkip);
|
|
|
|
AddSubsectionTitle(inner, rpl::single(qsl("Load chats for a period")));
|
|
|
|
SetupSupportChatsLimitSlice(inner);
|
|
|
|
AddSkip(inner, st::settingsCheckboxesSkip);
|
|
|
|
AddSkip(inner);
|
|
}
|
|
|
|
Chat::Chat(QWidget *parent, not_null<UserData*> self)
|
|
: Section(parent)
|
|
, _self(self) {
|
|
setupContent();
|
|
}
|
|
|
|
void Chat::setupContent() {
|
|
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
|
|
|
|
SetupThemeOptions(content);
|
|
SetupChatBackground(content);
|
|
SetupStickersEmoji(content);
|
|
SetupMessages(content);
|
|
|
|
Ui::ResizeFitChild(this, content);
|
|
}
|
|
|
|
} // namespace Settings
|