Sketcher: Make Elements UI style aware

This commit like previous one introduces ability to style list of
elements in the sketcher. It also completely refactors the rendering
delegate introducing dedicated method for calculating rects of each
element and method for rendering each sub control. Thanks to this
refactor we can be sure that areas drawn are the same as areas checked
for events in editorEvent.
This commit is contained in:
Kacper Donat
2024-01-20 16:55:38 +01:00
parent 899d0d9130
commit f52c0d12f0

View File

@@ -115,25 +115,37 @@ class ElementItemDelegate: public QStyledItemDelegate
{
Q_OBJECT
public:
/// Enum containing all controls rendered in this item. Controls in that enum MUST be in order.
enum SubControl : int {
CheckBox,
LineSelect,
StartSelect,
EndSelect,
MidSelect,
Label
};
explicit ElementItemDelegate(ElementView* parent);
~ElementItemDelegate() override;
~ElementItemDelegate() override = default;
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const override;
bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option,
const QModelIndex& index) override;
ElementItem* getElementtItem(const QModelIndex& index) const;
ElementItem* getElementItem(const QModelIndex& index) const;
const int border = 1; // 1px, looks good around buttons.
const int leftMargin = 4;// 4px on the left of icons, looks good.
mutable int customIconsMargin = 4;
QRect subControlRect(SubControl element, const QStyleOptionViewItem& option, const QModelIndex& index) const;
void drawSubControl(SubControl element, QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
const int gap = 4; // 4px of spacing between consecutive elements
Q_SIGNALS:
void itemHovered(QModelIndex);
void itemChecked(QModelIndex, Qt::CheckState state);
};
// clang-format on
// helper class to store additional information about the listWidget entry.
class ElementItem: public QListWidgetItem
{
@@ -153,8 +165,13 @@ public:
Hidden = 2,
};
ElementItem(int elementnr, int startingVertex, int midVertex, int endVertex,
Base::Type geometryType, GeometryState state, const QString& lab,
ElementItem(int elementnr,
int startingVertex,
int midVertex,
int endVertex,
Base::Type geometryType,
GeometryState state,
const QString& lab,
ViewProviderSketch* sketchView)
: ElementNbr(elementnr)
, StartingVertex(startingVertex)
@@ -178,7 +195,6 @@ public:
bool isVisible()
{
if (State != GeometryState::External) {
const auto geo = sketchView->getSketchObject()->getGeometry(ElementNbr);
if (geo) {
@@ -194,6 +210,35 @@ public:
return true;
}
QVariant data(int role) const override
{
// In order for content-box to include size of the 4 geometry icons we need to provide
// Qt with information about decoration (icon) size. This is hack to work around Qt
// limitation of not knowing about padding, border and margin boxes of stylesheets
// thus being unable to provide proper sizeHint for stylesheets to render correctly
if (role == Qt::DecorationRole) {
int size = listWidget()->style()->pixelMetric(QStyle::PM_ListViewIconSize);
return QIcon(QPixmap(QSize(size, size)));
}
return QListWidgetItem::data(role);
}
bool isGeometrySelected(Sketcher::PointPos pos) const
{
switch (pos) {
case Sketcher::PointPos::none:
return isLineSelected;
case Sketcher::PointPos::start:
return isStartingPointSelected;
case Sketcher::PointPos::end:
return isEndPointSelected;
case Sketcher::PointPos::mid:
return isMidPointSelected;
}
}
int ElementNbr;
int StartingVertex;
int MidVertex;
@@ -217,6 +262,7 @@ public:
private:
ViewProviderSketch* sketchView;
};
// clang-format off
class ElementFilterList: public QListWidget
{
@@ -497,6 +543,7 @@ ElementView::~ElementView()
void ElementView::changeLayer(int layer)
{
App::Document* doc = App::GetApplication().getActiveDocument();
if (!doc)
return;
@@ -507,7 +554,6 @@ void ElementView::changeLayer(int layer)
auto geoids = getGeoIdsOfEdgesFromNames(sketchobject, ft->getSubNames());
auto geometry = sketchobject->Geometry.getValues();
auto newgeometry(geometry);
@@ -737,6 +783,8 @@ void ElementView::deleteSelectedItems()
void ElementView::onIndexHovered(QModelIndex index)
{
update(index);
Q_EMIT onItemHovered(itemFromIndex(index));
}
@@ -755,166 +803,218 @@ ElementItem* ElementView::itemFromIndex(const QModelIndex& index)
return static_cast<ElementItem*>(QListWidget::itemFromIndex(index));
}
// clang-format on
/* ElementItem delegate ---------------------------------------------------- */
ElementItemDelegate::ElementItemDelegate(ElementView* parent)
: QStyledItemDelegate(parent)
{// This class relies on the parent being an ElementView, see getElementtItem
{ // This class relies on the parent being an ElementView, see getElementtItem
}
ElementItemDelegate::~ElementItemDelegate()
{}
void ElementItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
void ElementItemDelegate::paint(QPainter* painter,
const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
ElementItem* item = getElementtItem(index);
ElementItem* item = getElementItem(index);
if (item) {
auto style = option.widget ? option.widget->style() : QApplication::style();
if (!item) {
return;
}
QStyleOptionButton checkboxstyle;
auto style = option.widget ? option.widget->style() : QApplication::style();
checkboxstyle.rect = option.rect;
checkboxstyle.state |= QStyle::State_Enabled;
QStyleOptionViewItem itemOption = option;
if (item->isVisible())
checkboxstyle.state |= QStyle::State_On;
else
checkboxstyle.state |= QStyle::State_Off;
initStyleOption(&itemOption, index);
QRect checkboxrect
= style->subElementRect(QStyle::SE_CheckBoxIndicator, &checkboxstyle, option.widget);
if (item->isLineSelected || item->isStartingPointSelected || item->isEndPointSelected
|| item->isMidPointSelected) {
itemOption.state |= QStyle::State_Active;
}
checkboxstyle.rect = {
leftMargin,
option.rect.top() + (option.rect.height() - checkboxrect.height()) / 2, // vertically center the checkbox
checkboxrect.width(),
checkboxrect.height()
};
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &itemOption, painter, option.widget);
customIconsMargin = leftMargin + checkboxrect.width() + leftMargin;
drawSubControl(SubControl::CheckBox, painter, option, index);
drawSubControl(SubControl::LineSelect, painter, option, index);
drawSubControl(SubControl::StartSelect, painter, option, index);
drawSubControl(SubControl::EndSelect, painter, option, index);
drawSubControl(SubControl::MidSelect, painter, option, index);
drawSubControl(SubControl::Label, painter, option, index);
}
int height = option.rect.height();
int width = height;// icons are square.
int x0 = option.rect.x() + customIconsMargin;
int iconsize = height - 2 * border;
int btny = option.rect.y() + border;
QRect ElementItemDelegate::subControlRect(SubControl element,
const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
auto itemOption = option;
if (item->isLineSelected || item->isStartingPointSelected || item->isEndPointSelected
|| item->isMidPointSelected) {// option.state & QStyle::State_Selected
auto style = option.widget ? option.widget->style() : QApplication::style();
auto unselecticon = [&](int iconnumber) {
QRect rect {x0 + border + width * iconnumber, btny, iconsize, iconsize};
painter->fillRect(rect, option.palette.base());
};
initStyleOption(&itemOption, index);
QRect selection = QRect(option.rect.x(),
option.rect.y(),
option.rect.width(),
option.rect.height());
QRect checkBoxRect =
style->subElementRect(QStyle::SE_CheckBoxIndicator, &itemOption, option.widget);
painter->fillRect(selection, option.palette.highlight());// paint the item as selected
checkBoxRect.moveTo(gap,
option.rect.top() + (option.rect.height() - checkBoxRect.height()) / 2);
// Repaint individual icons
if (!item->isLineSelected)
unselecticon(0);
if (element == SubControl::CheckBox) {
return checkBoxRect;
}
if (!item->isStartingPointSelected)
unselecticon(1);
QRect selectRect =
style->subElementRect(QStyle::SE_ItemViewItemDecoration, &itemOption, option.widget)
.translated(checkBoxRect.right() + gap, 0);
if (!item->isEndPointSelected)
unselecticon(2);
unsigned pos = element - SubControl::LineSelect;
if (!item->isMidPointSelected)
unselecticon(3);
auto rect = selectRect.translated((selectRect.width() + gap) * pos, 0);
if (element != SubControl::Label) {
return rect;
}
rect.setRight(itemOption.rect.right());
return rect;
}
void ElementItemDelegate::drawSubControl(SubControl element,
QPainter* painter,
const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
auto item = getElementItem(index);
auto style = option.widget ? option.widget->style() : QApplication::style();
auto rect = subControlRect(element, option, index);
auto mousePos = option.widget->mapFromGlobal(QCursor::pos());
auto isHovered = rect.contains(mousePos);
auto drawSelectIcon = [&](Sketcher::PointPos pos) {
auto icon = ElementWidgetIcons::getIcon(item->GeometryType, pos, item->State);
auto opacity = 0.4f;
if (isHovered) {
opacity = 0.8f;
}
auto& iconEdge =
ElementWidgetIcons::getIcon(item->GeometryType, Sketcher::PointPos::none, item->State);
auto& iconStart =
ElementWidgetIcons::getIcon(item->GeometryType, Sketcher::PointPos::start, item->State);
auto& iconEnd =
ElementWidgetIcons::getIcon(item->GeometryType, Sketcher::PointPos::end, item->State);
auto& iconMid =
ElementWidgetIcons::getIcon(item->GeometryType, Sketcher::PointPos::mid, item->State);
// getIcon(item->GeometryType);
if (item->isGeometrySelected(pos)) {
opacity = 1.0f;
}
style->drawPrimitive(QStyle::PE_IndicatorCheckBox, &checkboxstyle, painter, option.widget);
painter->setOpacity(opacity);
painter->drawPixmap(rect, icon.pixmap(rect.size()));
};
painter->drawPixmap(x0 + border, btny, iconEdge.pixmap(iconsize, iconsize));
painter->drawPixmap(x0 + border + width, btny, iconStart.pixmap(iconsize, iconsize));
painter->drawPixmap(x0 + border + width * 2, btny, iconEnd.pixmap(iconsize, iconsize));
painter->drawPixmap(x0 + border + width * 3, btny, iconMid.pixmap(iconsize, iconsize));
painter->save();
// Label :
auto labelBoundingBox = painter->fontMetrics().tightBoundingRect(item->label);
painter->drawText(
x0 + width * 4 + 3 * border,
option.rect.bottom() - (option.rect.height() - labelBoundingBox.height()) / 2,
item->label
);
switch (element) {
case SubControl::CheckBox: {
QStyleOptionButton checkboxOption;
checkboxOption.initFrom(option.widget);
checkboxOption.state |= QStyle::State_Enabled;
checkboxOption.rect = rect;
if (item->isVisible()) {
checkboxOption.state |= QStyle::State_On;
}
else {
checkboxOption.state |= QStyle::State_Off;
}
style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck,
&checkboxOption,
painter,
option.widget);
break;
}
case LineSelect: {
drawSelectIcon(Sketcher::PointPos::none);
break;
}
case StartSelect: {
drawSelectIcon(Sketcher::PointPos::start);
break;
}
case EndSelect: {
drawSelectIcon(Sketcher::PointPos::end);
break;
}
case MidSelect: {
drawSelectIcon(Sketcher::PointPos::mid);
break;
}
case Label: {
QRect rect = subControlRect(SubControl::Label, option, index);
auto labelBoundingBox = painter->fontMetrics().tightBoundingRect(item->label);
painter->drawText(rect.x(),
option.rect.bottom()
- (option.rect.height() - labelBoundingBox.height()) / 2,
item->label);
break;
}
}
painter->restore();
}
// clang-format off
bool ElementItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model,
const QStyleOptionViewItem& option, const QModelIndex& index)
{
auto getSubElementType = [&](ElementItem* item, int xPos, int width) {
bool label = (xPos > option.rect.x() + customIconsMargin + width * 4 + border);
auto item = getElementItem(index);
if ((xPos < option.rect.x() + customIconsMargin + width + border)
|| (item->GeometryType != Part::GeomPoint::getClassTypeId() && label))
auto getSubElementType = [&](QPoint pos) {
if (subControlRect(SubControl::LineSelect, option, index).contains(pos)) {
return SubElementType::edge;
if (xPos < option.rect.x() + customIconsMargin + width * 2 + border
|| (item->GeometryType == Part::GeomPoint::getClassTypeId() && label))
} else if (subControlRect(SubControl::StartSelect, option, index).contains(pos)) {
return SubElementType::start;
if (xPos < option.rect.x() + customIconsMargin + width * 3 + border)
} else if (subControlRect(SubControl::EndSelect, option, index).contains(pos)) {
return SubElementType::end;
else if (xPos < option.rect.x() + customIconsMargin + width * 4 + border)
} else if (subControlRect(SubControl::MidSelect, option, index).contains(pos)) {
return SubElementType::mid;
else
return SubElementType::none;
} else {
// depending on geometry type by default we select either point or edge
return item->GeometryType == Part::GeomPoint::getClassTypeId() ? SubElementType::start : SubElementType::edge;
}
};
if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick) {
QMouseEvent* mEvent = static_cast<QMouseEvent*>(event);
ElementItem* item = getElementtItem(index);
auto mouseEvent = static_cast<QMouseEvent*>(event);
int xPos = mEvent->pos().x();
int width = option.rect.height();// icons are square
item->clickedOn = getSubElementType(mouseEvent->pos());
item->rightClicked = mouseEvent->button() == Qt::RightButton;
item->clickedOn = getSubElementType(item, xPos, width);
QRect checkboxRect = subControlRect(SubControl::CheckBox, option, index);
if (mEvent->button() == Qt::RightButton)
item->rightClicked = true;
QRect checkboxrect = QRect(
leftMargin, option.rect.y(), customIconsMargin - leftMargin, option.rect.height());
if (mEvent->button() == Qt::LeftButton && checkboxrect.contains(mEvent->pos())) {
if (mouseEvent->button() == Qt::LeftButton && checkboxRect.contains(mouseEvent->pos())) {
Q_EMIT itemChecked(index, item->isVisible() ? Qt::Unchecked : Qt::Checked);
}
}
else if (event->type() == QEvent::MouseMove) {
SubElementType typeUnderMouse;
QMouseEvent* mEvent = static_cast<QMouseEvent*>(event);
int xPos = mEvent->pos().x();
int width = option.rect.height();// icons are square
auto mouseEvent = static_cast<QMouseEvent*>(event);
ElementItem* item = getElementtItem(index);
item->hovered = getSubElementType(mouseEvent->pos());
typeUnderMouse = getSubElementType(item, xPos, width);
item->hovered = typeUnderMouse;
Q_EMIT itemHovered(index);
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
ElementItem* ElementItemDelegate::getElementtItem(const QModelIndex& index) const
const QWidget *w
ElementItem* ElementItemDelegate::getElementItem(const QModelIndex& index) const
{
ElementView* elementView = static_cast<ElementView*>(parent());
return elementView->itemFromIndex(index);