QmlDesigner: Handle QmlEditorMenu in Manhattan style

* QmlEditorMenu is handled in Manhattan style.
* Icons are available for cascading menu items in macOS.
* Shortkeys hints are handled and right aligned in the menu.
* The style is customized  QmlEditorMenu.

Task-number: QDS-8720
Change-Id: Iff5ebae0dce70dade5b48a1abe4232e70d6953d6
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Ali Kianian
2023-01-20 14:06:19 +02:00
parent 445e8d6dd2
commit 757a6f2e12
9 changed files with 834 additions and 24 deletions

View File

@@ -26,6 +26,7 @@
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QPixmapCache>
#include <QSpinBox>
#include <QStatusBar>
#include <QStyleFactory>
@@ -88,6 +89,23 @@ bool panelWidget(const QWidget *widget)
return false;
}
// Consider making this a QStyle state
static bool isQmlEditorMenu(const QWidget *widget)
{
const QMenu *menu = qobject_cast<const QMenu *> (widget);
if (!menu)
return false;
const QWidget *p = widget;
while (p) {
if (p->property("qmlEditorMenu").toBool())
return styleEnabled(widget);
p = p->parentWidget();
}
return false;
}
// Consider making this a QStyle state
bool lightColored(const QWidget *widget)
{
@@ -112,6 +130,246 @@ static bool isDarkFusionStyle(const QStyle *style)
&& strcmp(style->metaObject()->className(), "QFusionStyle") == 0;
}
QColor qmlEditorTextColor(bool enabled,
bool active,
bool checked)
{
Theme::Color themePenColorId = enabled ? (active
? (checked ? Theme::DSsubPanelBackground
: Theme::DSpanelBackground)
: Theme::DStextColor)
: Theme::DStextColorDisabled;
return creatorTheme()->color(themePenColorId);
}
QPixmap getDeletePixmap(bool enabled,
bool active,
const QSize &sizeLimit)
{
using Utils::Theme;
using Utils::creatorTheme;
using Utils::StyleHelper;
const double xRatio = 19;
const double yRatio = 9;
double sizeConst = std::min(xRatio * sizeLimit.height(), yRatio * sizeLimit.width());
sizeConst = std::max(xRatio, sizeConst);
const int height = sizeConst/xRatio;
const int width = sizeConst/yRatio;
QPixmap retval(width, height);
QPainter p(&retval);
const qreal devicePixelRatio = p.device()->devicePixelRatio();
QPixmap pixmap;
QString pixmapName = QLatin1String("StyleHelper::drawDelete")
+ "-" + QString::number(sizeConst)
+ "-" + QString::number(enabled)
+ "-" + QString::number(active)
+ "-" + QString::number(devicePixelRatio);
if (!QPixmapCache::find(pixmapName, &pixmap)) {
QImage image(width * devicePixelRatio, height * devicePixelRatio, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
QPainter painter(&image);
auto drawDelete = [&painter, yRatio](const QRect &rect, const QColor &color) -> void
{
static const QStyle* const style = QApplication::style();
if (!style)
return;
const int height = rect.height();
const int width = rect.width();
const int insideW = height / 2;
const int penWidth = std::ceil(height / yRatio);
const int pixelGuard = penWidth / 2;
const QRect xRect = {insideW + (4 * penWidth),
2 * penWidth,
4 * penWidth,
5 * penWidth};
// Workaround for QTCREATORBUG-28470
painter.save();
painter.setOpacity(color.alphaF());
QPen pen(color, penWidth);
pen.setJoinStyle(Qt::MiterJoin);
painter.setPen(pen);
QPainterPath pp(QPointF(pixelGuard, insideW));
pp.lineTo(insideW, pixelGuard);
pp.lineTo(width - pixelGuard, pixelGuard);
pp.lineTo(width - pixelGuard, height - pixelGuard);
pp.lineTo(insideW, height - pixelGuard);
pp.lineTo(pixelGuard, insideW);
painter.drawPath(pp);
// drawing X
painter.setPen(QPen(color, 1));
QPoint stepOver(penWidth, 0);
pp.clear();
pp.moveTo(xRect.topLeft());
pp.lineTo(xRect.topLeft() + stepOver);
pp.lineTo(xRect.bottomRight());
pp.lineTo(xRect.bottomRight() - stepOver);
pp.lineTo(xRect.topLeft());
painter.fillPath(pp, QBrush(color));
pp.clear();
pp.moveTo(xRect.topRight());
pp.lineTo(xRect.topRight() - stepOver);
pp.lineTo(xRect.bottomLeft());
pp.lineTo(xRect.bottomLeft() + stepOver);
pp.lineTo(xRect.topRight());
painter.fillPath(pp, QBrush(color));
painter.restore();
};
if (enabled && creatorTheme()->flag(Theme::ToolBarIconShadow))
drawDelete(image.rect().translated(0, devicePixelRatio), StyleHelper::toolBarDropShadowColor());
drawDelete(image.rect(), qmlEditorTextColor(enabled, active, false));
painter.end();
pixmap = QPixmap::fromImage(image);
pixmap.setDevicePixelRatio(devicePixelRatio);
QPixmapCache::insert(pixmapName, pixmap);
}
return pixmap;
}
struct ManhattanShortcut {
ManhattanShortcut(const QStyleOptionMenuItem *option,
const QString &shortcutText)
: shortcutText(shortcutText)
, enabled(option->state & QStyle::State_Enabled)
, active(option->state & QStyle::State_Selected)
, font(option->font)
, fm(font)
, defaultHeight(fm.height())
, palette(option->palette)
, spaceConst(fm.boundingRect(".").width())
{
reset();
}
QSize getSize()
{
if (isFirstParticle)
calcResult();
return _size;
}
QPixmap getPixmap()
{
if (!isFirstParticle && !_pixmap.isNull())
return _pixmap;
_pixmap = QPixmap(getSize());
_pixmap.fill(Qt::transparent);
QPainter painter(&_pixmap);
painter.setFont(font);
QPen pPen = painter.pen();
pPen.setColor(qmlEditorTextColor(enabled, active, false));
painter.setPen(pPen);
calcResult(&painter);
painter.end();
return _pixmap;
}
private:
void applySize(const QSize &itemSize) {
width += itemSize.width();
height = std::max(height, itemSize.height());
if (isFirstParticle)
isFirstParticle = false;
else
width += spaceConst;
};
void addText(const QString &txt, QPainter *painter = nullptr)
{
if (txt.size()) {
int textWidth = fm.boundingRect(txt).width();
QSize itemSize = {textWidth, defaultHeight};
if (painter) {
QRect placeRect({width, 0}, itemSize);
painter->drawText(placeRect, txt, textOption);
}
applySize(itemSize);
}
};
void addPixmap(const QPixmap &pixmap, QPainter *painter = nullptr)
{
if (painter)
painter->drawPixmap(QRect({width, 0}, pixmap.size()), pixmap);
applySize(pixmap.size());
};
void calcResult(QPainter *painter = nullptr)
{
reset();
#ifndef QT_NO_SHORTCUT
if (!shortcutText.isEmpty()) {
int fwdIndex = 0;
QRegularExpressionMatch mMatch = backspaceDetect.match(shortcutText);
int matchCount = mMatch.lastCapturedIndex();
for (int i = 0; i <= matchCount; ++i) {
QString mStr = mMatch.captured(i);
QPixmap pixmap = getDeletePixmap(enabled,
active,
{defaultHeight * 3, defaultHeight});
int lIndex = shortcutText.indexOf(mStr, fwdIndex);
int diffChars = lIndex - fwdIndex;
addText(shortcutText.mid(fwdIndex, diffChars), painter);
addPixmap(pixmap, painter);
fwdIndex = lIndex + mStr.size();
}
addText(shortcutText.mid(fwdIndex), painter);
}
#endif
_size = {width, height};
}
void reset()
{
isFirstParticle = true;
width = 0;
height = 0;
}
const QString shortcutText;
const bool enabled;
const bool active;
const QFont font;
const QFontMetrics fm;
const int defaultHeight;
const QPalette palette;
const int spaceConst;
static const QTextOption textOption;
static const QRegularExpression backspaceDetect;
bool isFirstParticle = true;
int width = 0;
int height = 0;
QSize _size;
QPixmap _pixmap;
};
const QRegularExpression ManhattanShortcut::backspaceDetect("\\+*backspace\\+*",
QRegularExpression::CaseInsensitiveOption);
const QTextOption ManhattanShortcut::textOption(Qt::AlignLeft | Qt::AlignVCenter);
class ManhattanStylePrivate
{
public:
@@ -152,10 +410,48 @@ QSize ManhattanStyle::sizeFromContents(ContentsType type, const QStyleOption *op
{
QSize newSize = QProxyStyle::sizeFromContents(type, option, size, widget);
if (type == CT_Splitter && widget && widget->property("minisplitter").toBool())
return QSize(1, 1);
else if (type == CT_ComboBox && panelWidget(widget))
newSize += QSize(14, 0);
switch (type) {
case CT_Splitter:
if (widget && widget->property("minisplitter").toBool())
newSize = QSize(1, 1);
break;
case CT_ComboBox:
if (panelWidget(widget))
newSize += QSize(14, 0);
break;
case CT_MenuItem:
if (isQmlEditorMenu(widget)) {
if (const auto mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) {
const int horizontalSpacing = pixelMetric(QStyle::PM_LayoutHorizontalSpacing, option, widget);
const int iconHeight = pixelMetric(QStyle::PM_SmallIconSize, option, widget) + horizontalSpacing;
int width = horizontalSpacing;
if (mbi->menuHasCheckableItems || mbi->maxIconWidth)
width += iconHeight + horizontalSpacing;
if (!mbi->text.isEmpty())
width += option->fontMetrics.boundingRect(mbi->text).width() + horizontalSpacing;
if (mbi->menuItemType == QStyleOptionMenuItem::SubMenu)
width += iconHeight + horizontalSpacing;
newSize.setWidth(width);
switch (mbi->menuItemType) {
case QStyleOptionMenuItem::Normal:
case QStyleOptionMenuItem::DefaultItem:
newSize += QSize(0, 5);
break;
default:
newSize += QSize(0, 7);
break;
}
}
}
break;
default:
break;
}
return newSize;
}
@@ -186,8 +482,8 @@ QStyle::SubControl ManhattanStyle::hitTestComplexControl(ComplexControl control,
int ManhattanStyle::pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const
{
int retval = 0;
retval = QProxyStyle::pixelMetric(metric, option, widget);
int retval = QProxyStyle::pixelMetric(metric, option, widget);
switch (metric) {
case PM_SplitterWidth:
if (widget && widget->property("minisplitter").toBool())
@@ -199,16 +495,27 @@ int ManhattanStyle::pixelMetric(PixelMetric metric, const QStyleOption *option,
retval = 16;
break;
case PM_SmallIconSize:
retval = 16;
if (isQmlEditorMenu(widget))
retval = 10;
else
retval = 16;
break;
case PM_DockWidgetHandleExtent:
case PM_DockWidgetSeparatorExtent:
return 1;
case PM_LayoutHorizontalSpacing:
if (isQmlEditorMenu(widget))
retval = 5;
break;
case PM_MenuHMargin:
if (isQmlEditorMenu(widget))
retval = 12;
break;
case PM_MenuPanelWidth:
case PM_MenuBarHMargin:
case PM_MenuBarVMargin:
case PM_ToolBarFrameWidth:
if (panelWidget(widget))
if (panelWidget(widget) || isQmlEditorMenu(widget))
retval = 1;
break;
case PM_ButtonShiftVertical:
@@ -534,8 +841,11 @@ static void drawPrimitiveTweakedForDarkTheme(QStyle::PrimitiveElement element,
void ManhattanStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *option,
QPainter *painter, const QWidget *widget) const
{
const bool isPanelWidget = panelWidget(widget);
if (!isPanelWidget) {
if (panelWidget(widget)) {
drawPrimitiveForPanelWidget(element, option, painter, widget);
} else if (isQmlEditorMenu(widget)) {
drawPrimitiveForQmlEditor(element, option, painter, widget);
} else {
const bool tweakDarkTheme =
(element == PE_Frame
|| element == PE_FrameLineEdit
@@ -550,7 +860,13 @@ void ManhattanStyle::drawPrimitive(PrimitiveElement element, const QStyleOption
QProxyStyle::drawPrimitive(element, option, painter, widget);
return;
}
}
void ManhattanStyle::drawPrimitiveForPanelWidget(PrimitiveElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget) const
{
bool animating = (option->state & State_Animating);
int state = option->state;
QRect rect = option->rect;
@@ -779,6 +1095,219 @@ void ManhattanStyle::drawPrimitive(PrimitiveElement element, const QStyleOption
}
}
void ManhattanStyle::drawPrimitiveForQmlEditor(PrimitiveElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget) const
{
const auto mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option);
if (!mbi) {
QProxyStyle::drawPrimitive(element, option, painter, widget);
return;
}
switch (element) {
case PE_IndicatorArrowUp:
case PE_IndicatorArrowDown:
{
QStyleOptionMenuItem item = *mbi;
item.palette = QPalette(Qt::white);
StyleHelper::drawMinimalArrow(element, painter, &item);
}
break;
case PE_IndicatorArrowRight:
drawQmlEditorIcon(element, option, "cascadeIconRight", painter, widget);
break;
case PE_IndicatorArrowLeft:
drawQmlEditorIcon(element, option, "cascadeIconLeft", painter, widget);
break;
case PE_PanelButtonCommand:
break;
case PE_IndicatorMenuCheckMark:
drawQmlEditorIcon(element, option, "tickIcon", painter, widget);
break;
case PE_FrameMenu:
case PE_PanelMenu:
{
painter->save();
painter->setBrush(creatorTheme()->color(Theme::DSsubPanelBackground));
painter->setPen(Qt::NoPen);
painter->drawRect(option->rect);
painter->restore();
}
break;
default:
QProxyStyle::drawPrimitive(element, option, painter, widget);
break;
}
}
void ManhattanStyle::drawControlForQmlEditor(ControlElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget) const
{
if (const auto mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) {
painter->save();
const int iconHeight = pixelMetric(QStyle::PM_SmallIconSize, option, widget);
const int horizontalSpacing = pixelMetric(QStyle::PM_LayoutHorizontalSpacing, option, widget);
const int iconWidth = iconHeight;
const bool isActive = mbi->state & State_Selected;
const bool isDisabled = !(mbi->state & State_Enabled);
const bool isCheckable = mbi->checkType != QStyleOptionMenuItem::NotCheckable;
const bool isChecked = isCheckable ? mbi->checked : false;
int forwardX = 0;
QStyleOptionMenuItem item = *mbi;
if (isActive) {
painter->fillRect(item.rect, creatorTheme()->color(Theme::DSinteraction));
}
forwardX += horizontalSpacing;
if (item.menuItemType == QStyleOptionMenuItem::Separator) {
int commonHeight = item.rect.center().y();
int additionalMargin = forwardX /*hmargin*/;
QLineF separatorLine (item.rect.left() + additionalMargin,
commonHeight,
item.rect.right() - additionalMargin,
commonHeight);
painter->setPen(creatorTheme()->color(Theme::DSstateSeparatorColor));
painter->drawLine(separatorLine);
item.text.clear();
painter->restore();
return;
}
QPixmap iconPixmap;
QIcon::Mode mode = isDisabled ? QIcon::Disabled : ((isActive) ? QIcon::Active : QIcon::Normal);
QIcon::State state = isChecked ? QIcon::On : QIcon::Off;
QColor themePenColor = qmlEditorTextColor(!isDisabled, isActive, isChecked);
if (!item.icon.isNull()) {
iconPixmap = item.icon.pixmap(QSize(iconHeight, iconHeight), mode, state);
} else if (isCheckable) {
iconPixmap = QPixmap(iconHeight, iconHeight);
iconPixmap.fill(Qt::transparent);
if (item.checked) {
QStyleOptionMenuItem so = item;
so.rect = iconPixmap.rect();
QPainter dPainter(&iconPixmap);
dPainter.setPen(themePenColor);
drawPrimitive(PE_IndicatorMenuCheckMark, &so, &dPainter, widget);
}
}
if (!iconPixmap.isNull()) {
QRect vCheckRect = visualRect(item.direction,
item.rect,
QRect(item.rect.x() + forwardX,
item.rect.y(),
iconWidth,
item.rect.height()));
QRect pmr(QPoint(0, 0), iconPixmap.deviceIndependentSize().toSize());
pmr.moveCenter(vCheckRect.center());
painter->setPen(themePenColor);
painter->drawPixmap(pmr.topLeft(), iconPixmap);
item.checkType = QStyleOptionMenuItem::NotCheckable;
item.checked = false;
item.icon = {};
}
if (item.menuHasCheckableItems || item.maxIconWidth > 0) {
forwardX += iconWidth + horizontalSpacing;
}
QString shortcutText;
int tabIndex = item.text.indexOf("\t");
if (tabIndex > -1) {
shortcutText = item.text.mid(tabIndex + 1);
item.text = item.text.left(tabIndex);
}
if (item.text.size()) {
painter->save();
QRect vTextRect = visualRect(item.direction,
item.rect,
item.rect.adjusted(forwardX, 0 , 0 , 0));
Qt::Alignment alignmentFlags = item.direction == Qt::LeftToRight ? Qt::AlignLeft
: Qt::AlignRight;
alignmentFlags |= Qt::AlignVCenter;
int textFlags = Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine;
if (!proxy()->styleHint(SH_UnderlineShortcut, &item, widget))
textFlags |= Qt::TextHideMnemonic;
textFlags |= alignmentFlags;
painter->setPen(themePenColor);
painter->drawText(vTextRect, textFlags, item.text);
painter->restore();
}
if (item.menuItemType == QStyleOptionMenuItem::SubMenu) {
PrimitiveElement dropDirElement = item.direction == Qt::LeftToRight ? PE_IndicatorArrowRight
: PE_IndicatorArrowLeft;
QSize elSize(iconHeight, iconHeight);
int xOffset = iconHeight;
int yOffset = (item.rect.height() - iconHeight) / 2;
QRect dropRect(item.rect.topRight(), elSize);
dropRect.adjust(-xOffset, yOffset, -xOffset, yOffset);
QStyleOptionMenuItem so = item;
so.rect = visualRect(item.direction,
item.rect,
dropRect);
drawPrimitive(dropDirElement, &so, painter, widget);
} else if (!shortcutText.isEmpty()) {
QPixmap pix = ManhattanShortcut(&item, shortcutText).getPixmap();
if (pix.width()) {
int xOffset = pix.width() + iconHeight/2;
QRect shortcutRect = item.rect.translated({item.rect.width() - xOffset, 0});
shortcutRect.setSize({pix.width(), item.rect.height()});
shortcutRect = visualRect(item.direction,
item.rect,
shortcutRect);
drawItemPixmap(painter,
shortcutRect,
Qt::AlignRight | Qt::AlignVCenter,
pix);
}
}
painter->restore();
}
}
void ManhattanStyle::drawQmlEditorIcon(PrimitiveElement element,
const QStyleOption *option,
const char *propertyName,
QPainter *painter,
const QWidget *widget) const
{
if (option->styleObject && option->styleObject->property(propertyName).isValid()) {
const auto mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option);
if (mbi) {
const bool checkable = mbi->checkType != QStyleOptionMenuItem::NotCheckable;
const bool isDisabled = !(mbi->state & State_Enabled);
const bool isActive = mbi->state & State_Selected;
QIcon icon = mbi->styleObject->property(propertyName).value<QIcon>();
QIcon::Mode mode = isDisabled ? QIcon::Disabled : ((isActive) ? QIcon::Active : QIcon::Normal);
QIcon::State state = (checkable && mbi->checked) ? QIcon::On : QIcon::Off;
QPixmap pix = icon.pixmap(option->rect.size(), mode, state);
drawItemPixmap(painter, option->rect, Qt::AlignCenter, pix);
return;
}
}
QProxyStyle::drawPrimitive(element, option, painter, widget);
}
void ManhattanStyle::drawControl(ControlElement element, const QStyleOption *option,
QPainter *painter, const QWidget *widget) const
{
@@ -802,7 +1331,11 @@ void ManhattanStyle::drawControl(ControlElement element, const QStyleOption *opt
pal.setBrush(QPalette::Text, color);
item.palette = pal;
}
QProxyStyle::drawControl(element, &item, painter, widget);
if (isQmlEditorMenu(widget))
drawControlForQmlEditor(element, &item, painter, widget);
else
QProxyStyle::drawControl(element, &item, painter, widget);
}
painter->restore();
break;
@@ -961,6 +1494,13 @@ void ManhattanStyle::drawControl(ControlElement element, const QStyleOption *opt
}
break;
case CE_MenuEmptyArea:
if (isQmlEditorMenu(widget))
drawPrimitive(PE_PanelMenu, option, painter, widget);
else
QProxyStyle::drawControl(element, option, painter, widget);
break;
case CE_ToolBar:
{
QRect rect = option->rect;