Storybook: Generic, figma-like inspection tool for checking structure/sizes/paddings

Closes: #10574
This commit is contained in:
Michał Cieślak 2023-05-08 14:49:39 +02:00 committed by Michał
parent 534e172397
commit cbaf1b8a78
8 changed files with 486 additions and 0 deletions

View File

@ -172,6 +172,30 @@ ApplicationWindow {
pageTitle: currentPageModelItem.object.title
})
}
onInspectClicked: {
const getItems = typeName =>
InspectionUtils.findItemsByTypeName(
viewLoader.item, typeName)
const items = [
...getItems(root.currentPage),
...getItems("Custom" + root.currentPage)
]
if (items.length === 0) {
console.warn(`Item of type "${root.currentPage}" not found. Nothing to inspect.`)
return
}
const lca = InspectionUtils.lowestCommonAncestor(
items, viewLoader.item)
inspectionWindow.inspect(lca.parent.contentItem === lca
? lca.parent : lca)
inspectionWindow.show()
inspectionWindow.requestActivate()
}
}
}
}
@ -217,6 +241,10 @@ ApplicationWindow {
figmaToken: settingsLayout.figmaToken
}
InspectionWindow {
id: inspectionWindow
}
Component {
id: figmaWindow

View File

@ -0,0 +1,121 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Rectangle {
id: root
readonly property color visualItemColor: "black"
readonly property color nonVisualItemColor: "green"
readonly property color visualItemSelectionColor: "red"
readonly property color nonVisualItemSelectionColor: "orange"
readonly property bool selected: containsMouse || forceSelect
readonly property color baseColor:
isVisual ? visualItemColor
: (showNonVisual ? nonVisualItemColor : "transparent")
readonly property color selectionColor: isVisual ? visualItemSelectionColor
: nonVisualItemSelectionColor
border.color: selected ? selectionColor : baseColor
border.width: selected ? 2 : 1
color: 'transparent'
required property string name
required property bool isVisual
property bool showNonVisual: false
property bool forceSelect: false
required property Item visualParent
readonly property real topSpacing: mapToItem(visualParent, 0, 0).y
readonly property real bottomSpacing:
visualParent.height - mapToItem(visualParent, 0, height).y
readonly property real leftSpacing: mapToItem(visualParent, 0, 0).x
readonly property real rightSpacing:
visualParent.width - mapToItem(visualParent, width, 0).x
readonly property alias containsMouse: mouseArea.containsMouse
component DistanceRectangle: Rectangle {
width: 1
height: 1
color: selectionColor
visible: root.selected
parent: root.parent
}
// top
DistanceRectangle {
height: topSpacing
anchors.bottom: root.top
anchors.horizontalCenter: root.horizontalCenter
}
// left
DistanceRectangle {
width: leftSpacing
anchors.right: root.left
anchors.verticalCenter: root.verticalCenter
}
// right
DistanceRectangle {
width: rightSpacing
anchors.left: root.right
anchors.verticalCenter: root.verticalCenter
}
// bottom
DistanceRectangle {
height: bottomSpacing
anchors.top: root.bottom
anchors.horizontalCenter: root.horizontalCenter
}
Popup {
x: parent.width + padding / 2
y: parent.height + padding / 2
visible: root.selected
margins: 0
ColumnLayout {
Label {
text: root.name
font.bold: true
}
Label {
text: `x: ${root.x}, y: ${root.y}`
}
Label {
text: `size: ${root.width} x ${root.height}`
}
Label {
text: `top space: ${root.topSpacing}`
}
Label {
text: `bottom space: ${root.bottomSpacing}`
}
Label {
text: `left space: ${root.leftSpacing}`
}
Label {
text: `right space: ${root.rightSpacing}`
}
}
}
MouseArea {
id: mouseArea
visible: isVisual || showNonVisual
anchors.fill: parent
hoverEnabled: true
}
}

View File

@ -0,0 +1,37 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
ListView {
ScrollBar.vertical: ScrollBar {}
readonly property color visualItemColor: "blue"
readonly property color nonVisualItemColor: "black"
readonly property color selectionColor: "red"
delegate: Text {
width: ListView.view.width
height: 30
text: " ".repeat(model.level * 4) + " " + model.name
readonly property color baseColor: model.visual ? visualItemColor
: nonVisualItemColor
color: model.item.containsMouse ? selectionColor
: (model.visual ? "blue" : "black")
elide: Text.ElideRight
Binding {
target: model.item
property: "forceSelect"
value: mouseArea.containsMouse
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
}
}

View File

@ -0,0 +1,92 @@
import QtQuick 2.15
Item {
id: root
property bool propagateClipping: false
property bool showNonVisualItems: false
property alias showScreenshot: image.visible
required property Item sourceItem
readonly property ListModel model: ListModel {}
implicitWidth: image.implicitWidth
implicitHeight: image.implicitHeight
Component {
id: inspectionItemComponent
InspectionItem {}
}
Image {
id: image
}
function itemsDepthFirst(root) {
const items = []
function iterate(item, parentIndex, level) {
if (!item.visible || item.opacity === 0)
return
const idx = items.length
items.push({item, parentIndex, level})
for (let i = 0; i < item.children.length; i++)
iterate(item.children[i], idx, level + 1)
}
iterate(root, -1, 0)
return items
}
Component.onCompleted: {
root.sourceItem.grabToImage(result => image.source = result.url)
const items = itemsDepthFirst(root.sourceItem)
const placeholders = []
const modelItems = []
items.forEach((entry) => {
const {item, parentIndex, level} = entry
const isRoot = parentIndex === -1
const parent = isRoot ? root : placeholders[parentIndex]
const visualParent = isRoot ? root : (parent.isVisual ? parent : parent.visualParent)
const x = isRoot ? 0 : item.x
const y = isRoot ? 0 : item.y
const name = InspectionUtils.simpleName(item)
const visual = InspectionUtils.isVisual(item)
const clip = item.clip
const props = {
name, x, y,
width: item.width,
height: item.height,
z: item.z,
isVisual: visual,
visualParent,
clip: Qt.binding(() => root.propagateClipping && item.clip),
showNonVisual: Qt.binding(() => root.showNonVisualItems)
}
const placeholder = inspectionItemComponent.createObject(
parent, props)
const modelEntryProps = {
name, visual, level,
item: placeholder
}
modelItems.push(modelEntryProps)
placeholders.push(placeholder)
})
root.model.append(modelItems)
}
}

View File

@ -0,0 +1,97 @@
pragma Singleton
import QtQml 2.15
import QtQuick 2.15
QtObject {
function isVisual(item) {
return item instanceof Text
|| item instanceof Rectangle
|| item instanceof Image
|| item instanceof TextEdit
|| item instanceof TextInput
}
function baseName(item) {
const fullName = item.toString()
const underscoreIndex = fullName.indexOf("_")
if (underscoreIndex !== -1)
return fullName.substring(0, underscoreIndex)
const bracketIndex = fullName.indexOf("(")
if (bracketIndex !== -1)
return fullName.substring(0, bracketIndex)
return fullName
}
function simpleName(item) {
if (item instanceof Text)
return "Text"
if (item instanceof Rectangle)
return "Rectangle"
if (item instanceof Image)
return "Image"
if (item instanceof TextEdit)
return "TextEdit"
if (item instanceof TextInput)
return "TextInput"
const name = baseName(item)
if (name.startsWith("QQuick"))
return name.substring(6)
return name
}
function findItemsByTypeName(root, typeName) {
const items = []
const stack = [root]
while (stack.length) {
const item = stack.pop()
if (!item.visible || item.opacity === 0)
continue
const name = baseName(item)
if (name === typeName) {
items.push(item)
continue
}
for (let i = 0; i < item.children.length; i++)
stack.push(item.children[i])
}
return items
}
function pathToAncestor(item, ancestor) {
const path = [item]
while (path[path.length - 1].parent !== ancestor)
path.push(path[path.length - 1].parent)
return path
}
function lowestCommonAncestor(items, commonAncestor) {
const paths = items.map(item => pathToAncestor(item, commonAncestor))
let candidate = null
while (true) {
const top = paths.map(path => path.pop())
if (top.every(val => val && val === top[0]))
candidate = top.shift()
else
return candidate
}
}
}

View File

@ -0,0 +1,95 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ApplicationWindow {
title: "Storybook Inspector"
width: 1024
height: 768
function inspect(sourceItem) {
const properties = {
sourceItem,
propagateClipping: Qt.binding(() => clipCheckBox.checked),
showNonVisualItems: Qt.binding(() => showNonVisualCheckBox.checked),
showScreenshot: Qt.binding(() => screenshotCheckBox.checked)
}
loader.setSource("InspectionPanel.qml", properties)
}
SplitView {
anchors.fill: parent
InspectionItemsList {
id: itemsListView
SplitView.preferredWidth: 300
SplitView.fillHeight: true
model: loader.item ? loader.item.model : null
clip: true
}
ColumnLayout {
SplitView.fillWidth: true
SplitView.fillHeight: true
Flickable {
id: flickable
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
contentWidth: content.width
contentHeight: content.height
Item {
id: content
width: Math.max(flickable.width, loader.implicitWidth)
height: Math.max(flickable.height, loader.implicitHeight)
Rectangle {
border.color: "gray"
color: "transparent"
anchors.fill: loader
}
Loader {
id: loader
anchors.centerIn: parent
}
}
}
Pane {
Layout.fillWidth: true
RowLayout {
CheckBox {
id: screenshotCheckBox
text: "Show screenshot"
checked: true
}
CheckBox {
id: clipCheckBox
text: "Propagate clipping"
}
CheckBox {
id: showNonVisualCheckBox
text: "Show non-visual items"
}
}
}
}
}
}

View File

@ -10,6 +10,7 @@ ToolBar {
property int figmaPagesCount: 0
signal figmaPreviewClicked
signal inspectClicked
RowLayout {
anchors.fill: parent
@ -60,5 +61,15 @@ ToolBar {
onClicked: root.figmaPreviewClicked()
}
ToolSeparator {}
ToolButton {
text: "Inspect"
Layout.rightMargin: parent.spacing
onClicked: root.inspectClicked()
}
}
}

View File

@ -11,6 +11,10 @@ HotReloaderControls 1.0 HotReloaderControls.qml
ImageSelectPopup 1.0 ImageSelectPopup.qml
ImagesGridView 1.0 ImagesGridView.qml
ImagesNavigationLayout 1.0 ImagesNavigationLayout.qml
InspectionItem 1.0 InspectionItem.qml
InspectionItemsList 1.0 InspectionItemsList.qml
InspectionPanel 1.0 InspectionPanel.qml
InspectionWindow 1.0 InspectionWindow.qml
Logs 1.0 Logs.qml
LogsAndControlsPanel 1.0 LogsAndControlsPanel.qml
LogsView 1.0 LogsView.qml
@ -21,4 +25,5 @@ SettingsLayout 1.0 SettingsLayout.qml
SingleItemProxyModel 1.0 SingleItemProxyModel.qml
SourceCodeBox 1.0 SourceCodeBox.qml
singleton FigmaUtils 1.0 FigmaUtils.qml
singleton InspectionUtils 1.0 InspectionUtils.qml
singleton StorybookUtils 1.0 StorybookUtils.qml