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
|
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
|
figmaToken: settingsLayout.figmaToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InspectionWindow {
|
||||||
|
id: inspectionWindow
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: figmaWindow
|
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
|
property int figmaPagesCount: 0
|
||||||
|
|
||||||
signal figmaPreviewClicked
|
signal figmaPreviewClicked
|
||||||
|
signal inspectClicked
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -60,5 +61,15 @@ ToolBar {
|
||||||
|
|
||||||
onClicked: root.figmaPreviewClicked()
|
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
|
ImageSelectPopup 1.0 ImageSelectPopup.qml
|
||||||
ImagesGridView 1.0 ImagesGridView.qml
|
ImagesGridView 1.0 ImagesGridView.qml
|
||||||
ImagesNavigationLayout 1.0 ImagesNavigationLayout.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
|
Logs 1.0 Logs.qml
|
||||||
LogsAndControlsPanel 1.0 LogsAndControlsPanel.qml
|
LogsAndControlsPanel 1.0 LogsAndControlsPanel.qml
|
||||||
LogsView 1.0 LogsView.qml
|
LogsView 1.0 LogsView.qml
|
||||||
|
@ -21,4 +25,5 @@ SettingsLayout 1.0 SettingsLayout.qml
|
||||||
SingleItemProxyModel 1.0 SingleItemProxyModel.qml
|
SingleItemProxyModel 1.0 SingleItemProxyModel.qml
|
||||||
SourceCodeBox 1.0 SourceCodeBox.qml
|
SourceCodeBox 1.0 SourceCodeBox.qml
|
||||||
singleton FigmaUtils 1.0 FigmaUtils.qml
|
singleton FigmaUtils 1.0 FigmaUtils.qml
|
||||||
|
singleton InspectionUtils 1.0 InspectionUtils.qml
|
||||||
singleton StorybookUtils 1.0 StorybookUtils.qml
|
singleton StorybookUtils 1.0 StorybookUtils.qml
|
||||||
|
|
Loading…
Reference in New Issue