#include "Menu.h" #include "Common/StyleSheet.h" #include "Common/Icon.h" #include "Common/SmoothScroll.h" #include "QFluentWidgets.h" #include #include #include #include #include #include #include #include #include CustomMenuStyle::CustomMenuStyle(int iconSize) : QProxyStyle(), m_iconSize(iconSize) { } int CustomMenuStyle::pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const { if (metric == QStyle::PM_SmallIconSize) { return m_iconSize; } return QProxyStyle::pixelMetric(metric, option, widget); } DWMMenu::DWMMenu(const QString &title, QWidget *parent) : QMenu(title, parent), m_customMenuStyle(new CustomMenuStyle) { // WindowEffect 阴影效果 setWindowFlags(Qt::FramelessWindowHint | Qt::Popup | Qt::NoDropShadowWindowHint); setAttribute(Qt::WA_StyledBackground); setStyle(m_customMenuStyle.data()); FluentStyleSheet::apply("MENU", this); } bool DWMMenu::event(QEvent *event) { if (event->type() == QEvent::WinIdChange) { // windowEffect.addMenuShadowEffect(self.winId()) } return QMenu::event(event); } MenuSeparator::MenuSeparator(QWidget *parent) : QWidget(parent) { setFixedHeight(9); } void MenuSeparator::paintEvent(QPaintEvent * /*event*/) { QPainter painter(this); int c = QFWIns.isDarkTheme() ? 255 : 0; QPen pen(QColor(c, c, c, 25), 1); pen.setCosmetic(true); painter.setPen(pen); painter.drawLine(0, 4, width(), 4); } /** * @brief Round corner menu * @param parent */ RoundMenu::RoundMenu(const QString &title, QWidget *parent) : QWidget(parent), m_title(title), m_isSubMenu(false), m_parentMenu(nullptr), m_menuItem(nullptr), m_lastHoverItem(nullptr), m_lastHoverSubMenuItem(nullptr), m_isHideBySystem(true), m_itemHeight(28), m_hBoxLayout(new QHBoxLayout(this)), m_view(new MenuActionListWidget(this)), m_ani(new QPropertyAnimation(this, "pos", this)), m_timer(new QTimer(this)), m_shadowEffect(nullptr), m_eventLoop(nullptr) { initWidgets(); } /// add shadow to dialog void RoundMenu::setShadowEffect(qreal blurRadius, const QPointF &offset, const QColor &color) { m_shadowEffect = new QGraphicsDropShadowEffect(m_view); m_shadowEffect->setBlurRadius(blurRadius); m_shadowEffect->setOffset(offset); m_shadowEffect->setColor(color); m_view->setGraphicsEffect(nullptr); m_view->setGraphicsEffect(m_shadowEffect); } void RoundMenu::initWidgets() { setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint); setAttribute(Qt::WA_TranslucentBackground); setMouseTracking(true); m_timer->setSingleShot(true); m_timer->setInterval(400); connect(m_timer, &QTimer::timeout, this, &RoundMenu::onShowMenuTimeOut); setShadowEffect(); m_hBoxLayout->setContentsMargins(12, 8, 12, 20); m_hBoxLayout->addWidget(m_view, 1, Qt::AlignCenter); setLayout(m_hBoxLayout); FluentStyleSheet::apply("MENU", this); connect(m_view, &MenuActionListWidget::itemClicked, this, &RoundMenu::onItemClicked); connect(m_view, &MenuActionListWidget::itemEntered, this, &RoundMenu::onItemEntered); connect(m_ani, &QPropertyAnimation::valueChanged, this, &RoundMenu::onSlideValueChanged); } QString RoundMenu::title() const { return m_title; } QIcon RoundMenu::icon() const { return m_icon; } void RoundMenu::setIcon(const QIcon &icon) { m_icon = icon; } /// clear all actions void RoundMenu::clear() { while (!m_actions.isEmpty()) { removeAction(m_actions.last()); } } /// remove action from menu void RoundMenu::removeAction(QAction *action) { if (!m_actions.contains(action)) { return; } int index = m_actions.indexOf(action); m_actions.removeAt(index); action->setProperty("item", 0); QListWidgetItem *item = m_view->takeItem(index); item->setData(Qt::UserRole, 0); // delete widget QWidget *widget = m_view->itemWidget(item); if (widget) { widget->deleteLater(); widget = nullptr; } delete item; action->deleteLater(); } /// add action to menu void RoundMenu::addAction(QAction *const action) { QListWidgetItem *item = createActionItem(action); if (!item) { return; } m_view->addItem(item); adjustSize(); } void RoundMenu::insertAction(QAction *const before, QAction *const action) { if (!m_actions.contains(before)) { return; } QListWidgetItem *beforeItem = before->property("item").value(); if (!beforeItem) { return; } int index = m_view->row(beforeItem); QListWidgetItem *item = createActionItem(action, before); m_view->insertItem(index, item); adjustSize(); } void RoundMenu::addActions(const QList &actions) { for (auto a : actions) { addAction(a); } } void RoundMenu::insertActions(QAction *const before, const QList &actions) { for (auto a : actions) { insertAction(before, a); } } void RoundMenu::setDefaultAction(QAction *const action) { if (!m_actions.contains(action)) { return; } int index = m_actions.indexOf(action); m_view->setCurrentRow(index); } void RoundMenu::addMenu(RoundMenu *const menu) { if (!menu) { return; } auto itemW = createSubMenuItem(menu); if (itemW.first == nullptr || itemW.second == nullptr) { return; } m_view->addItem(itemW.first); m_view->setItemWidget(itemW.first, itemW.second); this->adjustSize(); } void RoundMenu::insertMenu(QAction *const before, RoundMenu *const menu) { if (!menu) { qCritical() << "`menu` should be an instance of `RoundMenu`."; return; } if (!m_actions.contains(before)) { qCritical() << "`before` should be in menu action list"; return; } auto itemW = createSubMenuItem(menu); if (itemW.first == nullptr || itemW.second == nullptr) { return; } m_view->insertItem(m_view->row(before->property("item").value()), itemW.first); m_view->setItemWidget(itemW.first, itemW.second); this->adjustSize(); } void RoundMenu::setItemHeight(int itemHeight) { if (m_itemHeight == itemHeight) { return; } m_itemHeight = itemHeight; m_view->setItemHeight(itemHeight); } void RoundMenu::adjustSize() { QMargins m = this->layout()->contentsMargins(); int w = m_view->width() + m.left() + m.right(); int h = m_view->height() + m.top() + m.bottom(); setFixedSize(w, h); } void RoundMenu::setParentMenu(RoundMenu *parent, QListWidgetItem *item) { m_parentMenu = parent; m_menuItem = item; m_isSubMenu = parent == nullptr ? false : true; } QListWidgetItem *RoundMenu::createActionItem(QAction *const action, QAction *const before) { if (!before) { m_actions.append(action); } else if (m_actions.contains(before)) { int index = m_actions.indexOf(before); m_actions.insert(index, action); } else { qCritical() << QString("`before` is not in the action list"); return nullptr; } QListWidgetItem *item = new QListWidgetItem(createItemIcon(action), action->text()); int w = 0; if (!hasItemIcon()) { w = 28 + m_view->fontMetrics().width(action->text()); } else { // add a blank character to increase space between icon and text item->setText(" " + item->text()); w = 60 + m_view->fontMetrics().width(item->text()); } item->setSizeHint(QSize(w, m_itemHeight)); item->setData(Qt::UserRole, QVariant::fromValue(action)); action->setProperty("item", QVariant::fromValue(item)); connect(action, &QAction::changed, this, &RoundMenu::onActionChanged); return item; } bool RoundMenu::hasItemIcon() { for (auto a : m_actions) { if (!a->icon().isNull()) { return true; } } for (auto m : m_subMenus) { if (!m->icon().isNull()) { return true; } } return false; } QIcon RoundMenu::createItemIcon(QAction *const action) { bool hasIcon = hasItemIcon(); if (hasIcon && action->icon().isNull()) { QPixmap pixmap(m_view->iconSize()); pixmap.fill(Qt::transparent); return QIcon(pixmap); } else if (!hasIcon) { return QIcon(); } QIcon icon(new MenuIconEngine(action->icon())); return icon; } QIcon RoundMenu::createItemIcon(RoundMenu *const menu) { bool hasIcon = hasItemIcon(); if (hasIcon && menu->icon().isNull()) { QPixmap pixmap(m_view->iconSize()); pixmap.fill(Qt::transparent); return QIcon(pixmap); } else if (!hasIcon) { return QIcon(); } QIcon icon(new MenuIconEngine(menu->icon())); return icon; } QPair RoundMenu::createSubMenuItem(RoundMenu *const menu) { if (!menu) { return QPair(nullptr, nullptr); } m_subMenus.append(menu); QListWidgetItem *item = new QListWidgetItem(createItemIcon(menu), menu->title()); int w = 0; if (!hasItemIcon()) { w = 48 + m_view->fontMetrics().width(menu->title()); } else { // add a blank character to increase space between icon and text item->setText(" " + item->text()); w = 60 + m_view->fontMetrics().width(item->text()); } // add submenu item menu->setParentMenu(this, item); item->setSizeHint(QSize(w, m_itemHeight)); item->setData(Qt::UserRole, QVariant::fromValue(menu)); SubMenuItemWidget *subw = new SubMenuItemWidget(menu, item, this); connect(subw, &SubMenuItemWidget::showMenuSig, this, &RoundMenu::showSubMenu); subw->resize(item->sizeHint()); return QPair(item, subw); } void RoundMenu::hideMenu(bool hideBySystem) { m_isHideBySystem = hideBySystem; m_view->clearSelection(); if (m_isSubMenu) { hide(); } else { close(); } } void RoundMenu::closeParentMenu() { RoundMenu *menu = this; while (menu->m_parentMenu) { menu->close(); menu = menu->parentMenu(); } menu->close(); } void RoundMenu::mouseMoveEvent(QMouseEvent *event) { if (!m_isSubMenu) { return; } // hide submenu when mouse moves out of submenu item QPoint pos = event->globalPos(); MenuActionListWidget *view = m_parentMenu->view(); // get the rect of menu item QMargins margin = view->viewportMargins(); QRect rect = view->visualItemRect(m_menuItem).translated(view->mapToGlobal(QPoint())); rect = rect.translated(margin.left(), margin.top() + 2); if (m_parentMenu->geometry().contains(pos) && !rect.contains(pos) && !this->geometry().contains(pos)) { view->clearSelection(); hideMenu(false); // update style int index = view->row(m_menuItem); if (index > 0) { view->item(index - 1)->setFlags(Qt::ItemIsEnabled); } if (index < view->count() - 1) { view->item(index + 1)->setFlags(Qt::ItemIsEnabled); } } } void RoundMenu::closeEvent(QCloseEvent *event) { // 关闭窗口时结束事件循环,在exec()方法中返回选择结果; if (m_eventLoop != nullptr) { m_eventLoop->exit(); } event->accept(); emit closedSignal(); } void RoundMenu::hideEvent(QHideEvent *event) { if (m_isHideBySystem && m_isSubMenu) { closeParentMenu(); } m_isHideBySystem = true; event->accept(); } void RoundMenu::onShowMenuTimeOut() { if ((m_lastHoverSubMenuItem == nullptr) || (m_lastHoverItem == m_lastHoverSubMenuItem)) { return; } SubMenuItemWidget *sub = qobject_cast(m_view->itemWidget(m_lastHoverSubMenuItem)); RoundMenu *w = sub->roundMenu(); if (w && w->parentMenu()->isHidden()) { return; } QPoint pos = w->mapToGlobal(QPoint(w->width() + 5, -5)); w->exec(pos); } void RoundMenu::onItemClicked(QListWidgetItem *item) { QAction *action = item->data(Qt::UserRole).value(); if (!m_actions.contains(action)) { return; } hideMenu(); if (!m_isSubMenu) { action->trigger(); return; } // close parent menu closeParentMenu(); action->trigger(); } void RoundMenu::onItemEntered(QListWidgetItem *item) { m_lastHoverItem = item; RoundMenu *menu = item->data(Qt::UserRole).value(); if (!menu) { return; } showSubMenu(item); } void RoundMenu::onSlideValueChanged(const QVariant &value) { QMargins m = this->layout()->contentsMargins(); int w = m_view->width() + m.left() + m.right() + 120; int h = m_view->height() + m.top() + m.bottom() + 20; int y = m_ani->endValue().toPoint().y() - value.toPoint().y(); this->setMask(QRegion(0, y, w, h)); } void RoundMenu::onActionChanged() { QAction *action = static_cast(sender()); QListWidgetItem *item = action->property("item").value(); item->setIcon(createItemIcon(action)); int w = 0; if (!hasItemIcon()) { item->setText(action->text()); w = 28 + m_view->fontMetrics().width(action->text()); } else { // add a blank character to increase space between icon and text item->setText(" " + action->text()); w = 60 + m_view->fontMetrics().width(item->text()); } item->setSizeHint(QSize(w, m_itemHeight)); m_view->adjustSize(); this->adjustSize(); } void RoundMenu::showSubMenu(QListWidgetItem *const item) { m_lastHoverItem = item; m_lastHoverSubMenuItem = item; // delay 400 ms to anti-shake m_timer->stop(); m_timer->start(); } QList RoundMenu::menuActions() const { return m_actions; } MenuActionListWidget *RoundMenu::view() const { return m_view; } void RoundMenu::addSeparator() { QMargins m = m_view->viewportMargins(); int w = m_view->width() - m.left() - m.right(); // icon separator MenuSeparator *separator = new MenuSeparator(m_view); separator->resize(w, separator->height()); // add separator to list widget QListWidgetItem *item = new QListWidgetItem(m_view); item->setFlags(Qt::NoItemFlags); item->setSizeHint(QSize(w, separator->height())); m_view->addItem(item); m_view->setItemWidget(item, separator); this->adjustSize(); } RoundMenu *RoundMenu::parentMenu() const { return m_parentMenu; } /** * @brief show menu * @param pos: pop-up position * @param ani: Whether to show pop-up animation */ void RoundMenu::exec(const QPoint &pos, bool ani) { if (isVisible()) { return; } QPoint showPos; QRect rect = QApplication::screenAt(QCursor::pos())->availableGeometry(); int w = width() + 5; int h = height() + 5; showPos.setX(qMin(pos.x() - layout()->contentsMargins().left(), rect.right() - w)); showPos.setY(qMin(pos.y() - 4, rect.bottom() - h)); if (ani) { m_ani->setStartValue(showPos - QPoint(0, int(h / 2))); m_ani->setEndValue(pos); m_ani->setDuration(250); m_ani->setEasingCurve(QEasingCurve::OutQuad); m_ani->start(); } else { move(showPos); } this->setWindowModality(Qt::ApplicationModal); // 设置为模态状态 show(); if (m_isSubMenu) { m_menuItem->setSelected(true); // temporarily disable item to change style MenuActionListWidget *view = m_parentMenu->view(); int index = view->row(m_menuItem); if (index > 0) { view->item(index - 1)->setFlags(Qt::NoItemFlags); } if (index < view->count() - 1) { view->item(index + 1)->setFlags(Qt::NoItemFlags); } } // 使用事件循环QEventLoop // ,不让exec()方法结束,在用户选择确定或者取消后,关闭窗口结束事件循环,并返回最后用户选择的结果; // 根据返回结果得到用户按下了确定还是取消,采取相应的操作。从而模拟出QDialog类的exec()方法; m_eventLoop = new QEventLoop(this); m_eventLoop->exec(); } SubMenuItemWidget::SubMenuItemWidget(QMenu *menu, QListWidgetItem *item, QWidget *parent) : QWidget(parent), m_menu(menu), m_roundMenu(nullptr), m_item(item) { } SubMenuItemWidget::SubMenuItemWidget(RoundMenu *menu, QListWidgetItem *item, QWidget *parent) : QWidget(parent), m_menu(nullptr), m_roundMenu(menu), m_item(item) { } void SubMenuItemWidget::enterEvent(QEvent *event) { QWidget::enterEvent(event); emit showMenuSig(m_item); } void SubMenuItemWidget::paintEvent(QPaintEvent * /*event*/) { QPainter painter(this); painter.setRenderHints(QPainter::Antialiasing); // draw right arrow // NOTE: 未完成 QScopedPointer ficon(NEWFLICON(FluentIcon, CHEVRON_RIGHT)); ficon->render(&painter, QRect(width() - 10, height() / 2 - 9 / 2, 9, 9)); } RoundMenu *SubMenuItemWidget::roundMenu() const { return m_roundMenu; } MenuActionListWidget::MenuActionListWidget(QWidget *parent) : QListWidget(parent) { setViewportMargins(5, 6, 5, 6); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setTextElideMode(Qt::ElideNone); setDragEnabled(false); setMouseTracking(true); setVerticalScrollMode(ScrollPerPixel); setIconSize(QSize(14, 14)); m_smoothScroll.reset(new SmoothScroll(this, Qt::Vertical)); setStyleSheet("MenuActionListWidget{font: 14px \"Segoe UI\", \"Microsoft YaHei\"}"); } void MenuActionListWidget::insertItem(int row, QListWidgetItem *item) { QListWidget::insertItem(row, item); adjustSize(); } void MenuActionListWidget::addItem(QListWidgetItem *item) { QListWidget::addItem(item); adjustSize(); } QListWidgetItem *MenuActionListWidget::takeItem(int row) { QListWidgetItem *item = QListWidget::takeItem(row); adjustSize(); return item; } void MenuActionListWidget::adjustSize() { QSize size; for (int i = 0; i < this->count(); i++) { QSize s = this->item(i)->sizeHint(); size.setWidth(qMax(s.width(), size.width())); size.setHeight(size.height() + s.height()); } // adjust the height of viewport QSize ss = QApplication::screenAt(QCursor::pos())->availableSize(); int w = ss.width() - 100; int h = ss.height() - 100; QSize vSize(qMin(w - 12, size.width()), qMin(h - 12, size.height())); viewport()->adjustSize(); // adjust the height of list widget QMargins m = viewportMargins(); size += QSize(m.left() + m.right() + 2, m.top() + m.bottom()); size.setHeight(qMin(h, size.height() + 3)); size.setWidth(qMin(w, size.width())); setFixedSize(size); } void MenuActionListWidget::setItemHeight(int height) { for (int i = 0; i < this->count(); ++i) { QListWidgetItem *item = this->item(i); item->setSizeHint(QSize(item->sizeHint().width(), height)); } this->adjustSize(); } void MenuActionListWidget::wheelEvent(QWheelEvent *event) { // only process the wheel events triggered by mouse if ((m_smoothScroll->smoothMode() == SmoothMode::NO_SMOOTH) || (abs(event->angleDelta().y()) % 120 != 0)) { QListWidget::wheelEvent(event); } else { m_smoothScroll->wheelEvent(event); } } EditMenu::EditMenu(QWidget *parent) : RoundMenu("", parent), cutAct(nullptr), copyAct(nullptr), pasteAct(nullptr), cancelAct(nullptr), selectAllAct(nullptr) { createActions(); } void EditMenu::createActions() { cutAct = new QAction(QIcon(":/qfluentwidgets/images/icons/Cut_black.svg"), tr("Cut"), this); cutAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_X)); copyAct = new QAction(QIcon(":/qfluentwidgets/images/icons/Copy_black.svg"), tr("Copy"), this); copyAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C)); pasteAct = new QAction(QIcon(":/qfluentwidgets/images/icons/Paste_black.svg"), tr("Paste"), this); pasteAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_V)); cancelAct = new QAction(QIcon(":/qfluentwidgets/images/icons/Cancel_black.svg"), tr("Cancel"), this); cancelAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Z)); selectAllAct = new QAction(tr("Select all"), this); selectAllAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_A)); actionList << cutAct << copyAct << pasteAct << cancelAct << selectAllAct; } void EditMenu::exec(const QPoint &pos, bool ani) { // clear(); // createActions(); if (QApplication::clipboard()->mimeData()->hasText()) { if (!parentText().isEmpty()) { if (!parentSelectedText().isEmpty()) { addActions(actionList); } else { for (int i = 2; i < actionList.count(); ++i) { addAction(actionList.at(i)); } } } else { addAction(pasteAct); } } else { if (!parentText().isEmpty()) { if (!parentSelectedText().isEmpty()) { for (int i = 0; i < actionList.count(); ++i) { if (i != 2) { addAction(actionList.at(i)); } } } else { for (int i = 3; i < actionList.count(); ++i) { addAction(actionList.at(i)); } } } else { return; } } RoundMenu::exec(pos, ani); } LineEditMenu::LineEditMenu(QLineEdit *parent) : EditMenu(parent) { m_selectionStart = parent->selectionStart(); m_selectionLength = parent->selectionLength(); } void LineEditMenu::onItemClicked(QListWidgetItem *item) { QLineEdit *p = qobject_cast(parent()); if (m_selectionStart >= 0) { p->setSelection(m_selectionStart, m_selectionLength); } EditMenu::onItemClicked(item); } QString LineEditMenu::parentText() { QLineEdit *p = qobject_cast(parent()); return p->text(); } QString LineEditMenu::parentSelectedText() { QLineEdit *p = qobject_cast(parent()); return p->selectedText(); } TextEditMenu::TextEditMenu(QTextEdit *parent) : EditMenu(parent), m_textEdit(parent), m_plainTextEdit(nullptr) { m_selectionStart = parent->textCursor().selectionStart(); m_selectionLength = parent->textCursor().selectionEnd() - parent->textCursor().selectionStart() + 1; } TextEditMenu::TextEditMenu(QPlainTextEdit *parent) : EditMenu(parent), m_textEdit(nullptr), m_plainTextEdit(parent) { m_selectionStart = parent->textCursor().selectionStart(); m_selectionLength = parent->textCursor().selectionEnd() - parent->textCursor().selectionStart() + 1; } void TextEditMenu::onItemClicked(QListWidgetItem *item) { if (m_selectionStart >= 0) { QTextCursor cursor; if (m_textEdit) { cursor = m_textEdit->textCursor(); } else { cursor = m_plainTextEdit->textCursor(); } cursor.setPosition(m_selectionStart); cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, m_selectionLength); } EditMenu::onItemClicked(item); } QString TextEditMenu::parentText() { if (m_textEdit) { return m_textEdit->toPlainText(); } else { return m_plainTextEdit->toPlainText(); } } QString TextEditMenu::parentSelectedText() { if (m_textEdit) { return m_textEdit->textCursor().selectedText(); } else { return m_plainTextEdit->textCursor().selectedText(); } }