diff --git a/Telegram/Resources/art/sprite.png b/Telegram/Resources/art/sprite.png index fb2205f25..8ff946cf2 100644 Binary files a/Telegram/Resources/art/sprite.png and b/Telegram/Resources/art/sprite.png differ diff --git a/Telegram/Resources/art/sprite_200x.png b/Telegram/Resources/art/sprite_200x.png index be5056db6..bdb2af1b9 100644 Binary files a/Telegram/Resources/art/sprite_200x.png and b/Telegram/Resources/art/sprite_200x.png differ diff --git a/Telegram/Resources/basic.style b/Telegram/Resources/basic.style index f2c34e362..a9fa919e9 100644 --- a/Telegram/Resources/basic.style +++ b/Telegram/Resources/basic.style @@ -193,6 +193,7 @@ defaultInputArea: InputArea { heightMax: 128px; } defaultInputField: InputField { + textBg: white; textFg: black; textMargins: margins(0px, 6px, 0px, 4px); textAlign: align(topleft); @@ -216,15 +217,6 @@ defaultInputField: InputField { height: 32px; } -dialogsSearchField: InputField(defaultInputField) { - textMargins: margins(34px, 7px, 34px, 7px); - - iconSprite: sprite(227px, 21px, 24px, 24px); - iconPosition: point(6px, 5px); - - width: 240px; - height: 34px; -} defaultCheckbox: Checkbox { textFg: black; textBg: white; @@ -746,12 +738,11 @@ dlgFilter: flatInput(inpDefGray) { bgColor: #f2f2f2; phColor: #949494; phFocusColor: #a4a4a4; - imgRect: sprite(227px, 21px, 24px, 24px); + icon: icon {{ "box_search_icon", #aaaaaa, point(10px, 9px) }}; width: 240px; height: 34px; textMrg: margins(34px, 2px, 34px, 4px); - imgPos: point(6px, 5px); } topBarHeight: 54px; diff --git a/Telegram/Resources/basic_types.style b/Telegram/Resources/basic_types.style index 73918d110..f3baf7e69 100644 --- a/Telegram/Resources/basic_types.style +++ b/Telegram/Resources/basic_types.style @@ -130,8 +130,7 @@ flatInput { font: font; cursor: cursor; - imgRect: sprite; - imgPos: point; + icon: icon; borderWidth: pixels; borderColor: color; @@ -394,6 +393,7 @@ InputArea { } InputField { + textBg: color; textFg: color; textMargins: margins; textAlign: align; @@ -418,9 +418,6 @@ InputField { width: pixels; height: pixels; - - iconSprite: sprite; - iconPosition: point; } PeerAvatarButton { diff --git a/Telegram/Resources/icons/box_search_icon.png b/Telegram/Resources/icons/box_search_icon.png new file mode 100644 index 000000000..047352607 Binary files /dev/null and b/Telegram/Resources/icons/box_search_icon.png differ diff --git a/Telegram/Resources/icons/box_search_icon@2x.png b/Telegram/Resources/icons/box_search_icon@2x.png new file mode 100644 index 000000000..4d2c66f86 Binary files /dev/null and b/Telegram/Resources/icons/box_search_icon@2x.png differ diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 3bdfe8a83..47d4a5737 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -69,20 +69,19 @@ aboutRevokePublicLabel: flatLabel(labelDefFlat) { } boxSearchField: InputField(defaultInputField) { - textMargins: margins(41px, 16px, 41px, 0px); + textBg: transparent; + textMargins: margins(2px, 7px, 2px, 0px); placeholderFg: #999; placeholderFgActive: #aaa; - placeholderMargins: margins(4px, 0px, 4px, 0px); + placeholderMargins: margins(2px, 0px, 2px, 0px); + duration: 150; border: 0px; borderActive: 0px; borderError: 0px; - height: 48px; - - iconSprite: sprite(227px, 21px, 24px, 24px); - iconPosition: point(15px, 14px); + height: 32px; font: normalFont; } @@ -95,15 +94,46 @@ boxSearchCancel: IconButton { icon: icon {{ "box_search_cancel", #000000 }}; iconPosition: point(8px, 18px); - downIconPosition: point(8px, 18px); + downIconPosition: point(8px, 19px); duration: 150; } contactsMultiSelect: MultiSelect { - field: boxSearchField; - cancel: boxSearchCancel; + padding: margins(8px, 8px, 8px, 8px); maxHeight: 104px; + scroll: flatScroll(solidScroll) { + deltat: 3px; + deltab: 3px; + round: 1px; + width: 8px; + deltax: 3px; + hiding: 1000; + } + + item: MultiSelectItem { + padding: margins(6px, 7px, 12px, 0px); + maxWidth: 128px; + height: 32px; + font: normalFont; + textBg: contactsBgOver; + textFg: windowTextFg; + textActiveBg: titleBg; + textActiveFg: white; + deleteFg: white; + deleteLeft: 9px; + deleteStroke: 3px; + duration: 150; + minScale: 0.3; + } + itemSkip: 8px; + + field: boxSearchField; + fieldIcon: icon {{ "box_search_icon", #aaaaaa, point(11px, 9px) }}; + fieldIconSkip: 36px; + fieldCancel: boxSearchCancel; + fieldCancelSkip: 34px; + fieldMinWidth: 42px; } contactsPhotoCheckbox: RoundImageCheckbox { imageRadius: 21px; diff --git a/Telegram/SourceFiles/boxes/contactsbox.cpp b/Telegram/SourceFiles/boxes/contactsbox.cpp index ad98a8315..cbf186b8f 100644 --- a/Telegram/SourceFiles/boxes/contactsbox.cpp +++ b/Telegram/SourceFiles/boxes/contactsbox.cpp @@ -23,6 +23,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "dialogs/dialogs_indexed_list.h" #include "styles/style_dialogs.h" +#include "styles/style_history.h" #include "styles/style_boxes.h" #include "lang.h" #include "boxes/addcontactbox.h" @@ -97,10 +98,18 @@ ContactsBox::ContactsBox(UserData *bot) : ItemListBox(st::contactsScroll) } void ContactsBox::init() { - ItemListBox::init(_inner); + _select->resizeToWidth(st::boxWideWidth); + + auto inviting = (_inner->creating() == CreatingGroupGroup) || (_inner->channel() && _inner->membersFilter() == MembersFilter::Recent) || _inner->chat(); + auto topSkip = st::boxTitleHeight + _select->height(); + auto bottomSkip = inviting ? (st::boxButtonPadding.top() + _next.height() + st::boxButtonPadding.bottom()) : st::boxScrollSkip; + ItemListBox::init(_inner, bottomSkip, topSkip); - connect(_inner, SIGNAL(chosenChanged()), this, SLOT(onChosenChanged())); connect(_inner, SIGNAL(addRequested()), App::wnd(), SLOT(onShowAddContact())); + _inner->setPeerSelectedChangedCallback([this](PeerData *peer, bool checked) { + onPeerSelectedChanged(peer, checked); + }); + if (_inner->channel() && _inner->membersFilter() == MembersFilter::Admins) { _next.hide(); _cancel.hide(); @@ -120,7 +129,14 @@ void ContactsBox::init() { connect(&_cancel, SIGNAL(clicked()), this, SLOT(onClose())); connect(scrollArea(), SIGNAL(scrolled()), this, SLOT(onScroll())); _select->setQueryChangedCallback([this](const QString &query) { onFilterUpdate(query); }); + _select->setItemRemovedCallback([this](uint64 itemId) { + if (auto peer = App::peerLoaded(itemId)) { + _inner->peerUnselected(peer); + update(); + } + }); _select->setSubmittedCallback([this](bool) { onSubmit(); }); + _select->setResizedCallback([this] { updateScrollSkips(); }); connect(_inner, SIGNAL(mustScrollTo(int, int)), scrollArea(), SLOT(scrollToY(int, int))); connect(_inner, SIGNAL(searchByUsername()), this, SLOT(onNeedSearchByUsername())); connect(_inner, SIGNAL(adminAdded()), this, SIGNAL(adminAdded())); @@ -184,7 +200,7 @@ void ContactsBox::peopleReceived(const MTPcontacts_Found &result, mtpRequestId r } _peopleRequest = 0; - _inner->updateSel(); + _inner->updateSelection(); onScroll(); } } @@ -266,21 +282,31 @@ void ContactsBox::paintEvent(QPaintEvent *e) { } } +void ContactsBox::updateScrollSkips() { + auto oldScrollHeight = scrollArea()->height(); + auto inviting = (_inner->creating() == CreatingGroupGroup) || (_inner->channel() && _inner->membersFilter() == MembersFilter::Recent) || _inner->chat(); + auto topSkip = st::boxTitleHeight + _select->height(); + auto bottomSkip = inviting ? (st::boxButtonPadding.top() + _next.height() + st::boxButtonPadding.bottom()) : st::boxScrollSkip; + setScrollSkips(bottomSkip, topSkip); + auto scrollHeightDelta = scrollArea()->height() - oldScrollHeight; + if (scrollHeightDelta) { + scrollArea()->scrollToY(scrollArea()->scrollTop() - scrollHeightDelta); + } + + _topShadow.setGeometry(0, st::boxTitleHeight + _select->height(), width(), st::lineWidth); +} + void ContactsBox::resizeEvent(QResizeEvent *e) { ItemListBox::resizeEvent(e); _select->resizeToWidth(width()); _select->moveToLeft(0, st::boxTitleHeight); - auto inviting = (_inner->creating() == CreatingGroupGroup) || (_inner->channel() && _inner->membersFilter() == MembersFilter::Recent) || _inner->chat(); - auto topSkip = st::boxTitleHeight + _select->height(); - auto bottomSkip = inviting ? (st::boxButtonPadding.top() + _next.height() + st::boxButtonPadding.bottom()) : st::boxScrollSkip; - setScrollSkips(bottomSkip, topSkip); + updateScrollSkips(); _inner->resize(width(), _inner->height()); _next.moveToRight(st::boxButtonPadding.right(), height() - st::boxButtonPadding.bottom() - _next.height()); _cancel.moveToRight(st::boxButtonPadding.right() + _next.width() + st::boxButtonPadding.left(), _next.y()); - _topShadow.setGeometry(0, st::boxTitleHeight + _select->height(), width(), st::lineWidth); if (_bottomShadow) _bottomShadow->setGeometry(0, height() - st::boxButtonPadding.bottom() - _next.height() - st::boxButtonPadding.top() - st::lineWidth, width(), st::lineWidth); } @@ -295,7 +321,25 @@ void ContactsBox::onFilterUpdate(const QString &filter) { _inner->updateFilter(filter); } -void ContactsBox::onChosenChanged() { +void ContactsBox::onPeerSelectedChanged(PeerData *peer, bool checked) { + if (checked) { + auto getColor = [peer]() -> const style::color &{ + switch (peer->colorIndex) { + case 1: return st::historyPeer2UserpicFg; + case 2: return st::historyPeer3UserpicFg; + case 3: return st::historyPeer4UserpicFg; + case 4: return st::historyPeer5UserpicFg; + case 5: return st::historyPeer6UserpicFg; + case 6: return st::historyPeer7UserpicFg; + case 7: return st::historyPeer8UserpicFg; + default: return st::historyPeer1UserpicFg; + } + }; + _select->addItem(peer->id, peer->shortName(), getColor(), PaintUserpicCallback(peer)); + _select->clearQuery(); + } else { + _select->removeItem(peer->id); + } update(); } @@ -482,12 +526,8 @@ bool ContactsBox::creationFail(const RPCError &error) { return false; } -ContactsBox::Inner::ContactData::ContactData() : name(st::boxWideWidth) { -} - -ContactsBox::Inner::ContactData::ContactData(PeerData *peer, Ui::RoundImageCheckbox::UpdateCallback &&updateCallback) -: checkbox(std_::make_unique(st::contactsPhotoCheckbox, std_::move(updateCallback), PaintUserpicCallback(peer))) -, name(st::boxWideWidth) { +ContactsBox::Inner::ContactData::ContactData(PeerData *peer, base::lambda_wrap updateCallback) +: checkbox(std_::make_unique(st::contactsPhotoCheckbox, std_::move(updateCallback), PaintUserpicCallback(peer))) { } ContactsBox::Inner::Inner(QWidget *parent, CreatingGroupType creating) : ScrolledWidget(parent) @@ -613,7 +653,7 @@ void ContactsBox::Inner::initList() { if (i.key()->id == peerFromUser(_chat->creator)) continue; if (!_allAdmins.checked() && _chat->admins.contains(i.key())) { admins.push_back(i.key()); - _checkedContacts.insert(i.key(), true); + _checkedContacts.insert(i.key()); } else { others.push_back(i.key()); } @@ -1186,13 +1226,13 @@ void ContactsBox::Inner::leaveEvent(QEvent *e) { void ContactsBox::Inner::mouseMoveEvent(QMouseEvent *e) { _mouseSel = true; _lastMousePos = e->globalPos(); - updateSel(); + updateSelection(); } void ContactsBox::Inner::mousePressEvent(QMouseEvent *e) { _mouseSel = true; _lastMousePos = e->globalPos(); - updateSel(); + updateSelection(); if (e->button() == Qt::LeftButton) { chooseParticipant(); } @@ -1205,29 +1245,35 @@ void ContactsBox::Inner::chooseParticipant() { _time = unixtime(); if (_filter.isEmpty()) { if (_byUsernameSel >= 0 && _byUsernameSel < _byUsername.size()) { - if (d_byUsername[_byUsernameSel]->disabledChecked) return; - changeCheckState(d_byUsername[_byUsernameSel], _byUsername[_byUsernameSel]); - } else { - if (!_sel || contactData(_sel)->disabledChecked) return; + auto data = d_byUsername[_byUsernameSel]; + auto peer = _byUsername[_byUsernameSel]; + if (data->disabledChecked) return; + + changeCheckState(data, peer); + } else if (_sel) { + auto data = contactData(_sel); + auto peer = _sel->history()->peer; + if (data->disabledChecked) return; + changeCheckState(_sel); } } else { if (_byUsernameSel >= 0 && _byUsernameSel < _byUsernameFiltered.size()) { - if (d_byUsernameFiltered[_byUsernameSel]->disabledChecked) return; - changeCheckState(d_byUsernameFiltered[_byUsernameSel], _byUsernameFiltered[_byUsernameSel]); + auto data = d_byUsernameFiltered[_byUsernameSel]; + auto peer = _byUsernameFiltered[_byUsernameSel]; + if (data->disabledChecked) return; - ContactData *moving = d_byUsernameFiltered[_byUsernameSel]; - int32 i = 0, l = d_byUsername.size(); + int i = 0, l = d_byUsername.size(); for (; i < l; ++i) { - if (d_byUsername[i] == moving) { + if (d_byUsername[i] == data) { break; } } if (i == l) { - d_byUsername.push_back(moving); - _byUsername.push_back(_byUsernameFiltered[_byUsernameSel]); + d_byUsername.push_back(data); + _byUsername.push_back(peer); for (i = 0, l = _byUsernameDatas.size(); i < l;) { - if (_byUsernameDatas[i] == moving) { + if (_byUsernameDatas[i] == data) { _byUsernameDatas.removeAt(i); --l; } else { @@ -1235,9 +1281,14 @@ void ContactsBox::Inner::chooseParticipant() { } } } - } else { - if (_filteredSel < 0 || _filteredSel >= _filtered.size() || contactData(_filtered[_filteredSel])->disabledChecked) return; - changeCheckState(_filtered[_filteredSel]); + + changeCheckState(data, peer); + } else if (_filteredSel >= 0 && _filteredSel < _filtered.size()) { + auto data = contactData(_filtered[_filteredSel]); + auto peer = _filtered[_filteredSel]->history()->peer; + if (data->disabledChecked) return; + + changeCheckState(data, peer); } } } else { @@ -1304,25 +1355,43 @@ void ContactsBox::Inner::changeCheckState(Dialogs::Row *row) { void ContactsBox::Inner::changeCheckState(ContactData *data, PeerData *peer) { t_assert(usingMultiSelect()); - int32 cnt = _selCount; if (data->checkbox->checked()) { - data->checkbox->setChecked(false); - _checkedContacts.remove(peer); - --_selCount; + changePeerCheckState(data, peer, false); } else if (selectedCount() < ((_channel && _channel->isMegagroup()) ? Global::MegagroupSizeMax() : Global::ChatSizeMax())) { - data->checkbox->setChecked(true); - _checkedContacts.insert(peer, true); - ++_selCount; + changePeerCheckState(data, peer, true); } else if (_channel && !_channel->isMegagroup()) { Ui::showLayer(new MaxInviteBox(_channel->inviteLink()), KeepOtherLayers); } else if (!_channel && selectedCount() >= Global::ChatSizeMax() && selectedCount() < Global::MegagroupSizeMax()) { Ui::showLayer(new InformBox(lng_profile_add_more_after_upgrade(lt_count, Global::MegagroupSizeMax())), KeepOtherLayers); } - if (cnt != _selCount) emit chosenChanged(); +} + +void ContactsBox::Inner::peerUnselected(PeerData *peer) { + // If data is nullptr we simply won't do anything. + auto data = _contactsData.value(peer, nullptr); + changePeerCheckState(data, peer, false, ChangeStateWay::SkipCallback); +} + +void ContactsBox::Inner::setPeerSelectedChangedCallback(base::lambda_unique callback) { + _peerSelectedChangedCallback = std_::move(callback); +} + +void ContactsBox::Inner::changePeerCheckState(ContactData *data, PeerData *peer, bool checked, ChangeStateWay useCallback) { + if (data) { + data->checkbox->setChecked(checked); + } + if (checked) { + _checkedContacts.insert(peer); + } else { + _checkedContacts.remove(peer); + } + if (useCallback != ChangeStateWay::SkipCallback) { + _peerSelectedChangedCallback(peer, checked); + } } int32 ContactsBox::Inner::selectedCount() const { - int32 result = _selCount; + auto result = _checkedContacts.size(); if (_chat) { result += qMax(_chat->count, 1); } else if (_channel) { @@ -1333,7 +1402,7 @@ int32 ContactsBox::Inner::selectedCount() const { return result; } -void ContactsBox::Inner::updateSel() { +void ContactsBox::Inner::updateSelection() { if (!_mouseSel) return; QPoint p(mapFromGlobal(_lastMousePos)); diff --git a/Telegram/SourceFiles/boxes/contactsbox.h b/Telegram/SourceFiles/boxes/contactsbox.h index 8b1f519d5..9685bdd20 100644 --- a/Telegram/SourceFiles/boxes/contactsbox.h +++ b/Telegram/SourceFiles/boxes/contactsbox.h @@ -56,8 +56,7 @@ public: signals: void adminAdded(); -public slots: - void onChosenChanged(); +private slots: void onScroll(); void onInvite(); @@ -80,7 +79,9 @@ protected: private: void init(); + void updateScrollSkips(); void onFilterUpdate(const QString &filter); + void onPeerSelectedChanged(PeerData *peer, bool checked); class Inner; ChildWidget _inner; @@ -136,10 +137,11 @@ public: Inner(QWidget *parent, ChatData *chat, MembersFilter membersFilter); Inner(QWidget *parent, UserData *bot); - void init(); - void initList(); + void setPeerSelectedChangedCallback(base::lambda_unique callback); + void peerUnselected(PeerData *peer); void updateFilter(QString filter = QString()); + void updateSelection(); void selectSkip(int32 dir); void selectSkipPage(int32 h, int32 dir); @@ -177,14 +179,12 @@ public: signals: void mustScrollTo(int ymin, int ymax); void searchByUsername(); - void chosenChanged(); void adminAdded(); void addRequested(); -public slots: +private slots: void onDialogRowReplaced(Dialogs::Row *oldRow, Dialogs::Row *newRow); - void updateSel(); void peerUpdated(PeerData *peer); void onPeerNameChanged(PeerData *peer, const PeerData::Names &oldNames, const PeerData::NameFirstChars &oldChars); @@ -204,8 +204,8 @@ protected: private: struct ContactData { - ContactData(); - ContactData(PeerData *peer, Ui::RoundImageCheckbox::UpdateCallback &&updateCallback); + ContactData() = default; + ContactData(PeerData *peer, base::lambda_wrap updateCallback); std_::unique_ptr checkbox; Text name; @@ -214,6 +214,9 @@ private: bool disabledChecked = false; }; + void init(); + void initList(); + void updateRowWithTop(int rowTop); int getSelectedRowTop() const; void updateSelectedRow(); @@ -227,6 +230,11 @@ private: void changeCheckState(Dialogs::Row *row); void changeCheckState(ContactData *data, PeerData *peer); + enum class ChangeStateWay { + Default, + SkipCallback, + }; + void changePeerCheckState(ContactData *data, PeerData *peer, bool checked, ChangeStateWay useCallback = ChangeStateWay::Default); template void addDialogsToList(FilterCallback callback); @@ -235,6 +243,8 @@ private: return (_chat != nullptr) || (_creating != CreatingGroupNone && (!_channel || _membersFilter != MembersFilter::Admins)); } + base::lambda_unique _peerSelectedChangedCallback; + int32 _rowHeight; int _newItemHeight = 0; bool _newItemSel = false; @@ -261,24 +271,22 @@ private: Dialogs::IndexedList *_contacts = nullptr; Dialogs::Row *_sel = nullptr; QString _filter; - typedef QVector FilteredDialogs; + using FilteredDialogs = QVector; FilteredDialogs _filtered; int _filteredSel = -1; bool _mouseSel = false; - int _selCount = 0; - - typedef QMap ContactsData; + using ContactsData = QMap; ContactsData _contactsData; - typedef QMap CheckedContacts; + using CheckedContacts = OrderedSet; CheckedContacts _checkedContacts; ContactData *contactData(Dialogs::Row *row); bool _searching = false; QString _lastQuery; - typedef QVector ByUsernameRows; - typedef QVector ByUsernameDatas; + using ByUsernameRows = QVector; + using ByUsernameDatas = QVector; ByUsernameRows _byUsername, _byUsernameFiltered; ByUsernameDatas d_byUsername, d_byUsernameFiltered; // filtered is partly subset of d_byUsername, partly subset of _byUsernameDatas ByUsernameDatas _byUsernameDatas; diff --git a/Telegram/SourceFiles/boxes/sharebox.cpp b/Telegram/SourceFiles/boxes/sharebox.cpp index bc8135fda..ca2323138 100644 --- a/Telegram/SourceFiles/boxes/sharebox.cpp +++ b/Telegram/SourceFiles/boxes/sharebox.cpp @@ -473,7 +473,7 @@ void ShareBox::Inner::paintChat(Painter &p, Chat *chat, int index) { chat->name.drawLeftElided(p, x + nameLeft, y + nameTop, nameWidth, outerWidth, 2, style::al_top, 0, -1, 0, true); } -ShareBox::Inner::Chat::Chat(PeerData *peer, Ui::RoundImageCheckbox::UpdateCallback &&updateCallback) +ShareBox::Inner::Chat::Chat(PeerData *peer, base::lambda_wrap updateCallback) : peer(peer) , checkbox(st::sharePhotoCheckbox, std_::move(updateCallback), PaintUserpicCallback(peer)) , name(st::sharePhotoCheckbox.imageRadius * 2) { diff --git a/Telegram/SourceFiles/boxes/sharebox.h b/Telegram/SourceFiles/boxes/sharebox.h index 2e4252fc2..96da0fb1c 100644 --- a/Telegram/SourceFiles/boxes/sharebox.h +++ b/Telegram/SourceFiles/boxes/sharebox.h @@ -153,7 +153,7 @@ private: int displayedChatsCount() const; struct Chat { - Chat(PeerData *peer, Ui::RoundImageCheckbox::UpdateCallback &&updateCallback); + Chat(PeerData *peer, base::lambda_wrap updateCallback); PeerData *peer; Ui::RoundImageCheckbox checkbox; diff --git a/Telegram/SourceFiles/core/lambda_wrap.h b/Telegram/SourceFiles/core/lambda_wrap.h index fc0ce2224..f75ceb5ef 100644 --- a/Telegram/SourceFiles/core/lambda_wrap.h +++ b/Telegram/SourceFiles/core/lambda_wrap.h @@ -359,6 +359,10 @@ public: } } + lambda_wrap clone() const { + return *this; + } + template > lambda_wrap(const Lambda &other) : Parent(&internal::lambda_wrap_helper_copy::instance, typename Parent::Private()) { internal::lambda_wrap_helper_copy::construct_copy_lambda_method(this->storage_, &other); diff --git a/Telegram/SourceFiles/ui/animation.h b/Telegram/SourceFiles/ui/animation.h index 1018053f2..97d4f58a6 100644 --- a/Telegram/SourceFiles/ui/animation.h +++ b/Telegram/SourceFiles/ui/animation.h @@ -102,6 +102,22 @@ namespace anim { float64 easeInQuint(const float64 &delta, const float64 &dt); float64 easeOutQuint(const float64 &delta, const float64 &dt); + template + float64 bumpy(const float64 &delta, const float64 &dt) { + struct Bumpy { + Bumpy() + : bump(BumpRatioNumerator / float64(BumpRatioDenominator)) + , dt0(bump - sqrt(bump * (bump - 1.))) + , k(1 / (2 * dt0 - 1)) { + } + float64 bump; + float64 dt0; + float64 k; + }; + static Bumpy data; + return delta * (data.bump - data.k * (dt - data.dt0) * (dt - data.dt0)); + } + class fvalue { // float animated value public: using ValueType = float64; @@ -499,7 +515,7 @@ public: template void start(Lambda &&updateCallback, const ValueType &from, const ValueType &to, float64 duration, anim::transition transition = anim::linear) { if (!_data) { - _data = std_::make_unique(from, std_::move(updateCallback)); + _data = std_::make_unique(from, std_::forward(updateCallback)); } _data->value.start(to); _data->duration = duration; @@ -522,6 +538,11 @@ private: , a_animation(animation(this, &Data::step)) , updateCallback(std_::move(updateCallback)) { } + Data(const ValueType &from, const base::lambda_wrap &updateCallback) + : value(from, from) + , a_animation(animation(this, &Data::step)) + , updateCallback(base::lambda_wrap(updateCallback)) { + } void step(float64 ms, bool timer) { auto dt = (ms >= duration) ? 1. : (ms / duration); if (dt >= 1) { diff --git a/Telegram/SourceFiles/ui/effects/round_image_checkbox.cpp b/Telegram/SourceFiles/ui/effects/round_image_checkbox.cpp index fbe75582d..de70ee3a7 100644 --- a/Telegram/SourceFiles/ui/effects/round_image_checkbox.cpp +++ b/Telegram/SourceFiles/ui/effects/round_image_checkbox.cpp @@ -56,25 +56,9 @@ void prepareCheckCaches(const style::RoundImageCheckbox *st, QPixmap &checkBgCac checkFullCache.setDevicePixelRatio(cRetinaFactor()); } -struct AnimBumpy { - AnimBumpy(float64 bump) : bump(bump) - , dt0(bump - sqrt(bump * (bump - 1.))) - , k(1 / (2 * dt0 - 1)) { - } - float64 bump; - float64 dt0; - float64 k; -}; - -template -float64 anim_bumpy(const float64 &delta, const float64 &dt) { - static AnimBumpy data = { BumpRatioPercent / 100. }; - return delta * (data.bump - data.k * (dt - data.dt0) * (dt - data.dt0)); -} - } // namespace -RoundImageCheckbox::RoundImageCheckbox(const style::RoundImageCheckbox &st, UpdateCallback &&updateCallback, PaintRoundImage &&paintRoundImage) +RoundImageCheckbox::RoundImageCheckbox(const style::RoundImageCheckbox &st, base::lambda_wrap updateCallback, PaintRoundImage paintRoundImage) : _st(st) , _updateCallback(std_::move(updateCallback)) , _paintRoundImage(std_::move(paintRoundImage)) { @@ -84,13 +68,14 @@ RoundImageCheckbox::RoundImageCheckbox(const style::RoundImageCheckbox &st, Upda void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) { auto selectionLevel = _selection.current(_checked ? 1. : 0.); if (_selection.animating()) { - p.setRenderHint(QPainter::SmoothPixmapTransform, true); auto userpicRadius = qRound(kWideScale * (_st.imageRadius + (_st.imageSmallRadius - _st.imageRadius) * selectionLevel)); auto userpicShift = kWideScale * _st.imageRadius - userpicRadius; auto userpicLeft = x - (kWideScale - 1) * _st.imageRadius + userpicShift; auto userpicTop = y - (kWideScale - 1) * _st.imageRadius + userpicShift; auto to = QRect(userpicLeft, userpicTop, userpicRadius * 2, userpicRadius * 2); auto from = QRect(QPoint(0, 0), _wideCache.size()); + + p.setRenderHint(QPainter::SmoothPixmapTransform, true); p.drawPixmapLeft(to, outerWidth, _wideCache, from); p.setRenderHint(QPainter::SmoothPixmapTransform, false); } else { @@ -159,7 +144,7 @@ void RoundImageCheckbox::setChecked(bool checked, SetStyle speed) { _checked = checked; if (_checked) { _icons.push_back(Icon()); - _icons.back().fadeIn.start(UpdateCallback(_updateCallback), 0, 1, _st.selectDuration); + _icons.back().fadeIn.start(_updateCallback, 0, 1, _st.selectDuration); if (speed != SetStyle::Animated) { _icons.back().fadeIn.finish(); } @@ -176,7 +161,7 @@ void RoundImageCheckbox::setChecked(bool checked, SetStyle speed) { } if (speed == SetStyle::Animated) { prepareWideCache(); - _selection.start(UpdateCallback(_updateCallback), _checked ? 0 : 1, _checked ? 1 : 0, _st.selectDuration, anim_bumpy<125>); + _selection.start(_updateCallback, _checked ? 0 : 1, _checked ? 1 : 0, _st.selectDuration, anim::bumpy<125, 100>); } else { _selection.finish(); } diff --git a/Telegram/SourceFiles/ui/effects/round_image_checkbox.h b/Telegram/SourceFiles/ui/effects/round_image_checkbox.h index 972143625..c94a3c78d 100644 --- a/Telegram/SourceFiles/ui/effects/round_image_checkbox.h +++ b/Telegram/SourceFiles/ui/effects/round_image_checkbox.h @@ -27,8 +27,7 @@ namespace Ui { class RoundImageCheckbox { public: using PaintRoundImage = base::lambda_unique; - using UpdateCallback = base::lambda_wrap; - RoundImageCheckbox(const style::RoundImageCheckbox &st, UpdateCallback &&updateCallback, PaintRoundImage &&paintRoundImage); + RoundImageCheckbox(const style::RoundImageCheckbox &st, base::lambda_wrap updateCallback, PaintRoundImage paintRoundImage); void paint(Painter &p, int x, int y, int outerWidth); float64 checkedAnimationRatio() const; @@ -53,7 +52,7 @@ private: void prepareWideCheckIconCache(Icon *icon); const style::RoundImageCheckbox &_st; - UpdateCallback _updateCallback; + base::lambda_wrap _updateCallback; PaintRoundImage _paintRoundImage; bool _checked = false; diff --git a/Telegram/SourceFiles/ui/flatinput.cpp b/Telegram/SourceFiles/ui/flatinput.cpp index cc63eb4fe..9cb1f3aa2 100644 --- a/Telegram/SourceFiles/ui/flatinput.cpp +++ b/Telegram/SourceFiles/ui/flatinput.cpp @@ -184,8 +184,8 @@ void FlatInput::paintEvent(QPaintEvent *e) { p.drawRoundedRect(QRectF(0, 0, width(), height()).marginsRemoved(QMarginsF(_st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2.)), st::buttonRadius - (_st.borderWidth / 2.), st::buttonRadius - (_st.borderWidth / 2.)); p.setRenderHint(QPainter::HighQualityAntialiasing, false); - if (_st.imgRect.pxWidth()) { - p.drawSprite(_st.imgPos, _st.imgRect); + if (!_st.icon.empty()) { + _st.icon.paint(p, 0, 0, width()); } bool phDraw = _phVisible; @@ -683,10 +683,10 @@ void InputArea::checkContentHeight() { } } -InputArea::InputAreaInner::InputAreaInner(InputArea *parent) : QTextEdit(parent) { +InputArea::Inner::Inner(InputArea *parent) : QTextEdit(parent) { } -bool InputArea::InputAreaInner::viewportEvent(QEvent *e) { +bool InputArea::Inner::viewportEvent(QEvent *e) { if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) { QTouchEvent *ev = static_cast(e); if (ev->device()->type() == QTouchDevice::TouchScreen) { @@ -790,7 +790,7 @@ void InputArea::contextMenuEvent(QContextMenuEvent *e) { _inner.contextMenuEvent(e); } -void InputArea::InputAreaInner::focusInEvent(QFocusEvent *e) { +void InputArea::Inner::focusInEvent(QFocusEvent *e) { f()->focusInInner(); QTextEdit::focusInEvent(e); emit f()->focused(); @@ -807,7 +807,7 @@ void InputArea::focusInInner() { } } -void InputArea::InputAreaInner::focusOutEvent(QFocusEvent *e) { +void InputArea::Inner::focusOutEvent(QFocusEvent *e) { f()->focusOutInner(); QTextEdit::focusOutEvent(e); emit f()->blurred(); @@ -943,7 +943,7 @@ void InputArea::insertEmoji(EmojiPtr emoji, QTextCursor c) { c.insertText(objectReplacement, imageFormat); } -QVariant InputArea::InputAreaInner::loadResource(int type, const QUrl &name) { +QVariant InputArea::Inner::loadResource(int type, const QUrl &name) { QString imageName = name.toDisplayString(); if (imageName.startsWith(qstr("emoji://e."))) { if (EmojiPtr emoji = emojiFromUrl(imageName)) { @@ -1193,7 +1193,7 @@ void InputArea::updatePlaceholder() { } } -QMimeData *InputArea::InputAreaInner::createMimeDataFromSelection() const { +QMimeData *InputArea::Inner::createMimeDataFromSelection() const { QMimeData *result = new QMimeData(); QTextCursor c(textCursor()); int32 start = c.selectionStart(), end = c.selectionEnd(); @@ -1211,7 +1211,7 @@ void InputArea::setCtrlEnterSubmit(CtrlEnterSubmit ctrlEnterSubmit) { _ctrlEnterSubmit = ctrlEnterSubmit; } -void InputArea::InputAreaInner::keyPressEvent(QKeyEvent *e) { +void InputArea::Inner::keyPressEvent(QKeyEvent *e) { bool shift = e->modifiers().testFlag(Qt::ShiftModifier), alt = e->modifiers().testFlag(Qt::AltModifier); bool macmeta = (cPlatform() == dbipMac || cPlatform() == dbipMacOld) && e->modifiers().testFlag(Qt::ControlModifier) && !e->modifiers().testFlag(Qt::MetaModifier) && !e->modifiers().testFlag(Qt::AltModifier); bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier); @@ -1276,11 +1276,11 @@ void InputArea::InputAreaInner::keyPressEvent(QKeyEvent *e) { } } -void InputArea::InputAreaInner::paintEvent(QPaintEvent *e) { +void InputArea::Inner::paintEvent(QPaintEvent *e) { return QTextEdit::paintEvent(e); } -void InputArea::InputAreaInner::contextMenuEvent(QContextMenuEvent *e) { +void InputArea::Inner::contextMenuEvent(QContextMenuEvent *e) { if (QMenu *menu = createStandardContextMenu()) { (new PopupMenu(menu))->popup(e->globalPos()); } @@ -1338,7 +1338,9 @@ InputField::InputField(QWidget *parent, const style::InputField &st, const QStri _inner.setWordWrapMode(QTextOption::NoWrap); - setAttribute(Qt::WA_OpaquePaintEvent); + if (_st.textBg->c.alphaF() >= 1.) { + setAttribute(Qt::WA_OpaquePaintEvent); + } _inner.setFont(_st.font->f); _inner.setAlignment(_st.textAlign); @@ -1380,10 +1382,10 @@ void InputField::onTouchTimer() { _touchRightButton = true; } -InputField::InputFieldInner::InputFieldInner(InputField *parent) : QTextEdit(parent) { +InputField::Inner::Inner(InputField *parent) : QTextEdit(parent) { } -bool InputField::InputFieldInner::viewportEvent(QEvent *e) { +bool InputField::Inner::viewportEvent(QEvent *e) { if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) { QTouchEvent *ev = static_cast(e); if (ev->device()->type() == QTouchDevice::TouchScreen) { @@ -1437,7 +1439,9 @@ void InputField::paintEvent(QPaintEvent *e) { Painter p(this); QRect r(rect().intersected(e->rect())); - p.fillRect(r, st::white->b); + if (_st.textBg->c.alphaF() > 0.) { + p.fillRect(r, _st.textBg); + } if (_st.border) { p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg->b); } @@ -1446,9 +1450,6 @@ void InputField::paintEvent(QPaintEvent *e) { p.fillRect(0, height() - _st.borderActive, width(), _st.borderActive, a_borderFg.current()); p.setOpacity(1); } - if (_st.iconSprite.pxWidth()) { - p.drawSpriteLeft(_st.iconPosition, width(), _st.iconSprite); - } bool drawPlaceholder = _placeholderVisible; if (_a_placeholderShift.animating()) { @@ -1490,7 +1491,7 @@ void InputField::contextMenuEvent(QContextMenuEvent *e) { _inner.contextMenuEvent(e); } -void InputField::InputFieldInner::focusInEvent(QFocusEvent *e) { +void InputField::Inner::focusInEvent(QFocusEvent *e) { f()->focusInInner(); QTextEdit::focusInEvent(e); emit f()->focused(); @@ -1507,7 +1508,7 @@ void InputField::focusInInner() { } } -void InputField::InputFieldInner::focusOutEvent(QFocusEvent *e) { +void InputField::Inner::focusOutEvent(QFocusEvent *e) { f()->focusOutInner(); QTextEdit::focusOutEvent(e); emit f()->blurred(); @@ -1643,7 +1644,7 @@ void InputField::insertEmoji(EmojiPtr emoji, QTextCursor c) { c.insertText(objectReplacement, imageFormat); } -QVariant InputField::InputFieldInner::loadResource(int type, const QUrl &name) { +QVariant InputField::Inner::loadResource(int type, const QUrl &name) { QString imageName = name.toDisplayString(); if (imageName.startsWith(qstr("emoji://e."))) { if (EmojiPtr emoji = emojiFromUrl(imageName)) { @@ -1928,7 +1929,7 @@ void InputField::setPlaceholderHidden(bool forcePlaceholderHidden) { updatePlaceholder(); } -QMimeData *InputField::InputFieldInner::createMimeDataFromSelection() const { +QMimeData *InputField::Inner::createMimeDataFromSelection() const { QMimeData *result = new QMimeData(); QTextCursor c(textCursor()); int32 start = c.selectionStart(), end = c.selectionEnd(); @@ -1942,7 +1943,7 @@ void InputField::customUpDown(bool custom) { _customUpDown = custom; } -void InputField::InputFieldInner::keyPressEvent(QKeyEvent *e) { +void InputField::Inner::keyPressEvent(QKeyEvent *e) { bool shift = e->modifiers().testFlag(Qt::ShiftModifier), alt = e->modifiers().testFlag(Qt::AltModifier); bool macmeta = (cPlatform() == dbipMac || cPlatform() == dbipMacOld) && e->modifiers().testFlag(Qt::ControlModifier) && !e->modifiers().testFlag(Qt::MetaModifier) && !e->modifiers().testFlag(Qt::AltModifier); bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier), ctrlGood = true; @@ -1979,36 +1980,41 @@ void InputField::InputFieldInner::keyPressEvent(QKeyEvent *e) { } #endif // Q_OS_MAC } else { - QTextCursor tc(textCursor()); + auto oldCursorPosition = textCursor().position(); if (enter && ctrl) { e->setModifiers(e->modifiers() & ~Qt::ControlModifier); } QTextEdit::keyPressEvent(e); - if (tc == textCursor()) { + auto currentCursor = textCursor(); + if (textCursor().position() == oldCursorPosition) { bool check = false; if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) { - tc.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); + oldCursorPosition = currentCursor.position(); + currentCursor.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); check = true; } else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) { - tc.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); + oldCursorPosition = currentCursor.position(); + currentCursor.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); check = true; + } else if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Right || e->key() == Qt::Key_Backspace) { + e->ignore(); } if (check) { - if (tc == textCursor()) { + if (oldCursorPosition == currentCursor.position()) { e->ignore(); } else { - setTextCursor(tc); + setTextCursor(currentCursor); } } } } } -void InputField::InputFieldInner::paintEvent(QPaintEvent *e) { +void InputField::Inner::paintEvent(QPaintEvent *e) { return QTextEdit::paintEvent(e); } -void InputField::InputFieldInner::contextMenuEvent(QContextMenuEvent *e) { +void InputField::Inner::contextMenuEvent(QContextMenuEvent *e) { if (QMenu *menu = createStandardContextMenu()) { (new PopupMenu(menu))->popup(e->globalPos()); } @@ -2167,9 +2173,6 @@ void MaskedInputField::paintEvent(QPaintEvent *e) { p.fillRect(0, height() - _st.borderActive, width(), _st.borderActive, a_borderFg.current()); p.setOpacity(1); } - if (_st.iconSprite.pxWidth()) { - p.drawSpriteLeft(_st.iconPosition, width(), _st.iconSprite); - } p.setClipRect(r); paintPlaceholder(p); diff --git a/Telegram/SourceFiles/ui/flatinput.h b/Telegram/SourceFiles/ui/flatinput.h index 2535a019d..9b0ee960d 100644 --- a/Telegram/SourceFiles/ui/flatinput.h +++ b/Telegram/SourceFiles/ui/flatinput.h @@ -262,10 +262,9 @@ private: bool heightAutoupdated(); void checkContentHeight(); - friend class InputAreaInner; - class InputAreaInner : public QTextEdit { + class Inner : public QTextEdit { public: - InputAreaInner(InputArea *parent); + Inner(InputArea *parent); QVariant loadResource(int type, const QUrl &name) override; @@ -286,6 +285,7 @@ private: friend class InputArea; }; + friend class Inner; void focusInInner(); void focusOutInner(); @@ -294,7 +294,7 @@ private: void startBorderAnimation(); - InputAreaInner _inner; + Inner _inner; QString _oldtext; @@ -431,10 +431,9 @@ private: int32 _maxLength; bool _forcePlaceholderHidden = false; - friend class InputFieldInner; - class InputFieldInner : public QTextEdit { + class Inner : public QTextEdit { public: - InputFieldInner(InputField *parent); + Inner(InputField *parent); QVariant loadResource(int type, const QUrl &name) override; @@ -455,6 +454,7 @@ private: friend class InputField; }; + friend class Inner; void focusInInner(); void focusOutInner(); @@ -463,7 +463,7 @@ private: void startBorderAnimation(); - InputFieldInner _inner; + Inner _inner; QString _oldtext; diff --git a/Telegram/SourceFiles/ui/style/style_core_icon.h b/Telegram/SourceFiles/ui/style/style_core_icon.h index 822a491a9..7fb337a3f 100644 --- a/Telegram/SourceFiles/ui/style/style_core_icon.h +++ b/Telegram/SourceFiles/ui/style/style_core_icon.h @@ -105,6 +105,9 @@ public: return std_::make_unique(ColoredCopy { *this, colors }); } + bool empty() const { + return _parts.empty(); + } void paint(QPainter &p, const QPoint &pos, int outerw) const; void paint(QPainter &p, int x, int y, int outerw) const { paint(p, QPoint(x, y), outerw); diff --git a/Telegram/SourceFiles/ui/widgets/multi_select.cpp b/Telegram/SourceFiles/ui/widgets/multi_select.cpp index ab0888da7..373ad7e92 100644 --- a/Telegram/SourceFiles/ui/widgets/multi_select.cpp +++ b/Telegram/SourceFiles/ui/widgets/multi_select.cpp @@ -26,91 +26,476 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "lang.h" namespace Ui { +namespace { + +constexpr int kWideScale = 3; + +} // namespace + +class MultiSelect::Inner::Item { +public: + Item(const style::MultiSelectItem &st, uint64 id, const QString &text, const style::color &color, PaintRoundImage paintRoundImage); + + uint64 id() const { + return _id; + } + int getWidth() const { + return _width; + } + QRect rect() const { + return QRect(_x, _y, _width, _st.height); + } + bool isOverDelete() const { + return _overDelete; + } + void setActive(bool active) { + _active = active; + } + void setPosition(int x, int y, int outerWidth, int maxVisiblePadding); + QRect paintArea(int outerWidth) const; + + void setUpdateCallback(base::lambda_wrap updateCallback) { + _updateCallback = std_::move(updateCallback); + } + void setText(const QString &text); + void paint(Painter &p, int outerWidth, uint64 ms); + + void mouseMoveEvent(QPoint point); + void leaveEvent(); + + void showAnimated() { + setVisibleAnimated(true); + } + void hideAnimated() { + setVisibleAnimated(false); + } + bool hideFinished() const { + return (_hiding && !_visibility.animating()); + } + + +private: + void setOver(bool over); + void paintOnce(Painter &p, int x, int y, int outerWidth, uint64 ms); + void paintDeleteButton(Painter &p, int x, int y, int outerWidth, float64 overOpacity); + bool paintCached(Painter &p, int x, int y, int outerWidth); + void prepareCache(); + void setVisibleAnimated(bool visible); + + const style::MultiSelectItem &_st; + + uint64 _id; + struct SlideAnimation { + SlideAnimation(base::lambda_wrap updateCallback, int fromX, int toX, int y, float64 duration) + : fromX(fromX) + , toX(toX) + , y(y) { + x.start(std_::move(updateCallback), fromX, toX, duration); + } + IntAnimation x; + int fromX, toX; + int y; + }; + std_::vector_of_moveable _copies; + int _x = -1; + int _y = -1; + int _width = 0; + Text _text; + const style::color &_color; + bool _over = false; + QPixmap _cache; + FloatAnimation _visibility; + FloatAnimation _overOpacity; + bool _overDelete = false; + bool _active = false; + PaintRoundImage _paintRoundImage; + base::lambda_wrap _updateCallback; + bool _hiding = false; + +}; + +MultiSelect::Inner::Item::Item(const style::MultiSelectItem &st, uint64 id, const QString &text, const style::color &color, PaintRoundImage paintRoundImage) +: _st(st) +, _id(id) +, _color(color) +, _paintRoundImage(std_::move(paintRoundImage)) { + setText(text); +} + +void MultiSelect::Inner::Item::setText(const QString &text) { + _text.setText(_st.font, text, _textNameOptions); + _width = _st.height + _st.padding.left() + _text.maxWidth() + _st.padding.right(); + accumulate_min(_width, _st.maxWidth); +} + +void MultiSelect::Inner::Item::paint(Painter &p, int outerWidth, uint64 ms) { + if (!_cache.isNull() && !_visibility.animating(ms)) { + if (_hiding) { + return; + } else { + _cache = QPixmap(); + } + } + if (_copies.empty()) { + paintOnce(p, _x, _y, outerWidth, ms); + } else { + for (auto i = _copies.begin(), e = _copies.end(); i != e;) { + auto x = i->x.current(getms(), _x); + auto y = i->y; + auto animating = i->x.animating(); + if (animating || (y == _y)) { + paintOnce(p, x, y, outerWidth, ms); + } + if (animating) { + ++i; + } else { + i = _copies.erase(i); + e = _copies.end(); + } + } + } +} + +void MultiSelect::Inner::Item::paintOnce(Painter &p, int x, int y, int outerWidth, uint64 ms) { + if (!_cache.isNull()) { + paintCached(p, x, y, outerWidth); + return; + } + + auto radius = _st.height / 2; + auto inner = rtlrect(x + radius, y, _width - radius, _st.height, outerWidth); + + auto clipEnabled = p.hasClipping(); + auto clip = clipEnabled ? p.clipRegion() : QRegion(); + p.setRenderHint(QPainter::HighQualityAntialiasing); + p.setClipRect(inner); + + p.setPen(Qt::NoPen); + p.setBrush(_active ? _st.textActiveBg : _st.textBg); + p.drawRoundedRect(rtlrect(x, y, _width, _st.height, outerWidth), radius, radius); + + if (clipEnabled) { + p.setClipRegion(clip); + } else { + p.setClipping(false); + } + p.setRenderHint(QPainter::HighQualityAntialiasing, false); + + auto overOpacity = _overOpacity.current(ms, _over ? 1. : 0.); + if (overOpacity < 1.) { + _paintRoundImage(p, x, y, outerWidth, _st.height); + } + if (overOpacity > 0.) { + paintDeleteButton(p, x, y, outerWidth, overOpacity); + } + + auto textLeft = _st.height + _st.padding.left(); + auto textWidth = _width - textLeft - _st.padding.right(); + p.setPen(_active ? _st.textActiveFg : _st.textFg); + _text.drawLeftElided(p, x + textLeft, y + _st.padding.top(), textWidth, outerWidth); +} + +void MultiSelect::Inner::Item::paintDeleteButton(Painter &p, int x, int y, int outerWidth, float64 overOpacity) { + p.setOpacity(overOpacity); + p.setRenderHint(QPainter::HighQualityAntialiasing); + p.setPen(Qt::NoPen); + p.setBrush(_color); + p.drawEllipse(rtlrect(x, y, _st.height, _st.height, outerWidth)); + + auto deleteScale = overOpacity + _st.minScale * (1. - overOpacity); + auto deleteSkip = deleteScale * _st.deleteLeft + (1. - deleteScale) * (_st.height / 2); + auto sqrt2 = sqrt(2.); + auto deleteLeft = rtlpoint(x + deleteSkip, 0, outerWidth).x() + 0.; + auto deleteTop = y + deleteSkip + 0.; + auto deleteWidth = _st.height - 2 * deleteSkip; + auto deleteHeight = _st.height - 2 * deleteSkip; + auto deleteStroke = _st.deleteStroke / sqrt2; + QPointF pathDelete[] = { + { deleteLeft, deleteTop + deleteStroke }, + { deleteLeft + deleteStroke, deleteTop }, + { deleteLeft + (deleteWidth / 2.), deleteTop + (deleteHeight / 2.) - deleteStroke }, + { deleteLeft + deleteWidth - deleteStroke, deleteTop }, + { deleteLeft + deleteWidth, deleteTop + deleteStroke }, + { deleteLeft + (deleteWidth / 2.) + deleteStroke, deleteTop + (deleteHeight / 2.) }, + { deleteLeft + deleteWidth, deleteTop + deleteHeight - deleteStroke }, + { deleteLeft + deleteWidth - deleteStroke, deleteTop + deleteHeight }, + { deleteLeft + (deleteWidth / 2.), deleteTop + (deleteHeight / 2.) + deleteStroke }, + { deleteLeft + deleteStroke, deleteTop + deleteHeight }, + { deleteLeft, deleteTop + deleteHeight - deleteStroke }, + { deleteLeft + (deleteWidth / 2.) - deleteStroke, deleteTop + (deleteHeight / 2.) }, + }; + if (overOpacity < 1.) { + auto alpha = -(overOpacity - 1.) * M_PI_2; + auto cosalpha = cos(alpha); + auto sinalpha = sin(alpha); + auto shiftx = deleteLeft + (deleteWidth / 2.); + auto shifty = deleteTop + (deleteHeight / 2.); + for (auto &point : pathDelete) { + auto x = point.x() - shiftx; + auto y = point.y() - shifty; + point.setX(shiftx + x * cosalpha - y * sinalpha); + point.setY(shifty + y * cosalpha + x * sinalpha); + } + } + QPainterPath path; + path.moveTo(pathDelete[0]); + for (int i = 1; i != base::array_size(pathDelete); ++i) { + path.lineTo(pathDelete[i]); + } + p.fillPath(path, _st.deleteFg); + + p.setRenderHint(QPainter::HighQualityAntialiasing, false); + p.setOpacity(1.); +} + +bool MultiSelect::Inner::Item::paintCached(Painter &p, int x, int y, int outerWidth) { + auto opacity = _visibility.current(_hiding ? 0. : 1.); + auto scale = opacity + _st.minScale * (1. - opacity); + auto height = opacity * _cache.height() / _cache.devicePixelRatio(); + auto width = opacity * _cache.width() / _cache.devicePixelRatio(); + + p.setOpacity(opacity); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + p.drawPixmap(rtlrect(x + (_width - width) / 2., y + (_st.height - height) / 2., width, height, outerWidth), _cache); + p.setRenderHint(QPainter::SmoothPixmapTransform, false); + p.setOpacity(1.); + return true; +} + +void MultiSelect::Inner::Item::mouseMoveEvent(QPoint point) { + if (!_cache.isNull()) return; + _overDelete = QRect(0, 0, _st.height, _st.height).contains(point); + setOver(true); +} + +void MultiSelect::Inner::Item::leaveEvent() { + _overDelete = false; + setOver(false); +} + +void MultiSelect::Inner::Item::setPosition(int x, int y, int outerWidth, int maxVisiblePadding) { + if (_x >= 0 && _y >= 0 && (_x != x || _y != y)) { + // Make an animation if it is not the first setPosition(). + auto found = false; + auto leftHidden = -_width - maxVisiblePadding; + auto rightHidden = outerWidth + maxVisiblePadding; + for (auto i = _copies.begin(), e = _copies.end(); i != e;) { + if (i->x.animating()) { + if (i->y == y) { + i->x.start(_updateCallback, i->toX, x, _st.duration); + found = true; + } else { + i->x.start(_updateCallback, i->fromX, (i->toX > i->fromX) ? rightHidden : leftHidden, _st.duration); + } + ++i; + } else { + i = _copies.erase(i); + e = _copies.end(); + } + } + if (_copies.empty()) { + if (_y == y) { + auto copy = SlideAnimation(_updateCallback, _x, x, _y, _st.duration); + _copies.push_back(std_::move(copy)); + } else { + auto copyHiding = SlideAnimation(_updateCallback, _x, (y > _y) ? rightHidden : leftHidden, _y, _st.duration); + _copies.push_back(std_::move(copyHiding)); + auto copyShowing = SlideAnimation(_updateCallback, (y > _y) ? leftHidden : rightHidden, x, y, _st.duration); + _copies.push_back(std_::move(copyShowing)); + } + } else if (!found) { + auto copy = SlideAnimation(_updateCallback, (y > _y) ? leftHidden : rightHidden, x, y, _st.duration); + _copies.push_back(std_::move(copy)); + } + } + _x = x; + _y = y; +} + +QRect MultiSelect::Inner::Item::paintArea(int outerWidth) const { + if (_copies.empty()) { + return rect(); + } + auto yMin = 0, yMax = 0; + for_const (auto ©, _copies) { + accumulate_max(yMax, copy.y); + if (yMin) { + accumulate_min(yMin, copy.y); + } else { + yMin = copy.y; + } + } + return QRect(0, yMin, outerWidth, yMax - yMin + _st.height); +} + +void MultiSelect::Inner::Item::prepareCache() { + if (!_cache.isNull()) return; + + t_assert(!_visibility.animating()); + auto cacheWidth = _width * kWideScale * cIntRetinaFactor(); + auto cacheHeight = _st.height * kWideScale * cIntRetinaFactor(); + auto data = QImage(cacheWidth, cacheHeight, QImage::Format_ARGB32_Premultiplied); + data.fill(Qt::transparent); + data.setDevicePixelRatio(cRetinaFactor()); + { + Painter p(&data); + paintOnce(p, _width * (kWideScale - 1) / 2, _st.height * (kWideScale - 1) / 2, cacheWidth, getms()); + } + _cache = App::pixmapFromImageInPlace(std_::move(data)); +} + +void MultiSelect::Inner::Item::setVisibleAnimated(bool visible) { + _hiding = !visible; + prepareCache(); + auto from = visible ? 0. : 1.; + auto to = visible ? 1. : 0.; + auto transition = visible ? anim::bumpy<1125, 1000> : anim::linear; + _visibility.start(_updateCallback, from, to, _st.duration, transition); +} + +void MultiSelect::Inner::Item::setOver(bool over) { + if (over != _over) { + _over = over; + _overOpacity.start(_updateCallback, _over ? 0. : 1., _over ? 1. : 0., _st.duration); + } +} MultiSelect::MultiSelect(QWidget *parent, const style::MultiSelect &st, const QString &placeholder) : TWidget(parent) , _st(st) -, _scroll(this, st::boxScroll) -, _inner(this, st, placeholder) { +, _scroll(this, _st.scroll) +, _inner(this, st, placeholder, [this](int activeTop, int activeBottom) { scrollTo(activeTop, activeBottom); }) { _scroll->setOwnedWidget(_inner); + _scroll->installEventFilter(this); + _inner->setResizedCallback([this](int innerHeightDelta) { + auto newHeight = resizeGetHeight(width()); + if (innerHeightDelta > 0) { + _scroll->scrollToY(_scroll->scrollTop() + innerHeightDelta); + } + if (newHeight != height()) { + resize(width(), newHeight); + if (_resizedCallback) { + _resizedCallback(); + } + } + }); + _inner->setQueryChangedCallback([this](const QString &query) { + _scroll->scrollToY(_scroll->scrollTopMax()); + if (_queryChangedCallback) { + _queryChangedCallback(query); + } + }); + setAttribute(Qt::WA_OpaquePaintEvent); } +bool MultiSelect::eventFilter(QObject *o, QEvent *e) { + if (o == _scroll && e->type() == QEvent::KeyPress) { + e->ignore(); + return true; + } + return false; +} + +void MultiSelect::scrollTo(int activeTop, int activeBottom) { + auto scrollTop = _scroll->scrollTop(); + auto scrollHeight = _scroll->height(); + auto scrollBottom = scrollTop + scrollHeight; + if (scrollTop > activeTop) { + _scroll->scrollToY(activeTop); + } else if (scrollBottom < activeBottom) { + _scroll->scrollToY(activeBottom - scrollHeight); + } +} + void MultiSelect::setQueryChangedCallback(base::lambda_unique callback) { - _inner->setQueryChangedCallback(std_::move(callback)); + _queryChangedCallback = std_::move(callback); } void MultiSelect::setSubmittedCallback(base::lambda_unique callback) { _inner->setSubmittedCallback(std_::move(callback)); } +void MultiSelect::setResizedCallback(base::lambda_unique callback) { + _resizedCallback = std_::move(callback); +} + void MultiSelect::setInnerFocus() { if (_inner->setInnerFocus()) { _scroll->scrollToY(_scroll->scrollTopMax()); } } +void MultiSelect::clearQuery() { + _inner->clearQuery(); +} + QString MultiSelect::getQuery() const { return _inner->getQuery(); } -void MultiSelect::addItem(std_::unique_ptr item) { - _inner->addItem(std_::move(item)); +void MultiSelect::addItem(uint64 itemId, const QString &text, const style::color &color, PaintRoundImage paintRoundImage) { + _inner->addItem(std_::make_unique(_st.item, itemId, text, color, std_::move(paintRoundImage))); +} + +void MultiSelect::setItemRemovedCallback(base::lambda_unique callback) { + _inner->setItemRemovedCallback(std_::move(callback)); +} + +void MultiSelect::removeItem(uint64 itemId) { + _inner->removeItem(itemId); } int MultiSelect::resizeGetHeight(int newWidth) { - _inner->resizeToWidth(newWidth); + if (newWidth != _inner->width()) { + _inner->resizeToWidth(newWidth); + } auto newHeight = qMin(_inner->height(), _st.maxHeight); - _scroll->resize(newWidth, newHeight); + _scroll->setGeometryToLeft(0, 0, newWidth, newHeight); return newHeight; } -void MultiSelect::resizeEvent(QResizeEvent *e) { - _scroll->moveToLeft(0, 0); -} - -MultiSelect::Item::Item(uint64 id, const QString &text, const style::color &color) -: _id(id) { -} - -void MultiSelect::Item::setText(const QString &text) { - -} - -void MultiSelect::Item::paint(Painter &p, int x, int y) { - -} - -MultiSelect::Inner::Inner(QWidget *parent, const style::MultiSelect &st, const QString &placeholder) : ScrolledWidget(parent) +MultiSelect::Inner::Inner(QWidget *parent, const style::MultiSelect &st, const QString &placeholder, ScrollCallback callback) : ScrolledWidget(parent) , _st(st) -, _filter(this, _st.field, placeholder) -, _cancel(this, _st.cancel) { - connect(_filter, SIGNAL(changed()), this, SLOT(onQueryChanged())); - connect(_filter, SIGNAL(submitted(bool)), this, SLOT(onSubmitted(bool))); +, _scrollCallback(std_::move(callback)) +, _field(this, _st.field, placeholder) +, _cancel(this, _st.fieldCancel) { + _field->customUpDown(true); + connect(_field, SIGNAL(focused()), this, SLOT(onFieldFocused())); + connect(_field, SIGNAL(changed()), this, SLOT(onQueryChanged())); + connect(_field, SIGNAL(submitted(bool)), this, SLOT(onSubmitted(bool))); _cancel->hide(); _cancel->setClickedCallback([this] { - _filter->setText(QString()); - _filter->setFocus(); + clearQuery(); + _field->setFocus(); }); + setMouseTracking(true); } void MultiSelect::Inner::onQueryChanged() { auto query = getQuery(); _cancel->setVisible(!query.isEmpty()); + updateFieldGeometry(); if (_queryChangedCallback) { _queryChangedCallback(query); } } +QString MultiSelect::Inner::getQuery() const { + return _field->getLastText().trimmed(); +} + bool MultiSelect::Inner::setInnerFocus() { - if (!_filter->hasFocus()) { - _filter->setFocus(); + if (_active >= 0) { + setFocus(); + } else if (!_field->hasFocus()) { + _field->setFocus(); return true; } return false; } -QString MultiSelect::Inner::getQuery() const { - return _filter->getLastText().trimmed(); +void MultiSelect::Inner::clearQuery() { + _field->setText(QString()); } void MultiSelect::Inner::setQueryChangedCallback(base::lambda_unique callback) { @@ -121,41 +506,275 @@ void MultiSelect::Inner::setSubmittedCallback(base::lambda_uniqueresizeToWidth(newWidth); - return _filter->height(); +void MultiSelect::Inner::updateFieldGeometry() { + auto fieldFinalWidth = _fieldWidth; + if (!_cancel->isHidden()) { + fieldFinalWidth -= _st.fieldCancelSkip; + } + _field->resizeToWidth(fieldFinalWidth); + _field->moveToLeft(_st.padding.left() + _fieldLeft, _st.padding.top() + _fieldTop); } -void MultiSelect::Inner::resizeEvent(QResizeEvent *e) { - _filter->moveToLeft(0, 0); - _cancel->moveToRight(0, 0); +void MultiSelect::Inner::updateHasAnyItems(bool hasAnyItems) { + _field->setPlaceholderHidden(hasAnyItems); + updateCursor(); + _iconOpacity.start([this] { + rtlupdate(_st.padding.left(), _st.padding.top(), _st.fieldIcon.width(), _st.fieldIcon.height()); + }, hasAnyItems ? 1. : 0., hasAnyItems ? 0. : 1., _st.item.duration); +} + +void MultiSelect::Inner::updateCursor() { + setCursor(_items.empty() ? style::cur_text : (_overDelete ? style::cur_pointer : style::cur_default)); +} + +void MultiSelect::Inner::setActiveItem(int active, ChangeActiveWay skipSetFocus) { + if (_active == active) return; + + if (_active >= 0) { + t_assert(_active < _items.size()); + _items[_active]->setActive(false); + } + _active = active; + if (_active >= 0) { + t_assert(_active < _items.size()); + _items[_active]->setActive(true); + } + if (skipSetFocus != ChangeActiveWay::SkipSetFocus) { + setInnerFocus(); + } + if (_scrollCallback) { + auto rect = (_active >= 0) ? _items[_active]->rect() : _field->geometry().translated(-_st.padding.left(), -_st.padding.top()); + _scrollCallback(rect.y(), rect.y() + rect.height() + _st.padding.top() + _st.padding.bottom()); + } + update(); +} + +void MultiSelect::Inner::setActiveItemPrevious() { + if (_active > 0) { + setActiveItem(_active - 1); + } else if (_active < 0 && !_items.empty()) { + setActiveItem(_items.size() - 1); + } +} + +void MultiSelect::Inner::setActiveItemNext() { + if (_active >= 0 && _active + 1 < _items.size()) { + setActiveItem(_active + 1); + } else { + setActiveItem(-1); + } +} + +int MultiSelect::Inner::resizeGetHeight(int newWidth) { + computeItemsGeometry(newWidth); + updateFieldGeometry(); + + auto cancelLeft = _fieldLeft + _fieldWidth + _st.padding.right() - _cancel->width(); + auto cancelTop = _fieldTop - _st.padding.top(); + _cancel->moveToLeft(_st.padding.left() + cancelLeft, _st.padding.top() + cancelTop); + + return _field->y() + _field->height() + _st.padding.bottom(); +} + +void MultiSelect::Inner::paintEvent(QPaintEvent *e) { + Painter p(this); + + auto paintRect = e->rect(); + p.fillRect(paintRect, st::windowBg); + + auto offset = QPoint(rtl() ? _st.padding.right() : _st.padding.left(), _st.padding.top()); + p.translate(offset); + paintRect.translate(-offset); + + auto ms = getms(); + auto outerWidth = width() - _st.padding.left() - _st.padding.right(); + auto iconOpacity = _iconOpacity.current(ms, _items.empty() ? 1. : 0.); + if (iconOpacity > 0.) { + p.setOpacity(iconOpacity); + _st.fieldIcon.paint(p, 0, 0, outerWidth); + p.setOpacity(1.); + } + + auto checkRect = myrtlrect(paintRect); + auto paintMargins = itemPaintMargins(); + for (auto i = _removingItems.begin(), e = _removingItems.end(); i != e;) { + auto item = *i; + auto itemRect = item->paintArea(outerWidth); + itemRect = itemRect.marginsAdded(paintMargins); + if (checkRect.intersects(itemRect)) { + item->paint(p, outerWidth, ms); + } + if (item->hideFinished()) { + i = _removingItems.erase(i); + e = _removingItems.end(); + } else { + ++i; + } + } + for_const (auto item, _items) { + auto itemRect = item->paintArea(outerWidth); + itemRect = itemRect.marginsAdded(paintMargins); + if (checkRect.y() + checkRect.height() <= itemRect.y()) { + break; + } else if (checkRect.intersects(itemRect)) { + item->paint(p, outerWidth, ms); + } + } +} + +QMargins MultiSelect::Inner::itemPaintMargins() const { + return { + qMax(_st.itemSkip, _st.padding.left()), + _st.itemSkip, + qMax(_st.itemSkip, _st.padding.right()), + _st.itemSkip, + }; +} + +void MultiSelect::Inner::leaveEvent(QEvent *e) { + clearSelection(); +} + +void MultiSelect::Inner::mouseMoveEvent(QMouseEvent *e) { + updateSelection(e->pos()); +} + +void MultiSelect::Inner::keyPressEvent(QKeyEvent *e) { + if (_active >= 0) { + t_assert(_active < _items.size()); + if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) { + auto itemId = _items[_active]->id(); + setActiveItemNext(); + removeItem(itemId); + } else if (e->key() == Qt::Key_Left) { + setActiveItemPrevious(); + } else if (e->key() == Qt::Key_Right) { + setActiveItemNext(); + } else if (e->key() == Qt::Key_Escape) { + setActiveItem(-1); + } else { + e->ignore(); + } + } else if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Backspace) { + setActiveItemPrevious(); + } else { + e->ignore(); + } +} + +void MultiSelect::Inner::onFieldFocused() { + setActiveItem(-1, ChangeActiveWay::SkipSetFocus); +} + +void MultiSelect::Inner::updateSelection(QPoint mousePosition) { + auto point = myrtlpoint(mousePosition) - QPoint(_st.padding.left(), _st.padding.right()); + auto selected = -1; + for (auto i = 0, size = _items.size(); i != size; ++i) { + auto itemRect = _items[i]->rect(); + if (itemRect.y() > point.y()) { + break; + } else if (itemRect.contains(point)) { + point -= itemRect.topLeft(); + selected = i; + break; + } + } + if (_selected != selected) { + if (_selected >= 0) { + t_assert(_selected < _items.size()); + _items[_selected]->leaveEvent(); + } + _selected = selected; + update(); + } + auto overDelete = false; + if (_selected >= 0) { + _items[_selected]->mouseMoveEvent(point); + overDelete = _items[_selected]->isOverDelete(); + } + if (_overDelete != overDelete) { + _overDelete = overDelete; + updateCursor(); + } +} + +void MultiSelect::Inner::mousePressEvent(QMouseEvent *e) { + if (_overDelete) { + t_assert(_selected >= 0); + t_assert(_selected < _items.size()); + removeItem(_items[_selected]->id()); + } else if (_selected >= 0) { + setActiveItem(_selected); + } else { + setInnerFocus(); + } } void MultiSelect::Inner::addItem(std_::unique_ptr item) { + auto wasEmpty = _items.empty(); + item->setUpdateCallback([this, item = item.get()] { + auto itemRect = item->paintArea(width() - _st.padding.left() - _st.padding.top()); + itemRect = itemRect.translated(_st.padding.left(), _st.padding.top()); + itemRect = itemRect.marginsAdded(itemPaintMargins()); + rtlupdate(itemRect); + }); _items.push_back(item.release()); - refreshItemsGeometry(nullptr); + updateItemsGeometry(); + if (wasEmpty) { + updateHasAnyItems(true); + } + _items.back()->showAnimated(); } -void MultiSelect::Inner::refreshItemsGeometry(Item *startingFromRowWithItem) { - int startingFromRow = 0; - int startingFromIndex = 0; - for (int row = 1, rowsCount = qMin(_rows.size(), 1); row != rowsCount; ++row) { - if (startingFromRowWithItem) { - if (_rows[row - 1].contains(startingFromRowWithItem)) { - break; - } +void MultiSelect::Inner::computeItemsGeometry(int newWidth) { + newWidth -= _st.padding.left() + _st.padding.right(); + + auto itemLeft = 0; + auto itemTop = 0; + auto widthLeft = newWidth; + auto maxVisiblePadding = qMax(_st.padding.left(), _st.padding.right()); + for_const (auto item, _items) { + auto itemWidth = item->getWidth(); + t_assert(itemWidth <= newWidth); + if (itemWidth > widthLeft) { + itemLeft = 0; + itemTop += _st.item.height + _st.itemSkip; + widthLeft = newWidth; } - startingFromIndex += _rows[row - 1].size(); - ++startingFromRow; + item->setPosition(itemLeft, itemTop, newWidth, maxVisiblePadding); + itemLeft += itemWidth + _st.itemSkip; + widthLeft -= itemWidth + _st.itemSkip; } - while (_rows.size() > startingFromRow) { - _rows.pop_back(); - } - for (int i = startingFromIndex, count = _items.size(); i != count; ++i) { - Row row; - row.append(_items[i]); - _rows.append(row); + + auto fieldMinWidth = _st.fieldMinWidth + _st.fieldCancelSkip; + t_assert(fieldMinWidth <= newWidth); + if (fieldMinWidth > widthLeft) { + _fieldLeft = 0; + _fieldTop = itemTop + _st.item.height + _st.itemSkip; + } else { + _fieldLeft = itemLeft + (_items.empty() ? _st.fieldIconSkip : 0); + _fieldTop = itemTop; } + _fieldWidth = newWidth - _fieldLeft; +} + +void MultiSelect::Inner::updateItemsGeometry() { + computeItemsGeometry(width()); + updateFieldGeometry(); + auto newHeight = resizeGetHeight(width()); + if (newHeight == _newHeight) return; + + _newHeight = newHeight; + _height.start([this] { + auto newHeight = _height.current(_newHeight); + if (auto heightDelta = newHeight - height()) { + resize(width(), newHeight); + if (_resizedCallback) { + _resizedCallback(heightDelta); + } + update(); + } + }, height(), _newHeight, _st.item.duration); } void MultiSelect::Inner::setItemText(uint64 itemId, const QString &text) { @@ -163,7 +782,7 @@ void MultiSelect::Inner::setItemText(uint64 itemId, const QString &text) { auto item = _items[i]; if (item->id() == itemId) { item->setText(text); - refreshItemsGeometry(item); + updateItemsGeometry(); return; } } @@ -173,26 +792,50 @@ void MultiSelect::Inner::setItemRemovedCallback(base::lambda_unique callback) { + _resizedCallback = std_::move(callback); +} + void MultiSelect::Inner::removeItem(uint64 itemId) { for (int i = 0, count = _items.size(); i != count; ++i) { auto item = _items[i]; if (item->id() == itemId) { + clearSelection(); _items.removeAt(i); - refreshItemsGeometry(item); - delete item; + if (_active == i) { + _active = -1; + } else if (_active > i) { + --_active; + } + _removingItems.insert(item); + item->hideAnimated(); + + updateItemsGeometry(); + if (_items.empty()) { + updateHasAnyItems(false); + } + auto point = QCursor::pos(); + if (auto parent = parentWidget()) { + if (parent->rect().contains(parent->mapFromGlobal(point))) { + updateSelection(mapFromGlobal(point)); + } + } break; } } if (_itemRemovedCallback) { _itemRemovedCallback(itemId); } + setInnerFocus(); } MultiSelect::Inner::~Inner() { - base::take(_rows); for (auto item : base::take(_items)) { delete item; } + for (auto item : base::take(_removingItems)) { + delete item; + } } } // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/multi_select.h b/Telegram/SourceFiles/ui/widgets/multi_select.h index e44199d1e..6226fc4fc 100644 --- a/Telegram/SourceFiles/ui/widgets/multi_select.h +++ b/Telegram/SourceFiles/ui/widgets/multi_select.h @@ -34,6 +34,49 @@ public: QString getQuery() const; void setInnerFocus(); + void clearQuery(); + + void setQueryChangedCallback(base::lambda_unique callback); + void setSubmittedCallback(base::lambda_unique callback); + void setResizedCallback(base::lambda_unique callback); + + using PaintRoundImage = base::lambda_unique; + void addItem(uint64 itemId, const QString &text, const style::color &color, PaintRoundImage paintRoundImage); + void setItemText(uint64 itemId, const QString &text); + + void setItemRemovedCallback(base::lambda_unique callback); + void removeItem(uint64 itemId); + +protected: + int resizeGetHeight(int newWidth) override; + bool eventFilter(QObject *o, QEvent *e) override; + +private: + void scrollTo(int activeTop, int activeBottom); + + const style::MultiSelect &_st; + + ChildWidget _scroll; + + class Inner; + ChildWidget _inner; + + base::lambda_unique _resizedCallback; + base::lambda_unique _queryChangedCallback; + +}; + +// This class is hold in header because it requires Qt preprocessing. +class MultiSelect::Inner : public ScrolledWidget { + Q_OBJECT + +public: + using ScrollCallback = base::lambda_unique; + Inner(QWidget *parent, const style::MultiSelect &st, const QString &placeholder, ScrollCallback callback); + + QString getQuery() const; + bool setInnerFocus(); + void clearQuery(); void setQueryChangedCallback(base::lambda_unique callback); void setSubmittedCallback(base::lambda_unique callback); @@ -43,68 +86,20 @@ public: void setItemText(uint64 itemId, const QString &text); void setItemRemovedCallback(base::lambda_unique callback); - void removeItem(uint64 itemId); // Always calls the itemRemovedCallback(). + void removeItem(uint64 itemId); -protected: - int resizeGetHeight(int newWidth) override; - - void resizeEvent(QResizeEvent *e) override; - -private: - ChildWidget _scroll; - - class Inner; - ChildWidget _inner; - - const style::MultiSelect &_st; - -}; - -class MultiSelect::Item { -public: - Item(uint64 id, const QString &text, const style::color &color); - - uint64 id() const { - return _id; - } - void setText(const QString &text); - void paint(Painter &p, int x, int y); - - virtual ~Item() = default; - -protected: - virtual void paintImage(Painter &p, int x, int y, int outerWidth, int size) = 0; - -private: - uint64 _id; - -}; - -// This class is hold in header because it requires Qt preprocessing. -class MultiSelect::Inner : public ScrolledWidget { - Q_OBJECT - -public: - Inner(QWidget *parent, const style::MultiSelect &st, const QString &placeholder); - - QString getQuery() const; - bool setInnerFocus(); - - void setQueryChangedCallback(base::lambda_unique callback); - void setSubmittedCallback(base::lambda_unique callback); - - void addItem(std_::unique_ptr item); - void setItemText(uint64 itemId, const QString &text); - - void setItemRemovedCallback(base::lambda_unique callback); - void removeItem(uint64 itemId); // Always calls the itemRemovedCallback(). + void setResizedCallback(base::lambda_unique callback); ~Inner(); protected: int resizeGetHeight(int newWidth) override; - void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void leaveEvent(QEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; private slots: void onQueryChanged(); @@ -113,25 +108,55 @@ private slots: _submittedCallback(ctrlShiftEnter); } } + void onFieldFocused(); private: - void refreshItemsGeometry(Item *startingFromItem); + void computeItemsGeometry(int newWidth); + void updateItemsGeometry(); + void updateFieldGeometry(); + void updateHasAnyItems(bool hasAnyItems); + void updateSelection(QPoint mousePosition); + void clearSelection() { + updateSelection(QPoint(-1, -1)); + } + void updateCursor(); + enum class ChangeActiveWay { + Default, + SkipSetFocus, + }; + void setActiveItem(int active, ChangeActiveWay skipSetFocus = ChangeActiveWay::Default); + void setActiveItemPrevious(); + void setActiveItemNext(); + + QMargins itemPaintMargins() const; const style::MultiSelect &_st; + FloatAnimation _iconOpacity; - using Row = QList; - using Rows = QList; - Rows _rows; + ScrollCallback _scrollCallback; using Items = QList; Items _items; + using RemovingItems = OrderedSet; + RemovingItems _removingItems; - ChildWidget _filter; + int _selected = -1; + int _active = -1; + bool _overDelete = false; + + int _fieldLeft = 0; + int _fieldTop = 0; + int _fieldWidth = 0; + ChildWidget _field; ChildWidget _cancel; + int _newHeight = 0; + IntAnimation _height; + base::lambda_unique _queryChangedCallback; base::lambda_unique _submittedCallback; base::lambda_unique _itemRemovedCallback; + base::lambda_unique _resizedCallback; }; diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index 5db4677f0..8e34c14e4 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -67,10 +67,36 @@ RoundImageCheckbox { checkIcon: icon; } +MultiSelectItem { + padding: margins; + maxWidth: pixels; + height: pixels; + font: font; + textBg: color; + textFg: color; + textActiveBg: color; + textActiveFg: color; + deleteFg: color; + deleteLeft: pixels; + deleteStroke: pixels; + duration: int; + minScale: double; +} + MultiSelect { - field: InputField; - cancel: IconButton; + padding: margins; maxHeight: pixels; + scroll: flatScroll; + + item: MultiSelectItem; + itemSkip: pixels; + + field: InputField; + fieldIcon: icon; + fieldIconSkip: pixels; + fieldCancel: IconButton; + fieldCancelSkip: pixels; + fieldMinWidth: pixels; } widgetSlideDuration: 200;