/* 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 "boxes/abstract_box.h" #include "styles/style_boxes.h" #include "styles/style_profile.h" #include "storage/localstorage.h" #include "lang/lang_keys.h" #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/labels.h" #include "ui/widgets/shadow.h" #include "ui/wrap/fade_wrap.h" #include "ui/text/text_utilities.h" #include "mainwidget.h" #include "mainwindow.h" void BoxContent::setTitle(rpl::producer title) { getDelegate()->setTitle(std::move(title) | Ui::Text::ToWithEntities()); } QPointer BoxContent::addButton( rpl::producer text, Fn clickCallback) { return addButton( std::move(text), std::move(clickCallback), st::defaultBoxButton); } QPointer BoxContent::addLeftButton( rpl::producer text, Fn clickCallback) { return getDelegate()->addLeftButton( std::move(text), std::move(clickCallback), st::defaultBoxButton); } void BoxContent::setInner(object_ptr inner) { setInner(std::move(inner), st::boxLayerScroll); } void BoxContent::setInner(object_ptr inner, const style::ScrollArea &st) { if (inner) { getDelegate()->setLayerType(true); _scroll.create(this, st); _scroll->setGeometryToLeft(0, _innerTopSkip, width(), 0); _scroll->setOwnedWidget(std::move(inner)); if (_topShadow) { _topShadow->raise(); _bottomShadow->raise(); } else { _topShadow.create(this); _bottomShadow.create(this); } if (!_preparing) { // We didn't set dimensions yet, this will be called from finishPrepare(); finishScrollCreate(); } } else { getDelegate()->setLayerType(false); _scroll.destroyDelayed(); _topShadow.destroyDelayed(); _bottomShadow.destroyDelayed(); } } void BoxContent::finishPrepare() { _preparing = false; if (_scroll) { finishScrollCreate(); } setInnerFocus(); } void BoxContent::finishScrollCreate() { Expects(_scroll != nullptr); if (!_scroll->isHidden()) { _scroll->show(); } updateScrollAreaGeometry(); connect(_scroll, SIGNAL(scrolled()), this, SLOT(onScroll())); connect(_scroll, SIGNAL(innerResized()), this, SLOT(onInnerResize())); } void BoxContent::scrollToWidget(not_null widget) { if (_scroll) { _scroll->scrollToWidget(widget); } } void BoxContent::onScrollToY(int top, int bottom) { if (_scroll) { _scroll->scrollToY(top, bottom); } } void BoxContent::onDraggingScrollDelta(int delta) { _draggingScrollDelta = _scroll ? delta : 0; if (_draggingScrollDelta) { if (!_draggingScrollTimer) { _draggingScrollTimer.create(this); _draggingScrollTimer->setSingleShot(false); connect(_draggingScrollTimer, SIGNAL(timeout()), this, SLOT(onDraggingScrollTimer())); } _draggingScrollTimer->start(15); } else { _draggingScrollTimer.destroy(); } } void BoxContent::onDraggingScrollTimer() { auto delta = (_draggingScrollDelta > 0) ? qMin(_draggingScrollDelta * 3 / 20 + 1, int32(MaxScrollSpeed)) : qMax(_draggingScrollDelta * 3 / 20 - 1, -int32(MaxScrollSpeed)); _scroll->scrollToY(_scroll->scrollTop() + delta); } void BoxContent::updateInnerVisibleTopBottom() { if (auto widget = static_cast(_scroll ? _scroll->widget() : nullptr)) { auto top = _scroll->scrollTop(); widget->setVisibleTopBottom(top, top + _scroll->height()); } } void BoxContent::updateShadowsVisibility() { if (!_scroll) return; auto top = _scroll->scrollTop(); _topShadow->toggle( (top > 0 || _innerTopSkip > 0), anim::type::normal); _bottomShadow->toggle( (top < _scroll->scrollTopMax() || _innerBottomSkip > 0), anim::type::normal); } void BoxContent::onScroll() { updateInnerVisibleTopBottom(); updateShadowsVisibility(); } void BoxContent::onInnerResize() { updateInnerVisibleTopBottom(); updateShadowsVisibility(); } void BoxContent::setDimensionsToContent( int newWidth, not_null content) { content->resizeToWidth(newWidth); content->heightValue( ) | rpl::start_with_next([=](int height) { setDimensions(newWidth, height); }, content->lifetime()); } void BoxContent::setInnerTopSkip(int innerTopSkip, bool scrollBottomFixed) { if (_innerTopSkip != innerTopSkip) { auto delta = innerTopSkip - _innerTopSkip; _innerTopSkip = innerTopSkip; if (_scroll && width() > 0) { auto scrollTopWas = _scroll->scrollTop(); updateScrollAreaGeometry(); if (scrollBottomFixed) { _scroll->scrollToY(scrollTopWas + delta); } } } } void BoxContent::setInnerBottomSkip(int innerBottomSkip) { if (_innerBottomSkip != innerBottomSkip) { auto delta = innerBottomSkip - _innerBottomSkip; _innerBottomSkip = innerBottomSkip; if (_scroll && width() > 0) { updateScrollAreaGeometry(); } } } void BoxContent::setInnerVisible(bool scrollAreaVisible) { if (_scroll) { _scroll->setVisible(scrollAreaVisible); } } QPixmap BoxContent::grabInnerCache() { auto isTopShadowVisible = !_topShadow->isHidden(); auto isBottomShadowVisible = !_bottomShadow->isHidden(); if (isTopShadowVisible) _topShadow->setVisible(false); if (isBottomShadowVisible) _bottomShadow->setVisible(false); auto result = Ui::GrabWidget(this, _scroll->geometry()); if (isTopShadowVisible) _topShadow->setVisible(true); if (isBottomShadowVisible) _bottomShadow->setVisible(true); return result; } void BoxContent::resizeEvent(QResizeEvent *e) { if (_scroll) { updateScrollAreaGeometry(); } } void BoxContent::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape && !_closeByEscape) { e->accept(); } else { RpWidget::keyPressEvent(e); } } void BoxContent::updateScrollAreaGeometry() { auto newScrollHeight = height() - _innerTopSkip - _innerBottomSkip; auto changed = (_scroll->height() != newScrollHeight); _scroll->setGeometryToLeft(0, _innerTopSkip, width(), newScrollHeight); _topShadow->entity()->resize(width(), st::lineWidth); _topShadow->moveToLeft(0, _innerTopSkip); _bottomShadow->entity()->resize(width(), st::lineWidth); _bottomShadow->moveToLeft( 0, height() - _innerBottomSkip - st::lineWidth); if (changed) { updateInnerVisibleTopBottom(); auto top = _scroll->scrollTop(); _topShadow->toggle( (top > 0 || _innerTopSkip > 0), anim::type::instant); _bottomShadow->toggle( (top < _scroll->scrollTopMax() || _innerBottomSkip > 0), anim::type::instant); } } object_ptr BoxContent::doTakeInnerWidget() { return _scroll->takeWidget(); } void BoxContent::paintEvent(QPaintEvent *e) { Painter p(this); if (testAttribute(Qt::WA_OpaquePaintEvent)) { for (auto rect : e->region().rects()) { p.fillRect(rect, st::boxBg); } } } AbstractBox::AbstractBox( not_null layer, object_ptr content) : LayerWidget(layer) , _layer(layer) , _content(std::move(content)) { subscribe(Lang::Current().updated(), [=] { refreshLang(); }); _content->setParent(this); _content->setDelegate(this); _additionalTitle.changes( ) | rpl::start_with_next([=] { updateSize(); update(); }, lifetime()); } void AbstractBox::setLayerType(bool layerType) { _layerType = layerType; updateTitlePosition(); } int AbstractBox::titleHeight() const { return _layerType ? st::boxLayerTitleHeight : st::boxTitleHeight; } int AbstractBox::buttonsHeight() const { auto padding = _layerType ? st::boxLayerButtonPadding : st::boxButtonPadding; return padding.top() + st::defaultBoxButton.height + padding.bottom(); } int AbstractBox::buttonsTop() const { auto padding = _layerType ? st::boxLayerButtonPadding : st::boxButtonPadding; return height() - padding.bottom() - st::defaultBoxButton.height; } void AbstractBox::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); auto paintTopRounded = clip.intersects(QRect(0, 0, width(), st::boxRadius)); auto paintBottomRounded = clip.intersects(QRect(0, height() - st::boxRadius, width(), st::boxRadius)); if (paintTopRounded || paintBottomRounded) { auto parts = RectPart::None | 0; if (paintTopRounded) parts |= RectPart::FullTop; if (paintBottomRounded) parts |= RectPart::FullBottom; App::roundRect(p, rect(), st::boxBg, BoxCorners, nullptr, parts); } auto other = e->region().intersected(QRect(0, st::boxRadius, width(), height() - 2 * st::boxRadius)); if (!other.isEmpty()) { for (auto rect : other.rects()) { p.fillRect(rect, st::boxBg); } } if (!_additionalTitle.current().isEmpty() && clip.intersects(QRect(0, 0, width(), titleHeight()))) { paintAdditionalTitle(p); } } void AbstractBox::paintAdditionalTitle(Painter &p) { p.setFont(st::boxLayerTitleAdditionalFont); p.setPen(st::boxTitleAdditionalFg); p.drawTextLeft(_titleLeft + (_title ? _title->width() : 0) + st::boxLayerTitleAdditionalSkip, _titleTop + st::boxTitleFont->ascent - st::boxLayerTitleAdditionalFont->ascent, width(), _additionalTitle.current()); } void AbstractBox::parentResized() { auto newHeight = countRealHeight(); auto parentSize = parentWidget()->size(); setGeometry((parentSize.width() - width()) / 2, (parentSize.height() - newHeight) / 2, width(), newHeight); update(); } void AbstractBox::setTitle(rpl::producer title) { const auto wasTitle = hasTitle(); if (title) { _title.create(this, std::move(title), st::boxTitle); _title->show(); updateTitlePosition(); } else { _title.destroy(); } if (wasTitle != hasTitle()) { updateSize(); } } void AbstractBox::setAdditionalTitle(rpl::producer additional) { _additionalTitle = std::move(additional); } void AbstractBox::setCloseByOutsideClick(bool close) { _closeByOutsideClick = close; } bool AbstractBox::closeByOutsideClick() const { return _closeByOutsideClick; } void AbstractBox::refreshLang() { InvokeQueued(this, [this] { updateButtonsPositions(); }); } bool AbstractBox::hasTitle() const { return (_title != nullptr) || !_additionalTitle.current().isEmpty(); } void AbstractBox::showBox( object_ptr box, LayerOptions options, anim::type animated) { _layer->showBox(std::move(box), options, animated); } void AbstractBox::updateSize() { setDimensions(width(), _maxContentHeight); } void AbstractBox::updateButtonsPositions() { if (!_buttons.empty() || _leftButton) { auto padding = _layerType ? st::boxLayerButtonPadding : st::boxButtonPadding; auto right = padding.right(); auto top = buttonsTop(); if (_leftButton) { _leftButton->moveToLeft(right, top); } for (const auto &button : _buttons) { button->moveToRight(right, top); right += button->width() + padding.left(); } } if (_topButton) { _topButton->moveToRight(0, 0); } } QPointer AbstractBox::outerContainer() { return parentWidget(); } void AbstractBox::updateTitlePosition() { _titleLeft = _layerType ? st::boxLayerTitlePosition.x() : st::boxTitlePosition.x(); _titleTop = _layerType ? st::boxLayerTitlePosition.y() : st::boxTitlePosition.y(); if (_title) { _title->resizeToWidth(qMin(_title->naturalWidth(), width() - _titleLeft * 2)); _title->moveToLeft(_titleLeft, _titleTop); } } void AbstractBox::clearButtons() { for (auto &button : base::take(_buttons)) { button.destroy(); } _leftButton.destroy(); _topButton = nullptr; } QPointer AbstractBox::addButton( rpl::producer text, Fn clickCallback, const style::RoundButton &st) { _buttons.emplace_back(this, std::move(text), st); auto result = QPointer(_buttons.back()); result->setClickedCallback(std::move(clickCallback)); result->show(); updateButtonsPositions(); return result; } QPointer AbstractBox::addLeftButton( rpl::producer text, Fn clickCallback, const style::RoundButton &st) { _leftButton = object_ptr(this, std::move(text), st); auto result = QPointer(_leftButton); result->setClickedCallback(std::move(clickCallback)); result->show(); updateButtonsPositions(); return result; } QPointer AbstractBox::addTopButton(const style::IconButton &st, Fn clickCallback) { _topButton = base::make_unique_q(this, st); auto result = QPointer(_topButton.get()); result->setClickedCallback(std::move(clickCallback)); result->show(); updateButtonsPositions(); return result; } void AbstractBox::setDimensions(int newWidth, int maxHeight, bool forceCenterPosition) { _maxContentHeight = maxHeight; auto fullHeight = countFullHeight(); if (width() != newWidth || _fullHeight != fullHeight) { _fullHeight = fullHeight; if (parentWidget()) { auto oldGeometry = geometry(); resize(newWidth, countRealHeight()); auto newGeometry = geometry(); auto parentHeight = parentWidget()->height(); if (newGeometry.top() + newGeometry.height() + st::boxVerticalMargin > parentHeight || forceCenterPosition) { const auto top1 = parentHeight - int(st::boxVerticalMargin) - newGeometry.height(); const auto top2 = (parentHeight - newGeometry.height()) / 2; const auto newTop = forceCenterPosition ? std::min(top1, top2) : std::max(top1, top2); if (newTop != newGeometry.top()) { move(newGeometry.left(), newTop); resizeEvent(0); } } parentWidget()->update(oldGeometry.united(geometry()).marginsAdded(st::boxRoundShadow.extend)); } else { resize(newWidth, 0); } } } int AbstractBox::countRealHeight() const { return qMin(_fullHeight, parentWidget()->height() - 2 * st::boxVerticalMargin); } int AbstractBox::countFullHeight() const { return contentTop() + _maxContentHeight + buttonsHeight(); } int AbstractBox::contentTop() const { return hasTitle() ? titleHeight() : (_noContentMargin ? 0 : st::boxTopMargin); } void AbstractBox::resizeEvent(QResizeEvent *e) { updateButtonsPositions(); updateTitlePosition(); auto top = contentTop(); _content->resize(width(), height() - top - buttonsHeight()); _content->moveToLeft(0, top); LayerWidget::resizeEvent(e); } void AbstractBox::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape) { closeBox(); } else { LayerWidget::keyPressEvent(e); } } BoxContentDivider::BoxContentDivider(QWidget *parent) : BoxContentDivider(parent, st::rightsDividerHeight) { } BoxContentDivider::BoxContentDivider(QWidget *parent, int height) : RpWidget(parent) { resize(width(), height); } void BoxContentDivider::paintEvent(QPaintEvent *e) { Painter p(this); p.fillRect(e->rect(), st::contactsAboutBg); auto dividerFillTop = myrtlrect(0, 0, width(), st::profileDividerTop.height()); st::profileDividerTop.fill(p, dividerFillTop); auto dividerFillBottom = myrtlrect(0, height() - st::profileDividerBottom.height(), width(), st::profileDividerBottom.height()); st::profileDividerBottom.fill(p, dividerFillBottom); }