Storybook: Generic, figma-like inspection tool for checking structure/sizes/paddings
Closes: #10574
This commit is contained in:
parent
534e172397
commit
cbaf1b8a78
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue