From 069232ec1b75a38c3e1a7dbb134e2e7661b37631 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 31 Aug 2018 16:01:42 +0300 Subject: [PATCH] Allow changing limits for cache in Settings. --- Telegram/Resources/langs/lang.strings | 7 + Telegram/SourceFiles/boxes/boxes.style | 10 +- .../SourceFiles/boxes/local_storage_box.cpp | 203 ++++++++++++++++-- .../SourceFiles/boxes/local_storage_box.h | 26 ++- Telegram/SourceFiles/export/view/export.style | 10 +- .../export/view/export_view_settings.cpp | 27 +-- .../media/player/media_player.style | 12 +- Telegram/SourceFiles/storage/localstorage.cpp | 49 ++++- Telegram/SourceFiles/storage/localstorage.h | 3 +- .../ui/widgets/continuous_sliders.h | 37 ++++ Telegram/SourceFiles/ui/widgets/widgets.style | 12 ++ 11 files changed, 334 insertions(+), 62 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4b6463f7b..645c6a54a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -366,6 +366,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_local_storage_round#other" = "{count} video messages"; "lng_local_storage_animation#one" = "{count} animation"; "lng_local_storage_animation#other" = "{count} animations"; +"lng_local_storage_size_limit" = "Total size limit: {size}"; +"lng_local_storage_time_limit" = "Clear files older than: {limit}"; +"lng_local_storage_limit_weeks#one" = "{count} week"; +"lng_local_storage_limit_weeks#other" = "{count} weeks"; +"lng_local_storage_limit_months#one" = "{count} month"; +"lng_local_storage_limit_months#other" = "{count} months"; +"lng_local_storage_limit_never" = "Never"; "lng_local_storage_summary" = "Summary"; "lng_local_storage_clear_some" = "Clear"; "lng_local_storage_clear" = "Clear all"; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index e10dc851a..db0bc1b3e 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -355,7 +355,7 @@ peerListBox: PeerList(defaultPeerList) { } localStorageRowHeight: 50px; -localStorageRowPadding: margins(23px, 5px, 23px, 5px); +localStorageRowPadding: margins(23px, 5px, 20px, 5px); localStorageRowTitle: FlatLabel(defaultFlatLabel) { textFg: windowBoldFg; maxHeight: 20px; @@ -375,6 +375,14 @@ localStorageRowSize: FlatLabel(defaultFlatLabel) { } } localStorageClear: defaultBoxButton; +localStorageLimitLabel: LabelSimple(defaultLabelSimple) { + font: boxTextFont; +} +localStorageLimitLabelMargin: margins(23px, 10px, 20px, 5px); +localStorageLimitSlider: MediaSlider(defaultContinuousSlider) { + seekSize: size(15px, 15px); +} +localStorageLimitMargin: margins(23px, 5px, 20px, 10px); shareRowsTop: 12px; shareRowHeight: 108px; diff --git a/Telegram/SourceFiles/boxes/local_storage_box.cpp b/Telegram/SourceFiles/boxes/local_storage_box.cpp index 2a7b6fb02..a88b79346 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.cpp +++ b/Telegram/SourceFiles/boxes/local_storage_box.cpp @@ -12,14 +12,97 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" -#include "ui/effects/radial_animation.h" #include "ui/widgets/shadow.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/effects/radial_animation.h" #include "storage/localstorage.h" +#include "storage/cache/storage_cache_database.h" +#include "data/data_session.h" #include "lang/lang_keys.h" #include "mainwindow.h" #include "auth_session.h" #include "layout.h" +namespace { + +constexpr auto kSizeLimitsCount = 20; +constexpr auto kTimeLimitsCount = 16; +constexpr auto kMaxTimeLimitValue = std::numeric_limits::max(); + +int64 SizeLimitInMB(int index) { + if (index < 10) { + return int64(index + 1) * 100; + } + return int64(index - 9) * 1024; +} + +int64 SizeLimit(int index) { + return SizeLimitInMB(index) * 1024 * 1024; +} + +QString SizeLimitText(int64 limit) { + const auto mb = (limit / (1024 * 1024)); + const auto gb = (mb / 1024); + return (gb > 0) + ? (QString::number(gb) + " GB") + : (QString::number(mb) + " MB"); +} + +size_type TimeLimitInDays(int index) { + if (index < 3) { + const auto weeks = (index + 1); + return size_type(weeks) * 7; + } else if (index < 15) { + const auto month = (index - 2); + return (size_type(month) * 30) + + ((month >= 12) ? 5 : + (month >= 10) ? 4 : + (month >= 8) ? 3 : + (month >= 7) ? 2 : + (month >= 5) ? 1 : + (month >= 3) ? 0 : + (month >= 2) ? -1 : + (month >= 1) ? 1 : 0); + //+ (month >= 1 ? 1 : 0) + //- (month >= 2 ? 2 : 0) + //+ (month >= 3 ? 1 : 0) + //+ (month >= 5 ? 1 : 0) + //+ (month >= 7 ? 1 : 0) + //+ (month >= 8 ? 1 : 0) + //+ (month >= 10 ? 1 : 0) + //+ (month >= 12 ? 1 : 0); + } + return 0; +} + +size_type TimeLimit(int index) { + const auto days = TimeLimitInDays(index); + return days + ? (days * 24 * 60 * 60) + : kMaxTimeLimitValue; +} + +QString TimeLimitText(size_type limit) { + const auto days = (limit / (24 * 60 * 60)); + const auto weeks = (days / 7); + const auto months = (days / 29); + return (months > 0) + ? lng_local_storage_limit_months(lt_count, months) + : (limit > 0) + ? lng_local_storage_limit_weeks(lt_count, weeks) + : lang(lng_local_storage_limit_never); +} + +size_type LimitToValue(size_type timeLimit) { + return timeLimit ? timeLimit : kMaxTimeLimitValue; +} + +size_type ValueToLimit(size_type timeLimit) { + return (timeLimit != kMaxTimeLimitValue) ? timeLimit : 0; +} + +} // namespace + class LocalStorageBox::Row : public Ui::RpWidget { public: Row( @@ -134,7 +217,7 @@ int LocalStorageBox::Row::resizeGetHeight(int newWidth) { newWidth); } _clear->moveToRight( - st::boxButtonPadding.right(), + st::boxLayerButtonPadding.right(), (height - _clear->height()) / 2, newWidth); return height; @@ -174,6 +257,9 @@ LocalStorageBox::LocalStorageBox( not_null db, CreateTag) : _db(db) { + const auto &settings = Local::cacheSettings(); + _sizeLimit = settings.totalSizeLimit; + _timeLimit = settings.totalTimeLimit; } void LocalStorageBox::Show(not_null db) { @@ -234,17 +320,17 @@ void LocalStorageBox::clearByTag(uint8 tag) { } void LocalStorageBox::setupControls() { - _content.create(this); - + const auto container = setInnerWidget( + object_ptr(this)); const auto createRow = [&]( uint8 tag, Fn title, Fn clear, const Database::TaggedSummary &data) { - auto result = _content->add(object_ptr>( - _content, + auto result = container->add(object_ptr>( + container, object_ptr( - _content, + container, std::move(title), std::move(clear), data))); @@ -280,11 +366,11 @@ void LocalStorageBox::setupControls() { std::move(summaryTitle), langFactory(lng_local_storage_clear), _stats.full); - const auto shadow = _content->add(object_ptr>( - _content, - object_ptr(_content), - st::localStorageRowPadding) - ); + setupLimits(container); + const auto shadow = container->add(object_ptr>( + container, + object_ptr(container), + st::localStorageRowPadding)); createTagRow(Data::kImageCacheTag, lng_local_storage_image); createTagRow(Data::kStickerCacheTag, lng_local_storage_sticker); createTagRow(Data::kVoiceMessageCacheTag, lng_local_storage_voice); @@ -293,11 +379,98 @@ void LocalStorageBox::setupControls() { shadow->toggleOn( std::move(tracker).atLeastOneShownValue() ); - _content->resizeToWidth(st::boxWidth); - _content->heightValue( + container->resizeToWidth(st::boxWidth); + container->heightValue( ) | rpl::start_with_next([=](int height) { setDimensions(st::boxWidth, height); - }, _content->lifetime()); + }, container->lifetime()); +} + +template < + typename Value, + typename Convert, + typename Callback, + typename> +void LocalStorageBox::createLimitsSlider( + not_null container, + int valuesCount, + Convert &&convert, + Value currentValue, + Callback &&callback) { + const auto label = container->add( + object_ptr(container, st::localStorageLimitLabel), + st::localStorageLimitLabelMargin); + callback(label, currentValue); + const auto slider = container->add( + object_ptr(container, st::localStorageLimitSlider), + st::localStorageLimitMargin); + slider->resize(st::localStorageLimitSlider.seekSize); + slider->setPseudoDiscrete( + valuesCount, + std::forward(convert), + currentValue, + [=, callback = std::forward(callback)](Value value) { + callback(label, value); + }); +} + +void LocalStorageBox::setupLimits(not_null container) { + const auto shadow = container->add( + object_ptr(container), + st::localStorageRowPadding); + + createLimitsSlider( + container, + kSizeLimitsCount, + SizeLimit, + _sizeLimit, + [=](not_null label, int64 limit) { + const auto text = SizeLimitText(limit); + label->setText(lng_local_storage_size_limit(lt_size, text)); + _sizeLimit = limit; + limitsChanged(); + }); + + createLimitsSlider( + container, + kTimeLimitsCount, + TimeLimit, + LimitToValue(_timeLimit), + [=](not_null label, size_type limit) { + const auto text = TimeLimitText(ValueToLimit(limit)); + label->setText(lng_local_storage_time_limit(lt_limit, text)); + _timeLimit = limit; + limitsChanged(); + }); +} + +void LocalStorageBox::limitsChanged() { + const auto &settings = Local::cacheSettings(); + const auto changed = (settings.totalSizeLimit != _sizeLimit) + || (settings.totalTimeLimit != _timeLimit); + if (_limitsChanged != changed) { + _limitsChanged = changed; + clearButtons(); + if (_limitsChanged) { + addButton(langFactory(lng_settings_save), [=] { save(); }); + addButton(langFactory(lng_cancel), [=] { closeBox(); }); + } else { + addButton(langFactory(lng_box_ok), [=] { closeBox(); }); + } + } +} + +void LocalStorageBox::save() { + if (!_limitsChanged) { + closeBox(); + return; + } + auto update = Storage::Cache::Database::SettingsUpdate(); + update.totalSizeLimit = _sizeLimit; + update.totalTimeLimit = _timeLimit; + Local::updateCacheSettings(update); + Auth().data().cache().updateSettings(update); + closeBox(); } void LocalStorageBox::paintEvent(QPaintEvent *e) { diff --git a/Telegram/SourceFiles/boxes/local_storage_box.h b/Telegram/SourceFiles/boxes/local_storage_box.h index 8978ba828..e3c36377c 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.h +++ b/Telegram/SourceFiles/boxes/local_storage_box.h @@ -20,6 +20,7 @@ namespace Ui { class VerticalLayout; template class SlideWrap; +class LabelSimple; } // namespace Ui class LocalStorageBox : public BoxContent { @@ -47,11 +48,34 @@ private: not_null*> row, Database::TaggedSummary *data); void setupControls(); + void setupLimits(not_null container); + void limitsChanged(); + void save(); + + template < + typename Value, + typename Convert, + typename Callback, + typename = std::enable_if_t< + rpl::details::is_callable_plain_v< + Callback, + not_null, + Value> + && std::is_same_v()(1))>>> + void createLimitsSlider( + not_null container, + int valuesCount, + Convert &&convert, + Value currentValue, + Callback &&callback); not_null _db; Database::Stats _stats; - object_ptr _content = { nullptr }; base::flat_map*>> _rows; + int64 _sizeLimit = 0; + size_type _timeLimit = 0; + bool _limitsChanged = false; + }; diff --git a/Telegram/SourceFiles/export/view/export.style b/Telegram/SourceFiles/export/view/export.style index 1fd397ea1..33c5985c5 100644 --- a/Telegram/SourceFiles/export/view/export.style +++ b/Telegram/SourceFiles/export/view/export.style @@ -21,16 +21,8 @@ exportHeaderLabel: FlatLabel(boxTitle) { } } exportHeaderPadding: margins(22px, 20px, 22px, 9px); -exportFileSizeSlider: MediaSlider { - width: 3px; - activeFg: mediaPlayerActiveFg; - inactiveFg: mediaPlayerInactiveFg; - activeFgOver: mediaPlayerActiveFg; - inactiveFgOver: mediaPlayerInactiveFg; - activeFgDisabled: mediaPlayerInactiveFg; - inactiveFgDisabled: windowBg; +exportFileSizeSlider: MediaSlider(defaultContinuousSlider) { seekSize: size(15px, 15px); - duration: 150; } exportFileSizeLabel: LabelSimple(defaultLabelSimple) { font: boxTextFont; diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index 65bf79a1f..8cc6aa7aa 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -469,27 +469,19 @@ void SettingsWidget::addSizeSlider( object_ptr(container, st::exportFileSizeSlider), st::exportFileSizePadding); slider->resize(st::exportFileSizeSlider.seekSize); - slider->setAlwaysDisplayMarker(true); - slider->setDirection(Ui::ContinuousSlider::Direction::Horizontal); - for (auto i = 0; i != kSizeValueCount + 1; ++i) { - if (readData().media.sizeLimit <= SizeLimitByIndex(i)) { - slider->setValue(i / float64(kSizeValueCount)); - break; - } - } + slider->setPseudoDiscrete( + kSizeValueCount + 1, + SizeLimitByIndex, + readData().media.sizeLimit, + [=](int limit) { + changeData([&](Settings &data) { + data.media.sizeLimit = limit; + }); + }); const auto label = Ui::CreateChild( container.get(), st::exportFileSizeLabel); - slider->setAdjustCallback([=](float64 value) { - return std::round(value * kSizeValueCount) / kSizeValueCount; - }); - slider->setChangeProgressCallback([=](float64 value) { - const auto index = int(std::round(value * kSizeValueCount)); - changeData([&](Settings &data) { - data.media.sizeLimit = SizeLimitByIndex(index); - }); - }); value() | rpl::map([](const Settings &data) { return data.media.sizeLimit; }) | rpl::start_with_next([=](int sizeLimit) { @@ -511,7 +503,6 @@ void SettingsWidget::addSizeSlider( st::exportFileSizePadding.right(), geometry.y() - label->height() - st::exportFileSizeLabelBottom); }, label->lifetime()); - } void SettingsWidget::refreshButtons( diff --git a/Telegram/SourceFiles/media/player/media_player.style b/Telegram/SourceFiles/media/player/media_player.style index a3d57d19e..f012f67b7 100644 --- a/Telegram/SourceFiles/media/player/media_player.style +++ b/Telegram/SourceFiles/media/player/media_player.style @@ -193,17 +193,7 @@ mediaPlayerPanelPlaySkip: 7px; mediaPlayerPanelPlayTop: 58px; mediaPlayerPanelPlaybackTop: 32px; mediaPlayerPanelPlaybackPadding: 8px; -mediaPlayerPanelPlayback: MediaSlider { - width: 3px; - activeFg: mediaPlayerActiveFg; - inactiveFg: mediaPlayerInactiveFg; - activeFgOver: mediaPlayerActiveFg; - inactiveFgOver: mediaPlayerInactiveFg; - activeFgDisabled: mediaPlayerInactiveFg; - inactiveFgDisabled: windowBg; - seekSize: size(9px, 9px); - duration: 150; -} +mediaPlayerPanelPlayback: defaultContinuousSlider; mediaPlayerPanelVolumeTop: 65px; mediaPlayerPanelVolumeSkip: 3px; diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index 2ccee3932..55256da02 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -50,6 +50,7 @@ constexpr auto kSinglePeerTypeChannel = qint32(3); constexpr auto kSinglePeerTypeSelf = qint32(4); constexpr auto kSinglePeerTypeEmpty = qint32(0); +using Database = Storage::Cache::Database; using FileKey = quint64; constexpr char tdfMagic[] = { 'T', 'D', 'F', '$' }; @@ -590,6 +591,7 @@ enum { dbiTxtDomainString = 0x53, dbiThemeKey = 0x54, dbiTileBackground = 0x55, + dbiCacheSettings = 0x56, dbiEncryptedWithSalt = 333, dbiEncrypted = 444, @@ -647,6 +649,8 @@ bool _readingUserSettings = false; FileKey _userSettingsKey = 0; FileKey _recentHashtagsAndBotsKey = 0; bool _recentHashtagsAndBotsWereRead = false; +qint64 _cacheTotalSizeLimit = Database::Settings().totalSizeLimit; +qint32 _cacheTotalTimeLimit = Database::Settings().totalTimeLimit; FileKey _exportSettingsKey = 0; @@ -1010,6 +1014,20 @@ bool _readSetting(quint32 blockId, QDataStream &stream, int version, ReadSetting cSetUseExternalVideoPlayer(v == 1); } break; + case dbiCacheSettings: { + qint64 size; + qint32 time; + stream >> size >> time; + if (!_checkStreamStatus(stream) + || size <= Database::Settings().maxDataSize + || time < 0) { + return false; + } + + _cacheTotalSizeLimit = size; + _cacheTotalTimeLimit = time; + } + case dbiSoundNotify: { qint32 v; stream >> v; @@ -1905,6 +1923,7 @@ void _writeUserSettings() { size += sizeof(quint32) + 3 * sizeof(qint32); size += sizeof(quint32) + 2 * sizeof(qint32); size += sizeof(quint32) + 2 * sizeof(qint32); + size += sizeof(quint32) + sizeof(qint64) + sizeof(qint32); if (!Global::HiddenPinnedMessages().isEmpty()) { size += sizeof(quint32) + sizeof(qint32) + Global::HiddenPinnedMessages().size() * (sizeof(PeerId) + sizeof(MsgId)); } @@ -1940,6 +1959,7 @@ void _writeUserSettings() { data.stream << quint32(dbiModerateMode) << qint32(Global::ModerateModeEnabled() ? 1 : 0); data.stream << quint32(dbiAutoPlay) << qint32(cAutoPlayGif() ? 1 : 0); data.stream << quint32(dbiUseExternalVideoPlayer) << qint32(cUseExternalVideoPlayer()); + data.stream << quint32(dbiCacheSettings) << qint64(_cacheTotalSizeLimit) << qint32(_cacheTotalTimeLimit); if (!userData.isEmpty()) { data.stream << quint32(dbiAuthSessionSettings) << userData; } @@ -2608,6 +2628,8 @@ void reset() { Window::Theme::Background()->reset(); _userSettingsKey = _recentHashtagsAndBotsKey = _savedPeersKey = _exportSettingsKey = 0; _oldMapVersion = _oldSettingsVersion = 0; + _cacheTotalSizeLimit = Database::Settings().totalSizeLimit; + _cacheTotalTimeLimit = Database::Settings().totalTimeLimit; StoredAuthSessionCache.reset(); _mapChanged = true; _writeMap(WriteMapWhen::Now); @@ -2987,18 +3009,33 @@ QString cachePath() { return _userDbPath + "cache"; } -Storage::Cache::Database::Settings cacheSettings() { - auto result = Storage::Cache::Database::Settings(); - result.clearOnWrongKey = true; - return result; -} - Storage::EncryptionKey cacheKey() { Expects(LocalKey != nullptr); return Storage::EncryptionKey(bytes::make_vector(LocalKey->data())); } +Storage::Cache::Database::Settings cacheSettings() { + auto result = Storage::Cache::Database::Settings(); + result.clearOnWrongKey = true; + result.totalSizeLimit = _cacheTotalSizeLimit; + result.totalTimeLimit = _cacheTotalTimeLimit; + return result; +} + +void updateCacheSettings(Storage::Cache::Database::SettingsUpdate &update) { + Expects(update.totalSizeLimit > Database::Settings().maxDataSize); + Expects(update.totalTimeLimit >= 0); + + if (_cacheTotalSizeLimit == update.totalSizeLimit + && _cacheTotalTimeLimit == update.totalTimeLimit) { + return; + } + _cacheTotalSizeLimit = update.totalSizeLimit; + _cacheTotalTimeLimit = update.totalTimeLimit; + _writeUserSettings(); +} + class CountWaveformTask : public Task { public: CountWaveformTask(DocumentData *doc) diff --git a/Telegram/SourceFiles/storage/localstorage.h b/Telegram/SourceFiles/storage/localstorage.h index 03b291d9b..2f578e542 100644 --- a/Telegram/SourceFiles/storage/localstorage.h +++ b/Telegram/SourceFiles/storage/localstorage.h @@ -105,8 +105,9 @@ void writeFileLocation(MediaKey location, const FileLocation &local); FileLocation readFileLocation(MediaKey location, bool check = true); QString cachePath(); -Storage::Cache::Database::Settings cacheSettings(); Storage::EncryptionKey cacheKey(); +Storage::Cache::Database::Settings cacheSettings(); +void updateCacheSettings(Storage::Cache::Database::SettingsUpdate &update); void countVoiceWaveform(DocumentData *document); diff --git a/Telegram/SourceFiles/ui/widgets/continuous_sliders.h b/Telegram/SourceFiles/ui/widgets/continuous_sliders.h index f85b98807..ceb7eab17 100644 --- a/Telegram/SourceFiles/ui/widgets/continuous_sliders.h +++ b/Telegram/SourceFiles/ui/widgets/continuous_sliders.h @@ -130,6 +130,43 @@ public: } void disablePaint(bool disabled); + template < + typename Value, + typename Convert, + typename Callback, + typename = std::enable_if_t< + rpl::details::is_callable_plain_v + && std::is_same_v()(1))>>> + void setPseudoDiscrete( + int valuesCount, + Convert &&convert, + Value current, + Callback &&callback) { + Expects(valuesCount > 1); + + setAlwaysDisplayMarker(true); + setDirection(Ui::ContinuousSlider::Direction::Horizontal); + + const auto sectionsCount = (valuesCount - 1); + for (auto index = index_type(); index != valuesCount; ++index) { + if (current <= convert(index)) { + setValue(index / float64(sectionsCount)); + break; + } + } + setAdjustCallback([=](float64 value) { + return std::round(value * sectionsCount) / sectionsCount; + }); + setChangeProgressCallback([ + =, + convert = std::forward(convert), + callback = std::forward(callback) + ](float64 value) { + const auto index = int(std::round(value * sectionsCount)); + callback(convert(index)); + }); + } + protected: void paintEvent(QPaintEvent *e) override; diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index a299d55c5..5c1dd395d 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -888,6 +888,18 @@ defaultPanelAnimation: PanelAnimation { shadow: defaultRoundShadow; } +defaultContinuousSlider: MediaSlider { + width: 3px; + activeFg: mediaPlayerActiveFg; + inactiveFg: mediaPlayerInactiveFg; + activeFgOver: mediaPlayerActiveFg; + inactiveFgOver: mediaPlayerInactiveFg; + activeFgDisabled: mediaPlayerInactiveFg; + inactiveFgDisabled: windowBg; + seekSize: size(9px, 9px); + duration: 150; +} + defaultRoundCheckbox: RoundCheckbox { border: windowBg; bgActive: windowBgActive;