407 lines
		
	
	
	
		
			9.9 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			407 lines
		
	
	
	
		
			9.9 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/widgets/menu/menu.h"
 | |
| 
 | |
| #include "ui/widgets/menu/menu_action.h"
 | |
| #include "ui/widgets/menu/menu_item_base.h"
 | |
| #include "ui/widgets/menu/menu_separator.h"
 | |
| #include "ui/widgets/scroll_area.h"
 | |
| 
 | |
| #include <QtGui/QtEvents>
 | |
| 
 | |
| namespace Ui::Menu {
 | |
| 
 | |
| Menu::Menu(QWidget *parent, const style::Menu &st)
 | |
| : RpWidget(parent)
 | |
| , _st(st) {
 | |
| 	init();
 | |
| }
 | |
| 
 | |
| Menu::Menu(QWidget *parent, QMenu *menu, const style::Menu &st)
 | |
| : RpWidget(parent)
 | |
| , _st(st)
 | |
| , _wappedMenu(menu) {
 | |
| 	init();
 | |
| 
 | |
| 	_wappedMenu->setParent(this);
 | |
| 	for (auto action : _wappedMenu->actions()) {
 | |
| 		addAction(action);
 | |
| 	}
 | |
| 	_wappedMenu->hide();
 | |
| }
 | |
| 
 | |
| Menu::~Menu() = default;
 | |
| 
 | |
| void Menu::init() {
 | |
| 	resize(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2);
 | |
| 
 | |
| 	setMouseTracking(true);
 | |
| 
 | |
| 	if (_st.itemBg->c.alpha() == 255) {
 | |
| 		setAttribute(Qt::WA_OpaquePaintEvent);
 | |
| 	}
 | |
| 
 | |
| 	paintRequest(
 | |
| 	) | rpl::start_with_next([=](const QRect &clip) {
 | |
| 		Painter p(this);
 | |
| 		p.fillRect(clip, _st.itemBg);
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	positionValue(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		handleMouseMove(QCursor::pos());
 | |
| 	}, lifetime());
 | |
| }
 | |
| 
 | |
| not_null<QAction*> Menu::addAction(
 | |
| 		const QString &text,
 | |
| 		Fn<void()> callback,
 | |
| 		const style::icon *icon,
 | |
| 		const style::icon *iconOver) {
 | |
| 	auto action = CreateAction(this, text, std::move(callback));
 | |
| 	return addAction(std::move(action), icon, iconOver);
 | |
| }
 | |
| 
 | |
| not_null<QAction*> Menu::addAction(
 | |
| 		const QString &text,
 | |
| 		std::unique_ptr<QMenu> submenu,
 | |
| 		const style::icon *icon,
 | |
| 		const style::icon *iconOver) {
 | |
| 	const auto action = new QAction(text, this);
 | |
| 	action->setMenu(submenu.release());
 | |
| 	return addAction(action, icon, iconOver);
 | |
| }
 | |
| 
 | |
| not_null<QAction*> Menu::addAction(
 | |
| 		not_null<QAction*> action,
 | |
| 		const style::icon *icon,
 | |
| 		const style::icon *iconOver) {
 | |
| 	if (action->isSeparator()) {
 | |
| 		return addSeparator();
 | |
| 	}
 | |
| 	auto item = base::make_unique_q<Action>(
 | |
| 		this,
 | |
| 		_st,
 | |
| 		std::move(action),
 | |
| 		icon,
 | |
| 		iconOver ? iconOver : icon);
 | |
| 	return addAction(std::move(item));
 | |
| }
 | |
| 
 | |
| not_null<QAction*> Menu::addAction(base::unique_qptr<ItemBase> widget) {
 | |
| 	const auto action = widget->action();
 | |
| 	_actions.emplace_back(action);
 | |
| 
 | |
| 	widget->setParent(this);
 | |
| 
 | |
| 	const auto top = _actionWidgets.empty()
 | |
| 		? 0
 | |
| 		: _actionWidgets.back()->y() + _actionWidgets.back()->height();
 | |
| 
 | |
| 	widget->moveToLeft(0, top);
 | |
| 	widget->show();
 | |
| 
 | |
| 	widget->setIndex(_actionWidgets.size());
 | |
| 
 | |
| 	widget->selects(
 | |
| 	) | rpl::start_with_next([=](const CallbackData &data) {
 | |
| 		if (!data.selected) {
 | |
| 			if (!findSelectedAction()
 | |
| 				&& data.index < _actionWidgets.size()
 | |
| 				&& _childShownAction == data.action) {
 | |
| 				const auto widget = _actionWidgets[data.index].get();
 | |
| 				widget->setSelected(true, widget->lastTriggeredSource());
 | |
| 			}
 | |
| 			return;
 | |
| 		}
 | |
| 		_lastSelectedByMouse = (data.source == TriggeredSource::Mouse);
 | |
| 		for (auto i = 0; i < _actionWidgets.size(); i++) {
 | |
| 			if (i != data.index) {
 | |
| 				_actionWidgets[i]->setSelected(false);
 | |
| 			}
 | |
| 		}
 | |
| 		if (_activatedCallback) {
 | |
| 			_activatedCallback(data);
 | |
| 		}
 | |
| 	}, widget->lifetime());
 | |
| 
 | |
| 	widget->clicks(
 | |
| 	) | rpl::start_with_next([=](const CallbackData &data) {
 | |
| 		if (_triggeredCallback) {
 | |
| 			_triggeredCallback(data);
 | |
| 		}
 | |
| 	}, widget->lifetime());
 | |
| 
 | |
| 	QObject::connect(action.get(), &QAction::changed, widget.get(), [=] {
 | |
| 		// Select an item under mouse that was disabled and became enabled.
 | |
| 		if (_lastSelectedByMouse
 | |
| 			&& !findSelectedAction()
 | |
| 			&& action->isEnabled()) {
 | |
| 			updateSelected(QCursor::pos());
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	const auto raw = widget.get();
 | |
| 	_actionWidgets.push_back(std::move(widget));
 | |
| 
 | |
| 	const auto recountWidth = [=] {
 | |
| 		return _forceWidth
 | |
| 			? _forceWidth
 | |
| 			: std::clamp(
 | |
| 				(_actionWidgets.empty()
 | |
| 					? 0
 | |
| 					: (*ranges::max_element(
 | |
| 						_actionWidgets,
 | |
| 						std::less<>(),
 | |
| 						&ItemBase::minWidth))->minWidth()),
 | |
| 				_st.widthMin,
 | |
| 				_st.widthMax);
 | |
| 	};
 | |
| 	const auto recountHeight = [=] {
 | |
| 		auto result = 0;
 | |
| 		for (const auto &widget : _actionWidgets) {
 | |
| 			if (widget->y() != result) {
 | |
| 				widget->move(0, result);
 | |
| 			}
 | |
| 			result += widget->height();
 | |
| 		}
 | |
| 		return result;
 | |
| 	};
 | |
| 
 | |
| 	raw->minWidthValue(
 | |
| 	) | rpl::skip(1) | rpl::filter([=] {
 | |
| 		return !_forceWidth;
 | |
| 	}) | rpl::start_with_next([=] {
 | |
| 		resizeFromInner(recountWidth(), height());
 | |
| 	}, raw->lifetime());
 | |
| 
 | |
| 	raw->heightValue(
 | |
| 	) | rpl::skip(1) | rpl::start_with_next([=] {
 | |
| 		resizeFromInner(width(), recountHeight());
 | |
| 	}, raw->lifetime());
 | |
| 
 | |
| 	resizeFromInner(recountWidth(), recountHeight());
 | |
| 
 | |
| 	updateSelected(QCursor::pos());
 | |
| 
 | |
| 	return action;
 | |
| }
 | |
| 
 | |
| not_null<QAction*> Menu::addSeparator() {
 | |
| 	const auto separator = new QAction(this);
 | |
| 	separator->setSeparator(true);
 | |
| 	auto item = base::make_unique_q<Separator>(this, _st, separator);
 | |
| 	return addAction(std::move(item));
 | |
| }
 | |
| 
 | |
| void Menu::clearActions() {
 | |
| 	_actionWidgets.clear();
 | |
| 	for (auto action : base::take(_actions)) {
 | |
| 		if (action->parent() == this) {
 | |
| 			delete action;
 | |
| 		}
 | |
| 	}
 | |
| 	resizeFromInner(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2);
 | |
| }
 | |
| 
 | |
| void Menu::finishAnimating() {
 | |
| 	for (const auto &widget : _actionWidgets) {
 | |
| 		widget->finishAnimating();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool Menu::empty() const {
 | |
| 	return _actionWidgets.empty();
 | |
| }
 | |
| 
 | |
| void Menu::resizeFromInner(int w, int h) {
 | |
| 	if (const auto s = QSize(w, h); s != size()) {
 | |
| 		resize(s);
 | |
| 		_resizesFromInner.fire({});
 | |
| 	}
 | |
| }
 | |
| 
 | |
| rpl::producer<> Menu::resizesFromInner() const {
 | |
| 	return _resizesFromInner.events();
 | |
| }
 | |
| 
 | |
| rpl::producer<ScrollToRequest> Menu::scrollToRequests() const {
 | |
| 	return _scrollToRequests.events();
 | |
| }
 | |
| 
 | |
| void Menu::setShowSource(TriggeredSource source) {
 | |
| 	const auto mouseSelection = (source == TriggeredSource::Mouse);
 | |
| 	setSelected(
 | |
| 		(mouseSelection || _actions.empty()) ? -1 : 0,
 | |
| 		mouseSelection);
 | |
| }
 | |
| 
 | |
| const std::vector<not_null<QAction*>> &Menu::actions() const {
 | |
| 	return _actions;
 | |
| }
 | |
| 
 | |
| void Menu::setForceWidth(int forceWidth) {
 | |
| 	_forceWidth = forceWidth;
 | |
| 	resizeFromInner(_forceWidth, height());
 | |
| }
 | |
| 
 | |
| void Menu::updateSelected(QPoint globalPosition) {
 | |
| 	const auto p = mapFromGlobal(globalPosition) - QPoint(0, _st.skip);
 | |
| 	for (const auto &widget : _actionWidgets) {
 | |
| 		const auto widgetRect = QRect(widget->pos(), widget->size());
 | |
| 		if (widgetRect.contains(p)) {
 | |
| 			_lastSelectedByMouse = true;
 | |
| 
 | |
| 			// It may actually fail to become selected (if it is disabled).
 | |
| 			widget->setSelected(true);
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Menu::itemPressed(TriggeredSource source) {
 | |
| 	if (const auto action = findSelectedAction()) {
 | |
| 		if (action->lastTriggeredSource() == source) {
 | |
| 			action->setClicked(source);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Menu::keyPressEvent(QKeyEvent *e) {
 | |
| 	const auto key = e->key();
 | |
| 	if (!_keyPressDelegate || !_keyPressDelegate(key)) {
 | |
| 		handleKeyPress(e);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| ItemBase *Menu::findSelectedAction() const {
 | |
| 	const auto it = ranges::find_if(_actionWidgets, &ItemBase::isSelected);
 | |
| 	return (it == end(_actionWidgets)) ? nullptr : it->get();
 | |
| }
 | |
| 
 | |
| void Menu::handleKeyPress(not_null<QKeyEvent*> e) {
 | |
| 	const auto key = e->key();
 | |
| 	const auto selected = findSelectedAction();
 | |
| 	if ((key != Qt::Key_Up && key != Qt::Key_Down) || _actions.empty()) {
 | |
| 		if (selected) {
 | |
| 			selected->handleKeyPress(e);
 | |
| 		}
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto delta = (key == Qt::Key_Down ? 1 : -1);
 | |
| 	auto start = selected ? selected->index() : -1;
 | |
| 	if (start < 0 || start >= _actions.size()) {
 | |
| 		start = (delta > 0) ? (_actions.size() - 1) : 0;
 | |
| 	}
 | |
| 	auto newSelected = start;
 | |
| 	do {
 | |
| 		newSelected += delta;
 | |
| 		if (newSelected < 0) {
 | |
| 			newSelected += _actions.size();
 | |
| 		} else if (newSelected >= _actions.size()) {
 | |
| 			newSelected -= _actions.size();
 | |
| 		}
 | |
| 	} while (newSelected != start
 | |
| 		&& (!_actionWidgets[newSelected]->isEnabled()));
 | |
| 
 | |
| 	if (_actionWidgets[newSelected]->isEnabled()) {
 | |
| 		setSelected(newSelected, false);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Menu::clearSelection() {
 | |
| 	setSelected(-1, false);
 | |
| }
 | |
| 
 | |
| void Menu::clearMouseSelection() {
 | |
| 	const auto selected = findSelectedAction();
 | |
| 	const auto mouseSelection = selected
 | |
| 		? (selected->lastTriggeredSource() == TriggeredSource::Mouse)
 | |
| 		: false;
 | |
| 	if (mouseSelection && !_childShownAction) {
 | |
| 		clearSelection();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Menu::setSelected(int selected, bool isMouseSelection) {
 | |
| 	if (selected >= _actionWidgets.size()) {
 | |
| 		selected = -1;
 | |
| 	}
 | |
| 	const auto source = isMouseSelection
 | |
| 		? TriggeredSource::Mouse
 | |
| 		: TriggeredSource::Keyboard;
 | |
| 	if (selected >= 0 && source == TriggeredSource::Keyboard) {
 | |
| 		const auto widget = _actionWidgets[selected].get();
 | |
| 		_scrollToRequests.fire({
 | |
| 			widget->y(),
 | |
| 			widget->y() + widget->height(),
 | |
| 		});
 | |
| 	}
 | |
| 	if (const auto selectedItem = findSelectedAction()) {
 | |
| 		if (selectedItem->index() == selected) {
 | |
| 			return;
 | |
| 		}
 | |
| 		selectedItem->setSelected(false, source);
 | |
| 	}
 | |
| 	if (selected >= 0) {
 | |
| 		_actionWidgets[selected].get()->setSelected(true, source);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Menu::mouseMoveEvent(QMouseEvent *e) {
 | |
| 	handleMouseMove(e->globalPos());
 | |
| }
 | |
| 
 | |
| void Menu::handleMouseMove(QPoint globalPosition) {
 | |
| 	const auto margins = style::margins(0, _st.skip, 0, _st.skip);
 | |
| 	const auto inner = rect().marginsRemoved(margins);
 | |
| 	const auto localPosition = mapFromGlobal(globalPosition);
 | |
| 	if (inner.contains(localPosition)) {
 | |
| 		updateSelected(globalPosition);
 | |
| 	} else {
 | |
| 		clearMouseSelection();
 | |
| 		if (_mouseMoveDelegate) {
 | |
| 			_mouseMoveDelegate(globalPosition);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Menu::mousePressEvent(QMouseEvent *e) {
 | |
| 	handleMousePress(e->globalPos());
 | |
| }
 | |
| 
 | |
| void Menu::mouseReleaseEvent(QMouseEvent *e) {
 | |
| 	handleMouseRelease(e->globalPos());
 | |
| }
 | |
| 
 | |
| void Menu::handleMousePress(QPoint globalPosition) {
 | |
| 	handleMouseMove(globalPosition);
 | |
| 	const auto margins = style::margins(0, _st.skip, 0, _st.skip);
 | |
| 	const auto inner = rect().marginsRemoved(margins);
 | |
| 	const auto localPosition = mapFromGlobal(globalPosition);
 | |
| 	const auto pressed = (inner.contains(localPosition)
 | |
| 		&& _lastSelectedByMouse)
 | |
| 		? findSelectedAction()
 | |
| 		: nullptr;
 | |
| 	if (pressed) {
 | |
| 		pressed->setClicked();
 | |
| 	} else {
 | |
| 		if (_mousePressDelegate) {
 | |
| 			_mousePressDelegate(globalPosition);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Menu::handleMouseRelease(QPoint globalPosition) {
 | |
| 	if (!rect().contains(mapFromGlobal(globalPosition))
 | |
| 			&& _mouseReleaseDelegate) {
 | |
| 		_mouseReleaseDelegate(globalPosition);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| } // namespace Ui::Menu
 | 
