364 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			364 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // This file is part of Desktop App Toolkit,
 | |
| // a set of libraries for developing nice desktop applications.
 | |
| //
 | |
| // For license and copyright information please follow this link:
 | |
| // https://github.com/desktop-app/legal/blob/master/LEGAL
 | |
| //
 | |
| #include "ui/wrap/vertical_layout_reorder.h"
 | |
| 
 | |
| #include "ui/wrap/vertical_layout.h"
 | |
| #include "styles/style_basic.h"
 | |
| 
 | |
| #include <QtGui/QtEvents>
 | |
| #include <QtWidgets/QApplication>
 | |
| 
 | |
| namespace Ui {
 | |
| 
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kScrollFactor = 0.05;
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| VerticalLayoutReorder::VerticalLayoutReorder(
 | |
| 	not_null<VerticalLayout*> layout,
 | |
| 	not_null<ScrollArea*> scroll)
 | |
| : _layout(layout)
 | |
| , _scroll(scroll)
 | |
| , _scrollAnimation([=] { updateScrollCallback(); }) {
 | |
| }
 | |
| 
 | |
| VerticalLayoutReorder::VerticalLayoutReorder(not_null<VerticalLayout*> layout)
 | |
| : _layout(layout) {
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::cancel() {
 | |
| 	if (_currentWidget) {
 | |
| 		cancelCurrent(indexOf(_currentWidget));
 | |
| 	}
 | |
| 	_lifetime.destroy();
 | |
| 	for (auto i = 0, count = _layout->count(); i != count; ++i) {
 | |
| 		_layout->setVerticalShift(i, 0);
 | |
| 	}
 | |
| 	_entries.clear();
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::start() {
 | |
| 	const auto count = _layout->count();
 | |
| 	if (count < 2) {
 | |
| 		return;
 | |
| 	}
 | |
| 	for (auto i = 0; i != count; ++i) {
 | |
| 		const auto widget = _layout->widgetAt(i);
 | |
| 		const auto eventsProducer = _proxyWidgetCallback
 | |
| 			? _proxyWidgetCallback(i)
 | |
| 			: widget;
 | |
| 		eventsProducer->events(
 | |
| 		) | rpl::start_with_next_done([=](not_null<QEvent*> e) {
 | |
| 			switch (e->type()) {
 | |
| 			case QEvent::MouseMove:
 | |
| 				mouseMove(
 | |
| 					widget,
 | |
| 					static_cast<QMouseEvent*>(e.get())->globalPos());
 | |
| 				break;
 | |
| 			case QEvent::MouseButtonPress:
 | |
| 				mousePress(
 | |
| 					widget,
 | |
| 					static_cast<QMouseEvent*>(e.get())->button(),
 | |
| 					static_cast<QMouseEvent*>(e.get())->globalPos());
 | |
| 				break;
 | |
| 			case QEvent::MouseButtonRelease:
 | |
| 				mouseRelease(static_cast<QMouseEvent*>(e.get())->button());
 | |
| 				break;
 | |
| 			}
 | |
| 		}, [=] {
 | |
| 			cancel();
 | |
| 		}, _lifetime);
 | |
| 		_entries.push_back({ widget });
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::addPinnedInterval(int from, int length) {
 | |
| 	_pinnedIntervals.push_back({ from, length });
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::clearPinnedIntervals() {
 | |
| 	_pinnedIntervals.clear();
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::setMouseEventProxy(ProxyCallback callback) {
 | |
| 	_proxyWidgetCallback = std::move(callback);
 | |
| }
 | |
| 
 | |
| bool VerticalLayoutReorder::Interval::isIn(int index) const {
 | |
| 	return (index >= from) && (index < (from + length));
 | |
| }
 | |
| 
 | |
| bool VerticalLayoutReorder::isIndexPinned(int index) const {
 | |
| 	return ranges::any_of(_pinnedIntervals, [&](const Interval &i) {
 | |
| 		return i.isIn(index);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::mouseMove(
 | |
| 		not_null<RpWidget*> widget,
 | |
| 		QPoint position) {
 | |
| 	if (_currentWidget != widget) {
 | |
| 		return;
 | |
| 	} else if (_currentState != State::Started) {
 | |
| 		checkForStart(position);
 | |
| 	} else {
 | |
| 		updateOrder(indexOf(_currentWidget), position);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::checkForStart(QPoint position) {
 | |
| 	const auto shift = position.y() - _currentStart;
 | |
| 	const auto delta = QApplication::startDragDistance();
 | |
| 	if (std::abs(shift) <= delta) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_currentWidget->raise();
 | |
| 	_currentState = State::Started;
 | |
| 	_currentStart += (shift > 0) ? delta : -delta;
 | |
| 
 | |
| 	const auto index = indexOf(_currentWidget);
 | |
| 	_currentDesiredIndex = index;
 | |
| 	_updates.fire({ _currentWidget, index, index, _currentState });
 | |
| 
 | |
| 	updateOrder(index, position);
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::updateOrder(int index, QPoint position) {
 | |
| 	if (isIndexPinned(index)) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto shift = position.y() - _currentStart;
 | |
| 	auto ¤t = _entries[index];
 | |
| 	current.shiftAnimation.stop();
 | |
| 	current.shift = current.finalShift = shift;
 | |
| 	_layout->setVerticalShift(index, shift);
 | |
| 
 | |
| 	checkForScrollAnimation();
 | |
| 
 | |
| 	const auto count = _entries.size();
 | |
| 	const auto currentHeight = current.widget->height();
 | |
| 	const auto currentMiddle = current.widget->y() + currentHeight / 2;
 | |
| 	_currentDesiredIndex = index;
 | |
| 	if (shift > 0) {
 | |
| 		auto top = current.widget->y() - shift;
 | |
| 		for (auto next = index + 1; next != count; ++next) {
 | |
| 			if (isIndexPinned(next)) {
 | |
| 				return;
 | |
| 			}
 | |
| 			const auto &entry = _entries[next];
 | |
| 			top += entry.widget->height();
 | |
| 			if (currentMiddle < top) {
 | |
| 				moveToShift(next, 0);
 | |
| 			} else {
 | |
| 				_currentDesiredIndex = next;
 | |
| 				moveToShift(next, -currentHeight);
 | |
| 			}
 | |
| 		}
 | |
| 		for (auto prev = index - 1; prev >= 0; --prev) {
 | |
| 			moveToShift(prev, 0);
 | |
| 		}
 | |
| 	} else {
 | |
| 		for (auto next = index + 1; next != count; ++next) {
 | |
| 			moveToShift(next, 0);
 | |
| 		}
 | |
| 		for (auto prev = index - 1; prev >= 0; --prev) {
 | |
| 			if (isIndexPinned(prev)) {
 | |
| 				return;
 | |
| 			}
 | |
| 			const auto &entry = _entries[prev];
 | |
| 			if (currentMiddle >= entry.widget->y() - entry.shift + currentHeight) {
 | |
| 				moveToShift(prev, 0);
 | |
| 			} else {
 | |
| 				_currentDesiredIndex = prev;
 | |
| 				moveToShift(prev, currentHeight);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::mousePress(
 | |
| 		not_null<RpWidget*> widget,
 | |
| 		Qt::MouseButton button,
 | |
| 		QPoint position) {
 | |
| 	if (button != Qt::LeftButton) {
 | |
| 		return;
 | |
| 	}
 | |
| 	cancelCurrent();
 | |
| 	_currentWidget = widget;
 | |
| 	_currentStart = position.y();
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::mouseRelease(Qt::MouseButton button) {
 | |
| 	if (button != Qt::LeftButton) {
 | |
| 		return;
 | |
| 	}
 | |
| 	finishReordering();
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::cancelCurrent() {
 | |
| 	if (_currentWidget) {
 | |
| 		cancelCurrent(indexOf(_currentWidget));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::cancelCurrent(int index) {
 | |
| 	Expects(_currentWidget != nullptr);
 | |
| 
 | |
| 	if (_currentState == State::Started) {
 | |
| 		_currentState = State::Cancelled;
 | |
| 		_updates.fire({ _currentWidget, index, index, _currentState });
 | |
| 	}
 | |
| 	_currentWidget = nullptr;
 | |
| 	for (auto i = 0, count = int(_entries.size()); i != count; ++i) {
 | |
| 		moveToShift(i, 0);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::finishReordering() {
 | |
| 	if (_scroll) {
 | |
| 		_scrollAnimation.stop();
 | |
| 	}
 | |
| 	finishCurrent();
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::finishCurrent() {
 | |
| 	if (!_currentWidget) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto index = indexOf(_currentWidget);
 | |
| 	if (_currentDesiredIndex == index || _currentState != State::Started) {
 | |
| 		cancelCurrent(index);
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto result = _currentDesiredIndex;
 | |
| 	const auto widget = _currentWidget;
 | |
| 	_currentState = State::Cancelled;
 | |
| 	_currentWidget = nullptr;
 | |
| 
 | |
| 	auto ¤t = _entries[index];
 | |
| 	const auto height = current.widget->height();
 | |
| 	if (index < result) {
 | |
| 		auto sum = 0;
 | |
| 		for (auto i = index; i != result; ++i) {
 | |
| 			auto &entry = _entries[i + 1];
 | |
| 			const auto widget = entry.widget;
 | |
| 			entry.deltaShift += height;
 | |
| 			updateShift(widget, i + 1);
 | |
| 			sum += widget->height();
 | |
| 		}
 | |
| 		current.finalShift -= sum;
 | |
| 	} else if (index > result) {
 | |
| 		auto sum = 0;
 | |
| 		for (auto i = result; i != index; ++i) {
 | |
| 			auto &entry = _entries[i];
 | |
| 			const auto widget = entry.widget;
 | |
| 			entry.deltaShift -= height;
 | |
| 			updateShift(widget, i);
 | |
| 			sum += widget->height();
 | |
| 		}
 | |
| 		current.finalShift += sum;
 | |
| 	}
 | |
| 	if (!(current.finalShift + current.deltaShift)) {
 | |
| 		current.shift = 0;
 | |
| 		_layout->setVerticalShift(index, 0);
 | |
| 	}
 | |
| 	base::reorder(_entries, index, result);
 | |
| 	_layout->reorderRows(index, _currentDesiredIndex);
 | |
| 	for (auto i = 0, count = int(_entries.size()); i != count; ++i) {
 | |
| 		moveToShift(i, 0);
 | |
| 	}
 | |
| 
 | |
| 	_updates.fire({ widget, index, result, State::Applied });
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::moveToShift(int index, int shift) {
 | |
| 	auto &entry = _entries[index];
 | |
| 	if (entry.finalShift + entry.deltaShift == shift) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto widget = entry.widget;
 | |
| 	entry.shiftAnimation.start(
 | |
| 		[=] { updateShift(widget, index); },
 | |
| 		entry.finalShift,
 | |
| 		shift - entry.deltaShift,
 | |
| 		st::slideWrapDuration);
 | |
| 	entry.finalShift = shift - entry.deltaShift;
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::updateShift(
 | |
| 		not_null<RpWidget*> widget,
 | |
| 		int indexHint) {
 | |
| 	Expects(indexHint >= 0 && indexHint < _entries.size());
 | |
| 
 | |
| 	const auto index = (_entries[indexHint].widget == widget)
 | |
| 		? indexHint
 | |
| 		: indexOf(widget);
 | |
| 	auto &entry = _entries[index];
 | |
| 	entry.shift = base::SafeRound(
 | |
| 		entry.shiftAnimation.value(entry.finalShift)
 | |
| 	) + entry.deltaShift;
 | |
| 	if (entry.deltaShift && !entry.shiftAnimation.animating()) {
 | |
| 		entry.finalShift += entry.deltaShift;
 | |
| 		entry.deltaShift = 0;
 | |
| 	}
 | |
| 	_layout->setVerticalShift(index, entry.shift);
 | |
| }
 | |
| 
 | |
| int VerticalLayoutReorder::indexOf(not_null<RpWidget*> widget) const {
 | |
| 	const auto i = ranges::find(_entries, widget, &Entry::widget);
 | |
| 	Assert(i != end(_entries));
 | |
| 	return i - begin(_entries);
 | |
| }
 | |
| 
 | |
| auto VerticalLayoutReorder::updates() const -> rpl::producer<Single> {
 | |
| 	return _updates.events();
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::updateScrollCallback() {
 | |
| 	if (!_scroll) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto delta = deltaFromEdge();
 | |
| 	const auto oldTop = _scroll->scrollTop();
 | |
| 	_scroll->scrollToY(oldTop + delta);
 | |
| 	const auto newTop = _scroll->scrollTop();
 | |
| 
 | |
| 	_currentStart += oldTop - newTop;
 | |
| 	if (newTop == 0 || newTop == _scroll->scrollTopMax()) {
 | |
| 		_scrollAnimation.stop();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void VerticalLayoutReorder::checkForScrollAnimation() {
 | |
| 	if (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_scrollAnimation.start();
 | |
| }
 | |
| 
 | |
| int VerticalLayoutReorder::deltaFromEdge() {
 | |
| 	Expects(_currentWidget != nullptr);
 | |
| 	Expects(_scroll);
 | |
| 
 | |
| 	const auto globalPosition = _currentWidget->mapToGlobal(QPoint(0, 0));
 | |
| 	const auto localTop = _scroll->mapFromGlobal(globalPosition).y();
 | |
| 	const auto localBottom = localTop
 | |
| 		+ _currentWidget->height()
 | |
| 		- _scroll->height();
 | |
| 
 | |
| 	const auto isTopEdge = (localTop < 0);
 | |
| 	const auto isBottomEdge = (localBottom > 0);
 | |
| 	if (!isTopEdge && !isBottomEdge) {
 | |
| 		_scrollAnimation.stop();
 | |
| 		return 0;
 | |
| 	}
 | |
| 	return int((isBottomEdge ? localBottom : localTop) * kScrollFactor);
 | |
| }
 | |
| 
 | |
| } // namespace Ui
 | 
