diff --git a/share/qtcreator/qml/qmlpuppet/qmlprivategate/qmlprivategate_56.cpp b/share/qtcreator/qml/qmlpuppet/qmlprivategate/qmlprivategate_56.cpp index f0ef76cb19d..8b2f6041657 100644 --- a/share/qtcreator/qml/qmlpuppet/qmlprivategate/qmlprivategate_56.cpp +++ b/share/qtcreator/qml/qmlpuppet/qmlprivategate/qmlprivategate_56.cpp @@ -167,6 +167,26 @@ void registerNodeInstanceMetaObject(QObject *object, QQmlEngine *engine) QQuickDesignerSupportProperties::registerNodeInstanceMetaObject(object, engine); } +static bool isQuickStyleItemMetaObject(const QMetaObject *metaObject) +{ + if (metaObject) { + if (metaObject->className() == QByteArrayLiteral("QQuickStyleItem")) + return true; + + return isQuickStyleItemMetaObject(metaObject->superClass()); + } + + return false; +} + +static bool isQuickStyleItem(QObject *object) +{ + if (object) + return isQuickStyleItemMetaObject(object->metaObject()); + + return false; +} + // This is used in share/qtcreator/qml/qmlpuppet/qml2puppet/instances/objectnodeinstance.cpp QObject *createPrimitive(const QString &typeName, int majorNumber, int minorNumber, QQmlContext *context) { @@ -357,12 +377,15 @@ void doComponentCompleteRecursive(QObject *object, NodeInstanceServer *nodeInsta doComponentCompleteRecursive(child, nodeInstanceServer); } - if (item) { - static_cast(item)->componentComplete(); - } else { - QQmlParserStatus *qmlParserStatus = dynamic_cast< QQmlParserStatus*>(object); - if (qmlParserStatus) - qmlParserStatus->componentComplete(); + if (!isQuickStyleItem(item)) { + qDebug() << Q_FUNC_INFO << item; + if (item) { + static_cast(item)->componentComplete(); + } else { + QQmlParserStatus *qmlParserStatus = dynamic_cast(object); + if (qmlParserStatus) + qmlParserStatus->componentComplete(); + } } } } diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ColorEditor.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ColorEditor.qml index 19746097d41..a34d7ad9bfe 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ColorEditor.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ColorEditor.qml @@ -413,7 +413,7 @@ SecondColumnLayout { id: transparentIndicator icon: StudioTheme.Constants.transparent pixelSize: StudioTheme.Values.myIconFontSize * 1.4 - tooltip: qsTr("Transparent TODO") + tooltip: qsTr("Transparent") onClicked: { colorPicker.alpha = 0 colorPicker.updateColor() @@ -575,22 +575,29 @@ SecondColumnLayout { onRightMouseButtonClicked: contextMenu.popup(colorPicker) onColorInvalidated: { - if (colorPicker.saturation > 0.0 && colorPicker.lightness > 0.0) { - hueSpinBox.value = colorPicker.hue + switch (colorPicker.mode) { + case ColorPicker.Mode.HSLA: + hslHueSpinBox.value = colorPicker.hue + hslSaturationSpinBox.value = colorPicker.saturationHSL + hslLightnessSpinBox.value = colorPicker.lightness + hslAlphaSpinBox.value = colorPicker.alpha + break + + case ColorPicker.Mode.RGBA: + redSpinBox.value = (colorPicker.color.r * 255) + greenSpinBox.value = (colorPicker.color.g * 255) + blueSpinBox.value = (colorPicker.color.b * 255) + rgbAlphaSpinBox.value = (colorPicker.alpha * 255) + break + + case ColorPicker.Mode.HSVA: + default: + hsvHueSpinBox.value = colorPicker.hue + hsvSaturationSpinBox.value = colorPicker.saturationHSV + hsvValueSpinBox.value = colorPicker.value + hsvAlphaSpinBox.value = colorPicker.alpha + break } - - if (colorPicker.lightness > 0.0) - saturationSpinBox.value = colorPicker.saturation - else - colorPicker.saturation = saturationSpinBox.value - - lightnessSpinBox.value = colorPicker.lightness - hslaAlphaSpinBox.value = colorPicker.alpha - - redSpinBox.value = (colorPicker.color.r * 255) - greenSpinBox.value = (colorPicker.color.g * 255) - blueSpinBox.value = (colorPicker.color.b * 255) - rgbaAlphaSpinBox.value = (colorPicker.alpha * 255) } } @@ -766,29 +773,22 @@ SecondColumnLayout { + 4 * StudioTheme.Values.colorEditorPopupSpinBoxWidth width: implicitWidth actionIndicatorVisible: false - model: ["RGBA", "HSLA"] - onActivated: { - switch (colorMode.currentText) { - case "RGBA": - rgbaRow.visible = true - hslaRow.visible = false - break - case "HSLA": - rgbaRow.visible = false - hslaRow.visible = true - break - default: - console.log("Unknown color mode selected.") - rgbaRow.visible = true - hslaRow.visible = false - } - } + textRole: "text" + valueRole: "value" + model: [ + { value: ColorPicker.Mode.HSVA, text: "HSVA" }, + { value: ColorPicker.Mode.RGBA, text: "RGBA" }, + { value: ColorPicker.Mode.HSLA, text: "HSLA" } + ] + + onActivated: colorPicker.mode = colorMode.currentValue } } RowLayout { id: rgbaRow + visible: colorPicker.mode === ColorPicker.Mode.RGBA Layout.fillWidth: true spacing: StudioTheme.Values.controlGap @@ -847,7 +847,7 @@ SecondColumnLayout { } DoubleSpinBox { - id: rgbaAlphaSpinBox + id: rgbAlphaSpinBox width: StudioTheme.Values.colorEditorPopupSpinBoxWidth stepSize: 1 @@ -856,7 +856,7 @@ SecondColumnLayout { decimals: 0 onValueModified: { - var tmp = rgbaAlphaSpinBox.value / 255.0 + var tmp = rgbAlphaSpinBox.value / 255.0 if (colorPicker.alpha !== tmp && !colorPicker.block) { colorPicker.alpha = tmp colorPicker.updateColor() @@ -868,49 +868,109 @@ SecondColumnLayout { RowLayout { id: hslaRow - visible: false + visible: colorPicker.mode === ColorPicker.Mode.HSLA Layout.fillWidth: true spacing: StudioTheme.Values.controlGap DoubleSpinBox { - id: hueSpinBox + id: hslHueSpinBox width: StudioTheme.Values.colorEditorPopupSpinBoxWidth onValueModified: { - if (colorPicker.hue !== hueSpinBox.value && !colorPicker.block) { - colorPicker.hue = hueSpinBox.value + if (colorPicker.hue !== hslHueSpinBox.value + && !colorPicker.block) { + colorPicker.hue = hslHueSpinBox.value colorPicker.updateColor() } } } DoubleSpinBox { - id: saturationSpinBox + id: hslSaturationSpinBox width: StudioTheme.Values.colorEditorPopupSpinBoxWidth onValueModified: { - if (colorPicker.saturation !== saturationSpinBox.value && !colorPicker.block) { - colorPicker.saturation = saturationSpinBox.value + if (colorPicker.saturationHSL !== hslSaturationSpinBox.value + && !colorPicker.block) { + colorPicker.saturationHSL = hslSaturationSpinBox.value colorPicker.updateColor() } } } DoubleSpinBox { - id: lightnessSpinBox + id: hslLightnessSpinBox width: StudioTheme.Values.colorEditorPopupSpinBoxWidth onValueModified: { - if (colorPicker.lightness !== lightnessSpinBox.value && !colorPicker.block) { - colorPicker.lightness = lightnessSpinBox.value + if (colorPicker.lightness !== hslLightnessSpinBox.value + && !colorPicker.block) { + colorPicker.lightness = hslLightnessSpinBox.value colorPicker.updateColor() } } } DoubleSpinBox { - id: hslaAlphaSpinBox + id: hslAlphaSpinBox width: StudioTheme.Values.colorEditorPopupSpinBoxWidth onValueModified: { - if (colorPicker.alpha !== hslaAlphaSpinBox.value && !colorPicker.block) { - colorPicker.alpha = hslaAlphaSpinBox.value + if (colorPicker.alpha !== hslAlphaSpinBox.value + && !colorPicker.block) { + colorPicker.alpha = hslAlphaSpinBox.value + colorPicker.updateColor() + } + } + } + } + + RowLayout { + id: hsvaRow + + visible: colorPicker.mode === ColorPicker.Mode.HSVA + Layout.fillWidth: true + spacing: StudioTheme.Values.controlGap + + DoubleSpinBox { + id: hsvHueSpinBox + width: StudioTheme.Values.colorEditorPopupSpinBoxWidth + onValueModified: { + if (colorPicker.hue !== hsvHueSpinBox.value + && !colorPicker.block) { + colorPicker.hue = hsvHueSpinBox.value + colorPicker.updateColor() + } + } + } + + DoubleSpinBox { + id: hsvSaturationSpinBox + width: StudioTheme.Values.colorEditorPopupSpinBoxWidth + onValueModified: { + if (colorPicker.saturationHSV !== hsvSaturationSpinBox.value + && !colorPicker.block) { + colorPicker.saturationHSV = hsvSaturationSpinBox.value + colorPicker.updateColor() + } + } + } + + DoubleSpinBox { + id: hsvValueSpinBox + width: StudioTheme.Values.colorEditorPopupSpinBoxWidth + onValueModified: { + if (colorPicker.value !== hsvValueSpinBox.value + && !colorPicker.block) { + colorPicker.value = hsvValueSpinBox.value + colorPicker.updateColor() + } + } + } + + DoubleSpinBox { + id: hsvAlphaSpinBox + width: StudioTheme.Values.colorEditorPopupSpinBoxWidth + onValueModified: { + if (colorPicker.alpha !== hsvAlphaSpinBox.value + && !colorPicker.block) { + colorPicker.alpha = hsvAlphaSpinBox.value colorPicker.updateColor() } } diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ColorPicker.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ColorPicker.qml index 07fc29a64d3..13f43f79955 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ColorPicker.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ColorPicker.qml @@ -29,15 +29,26 @@ import StudioTheme 1.0 as StudioTheme Column { id: root + enum Mode { + HSVA, + RGBA, + HSLA + } + + property int mode: ColorPicker.Mode.HSVA property color color - property real alpha: 1 property real hue: 0 - property real saturation: 0 + property real saturationHSL: 0 + property real saturationHSV: 0 property real lightness: 0 + property real value: 0 + + property real alpha: 1 + + property bool achromatic: false property int sliderMargins: 6 - property bool block: false signal updateColor @@ -46,30 +57,84 @@ Column { spacing: 10 - onAlphaChanged: invalidateColor() - onSaturationChanged: invalidateColor() - onLightnessChanged: invalidateColor() - onHueChanged: invalidateColor() - onColorChanged: { - var myAlpha = root.color.a - rgbToHsl(root.color) - root.alpha = myAlpha + onModeChanged: { + switch (root.mode) { + case ColorPicker.Mode.RGBA: + root.color = Qt.rgba(root.color.r, root.color.g, root.color.b, root.alpha) + break + case ColorPicker.Mode.HSLA: + root.color = Qt.hsla(root.hue, root.saturationHSL, root.lightness, root.alpha) + break + case ColorPicker.Mode.HSVA: + default: + root.color = Qt.hsva(root.hue, root.saturationHSV, root.value, root.alpha) + break + } + + gradientOverlay.requestPaint() } + onHueChanged: { + if (root.mode === ColorPicker.Mode.HSLA) + root.color.hslHue = root.hue + else + root.color.hsvHue = root.hue + } + onSaturationHSLChanged: { + root.color.hslSaturation = root.saturationHSL + invalidateColor() + } + onSaturationHSVChanged: { + root.color.hsvSaturation = root.saturationHSV + } + onLightnessChanged: { + root.color.hslLightness = root.lightness + } + onValueChanged: { + root.color.hsvValue = root.value + } + onAlphaChanged: invalidateColor() + onColorChanged: invalidateColor() + function invalidateColor() { if (root.block) return root.block = true - root.color = Qt.hsla(root.hue, - root.saturation, - root.lightness, - root.alpha) + if (root.color.hsvSaturation > 0.0 + && root.color.hsvValue > 0.0 + && root.color.hsvHue !== -1.0) + root.hue = root.color.hsvHue - if (root.saturation > 0.0 && root.lightness > 0.0) - hueSlider.value = root.hue + if (root.color.hslSaturation > 0.0 + && root.color.hslLightness > 0.0 + && root.color.hslHue !== -1.0) + root.hue = root.color.hslHue + if (root.color.hslLightness !== 0.0 && root.color.hslLightness !== 1.0 && !root.achromatic) + root.saturationHSL = root.color.hslSaturation + + if (root.color.hsvValue !== 0.0 && root.color.hsvValue !== 1.0 && !root.achromatic) + root.saturationHSV = root.color.hsvSaturation + + root.lightness = root.color.hslLightness + root.value = root.color.hsvValue + + if (root.color.hslLightness === 0.0 || root.color.hslLightness === 1.0 + || root.color.hsvValue === 0.0 || root.color.hsvValue === 1.0 + || root.color.hsvHue === -1.0 || root.color.hslHue === -1.0) + root.achromatic = true + else + root.achromatic = false + + if (root.mode === ColorPicker.Mode.HSLA) + root.color = Qt.hsla(root.hue, root.saturationHSL, root.lightness, root.alpha) + else + root.color = Qt.hsva(root.hue, root.saturationHSV, root.value, root.alpha) + + luminanceSlider.value = (1.0 - root.value) + hueSlider.value = root.hue opacitySlider.value = (1.0 - root.alpha) root.colorInvalidated() @@ -77,39 +142,51 @@ Column { root.block = false } - function rgbToHsl(color) { - var r = color.r - var g = color.g - var b = color.b + function drawHSVA(ctx) { + for (var row = 0; row < gradientOverlay.height; row++) { + var gradient = ctx.createLinearGradient(0, 0, gradientOverlay.width, 0) + var v = Math.abs(row - gradientOverlay.height) / gradientOverlay.height - var max = Math.max(r, g, b), min = Math.min(r, g, b) - var h, s, l = (max + min) / 2 + gradient.addColorStop(0, Qt.hsva(root.hue, 0, v, 1)) + gradient.addColorStop(1, Qt.hsva(root.hue, 1, v, 1)) - if (max === min) { - h = 0 - s = 0 - } else { - var d = max - min - s = l > 0.5 ? d / (2 - max - min) : d / (max + min) - switch (max) { - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; break; - } - - h /= 6 + ctx.fillStyle = gradient + ctx.fillRect(0, row, gradientOverlay.width, 1) } + } - root.block = true + function drawRGBA(ctx) { + var gradient = ctx.createLinearGradient(0, 0, gradientOverlay.width, 0) + gradient.addColorStop(0.000, Qt.rgba(1, 0, 0, 1)) + gradient.addColorStop(0.167, Qt.rgba(1, 1, 0, 1)) + gradient.addColorStop(0.333, Qt.rgba(0, 1, 0, 1)) + gradient.addColorStop(0.500, Qt.rgba(0, 1, 1, 1)) + gradient.addColorStop(0.667, Qt.rgba(0, 0, 1, 1)) + gradient.addColorStop(0.833, Qt.rgba(1, 0, 1, 1)) + gradient.addColorStop(1.000, Qt.rgba(1, 0, 0, 1)) - if (s > 0) - root.hue = h + ctx.fillStyle = gradient + ctx.fillRect(0, 0, gradientOverlay.width, gradientOverlay.height) - root.saturation = s - root.lightness = l + gradient = ctx.createLinearGradient(0, 0, 0, gradientOverlay.height) + gradient.addColorStop(0.000, Qt.rgba(0, 0, 0, 0)) + gradient.addColorStop(1.000, Qt.rgba(1, 1, 1, 1)) - root.block = false - invalidateColor() + ctx.fillStyle = gradient + ctx.fillRect(0, 0, gradientOverlay.width, gradientOverlay.height) + } + + function drawHSLA(ctx) { + for (var row = 0; row < gradientOverlay.height; row++) { + var gradient = ctx.createLinearGradient(0, 0, gradientOverlay.width, 0) + var l = Math.abs(row - gradientOverlay.height) / gradientOverlay.height + + gradient.addColorStop(0, Qt.hsla(root.hue, 0, l, 1)) + gradient.addColorStop(1, Qt.hsla(root.hue, 1, l, 1)) + + ctx.fillStyle = gradient + ctx.fillRect(0, row, gradientOverlay.width, 1) + } } Rectangle { @@ -133,26 +210,30 @@ Column { Canvas { id: gradientOverlay - property real hue: root.hue - anchors.fill: parent - opacity: root.alpha + opacity: root.color.a + + Connections { + target: root + function onHueChanged() { gradientOverlay.requestPaint() } + } - onHueChanged: requestPaint() onPaint: { var ctx = gradientOverlay.getContext('2d') ctx.save() ctx.clearRect(0, 0, gradientOverlay.width, gradientOverlay.height) - for (var row = 0; row < gradientOverlay.height; row++) { - var gradient = ctx.createLinearGradient(0, 0, gradientOverlay.width,0) - var l = Math.abs(row - gradientOverlay.height) / gradientOverlay.height - - gradient.addColorStop(0, Qt.hsla(gradientOverlay.hue, 0, l, 1)) - gradient.addColorStop(1, Qt.hsla(gradientOverlay.hue, 1, l, 1)) - - ctx.fillStyle = gradient - ctx.fillRect(0, row, gradientOverlay.width, 1) + switch (root.mode) { + case ColorPicker.Mode.RGBA: + root.drawRGBA(ctx) + break + case ColorPicker.Mode.HSLA: + root.drawHSLA(ctx) + break + case ColorPicker.Mode.HSVA: + default: + root.drawHSVA(ctx) + break } ctx.restore() @@ -162,25 +243,41 @@ Column { Canvas { id: pickerCross - property real cavnasSaturation: root.saturation - property real canvasLightness: root.lightness property color strokeStyle: "lightGray" opacity: 0.8 anchors.fill: parent antialiasing: true - onCavnasSaturationChanged: requestPaint(); - onCanvasLightnessChanged: requestPaint(); + Connections { + target: root + function onColorInvalidated() { pickerCross.requestPaint() } + function onColorChanged() { pickerCross.requestPaint() } + function onModeChanged() { pickerCross.requestPaint() } + } + onPaint: { var ctx = pickerCross.getContext('2d') - ctx.save() - ctx.clearRect(0, 0, pickerCross.width, pickerCross.height) - var yy = pickerCross.height -root.lightness * pickerCross.height - var xx = root.saturation * pickerCross.width + var yy, xx = 0 + + switch (root.mode) { + case ColorPicker.Mode.RGBA: + yy = pickerCross.height - root.saturationHSV * pickerCross.height + xx = root.hue * pickerCross.width + break + case ColorPicker.Mode.HSLA: + yy = pickerCross.height - root.lightness * pickerCross.height + xx = root.saturationHSL * pickerCross.width + break + case ColorPicker.Mode.HSVA: + default: + yy = pickerCross.height - root.value * pickerCross.height + xx = root.saturationHSV * pickerCross.width + break + } ctx.strokeStyle = pickerCross.strokeStyle ctx.lineWidth = 1 @@ -200,24 +297,37 @@ Column { } MouseArea { - id: mapMouseArea + id: mouseArea anchors.fill: parent preventStealing: true acceptedButtons: Qt.LeftButton | Qt.RightButton onPositionChanged: function(mouse) { - if (pressed && mouse.buttons === Qt.LeftButton) { + if (mouseArea.pressed && mouse.buttons === Qt.LeftButton) { var xx = Math.max(0, Math.min(mouse.x, parent.width)) var yy = Math.max(0, Math.min(mouse.y, parent.height)) - root.lightness = 1.0 - yy / parent.height - root.saturation = xx / parent.width + switch (root.mode) { + case ColorPicker.Mode.RGBA: + root.saturationHSV = 1.0 - yy / parent.height + root.hue = xx / parent.width + break + case ColorPicker.Mode.HSLA: + root.saturationHSL = xx / parent.width + root.lightness = 1.0 - yy / parent.height + break + case ColorPicker.Mode.HSVA: + default: + root.saturationHSV = xx / parent.width + root.value = 1.0 - yy / parent.height + break + } } } onPressed: function(mouse) { if (mouse.button === Qt.LeftButton) - positionChanged(mouse) + mouseArea.positionChanged(mouse) } onReleased: function(mouse) { if (mouse.button === Qt.LeftButton) @@ -233,10 +343,23 @@ Column { HueSlider { id: hueSlider + visible: root.mode !== ColorPicker.Mode.RGBA width: parent.width onValueChanged: { - if (root.hue !== value) - root.hue = value + if (root.hue !== hueSlider.value) + root.hue = hueSlider.value + } + onClicked: root.updateColor() + } + + LuminanceSlider { + id: luminanceSlider + visible: root.mode === ColorPicker.Mode.RGBA + width: parent.width + color: Qt.hsva(root.hue, root.color.hsvSaturation, 1, 1) + onValueChanged: { + if (root.value !== luminanceSlider.value) + root.value = (1.0 - luminanceSlider.value) } onClicked: root.updateColor() } @@ -246,8 +369,8 @@ Column { width: parent.width color: Qt.rgba(root.color.r, root.color.g, root.color.b, 1) onValueChanged: { - if (root.alpha !== value) - root.alpha = (1.0 - value) + if (root.alpha !== opacitySlider.value) + root.alpha = (1.0 - opacitySlider.value) } onClicked: root.updateColor() } diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/LuminanceSlider.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/LuminanceSlider.qml new file mode 100644 index 00000000000..a1fe726a9cd --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/LuminanceSlider.qml @@ -0,0 +1,121 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick 2.15 +import StudioTheme 1.0 as StudioTheme + +Item { + id: root + + property real value: 1 + property real minimum: 0 + property real maximum: 1 + property bool pressed: mouseArea.pressed + property bool integer: false + property color color + + signal clicked + + height: StudioTheme.Values.hueSliderHeight + + function updatePos() { + if (root.maximum > root.minimum) { + var pos = (track.width - handle.width) * (root.value - root.minimum) / (root.maximum - root.minimum) + return Math.min(Math.max(pos, 0), track.width - handle.width) + } else { + return 0 + } + } + + Item { + id: track + + width: parent.width + height: parent.height + + Image { + id: checkerboard + anchors.fill: parent + source: "images/checkers.png" + fillMode: Image.Tile + } + + Rectangle { + anchors.fill: parent + border.color: StudioTheme.Values.themeControlOutline + border.width: StudioTheme.Values.border + + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.000; color: root.color } + GradientStop { position: 1.000; color: "black" } + } + } + + Rectangle { + id: handle + width: StudioTheme.Values.hueSliderHandleWidth + height: track.height - 4 + anchors.verticalCenter: parent.verticalCenter + smooth: true + color: "transparent" + radius: 2 + border.color: "black" + border.width: 1 + x: root.updatePos() + y: 2 + z: 1 + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + color: "transparent" + radius: 1 + border.color: "white" + border.width: 1 + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + preventStealing: true + + function calculateValue() { + var handleX = Math.max(0, Math.min(mouseArea.mouseX, mouseArea.width)) + var realValue = (root.maximum - root.minimum) * handleX / mouseArea.width + root.minimum + root.value = root.integer ? Math.round(realValue) : realValue + } + + onPressed: calculateValue() + onReleased: root.clicked() + onPositionChanged: { + if (pressed) + calculateValue() + } + } + + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/qmldir b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/qmldir index 13e7de2f885..c0ad517e36c 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/qmldir +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/qmldir @@ -45,6 +45,7 @@ Label 2.0 Label.qml LineEdit 2.0 LineEdit.qml LinkIndicator2D 2.0 LinkIndicator2D.qml ListViewComboBox 2.0 ListViewComboBox.qml +LuminanceSlider 2.0 LuminanceSlider.qml MarginSection 2.0 MarginSection.qml MultiIconLabel 2.0 MultiIconLabel.qml OpacitySlider 2.0 OpacitySlider.qml diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 9035f01b11d..a89d873f91e 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -52,6 +52,7 @@ add_qtc_library(Utils filecrumblabel.cpp filecrumblabel.h fileinprojectfinder.cpp fileinprojectfinder.h filenamevalidatinglineedit.cpp filenamevalidatinglineedit.h + filepath.cpp filepath.h filesearch.cpp filesearch.h filesystemwatcher.cpp filesystemwatcher.h fileutils.cpp fileutils.h diff --git a/src/libs/utils/filepath.cpp b/src/libs/utils/filepath.cpp new file mode 100644 index 00000000000..a5264d17987 --- /dev/null +++ b/src/libs/utils/filepath.cpp @@ -0,0 +1,1295 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "filepath.h" + +#include "algorithm.h" +#include "commandline.h" +#include "environment.h" +#include "fileutils.h" +#include "hostosinfo.h" +#include "qtcassert.h" +#include "savefile.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#ifdef QTCREATOR_PCH_H +#define CALLBACK WINAPI +#endif +#include +#include +#endif + +#ifdef Q_OS_OSX +#include "fileutils_mac.h" +#endif + +QT_BEGIN_NAMESPACE +QDebug operator<<(QDebug dbg, const Utils::FilePath &c) +{ + return dbg << c.toUserOutput(); +} + +QT_END_NAMESPACE + +namespace Utils { + +static DeviceFileHooks s_deviceHooks; + +/*! \class Utils::FileUtils + + \brief The FileUtils class contains file and directory related convenience + functions. + +*/ + +static bool removeRecursivelyLocal(const FilePath &filePath, QString *error) +{ + QTC_ASSERT(!filePath.needsDevice(), return false); + QFileInfo fileInfo = filePath.toFileInfo(); + if (!fileInfo.exists() && !fileInfo.isSymLink()) + return true; + QFile::setPermissions(filePath.toString(), fileInfo.permissions() | QFile::WriteUser); + if (fileInfo.isDir()) { + QDir dir(filePath.toString()); + dir.setPath(dir.canonicalPath()); + if (dir.isRoot()) { + if (error) { + *error = QCoreApplication::translate("Utils::FileUtils", + "Refusing to remove root directory."); + } + return false; + } + if (dir.path() == QDir::home().canonicalPath()) { + if (error) { + *error = QCoreApplication::translate("Utils::FileUtils", + "Refusing to remove your home directory."); + } + return false; + } + + const QStringList fileNames = dir.entryList( + QDir::Files | QDir::Hidden | QDir::System | QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString &fileName : fileNames) { + if (!removeRecursivelyLocal(filePath / fileName, error)) + return false; + } + if (!QDir::root().rmdir(dir.path())) { + if (error) { + *error = QCoreApplication::translate("Utils::FileUtils", "Failed to remove directory \"%1\".") + .arg(filePath.toUserOutput()); + } + return false; + } + } else { + if (!QFile::remove(filePath.toString())) { + if (error) { + *error = QCoreApplication::translate("Utils::FileUtils", "Failed to remove file \"%1\".") + .arg(filePath.toUserOutput()); + } + return false; + } + } + return true; +} + +/*! + Copies the directory specified by \a srcFilePath recursively to \a tgtFilePath. \a tgtFilePath will contain + the target directory, which will be created. Example usage: + + \code + QString error; + book ok = Utils::FileUtils::copyRecursively("/foo/bar", "/foo/baz", &error); + if (!ok) + qDebug() << error; + \endcode + + This will copy the contents of /foo/bar into to the baz directory under /foo, which will be created in the process. + + \note The \a error parameter is optional. + + Returns whether the operation succeeded. +*/ + +bool FileUtils::copyRecursively(const FilePath &srcFilePath, const FilePath &tgtFilePath, QString *error) +{ + return copyRecursively( + srcFilePath, tgtFilePath, error, [](const QFileInfo &src, const QFileInfo &dest, QString *error) { + if (!QFile::copy(src.filePath(), dest.filePath())) { + if (error) { + *error = QCoreApplication::translate("Utils::FileUtils", + "Could not copy file \"%1\" to \"%2\".") + .arg(FilePath::fromFileInfo(src).toUserOutput(), + FilePath::fromFileInfo(dest).toUserOutput()); + } + return false; + } + return true; + }); +} + +/*! + Copies a file specified by \a srcFilePath to \a tgtFilePath only if \a srcFilePath is different + (file contents and last modification time). + + Returns whether the operation succeeded. +*/ + +bool FileUtils::copyIfDifferent(const FilePath &srcFilePath, const FilePath &tgtFilePath) +{ + QTC_ASSERT(srcFilePath.exists(), return false); + QTC_ASSERT(srcFilePath.scheme() == tgtFilePath.scheme(), return false); + QTC_ASSERT(srcFilePath.host() == tgtFilePath.host(), return false); + + if (tgtFilePath.exists()) { + const QDateTime srcModified = srcFilePath.lastModified(); + const QDateTime tgtModified = tgtFilePath.lastModified(); + if (srcModified == tgtModified) { + const QByteArray srcContents = srcFilePath.fileContents(); + const QByteArray tgtContents = srcFilePath.fileContents(); + if (srcContents == tgtContents) + return true; + } + tgtFilePath.removeFile(); + } + + return srcFilePath.copyFile(tgtFilePath); +} + +/*! + If this is a directory, the function will recursively check all files and return + true if one of them is newer than \a timeStamp. If this is a single file, true will + be returned if the file is newer than \a timeStamp. + + Returns whether at least one file in \a filePath has a newer date than + \a timeStamp. +*/ +bool FilePath::isNewerThan(const QDateTime &timeStamp) const +{ + if (!exists() || lastModified() >= timeStamp) + return true; + if (isDir()) { + const FilePaths dirContents = dirEntries(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + for (const FilePath &entry : dirContents) { + if (entry.isNewerThan(timeStamp)) + return true; + } + } + return false; +} + +Qt::CaseSensitivity FilePath::caseSensitivity() const +{ + if (m_scheme.isEmpty()) + return HostOsInfo::fileNameCaseSensitivity(); + + // FIXME: This could or possibly should the target device's file name case sensitivity + // into account by diverting to IDevice. However, as this is expensive and we are + // in time-critical path here, we go with "good enough" for now: + // The first approximation is "Anything unusual is not case sensitive" + return Qt::CaseSensitive; +} + +/*! + Recursively resolves symlinks if this is a symlink. + To resolve symlinks anywhere in the path, see canonicalPath. + Unlike QFileInfo::canonicalFilePath(), this function will still return the expected deepest + target file even if the symlink is dangling. + + \note Maximum recursion depth == 16. + + Returns the symlink target file path. +*/ +FilePath FilePath::resolveSymlinks() const +{ + FilePath current = *this; + int links = 16; + while (links--) { + const FilePath target = current.symLinkTarget(); + if (target.isEmpty()) + return current; + current = target; + } + return current; +} + +/*! + Recursively resolves possibly present symlinks in this file name. + Unlike QFileInfo::canonicalFilePath(), this function will not return an empty + string if path doesn't exist. + + Returns the canonical path. +*/ +FilePath FilePath::canonicalPath() const +{ + if (needsDevice()) { + // FIXME: Not a full solution, but it stays on the right device. + return *this; + } + const QString result = toFileInfo().canonicalFilePath(); + if (result.isEmpty()) + return *this; + return FilePath::fromString(result); +} + +FilePath FilePath::operator/(const QString &str) const +{ + return pathAppended(str); +} + +void FilePath::clear() +{ + m_data.clear(); + m_host.clear(); + m_scheme.clear(); +} + +bool FilePath::isEmpty() const +{ + return m_data.isEmpty(); +} + +/*! + Like QDir::toNativeSeparators(), but use prefix '~' instead of $HOME on unix systems when an + absolute path is given. + + Returns the possibly shortened path with native separators. +*/ +QString FilePath::shortNativePath() const +{ + if (HostOsInfo::isAnyUnixHost()) { + const FilePath home = FileUtils::homePath(); + if (isChildOf(home)) { + return QLatin1Char('~') + QDir::separator() + + QDir::toNativeSeparators(relativeChildPath(home).toString()); + } + } + return toUserOutput(); +} + +QString FileUtils::fileSystemFriendlyName(const QString &name) +{ + QString result = name; + result.replace(QRegularExpression(QLatin1String("\\W")), QLatin1String("_")); + result.replace(QRegularExpression(QLatin1String("_+")), QLatin1String("_")); // compact _ + result.remove(QRegularExpression(QLatin1String("^_*"))); // remove leading _ + result.remove(QRegularExpression(QLatin1String("_+$"))); // remove trailing _ + if (result.isEmpty()) + result = QLatin1String("unknown"); + return result; +} + +int FileUtils::indexOfQmakeUnfriendly(const QString &name, int startpos) +{ + static const QRegularExpression checkRegExp(QLatin1String("[^a-zA-Z0-9_.-]")); + return checkRegExp.match(name, startpos).capturedStart(); +} + +QString FileUtils::qmakeFriendlyName(const QString &name) +{ + QString result = name; + + // Remove characters that might trip up a build system (especially qmake): + int pos = indexOfQmakeUnfriendly(result); + while (pos >= 0) { + result[pos] = QLatin1Char('_'); + pos = indexOfQmakeUnfriendly(result, pos); + } + return fileSystemFriendlyName(result); +} + +bool FileUtils::makeWritable(const FilePath &path) +{ + const QString filePath = path.toString(); + return QFile::setPermissions(filePath, QFile::permissions(filePath) | QFile::WriteUser); +} + +// makes sure that capitalization of directories is canonical on Windows and OS X. +// This mimics the logic in QDeclarative_isFileCaseCorrect +QString FileUtils::normalizePathName(const QString &name) +{ +#ifdef Q_OS_WIN + const QString nativeSeparatorName(QDir::toNativeSeparators(name)); + const auto nameC = reinterpret_cast(nativeSeparatorName.utf16()); // MinGW + PIDLIST_ABSOLUTE file; + HRESULT hr = SHParseDisplayName(nameC, NULL, &file, 0, NULL); + if (FAILED(hr)) + return name; + TCHAR buffer[MAX_PATH]; + const bool success = SHGetPathFromIDList(file, buffer); + ILFree(file); + return success ? QDir::fromNativeSeparators(QString::fromUtf16(reinterpret_cast(buffer))) + : name; +#elif defined(Q_OS_OSX) + return Internal::normalizePathName(name); +#else // do not try to handle case-insensitive file systems on Linux + return name; +#endif +} + +static bool isRelativePathHelper(const QString &path, OsType osType) +{ + if (path.startsWith('/')) + return false; + if (osType == OsType::OsTypeWindows) { + if (path.startsWith('\\')) + return false; + // Unlike QFileInfo, this won't accept a relative path with a drive letter. + // Such paths result in a royal mess anyway ... + if (path.length() >= 3 && path.at(1) == ':' && path.at(0).isLetter() + && (path.at(2) == '/' || path.at(2) == '\\')) + return false; + } + return true; +} + +bool FileUtils::isRelativePath(const QString &path) +{ + return isRelativePathHelper(path, HostOsInfo::hostOs()); +} + +bool FilePath::isRelativePath() const +{ + return isRelativePathHelper(m_data, osType()); +} + +FilePath FilePath::resolvePath(const QString &fileName) const +{ + if (FileUtils::isAbsolutePath(fileName)) + return FilePath::fromString(QDir::cleanPath(fileName)); + FilePath result = *this; + result.setPath(QDir::cleanPath(m_data + '/' + fileName)); + return result; +} + +FilePath FilePath::cleanPath() const +{ + FilePath result = *this; + result.setPath(QDir::cleanPath(result.path())); + return result; +} + +FilePath FileUtils::commonPath(const FilePath &oldCommonPath, const FilePath &filePath) +{ + FilePath newCommonPath = oldCommonPath; + while (!newCommonPath.isEmpty() && !filePath.isChildOf(newCommonPath)) + newCommonPath = newCommonPath.parentDir(); + return newCommonPath.canonicalPath(); +} + +FilePath FileUtils::homePath() +{ + return FilePath::fromString(QDir::cleanPath(QDir::homePath())); +} + +bool FileUtils::renameFile(const FilePath &srcFilePath, const FilePath &tgtFilePath) +{ + QTC_ASSERT(!srcFilePath.needsDevice(), return false); + QTC_ASSERT(srcFilePath.scheme() == tgtFilePath.scheme(), return false); + return QFile::rename(srcFilePath.path(), tgtFilePath.path()); +} + + +/*! \class Utils::FilePath + + \brief The FilePath class is a light-weight convenience class for filenames. + + On windows filenames are compared case insensitively. +*/ + +FilePath::FilePath() +{ +} + +/// Constructs a FilePath from \a info +FilePath FilePath::fromFileInfo(const QFileInfo &info) +{ + return FilePath::fromString(info.absoluteFilePath()); +} + +/// \returns a QFileInfo +QFileInfo FilePath::toFileInfo() const +{ + return QFileInfo(m_data); +} + +FilePath FilePath::fromUrl(const QUrl &url) +{ + FilePath fn; + fn.m_scheme = url.scheme(); + fn.m_host = url.host(); + fn.m_data = url.path(); + return fn; +} + +/// \returns a QString for passing on to QString based APIs +QString FilePath::toString() const +{ + if (m_scheme.isEmpty()) + return m_data; + if (m_data.startsWith('/')) + return m_scheme + "://" + m_host + m_data; + return m_scheme + "://" + m_host + "/./" + m_data; +} + +QUrl FilePath::toUrl() const +{ + QUrl url; + url.setScheme(m_scheme); + url.setHost(m_host); + url.setPath(m_data); + return url; +} + +void FileUtils::setDeviceFileHooks(const DeviceFileHooks &hooks) +{ + s_deviceHooks = hooks; +} + +/// \returns a QString to display to the user +/// Converts the separators to the native format +QString FilePath::toUserOutput() const +{ + if (m_scheme.isEmpty()) + return QDir::toNativeSeparators(m_data); + return toString(); +} + +QString FilePath::fileName() const +{ + const QChar slash = QLatin1Char('/'); + return m_data.mid(m_data.lastIndexOf(slash) + 1); +} + +QString FilePath::fileNameWithPathComponents(int pathComponents) const +{ + if (pathComponents < 0) + return m_data; + const QChar slash = QLatin1Char('/'); + int i = m_data.lastIndexOf(slash); + if (pathComponents == 0 || i == -1) + return m_data.mid(i + 1); + int component = i + 1; + // skip adjacent slashes + while (i > 0 && m_data.at(--i) == slash) + ; + while (i >= 0 && --pathComponents >= 0) { + i = m_data.lastIndexOf(slash, i); + component = i + 1; + while (i > 0 && m_data.at(--i) == slash) + ; + } + + // If there are no more slashes before the found one, return the entire string + if (i > 0 && m_data.lastIndexOf(slash, i) != -1) + return m_data.mid(component); + return m_data; +} + +/// \returns the base name of the file without the path. +/// +/// The base name consists of all characters in the file up to +/// (but not including) the first '.' character. + +QString FilePath::baseName() const +{ + const QString &name = fileName(); + return name.left(name.indexOf('.')); +} + +/// \returns the complete base name of the file without the path. +/// +/// The complete base name consists of all characters in the file up to +/// (but not including) the last '.' character + +QString FilePath::completeBaseName() const +{ + const QString &name = fileName(); + return name.left(name.lastIndexOf('.')); +} + +/// \returns the suffix (extension) of the file. +/// +/// The suffix consists of all characters in the file after +/// (but not including) the last '.'. + +QString FilePath::suffix() const +{ + const QString &name = fileName(); + const int index = name.lastIndexOf('.'); + if (index >= 0) + return name.mid(index + 1); + return {}; +} + +/// \returns the complete suffix (extension) of the file. +/// +/// The complete suffix consists of all characters in the file after +/// (but not including) the first '.'. + +QString FilePath::completeSuffix() const +{ + const QString &name = fileName(); + const int index = name.indexOf('.'); + if (index >= 0) + return name.mid(index + 1); + return {}; +} + +void FilePath::setScheme(const QString &scheme) +{ + QTC_CHECK(!scheme.contains('/')); + m_scheme = scheme; +} + +void FilePath::setHost(const QString &host) +{ + QTC_CHECK(!host.contains('/')); + m_host = host; +} + + +/// \returns a bool indicating whether a file with this +/// FilePath exists. +bool FilePath::exists() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.exists, return false); + return s_deviceHooks.exists(*this); + } + return !isEmpty() && QFileInfo::exists(m_data); +} + +/// \returns a bool indicating whether a path is writable. +bool FilePath::isWritableDir() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.isWritableDir, return false); + return s_deviceHooks.isWritableDir(*this); + } + const QFileInfo fi{m_data}; + return exists() && fi.isDir() && fi.isWritable(); +} + +bool FilePath::isWritableFile() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.isWritableFile, return false); + return s_deviceHooks.isWritableFile(*this); + } + const QFileInfo fi{m_data}; + return fi.exists() && fi.isWritable() && !fi.isDir(); +} + +bool FilePath::ensureWritableDir() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.ensureWritableDir, return false); + return s_deviceHooks.ensureWritableDir(*this); + } + const QFileInfo fi{m_data}; + if (exists() && fi.isDir() && fi.isWritable()) + return true; + return QDir().mkpath(m_data); +} + +bool FilePath::ensureExistingFile() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.ensureExistingFile, return false); + return s_deviceHooks.ensureExistingFile(*this); + } + QFile f(m_data); + if (f.exists()) + return true; + f.open(QFile::WriteOnly); + f.close(); + return f.exists(); +} + +bool FilePath::isExecutableFile() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.isExecutableFile, return false); + return s_deviceHooks.isExecutableFile(*this); + } + const QFileInfo fi{m_data}; + return fi.exists() && fi.isExecutable() && !fi.isDir(); +} + +bool FilePath::isReadableFile() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.isReadableFile, return false); + return s_deviceHooks.isReadableFile(*this); + } + const QFileInfo fi{m_data}; + return fi.exists() && fi.isReadable() && !fi.isDir(); +} + +bool FilePath::isReadableDir() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.isReadableDir, return false); + return s_deviceHooks.isReadableDir(*this); + } + const QFileInfo fi{m_data}; + return fi.exists() && fi.isReadable() && fi.isDir(); +} + +bool FilePath::isFile() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.isFile, return false); + return s_deviceHooks.isFile(*this); + } + const QFileInfo fi{m_data}; + return fi.exists() && fi.isFile(); +} + +bool FilePath::isDir() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.isDir, return false); + return s_deviceHooks.isDir(*this); + } + const QFileInfo fi{m_data}; + return fi.exists() && fi.isDir(); +} + +bool FilePath::createDir() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.createDir, return false); + return s_deviceHooks.createDir(*this); + } + QDir dir(m_data); + return dir.mkpath(dir.absolutePath()); +} + +FilePaths FilePath::dirEntries(const QStringList &nameFilters, + QDir::Filters filters, + QDir::SortFlags sort) const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.dirEntries, return {}); + return s_deviceHooks.dirEntries(*this, nameFilters, filters, sort); + } + + const QFileInfoList entryInfoList = QDir(m_data).entryInfoList(nameFilters, filters, sort); + return Utils::transform(entryInfoList, &FilePath::fromFileInfo); +} + + +QList FilePath::dirEntries(QDir::Filters filters) const +{ + return dirEntries({}, filters); +} + +QByteArray FilePath::fileContents(qint64 maxSize, qint64 offset) const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.fileContents, return {}); + return s_deviceHooks.fileContents(*this, maxSize, offset); + } + + const QString path = toString(); + QFile f(path); + if (!f.exists()) + return {}; + + if (!f.open(QFile::ReadOnly)) + return {}; + + if (offset != 0) + f.seek(offset); + + if (maxSize != -1) + return f.read(maxSize); + + return f.readAll(); +} + +bool FilePath::writeFileContents(const QByteArray &data) const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.writeFileContents, return {}); + return s_deviceHooks.writeFileContents(*this, data); + } + + QFile file(path()); + QTC_ASSERT(file.open(QFile::WriteOnly | QFile::Truncate), return false); + qint64 res = file.write(data); + return res == data.size(); +} + +bool FilePath::needsDevice() const +{ + return !m_scheme.isEmpty(); +} + +/// \returns an empty FilePath if this is not a symbolic linl +FilePath FilePath::symLinkTarget() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.symLinkTarget, return {}); + return s_deviceHooks.symLinkTarget(*this); + } + const QFileInfo info(m_data); + if (!info.isSymLink()) + return {}; + return FilePath::fromString(info.symLinkTarget()); +} + +FilePath FilePath::withExecutableSuffix() const +{ + FilePath res = *this; + res.setPath(OsSpecificAspects::withExecutableSuffix(osType(), m_data)); + return res; +} + +/// Find the parent directory of a given directory. + +/// Returns an empty FilePath if the current directory is already +/// a root level directory. + +/// \returns \a FilePath with the last segment removed. +FilePath FilePath::parentDir() const +{ + const QString basePath = path(); + if (basePath.isEmpty()) + return FilePath(); + + const QDir base(basePath); + if (base.isRoot()) + return FilePath(); + + const QString path = basePath + QLatin1String("/.."); + const QString parent = QDir::cleanPath(path); + QTC_ASSERT(parent != path, return FilePath()); + + FilePath result = *this; + result.setPath(parent); + return result; +} + +FilePath FilePath::absolutePath() const +{ + FilePath result = *this; + result.m_data = QFileInfo(m_data).absolutePath(); + return result; +} + +FilePath FilePath::absoluteFilePath() const +{ + FilePath result = *this; + result.m_data = QFileInfo(m_data).absoluteFilePath(); + return result; +} + +FilePath FilePath::absoluteFilePath(const FilePath &tail) const +{ + if (isRelativePathHelper(tail.m_data, osType())) + return pathAppended(tail.m_data); + return tail; +} + +/// Constructs an absolute FilePath from this path which +/// is interpreted as being relative to \a anchor. +FilePath FilePath::absoluteFromRelativePath(const FilePath &anchor) const +{ + QDir anchorDir = QFileInfo(anchor.m_data).absoluteDir(); + QString absoluteFilePath = QFileInfo(anchorDir, m_data).canonicalFilePath(); + return FilePath::fromString(absoluteFilePath); +} + +/// Constructs a FilePath from \a filename +/// \a filename is not checked for validity. +FilePath FilePath::fromString(const QString &filename) +{ + FilePath fn; + if (filename.startsWith('/')) { + fn.m_data = filename; // fast track: absolute local paths + } else { + int pos1 = filename.indexOf("://"); + if (pos1 >= 0) { + fn.m_scheme = filename.left(pos1); + pos1 += 3; + int pos2 = filename.indexOf('/', pos1); + if (pos2 == -1) { + fn.m_data = filename.mid(pos1); + } else { + fn.m_host = filename.mid(pos1, pos2 - pos1); + fn.m_data = filename.mid(pos2); + } + if (fn.m_data.startsWith("/./")) + fn.m_data = fn.m_data.mid(3); + } else { + fn.m_data = filename; // treat everything else as local, too. + } + } + return fn; +} + +/// Constructs a FilePath from \a filePath. The \a defaultExtension is appended +/// to \a filename if that does not have an extension already. +/// \a filePath is not checked for validity. +FilePath FilePath::fromStringWithExtension(const QString &filepath, const QString &defaultExtension) +{ + if (filepath.isEmpty() || defaultExtension.isEmpty()) + return FilePath::fromString(filepath); + + QString rc = filepath; + QFileInfo fi(filepath); + // Add extension unless user specified something else + const QChar dot = QLatin1Char('.'); + if (!fi.fileName().contains(dot)) { + if (!defaultExtension.startsWith(dot)) + rc += dot; + rc += defaultExtension; + } + return FilePath::fromString(rc); +} + +/// Constructs a FilePath from \a filePath +/// \a filePath is only passed through QDir::fromNativeSeparators +FilePath FilePath::fromUserInput(const QString &filePath) +{ + QString clean = QDir::fromNativeSeparators(filePath); + if (clean.startsWith(QLatin1String("~/"))) + return FileUtils::homePath().pathAppended(clean.mid(2)); + return FilePath::fromString(clean); +} + +/// Constructs a FilePath from \a filePath, which is encoded as UTF-8. +/// \a filePath is not checked for validity. +FilePath FilePath::fromUtf8(const char *filename, int filenameSize) +{ + return FilePath::fromString(QString::fromUtf8(filename, filenameSize)); +} + +FilePath FilePath::fromVariant(const QVariant &variant) +{ + if (variant.type() == QVariant::Url) + return FilePath::fromUrl(variant.toUrl()); + return FilePath::fromString(variant.toString()); +} + +QVariant FilePath::toVariant() const +{ + return toString(); +} + +QDir FilePath::toDir() const +{ + return QDir(m_data); +} + +bool FilePath::operator==(const FilePath &other) const +{ + return QString::compare(m_data, other.m_data, caseSensitivity()) == 0 + && m_host == other.m_host + && m_scheme == other.m_scheme; +} + +bool FilePath::operator!=(const FilePath &other) const +{ + return !(*this == other); +} + +bool FilePath::operator<(const FilePath &other) const +{ + const int cmp = QString::compare(m_data, other.m_data, caseSensitivity()); + if (cmp != 0) + return cmp < 0; + if (m_host != other.m_host) + return m_host < other.m_host; + return m_scheme < other.m_scheme; +} + +bool FilePath::operator<=(const FilePath &other) const +{ + return !(other < *this); +} + +bool FilePath::operator>(const FilePath &other) const +{ + return other < *this; +} + +bool FilePath::operator>=(const FilePath &other) const +{ + return !(*this < other); +} + +FilePath FilePath::operator+(const QString &s) const +{ + FilePath res = *this; + res.m_data += s; + return res; +} + +/// \returns whether FilePath is a child of \a s +bool FilePath::isChildOf(const FilePath &s) const +{ + if (s.isEmpty()) + return false; + if (!m_data.startsWith(s.m_data, caseSensitivity())) + return false; + if (m_data.size() <= s.m_data.size()) + return false; + // s is root, '/' was already tested in startsWith + if (s.m_data.endsWith(QLatin1Char('/'))) + return true; + // s is a directory, next character should be '/' (/tmpdir is NOT a child of /tmp) + return m_data.at(s.m_data.size()) == QLatin1Char('/'); +} + +/// \overload +bool FilePath::isChildOf(const QDir &dir) const +{ + return isChildOf(FilePath::fromString(dir.absolutePath())); +} + +/// \returns whether FilePath startsWith \a s +bool FilePath::startsWith(const QString &s) const +{ + return m_data.startsWith(s, caseSensitivity()); +} + +/// \returns whether FilePath endsWith \a s +bool FilePath::endsWith(const QString &s) const +{ + return m_data.endsWith(s, caseSensitivity()); +} + +/// \returns the relativeChildPath of FilePath to parent if FilePath is a child of parent +/// \note returns a empty FilePath if FilePath is not a child of parent +/// That is, this never returns a path starting with "../" +FilePath FilePath::relativeChildPath(const FilePath &parent) const +{ + FilePath res; + if (isChildOf(parent)) + res.m_data = m_data.mid(parent.m_data.size() + 1, -1); + return res; +} + +/// \returns the relativePath of FilePath to given \a anchor. +/// Both, FilePath and anchor may be files or directories. +/// Example usage: +/// +/// \code +/// FilePath filePath("/foo/b/ar/file.txt"); +/// FilePath relativePath = filePath.relativePath("/foo/c"); +/// qDebug() << relativePath +/// \endcode +/// +/// The debug output will be "../b/ar/file.txt". +/// +FilePath FilePath::relativePath(const FilePath &anchor) const +{ + const QFileInfo fileInfo(m_data); + QString absolutePath; + QString filename; + if (fileInfo.isFile()) { + absolutePath = fileInfo.absolutePath(); + filename = fileInfo.fileName(); + } else if (fileInfo.isDir()) { + absolutePath = fileInfo.absoluteFilePath(); + } else { + return {}; + } + const QFileInfo anchorInfo(anchor.m_data); + QString absoluteAnchorPath; + if (anchorInfo.isFile()) + absoluteAnchorPath = anchorInfo.absolutePath(); + else if (anchorInfo.isDir()) + absoluteAnchorPath = anchorInfo.absoluteFilePath(); + else + return {}; + QString relativeFilePath = calcRelativePath(absolutePath, absoluteAnchorPath); + if (!filename.isEmpty()) { + if (!relativeFilePath.isEmpty()) + relativeFilePath += '/'; + relativeFilePath += filename; + } + return FilePath::fromString(relativeFilePath); +} + +/// \returns the relativePath of \a absolutePath to given \a absoluteAnchorPath. +/// Both paths must be an absolute path to a directory. Example usage: +/// +/// \code +/// qDebug() << FilePath::calcRelativePath("/foo/b/ar", "/foo/c"); +/// \endcode +/// +/// The debug output will be "../b/ar". +/// +/// \see FilePath::relativePath +/// +QString FilePath::calcRelativePath(const QString &absolutePath, const QString &absoluteAnchorPath) +{ + if (absolutePath.isEmpty() || absoluteAnchorPath.isEmpty()) + return QString(); + // TODO using split() instead of parsing the strings by char index is slow + // and needs more memory (but the easiest implementation for now) + const QStringList splits1 = absolutePath.split('/'); + const QStringList splits2 = absoluteAnchorPath.split('/'); + int i = 0; + while (i < splits1.count() && i < splits2.count() && splits1.at(i) == splits2.at(i)) + ++i; + QString relativePath; + int j = i; + bool addslash = false; + while (j < splits2.count()) { + if (!splits2.at(j).isEmpty()) { + if (addslash) + relativePath += '/'; + relativePath += ".."; + addslash = true; + } + ++j; + } + while (i < splits1.count()) { + if (!splits1.at(i).isEmpty()) { + if (addslash) + relativePath += '/'; + relativePath += splits1.at(i); + addslash = true; + } + ++i; + } + return relativePath; +} + +/*! + Returns a path corresponding to the current object on the + same device as \a deviceTemplate. + + Example usage: + \code + localDir = FilePath::fromString("/tmp/workingdir"); + executable = FilePath::fromUrl("docker://123/bin/ls") + realDir = localDir.onDevice(executable) + assert(realDir == FilePath::fromUrl("docker://123/tmp/workingdir")) + \endcode +*/ +FilePath FilePath::onDevice(const FilePath &deviceTemplate) const +{ + FilePath res; + res.m_data = m_data; + res.m_host = deviceTemplate.m_host; + res.m_scheme = deviceTemplate.m_scheme; + return res; +} + +/*! + Returns a FilePath with local path \a newPath on the same device + as the current object. + + Example usage: + \code + devicePath = FilePath::fromString("docker://123/tmp"); + newPath = devicePath.withNewPath("/bin/ls"); + assert(realDir == FilePath::fromUrl("docker://123/bin/ls")) + \endcode +*/ +FilePath FilePath::withNewPath(const QString &newPath) const +{ + FilePath res; + res.m_data = newPath; + res.m_host = m_host; + res.m_scheme = m_scheme; + return res; +} + +/*! + Searched a binary corresponding to this object in the PATH of + the device implied by this object's scheme and host. + + Example usage: + \code + binary = FilePath::fromUrl("docker://123/./make); + fullPath = binary.searchOnDevice(); + assert(fullPath == FilePath::fromUrl("docker://123/usr/bin/make")) + \endcode +*/ +FilePath FilePath::searchOnDevice(const FilePaths &dirs) const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.searchInPath, return {}); + return s_deviceHooks.searchInPath(*this, dirs); + } + return Environment::systemEnvironment().searchInPath(path(), dirs); +} + +Environment FilePath::deviceEnvironment() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.environment, return {}); + return s_deviceHooks.environment(*this); + } + return Environment::systemEnvironment(); +} + +QString FilePath::formatFilePaths(const QList &files, const QString &separator) +{ + const QStringList nativeFiles = Utils::transform(files, &FilePath::toUserOutput); + return nativeFiles.join(separator); +} + +void FilePath::removeDuplicates(QList &files) +{ + // FIXME: Improve. + QStringList list = Utils::transform(files, &FilePath::toString); + list.removeDuplicates(); + files = Utils::transform(list, &FilePath::fromString); +} + +void FilePath::sort(QList &files) +{ + // FIXME: Improve. + QStringList list = Utils::transform(files, &FilePath::toString); + list.sort(); + files = Utils::transform(list, &FilePath::fromString); +} + +FilePath FilePath::pathAppended(const QString &path) const +{ + FilePath fn = *this; + if (path.isEmpty()) + return fn; + if (!fn.m_data.isEmpty() && !fn.m_data.endsWith(QLatin1Char('/'))) + fn.m_data.append('/'); + fn.m_data.append(path); + return fn; +} + +FilePath FilePath::stringAppended(const QString &str) const +{ + FilePath fn = *this; + fn.m_data.append(str); + return fn; +} + +uint FilePath::hash(uint seed) const +{ + if (Utils::HostOsInfo::fileNameCaseSensitivity() == Qt::CaseInsensitive) + return qHash(m_data.toUpper(), seed); + return qHash(m_data, seed); +} + +QDateTime FilePath::lastModified() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.lastModified, return {}); + return s_deviceHooks.lastModified(*this); + } + return toFileInfo().lastModified(); +} + +QFile::Permissions FilePath::permissions() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.permissions, return {}); + return s_deviceHooks.permissions(*this); + } + return toFileInfo().permissions(); +} + +OsType FilePath::osType() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.osType, return {}); + return s_deviceHooks.osType(*this); + } + return HostOsInfo::hostOs(); +} + +bool FilePath::removeFile() const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.removeFile, return false); + return s_deviceHooks.removeFile(*this); + } + return QFile::remove(path()); +} + +/*! + Removes the directory this filePath refers too and its subdirectories recursively. + + \note The \a error parameter is optional. + + Returns whether the operation succeeded. +*/ +bool FilePath::removeRecursively(QString *error) const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.removeRecursively, return false); + return s_deviceHooks.removeRecursively(*this); + } + return removeRecursivelyLocal(*this, error); +} + +bool FilePath::copyFile(const FilePath &target) const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.copyFile, return false); + return s_deviceHooks.copyFile(*this, target); + } + return QFile::copy(path(), target.path()); +} + +bool FilePath::renameFile(const FilePath &target) const +{ + if (needsDevice()) { + QTC_ASSERT(s_deviceHooks.renameFile, return false); + return s_deviceHooks.renameFile(*this, target); + } + return QFile::rename(path(), target.path()); +} + +QTextStream &operator<<(QTextStream &s, const FilePath &fn) +{ + return s << fn.toString(); +} + +} // namespace Utils + +std::hash::result_type + std::hash::operator()(const std::hash::argument_type &fn) const +{ + if (fn.caseSensitivity() == Qt::CaseInsensitive) + return hash()(fn.toString().toUpper().toStdString()); + return hash()(fn.toString().toStdString()); +} diff --git a/src/libs/utils/filepath.h b/src/libs/utils/filepath.h new file mode 100644 index 00000000000..6ece9ea162a --- /dev/null +++ b/src/libs/utils/filepath.h @@ -0,0 +1,193 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "utils_global.h" + +#include "hostosinfo.h" + +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE +class QDateTime; +class QDebug; +class QFileInfo; +class QUrl; +QT_END_NAMESPACE + +class tst_fileutils; // This becomes a friend of Utils::FilePath for testing private methods. + +namespace Utils { + +class Environment; + +class QTCREATOR_UTILS_EXPORT FilePath +{ +public: + FilePath(); + + static FilePath fromString(const QString &filepath); + static FilePath fromFileInfo(const QFileInfo &info); + static FilePath fromStringWithExtension(const QString &filepath, const QString &defaultExtension); + static FilePath fromUserInput(const QString &filepath); + static FilePath fromUtf8(const char *filepath, int filepathSize = -1); + static FilePath fromVariant(const QVariant &variant); + + QString toString() const; + FilePath onDevice(const FilePath &deviceTemplate) const; + FilePath withNewPath(const QString &newPath) const; + + QFileInfo toFileInfo() const; + QVariant toVariant() const; + QDir toDir() const; + + QString toUserOutput() const; + QString shortNativePath() const; + + QString fileName() const; + QString fileNameWithPathComponents(int pathComponents) const; + + QString baseName() const; + QString completeBaseName() const; + QString suffix() const; + QString completeSuffix() const; + + QString scheme() const { return m_scheme; } + void setScheme(const QString &scheme); + + QString host() const { return m_host; } + void setHost(const QString &host); + + QString path() const { return m_data; } + void setPath(const QString &path) { m_data = path; } + + bool needsDevice() const; + bool exists() const; + + bool isWritablePath() const { return isWritableDir(); } // Remove. + bool isWritableDir() const; + bool isWritableFile() const; + bool ensureWritableDir() const; + bool ensureExistingFile() const; + bool isExecutableFile() const; + bool isReadableFile() const; + bool isReadableDir() const; + bool isRelativePath() const; + bool isAbsolutePath() const { return !isRelativePath(); } + bool isFile() const; + bool isDir() const; + + bool createDir() const; + QList dirEntries(const QStringList &nameFilters, + QDir::Filters filters, + QDir::SortFlags sort = QDir::NoSort) const; + QList dirEntries(QDir::Filters filters) const; + QByteArray fileContents(qint64 maxSize = -1, qint64 offset = 0) const; + bool writeFileContents(const QByteArray &data) const; + + FilePath parentDir() const; + FilePath absolutePath() const; + FilePath absoluteFilePath() const; + FilePath absoluteFilePath(const FilePath &tail) const; + FilePath absoluteFromRelativePath(const FilePath &anchor) const; + + bool operator==(const FilePath &other) const; + bool operator!=(const FilePath &other) const; + bool operator<(const FilePath &other) const; + bool operator<=(const FilePath &other) const; + bool operator>(const FilePath &other) const; + bool operator>=(const FilePath &other) const; + FilePath operator+(const QString &s) const; + + bool isChildOf(const FilePath &s) const; + bool isChildOf(const QDir &dir) const; + bool startsWith(const QString &s) const; + bool endsWith(const QString &s) const; + + bool isNewerThan(const QDateTime &timeStamp) const; + QDateTime lastModified() const; + QFile::Permissions permissions() const; + OsType osType() const; + bool removeFile() const; + bool removeRecursively(QString *error = nullptr) const; + bool copyFile(const FilePath &target) const; + bool renameFile(const FilePath &target) const; + + Qt::CaseSensitivity caseSensitivity() const; + + FilePath relativeChildPath(const FilePath &parent) const; + FilePath relativePath(const FilePath &anchor) const; + FilePath pathAppended(const QString &str) const; + FilePath stringAppended(const QString &str) const; + FilePath resolvePath(const QString &fileName) const; + FilePath cleanPath() const; + + FilePath canonicalPath() const; + FilePath symLinkTarget() const; + FilePath resolveSymlinks() const; + FilePath withExecutableSuffix() const; + + FilePath operator/(const QString &str) const; + + void clear(); + bool isEmpty() const; + + uint hash(uint seed) const; + + // NOTE: Most FilePath operations on FilePath created from URL currently + // do not work. Among the working are .toVariant() and .toUrl(). + static FilePath fromUrl(const QUrl &url); + QUrl toUrl() const; + + FilePath searchOnDevice(const QList &dirs) const; + Environment deviceEnvironment() const; + + static QString formatFilePaths(const QList &files, const QString &separator); + static void removeDuplicates(QList &files); + static void sort(QList &files); + +private: + friend class ::tst_fileutils; + static QString calcRelativePath(const QString &absolutePath, const QString &absoluteAnchorPath); + + QString m_scheme; + QString m_host; + QString m_data; +}; + +using FilePaths = QList; + +} // namespace Utils + +QT_BEGIN_NAMESPACE +QTCREATOR_UTILS_EXPORT QDebug operator<<(QDebug dbg, const Utils::FilePath &c); +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(Utils::FilePath) diff --git a/src/libs/utils/fileutils.cpp b/src/libs/utils/fileutils.cpp index fceca709372..ff7745761c8 100644 --- a/src/libs/utils/fileutils.cpp +++ b/src/libs/utils/fileutils.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. @@ -36,7 +36,6 @@ #include #include #include -#include #include #include #include @@ -57,438 +56,9 @@ #include "fileutils_mac.h" #endif -QT_BEGIN_NAMESPACE -QDebug operator<<(QDebug dbg, const Utils::FilePath &c) -{ - return dbg << c.toUserOutput(); -} - -QT_END_NAMESPACE - namespace Utils { -static DeviceFileHooks s_deviceHooks; - -/*! \class Utils::FileUtils - - \brief The FileUtils class contains file and directory related convenience - functions. - -*/ - -static bool removeRecursivelyLocal(const FilePath &filePath, QString *error) -{ - QTC_ASSERT(!filePath.needsDevice(), return false); - QFileInfo fileInfo = filePath.toFileInfo(); - if (!fileInfo.exists() && !fileInfo.isSymLink()) - return true; - QFile::setPermissions(filePath.toString(), fileInfo.permissions() | QFile::WriteUser); - if (fileInfo.isDir()) { - QDir dir(filePath.toString()); - dir.setPath(dir.canonicalPath()); - if (dir.isRoot()) { - if (error) { - *error = QCoreApplication::translate("Utils::FileUtils", - "Refusing to remove root directory."); - } - return false; - } - if (dir.path() == QDir::home().canonicalPath()) { - if (error) { - *error = QCoreApplication::translate("Utils::FileUtils", - "Refusing to remove your home directory."); - } - return false; - } - - const QStringList fileNames = dir.entryList( - QDir::Files | QDir::Hidden | QDir::System | QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString &fileName : fileNames) { - if (!removeRecursivelyLocal(filePath / fileName, error)) - return false; - } - if (!QDir::root().rmdir(dir.path())) { - if (error) { - *error = QCoreApplication::translate("Utils::FileUtils", "Failed to remove directory \"%1\".") - .arg(filePath.toUserOutput()); - } - return false; - } - } else { - if (!QFile::remove(filePath.toString())) { - if (error) { - *error = QCoreApplication::translate("Utils::FileUtils", "Failed to remove file \"%1\".") - .arg(filePath.toUserOutput()); - } - return false; - } - } - return true; -} - -/*! - Copies the directory specified by \a srcFilePath recursively to \a tgtFilePath. \a tgtFilePath will contain - the target directory, which will be created. Example usage: - - \code - QString error; - book ok = Utils::FileUtils::copyRecursively("/foo/bar", "/foo/baz", &error); - if (!ok) - qDebug() << error; - \endcode - - This will copy the contents of /foo/bar into to the baz directory under /foo, which will be created in the process. - - \note The \a error parameter is optional. - - Returns whether the operation succeeded. -*/ - -bool FileUtils::copyRecursively(const FilePath &srcFilePath, const FilePath &tgtFilePath, QString *error) -{ - return copyRecursively( - srcFilePath, tgtFilePath, error, [](const QFileInfo &src, const QFileInfo &dest, QString *error) { - if (!QFile::copy(src.filePath(), dest.filePath())) { - if (error) { - *error = QCoreApplication::translate("Utils::FileUtils", - "Could not copy file \"%1\" to \"%2\".") - .arg(FilePath::fromFileInfo(src).toUserOutput(), - FilePath::fromFileInfo(dest).toUserOutput()); - } - return false; - } - return true; - }); -} - -/*! - Copies a file specified by \a srcFilePath to \a tgtFilePath only if \a srcFilePath is different - (file contents and last modification time). - - Returns whether the operation succeeded. -*/ - -bool FileUtils::copyIfDifferent(const FilePath &srcFilePath, const FilePath &tgtFilePath) -{ - QTC_ASSERT(srcFilePath.exists(), return false); - QTC_ASSERT(srcFilePath.scheme() == tgtFilePath.scheme(), return false); - QTC_ASSERT(srcFilePath.host() == tgtFilePath.host(), return false); - - if (tgtFilePath.exists()) { - const QDateTime srcModified = srcFilePath.lastModified(); - const QDateTime tgtModified = tgtFilePath.lastModified(); - if (srcModified == tgtModified) { - const QByteArray srcContents = srcFilePath.fileContents(); - const QByteArray tgtContents = srcFilePath.fileContents(); - if (srcContents == tgtContents) - return true; - } - tgtFilePath.removeFile(); - } - - return srcFilePath.copyFile(tgtFilePath); -} - -/*! - If this is a directory, the function will recursively check all files and return - true if one of them is newer than \a timeStamp. If this is a single file, true will - be returned if the file is newer than \a timeStamp. - - Returns whether at least one file in \a filePath has a newer date than - \a timeStamp. -*/ -bool FilePath::isNewerThan(const QDateTime &timeStamp) const -{ - if (!exists() || lastModified() >= timeStamp) - return true; - if (isDir()) { - const FilePaths dirContents = dirEntries(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); - for (const FilePath &entry : dirContents) { - if (entry.isNewerThan(timeStamp)) - return true; - } - } - return false; -} - -Qt::CaseSensitivity FilePath::caseSensitivity() const -{ - if (m_scheme.isEmpty()) - return HostOsInfo::fileNameCaseSensitivity(); - - // FIXME: This could or possibly should the target device's file name case sensitivity - // into account by diverting to IDevice. However, as this is expensive and we are - // in time-critical path here, we go with "good enough" for now: - // The first approximation is "Anything unusual is not case sensitive" - return Qt::CaseSensitive; -} - -/*! - Recursively resolves symlinks if this is a symlink. - To resolve symlinks anywhere in the path, see canonicalPath. - Unlike QFileInfo::canonicalFilePath(), this function will still return the expected deepest - target file even if the symlink is dangling. - - \note Maximum recursion depth == 16. - - Returns the symlink target file path. -*/ -FilePath FilePath::resolveSymlinks() const -{ - FilePath current = *this; - int links = 16; - while (links--) { - const FilePath target = current.symLinkTarget(); - if (target.isEmpty()) - return current; - current = target; - } - return current; -} - -/*! - Recursively resolves possibly present symlinks in this file name. - Unlike QFileInfo::canonicalFilePath(), this function will not return an empty - string if path doesn't exist. - - Returns the canonical path. -*/ -FilePath FilePath::canonicalPath() const -{ - if (needsDevice()) { - // FIXME: Not a full solution, but it stays on the right device. - return *this; - } - const QString result = toFileInfo().canonicalFilePath(); - if (result.isEmpty()) - return *this; - return FilePath::fromString(result); -} - -FilePath FilePath::operator/(const QString &str) const -{ - return pathAppended(str); -} - -void FilePath::clear() -{ - m_data.clear(); - m_host.clear(); - m_scheme.clear(); -} - -bool FilePath::isEmpty() const -{ - return m_data.isEmpty(); -} - -/*! - Like QDir::toNativeSeparators(), but use prefix '~' instead of $HOME on unix systems when an - absolute path is given. - - Returns the possibly shortened path with native separators. -*/ -QString FilePath::shortNativePath() const -{ - if (HostOsInfo::isAnyUnixHost()) { - const FilePath home = FileUtils::homePath(); - if (isChildOf(home)) { - return QLatin1Char('~') + QDir::separator() - + QDir::toNativeSeparators(relativeChildPath(home).toString()); - } - } - return toUserOutput(); -} - -QString FileUtils::fileSystemFriendlyName(const QString &name) -{ - QString result = name; - result.replace(QRegularExpression(QLatin1String("\\W")), QLatin1String("_")); - result.replace(QRegularExpression(QLatin1String("_+")), QLatin1String("_")); // compact _ - result.remove(QRegularExpression(QLatin1String("^_*"))); // remove leading _ - result.remove(QRegularExpression(QLatin1String("_+$"))); // remove trailing _ - if (result.isEmpty()) - result = QLatin1String("unknown"); - return result; -} - -int FileUtils::indexOfQmakeUnfriendly(const QString &name, int startpos) -{ - static const QRegularExpression checkRegExp(QLatin1String("[^a-zA-Z0-9_.-]")); - return checkRegExp.match(name, startpos).capturedStart(); -} - -QString FileUtils::qmakeFriendlyName(const QString &name) -{ - QString result = name; - - // Remove characters that might trip up a build system (especially qmake): - int pos = indexOfQmakeUnfriendly(result); - while (pos >= 0) { - result[pos] = QLatin1Char('_'); - pos = indexOfQmakeUnfriendly(result, pos); - } - return fileSystemFriendlyName(result); -} - -bool FileUtils::makeWritable(const FilePath &path) -{ - const QString filePath = path.toString(); - return QFile::setPermissions(filePath, QFile::permissions(filePath) | QFile::WriteUser); -} - -// makes sure that capitalization of directories is canonical on Windows and OS X. -// This mimics the logic in QDeclarative_isFileCaseCorrect -QString FileUtils::normalizePathName(const QString &name) -{ -#ifdef Q_OS_WIN - const QString nativeSeparatorName(QDir::toNativeSeparators(name)); - const auto nameC = reinterpret_cast(nativeSeparatorName.utf16()); // MinGW - PIDLIST_ABSOLUTE file; - HRESULT hr = SHParseDisplayName(nameC, NULL, &file, 0, NULL); - if (FAILED(hr)) - return name; - TCHAR buffer[MAX_PATH]; - const bool success = SHGetPathFromIDList(file, buffer); - ILFree(file); - return success ? QDir::fromNativeSeparators(QString::fromUtf16(reinterpret_cast(buffer))) - : name; -#elif defined(Q_OS_OSX) - return Internal::normalizePathName(name); -#else // do not try to handle case-insensitive file systems on Linux - return name; -#endif -} - -static bool isRelativePathHelper(const QString &path, OsType osType) -{ - if (path.startsWith('/')) - return false; - if (osType == OsType::OsTypeWindows) { - if (path.startsWith('\\')) - return false; - // Unlike QFileInfo, this won't accept a relative path with a drive letter. - // Such paths result in a royal mess anyway ... - if (path.length() >= 3 && path.at(1) == ':' && path.at(0).isLetter() - && (path.at(2) == '/' || path.at(2) == '\\')) - return false; - } - return true; -} - -bool FileUtils::isRelativePath(const QString &path) -{ - return isRelativePathHelper(path, HostOsInfo::hostOs()); -} - -bool FilePath::isRelativePath() const -{ - return isRelativePathHelper(m_data, osType()); -} - -FilePath FilePath::resolvePath(const QString &fileName) const -{ - if (FileUtils::isAbsolutePath(fileName)) - return FilePath::fromString(QDir::cleanPath(fileName)); - FilePath result = *this; - result.setPath(QDir::cleanPath(m_data + '/' + fileName)); - return result; -} - -FilePath FilePath::cleanPath() const -{ - FilePath result = *this; - result.setPath(QDir::cleanPath(result.path())); - return result; -} - -FilePath FileUtils::commonPath(const FilePath &oldCommonPath, const FilePath &filePath) -{ - FilePath newCommonPath = oldCommonPath; - while (!newCommonPath.isEmpty() && !filePath.isChildOf(newCommonPath)) - newCommonPath = newCommonPath.parentDir(); - return newCommonPath.canonicalPath(); -} - -// Copied from qfilesystemengine_win.cpp -#ifdef Q_OS_WIN - -// File ID for Windows up to version 7. -static inline QByteArray fileIdWin7(HANDLE handle) -{ - BY_HANDLE_FILE_INFORMATION info; - if (GetFileInformationByHandle(handle, &info)) { - char buffer[sizeof "01234567:0123456701234567\0"]; - qsnprintf(buffer, sizeof(buffer), "%lx:%08lx%08lx", - info.dwVolumeSerialNumber, - info.nFileIndexHigh, - info.nFileIndexLow); - return QByteArray(buffer); - } - return QByteArray(); -} - -// File ID for Windows starting from version 8. -static QByteArray fileIdWin8(HANDLE handle) -{ - QByteArray result; - FILE_ID_INFO infoEx; - if (GetFileInformationByHandleEx(handle, - static_cast(18), // FileIdInfo in Windows 8 - &infoEx, sizeof(FILE_ID_INFO))) { - result = QByteArray::number(infoEx.VolumeSerialNumber, 16); - result += ':'; - // Note: MinGW-64's definition of FILE_ID_128 differs from the MSVC one. - result += QByteArray(reinterpret_cast(&infoEx.FileId), int(sizeof(infoEx.FileId))).toHex(); - } - return result; -} - -static QByteArray fileIdWin(HANDLE fHandle) -{ - return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8 ? - fileIdWin8(HANDLE(fHandle)) : fileIdWin7(HANDLE(fHandle)); -} -#endif - -QByteArray FileUtils::fileId(const FilePath &fileName) -{ - QByteArray result; - -#ifdef Q_OS_WIN - const HANDLE handle = - CreateFile((wchar_t*)fileName.toUserOutput().utf16(), 0, - FILE_SHARE_READ, NULL, OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS, NULL); - if (handle != INVALID_HANDLE_VALUE) { - result = fileIdWin(handle); - CloseHandle(handle); - } -#else // Copied from qfilesystemengine_unix.cpp - if (Q_UNLIKELY(fileName.isEmpty())) - return result; - - QT_STATBUF statResult; - if (QT_STAT(fileName.toString().toLocal8Bit().constData(), &statResult)) - return result; - result = QByteArray::number(quint64(statResult.st_dev), 16); - result += ':'; - result += QByteArray::number(quint64(statResult.st_ino)); -#endif - return result; -} - -FilePath FileUtils::homePath() -{ - return FilePath::fromString(QDir::cleanPath(QDir::homePath())); -} - -bool FileUtils::renameFile(const FilePath &srcFilePath, const FilePath &tgtFilePath) -{ - QTC_ASSERT(!srcFilePath.needsDevice(), return false); - QTC_ASSERT(srcFilePath.scheme() == tgtFilePath.scheme(), return false); - return QFile::rename(srcFilePath.path(), tgtFilePath.path()); -} +// FileReader QByteArray FileReader::fetchQrc(const QString &fileName) { @@ -544,6 +114,8 @@ bool FileReader::fetch(const FilePath &filePath, QIODevice::OpenMode mode, QWidg } #endif // QT_GUI_LIB +// FileSaver + FileSaverBase::FileSaverBase() = default; FileSaverBase::~FileSaverBase() = default; @@ -619,6 +191,7 @@ bool FileSaverBase::setResult(QXmlStreamWriter *stream) return setResult(!stream->hasError()); } +// FileSaver FileSaver::FileSaver(const FilePath &filePath, QIODevice::OpenMode mode) { @@ -692,904 +265,6 @@ TempFileSaver::~TempFileSaver() QFile::remove(m_filePath.toString()); } -/*! \class Utils::FilePath - - \brief The FilePath class is a light-weight convenience class for filenames. - - On windows filenames are compared case insensitively. -*/ - -FilePath::FilePath() -{ -} - -/// Constructs a FilePath from \a info -FilePath FilePath::fromFileInfo(const QFileInfo &info) -{ - return FilePath::fromString(info.absoluteFilePath()); -} - -/// \returns a QFileInfo -QFileInfo FilePath::toFileInfo() const -{ - return QFileInfo(m_data); -} - -FilePath FilePath::fromUrl(const QUrl &url) -{ - FilePath fn; - fn.m_scheme = url.scheme(); - fn.m_host = url.host(); - fn.m_data = url.path(); - return fn; -} - -/// \returns a QString for passing on to QString based APIs -QString FilePath::toString() const -{ - if (m_scheme.isEmpty()) - return m_data; - if (m_data.startsWith('/')) - return m_scheme + "://" + m_host + m_data; - return m_scheme + "://" + m_host + "/./" + m_data; -} - -QUrl FilePath::toUrl() const -{ - QUrl url; - url.setScheme(m_scheme); - url.setHost(m_host); - url.setPath(m_data); - return url; -} - -void FilePath::setDeviceFileHooks(const DeviceFileHooks &hooks) -{ - s_deviceHooks = hooks; -} - -/// \returns a QString to display to the user -/// Converts the separators to the native format -QString FilePath::toUserOutput() const -{ - if (m_scheme.isEmpty()) - return QDir::toNativeSeparators(m_data); - return toString(); -} - -QString FilePath::fileName() const -{ - const QChar slash = QLatin1Char('/'); - return m_data.mid(m_data.lastIndexOf(slash) + 1); -} - -QString FilePath::fileNameWithPathComponents(int pathComponents) const -{ - if (pathComponents < 0) - return m_data; - const QChar slash = QLatin1Char('/'); - int i = m_data.lastIndexOf(slash); - if (pathComponents == 0 || i == -1) - return m_data.mid(i + 1); - int component = i + 1; - // skip adjacent slashes - while (i > 0 && m_data.at(--i) == slash) - ; - while (i >= 0 && --pathComponents >= 0) { - i = m_data.lastIndexOf(slash, i); - component = i + 1; - while (i > 0 && m_data.at(--i) == slash) - ; - } - - // If there are no more slashes before the found one, return the entire string - if (i > 0 && m_data.lastIndexOf(slash, i) != -1) - return m_data.mid(component); - return m_data; -} - -/// \returns the base name of the file without the path. -/// -/// The base name consists of all characters in the file up to -/// (but not including) the first '.' character. - -QString FilePath::baseName() const -{ - const QString &name = fileName(); - return name.left(name.indexOf('.')); -} - -/// \returns the complete base name of the file without the path. -/// -/// The complete base name consists of all characters in the file up to -/// (but not including) the last '.' character - -QString FilePath::completeBaseName() const -{ - const QString &name = fileName(); - return name.left(name.lastIndexOf('.')); -} - -/// \returns the suffix (extension) of the file. -/// -/// The suffix consists of all characters in the file after -/// (but not including) the last '.'. - -QString FilePath::suffix() const -{ - const QString &name = fileName(); - const int index = name.lastIndexOf('.'); - if (index >= 0) - return name.mid(index + 1); - return {}; -} - -/// \returns the complete suffix (extension) of the file. -/// -/// The complete suffix consists of all characters in the file after -/// (but not including) the first '.'. - -QString FilePath::completeSuffix() const -{ - const QString &name = fileName(); - const int index = name.indexOf('.'); - if (index >= 0) - return name.mid(index + 1); - return {}; -} - -void FilePath::setScheme(const QString &scheme) -{ - QTC_CHECK(!scheme.contains('/')); - m_scheme = scheme; -} - -void FilePath::setHost(const QString &host) -{ - QTC_CHECK(!host.contains('/')); - m_host = host; -} - - -/// \returns a bool indicating whether a file with this -/// FilePath exists. -bool FilePath::exists() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.exists, return false); - return s_deviceHooks.exists(*this); - } - return !isEmpty() && QFileInfo::exists(m_data); -} - -/// \returns a bool indicating whether a path is writable. -bool FilePath::isWritableDir() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.isWritableDir, return false); - return s_deviceHooks.isWritableDir(*this); - } - const QFileInfo fi{m_data}; - return exists() && fi.isDir() && fi.isWritable(); -} - -bool FilePath::isWritableFile() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.isWritableFile, return false); - return s_deviceHooks.isWritableFile(*this); - } - const QFileInfo fi{m_data}; - return fi.exists() && fi.isWritable() && !fi.isDir(); -} - -bool FilePath::ensureWritableDir() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.ensureWritableDir, return false); - return s_deviceHooks.ensureWritableDir(*this); - } - const QFileInfo fi{m_data}; - if (exists() && fi.isDir() && fi.isWritable()) - return true; - return QDir().mkpath(m_data); -} - -bool FilePath::ensureExistingFile() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.ensureExistingFile, return false); - return s_deviceHooks.ensureExistingFile(*this); - } - QFile f(m_data); - if (f.exists()) - return true; - f.open(QFile::WriteOnly); - f.close(); - return f.exists(); -} - -bool FilePath::isExecutableFile() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.isExecutableFile, return false); - return s_deviceHooks.isExecutableFile(*this); - } - const QFileInfo fi{m_data}; - return fi.exists() && fi.isExecutable() && !fi.isDir(); -} - -bool FilePath::isReadableFile() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.isReadableFile, return false); - return s_deviceHooks.isReadableFile(*this); - } - const QFileInfo fi{m_data}; - return fi.exists() && fi.isReadable() && !fi.isDir(); -} - -bool FilePath::isReadableDir() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.isReadableDir, return false); - return s_deviceHooks.isReadableDir(*this); - } - const QFileInfo fi{m_data}; - return fi.exists() && fi.isReadable() && fi.isDir(); -} - -bool FilePath::isFile() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.isFile, return false); - return s_deviceHooks.isFile(*this); - } - const QFileInfo fi{m_data}; - return fi.exists() && fi.isFile(); -} - -bool FilePath::isDir() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.isDir, return false); - return s_deviceHooks.isDir(*this); - } - const QFileInfo fi{m_data}; - return fi.exists() && fi.isDir(); -} - -bool FilePath::createDir() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.createDir, return false); - return s_deviceHooks.createDir(*this); - } - QDir dir(m_data); - return dir.mkpath(dir.absolutePath()); -} - -FilePaths FilePath::dirEntries(const QStringList &nameFilters, - QDir::Filters filters, - QDir::SortFlags sort) const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.dirEntries, return {}); - return s_deviceHooks.dirEntries(*this, nameFilters, filters, sort); - } - - const QFileInfoList entryInfoList = QDir(m_data).entryInfoList(nameFilters, filters, sort); - return Utils::transform(entryInfoList, &FilePath::fromFileInfo); -} - -FilePaths FilePath::filterEntriesHelper(const FilePath &base, - const QStringList &entries, - const QStringList &nameFilters, - QDir::Filters filters, - QDir::SortFlags sort) -{ - const QList nameRegexps = transform(nameFilters, [](const QString &filter) { - QRegularExpression re; - re.setPattern(QRegularExpression::wildcardToRegularExpression(filter)); - QTC_CHECK(re.isValid()); - return re; - }); - - const auto nameMatches = [&nameRegexps](const QString &fileName) { - for (const QRegularExpression &re : nameRegexps) { - const QRegularExpressionMatch match = re.match(fileName); - if (match.hasMatch()) - return true; - } - return false; - }; - - // FIXME: Handle sort and filters. For now bark on unsupported options. - QTC_CHECK(filters == QDir::NoFilter); - QTC_CHECK(sort == QDir::NoSort); - - FilePaths result; - for (const QString &entry : entries) { - if (!nameMatches(entry)) - continue; - result.append(base.pathAppended(entry)); - } - return result; -} - -QList FilePath::dirEntries(QDir::Filters filters) const -{ - return dirEntries({}, filters); -} - -QByteArray FilePath::fileContents(qint64 maxSize, qint64 offset) const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.fileContents, return {}); - return s_deviceHooks.fileContents(*this, maxSize, offset); - } - - const QString path = toString(); - QFile f(path); - if (!f.exists()) - return {}; - - if (!f.open(QFile::ReadOnly)) - return {}; - - if (offset != 0) - f.seek(offset); - - if (maxSize != -1) - return f.read(maxSize); - - return f.readAll(); -} - -bool FilePath::writeFileContents(const QByteArray &data) const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.writeFileContents, return {}); - return s_deviceHooks.writeFileContents(*this, data); - } - - QFile file(path()); - QTC_ASSERT(file.open(QFile::WriteOnly | QFile::Truncate), return false); - qint64 res = file.write(data); - return res == data.size(); -} - -bool FilePath::needsDevice() const -{ - return !m_scheme.isEmpty(); -} - -/// \returns an empty FilePath if this is not a symbolic linl -FilePath FilePath::symLinkTarget() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.symLinkTarget, return {}); - return s_deviceHooks.symLinkTarget(*this); - } - const QFileInfo info(m_data); - if (!info.isSymLink()) - return {}; - return FilePath::fromString(info.symLinkTarget()); -} - -FilePath FilePath::withExecutableSuffix() const -{ - FilePath res = *this; - res.setPath(OsSpecificAspects::withExecutableSuffix(osType(), m_data)); - return res; -} - -/// Find the parent directory of a given directory. - -/// Returns an empty FilePath if the current directory is already -/// a root level directory. - -/// \returns \a FilePath with the last segment removed. -FilePath FilePath::parentDir() const -{ - const QString basePath = path(); - if (basePath.isEmpty()) - return FilePath(); - - const QDir base(basePath); - if (base.isRoot()) - return FilePath(); - - const QString path = basePath + QLatin1String("/.."); - const QString parent = QDir::cleanPath(path); - QTC_ASSERT(parent != path, return FilePath()); - - FilePath result = *this; - result.setPath(parent); - return result; -} - -FilePath FilePath::absolutePath() const -{ - FilePath result = *this; - result.m_data = QFileInfo(m_data).absolutePath(); - return result; -} - -FilePath FilePath::absoluteFilePath() const -{ - FilePath result = *this; - result.m_data = QFileInfo(m_data).absoluteFilePath(); - return result; -} - -FilePath FilePath::absoluteFilePath(const FilePath &tail) const -{ - if (isRelativePathHelper(tail.m_data, osType())) - return pathAppended(tail.m_data); - return tail; -} - -/// Constructs an absolute FilePath from this path which -/// is interpreted as being relative to \a anchor. -FilePath FilePath::absoluteFromRelativePath(const FilePath &anchor) const -{ - QDir anchorDir = QFileInfo(anchor.m_data).absoluteDir(); - QString absoluteFilePath = QFileInfo(anchorDir, m_data).canonicalFilePath(); - return FilePath::fromString(absoluteFilePath); -} - -/// Constructs a FilePath from \a filename -/// \a filename is not checked for validity. -FilePath FilePath::fromString(const QString &filename) -{ - FilePath fn; - if (filename.startsWith('/')) { - fn.m_data = filename; // fast track: absolute local paths - } else { - int pos1 = filename.indexOf("://"); - if (pos1 >= 0) { - fn.m_scheme = filename.left(pos1); - pos1 += 3; - int pos2 = filename.indexOf('/', pos1); - if (pos2 == -1) { - fn.m_data = filename.mid(pos1); - } else { - fn.m_host = filename.mid(pos1, pos2 - pos1); - fn.m_data = filename.mid(pos2); - } - if (fn.m_data.startsWith("/./")) - fn.m_data = fn.m_data.mid(3); - } else { - fn.m_data = filename; // treat everything else as local, too. - } - } - return fn; -} - -/// Constructs a FilePath from \a filePath. The \a defaultExtension is appended -/// to \a filename if that does not have an extension already. -/// \a filePath is not checked for validity. -FilePath FilePath::fromStringWithExtension(const QString &filepath, const QString &defaultExtension) -{ - if (filepath.isEmpty() || defaultExtension.isEmpty()) - return FilePath::fromString(filepath); - - QString rc = filepath; - QFileInfo fi(filepath); - // Add extension unless user specified something else - const QChar dot = QLatin1Char('.'); - if (!fi.fileName().contains(dot)) { - if (!defaultExtension.startsWith(dot)) - rc += dot; - rc += defaultExtension; - } - return FilePath::fromString(rc); -} - -/// Constructs a FilePath from \a filePath -/// \a filePath is only passed through QDir::fromNativeSeparators -FilePath FilePath::fromUserInput(const QString &filePath) -{ - QString clean = QDir::fromNativeSeparators(filePath); - if (clean.startsWith(QLatin1String("~/"))) - return FileUtils::homePath().pathAppended(clean.mid(2)); - return FilePath::fromString(clean); -} - -/// Constructs a FilePath from \a filePath, which is encoded as UTF-8. -/// \a filePath is not checked for validity. -FilePath FilePath::fromUtf8(const char *filename, int filenameSize) -{ - return FilePath::fromString(QString::fromUtf8(filename, filenameSize)); -} - -FilePath FilePath::fromVariant(const QVariant &variant) -{ - if (variant.type() == QVariant::Url) - return FilePath::fromUrl(variant.toUrl()); - return FilePath::fromString(variant.toString()); -} - -QVariant FilePath::toVariant() const -{ - return toString(); -} - -QDir FilePath::toDir() const -{ - return QDir(m_data); -} - -bool FilePath::operator==(const FilePath &other) const -{ - return QString::compare(m_data, other.m_data, caseSensitivity()) == 0 - && m_host == other.m_host - && m_scheme == other.m_scheme; -} - -bool FilePath::operator!=(const FilePath &other) const -{ - return !(*this == other); -} - -bool FilePath::operator<(const FilePath &other) const -{ - const int cmp = QString::compare(m_data, other.m_data, caseSensitivity()); - if (cmp != 0) - return cmp < 0; - if (m_host != other.m_host) - return m_host < other.m_host; - return m_scheme < other.m_scheme; -} - -bool FilePath::operator<=(const FilePath &other) const -{ - return !(other < *this); -} - -bool FilePath::operator>(const FilePath &other) const -{ - return other < *this; -} - -bool FilePath::operator>=(const FilePath &other) const -{ - return !(*this < other); -} - -FilePath FilePath::operator+(const QString &s) const -{ - FilePath res = *this; - res.m_data += s; - return res; -} - -/// \returns whether FilePath is a child of \a s -bool FilePath::isChildOf(const FilePath &s) const -{ - if (s.isEmpty()) - return false; - if (!m_data.startsWith(s.m_data, caseSensitivity())) - return false; - if (m_data.size() <= s.m_data.size()) - return false; - // s is root, '/' was already tested in startsWith - if (s.m_data.endsWith(QLatin1Char('/'))) - return true; - // s is a directory, next character should be '/' (/tmpdir is NOT a child of /tmp) - return m_data.at(s.m_data.size()) == QLatin1Char('/'); -} - -/// \overload -bool FilePath::isChildOf(const QDir &dir) const -{ - return isChildOf(FilePath::fromString(dir.absolutePath())); -} - -/// \returns whether FilePath startsWith \a s -bool FilePath::startsWith(const QString &s) const -{ - return m_data.startsWith(s, caseSensitivity()); -} - -/// \returns whether FilePath endsWith \a s -bool FilePath::endsWith(const QString &s) const -{ - return m_data.endsWith(s, caseSensitivity()); -} - -/// \returns the relativeChildPath of FilePath to parent if FilePath is a child of parent -/// \note returns a empty FilePath if FilePath is not a child of parent -/// That is, this never returns a path starting with "../" -FilePath FilePath::relativeChildPath(const FilePath &parent) const -{ - FilePath res; - if (isChildOf(parent)) - res.m_data = m_data.mid(parent.m_data.size() + 1, -1); - return res; -} - -/// \returns the relativePath of FilePath to given \a anchor. -/// Both, FilePath and anchor may be files or directories. -/// Example usage: -/// -/// \code -/// FilePath filePath("/foo/b/ar/file.txt"); -/// FilePath relativePath = filePath.relativePath("/foo/c"); -/// qDebug() << relativePath -/// \endcode -/// -/// The debug output will be "../b/ar/file.txt". -/// -FilePath FilePath::relativePath(const FilePath &anchor) const -{ - const QFileInfo fileInfo(m_data); - QString absolutePath; - QString filename; - if (fileInfo.isFile()) { - absolutePath = fileInfo.absolutePath(); - filename = fileInfo.fileName(); - } else if (fileInfo.isDir()) { - absolutePath = fileInfo.absoluteFilePath(); - } else { - return {}; - } - const QFileInfo anchorInfo(anchor.m_data); - QString absoluteAnchorPath; - if (anchorInfo.isFile()) - absoluteAnchorPath = anchorInfo.absolutePath(); - else if (anchorInfo.isDir()) - absoluteAnchorPath = anchorInfo.absoluteFilePath(); - else - return {}; - QString relativeFilePath = calcRelativePath(absolutePath, absoluteAnchorPath); - if (!filename.isEmpty()) { - if (!relativeFilePath.isEmpty()) - relativeFilePath += '/'; - relativeFilePath += filename; - } - return FilePath::fromString(relativeFilePath); -} - -/// \returns the relativePath of \a absolutePath to given \a absoluteAnchorPath. -/// Both paths must be an absolute path to a directory. Example usage: -/// -/// \code -/// qDebug() << FilePath::calcRelativePath("/foo/b/ar", "/foo/c"); -/// \endcode -/// -/// The debug output will be "../b/ar". -/// -/// \see FilePath::relativePath -/// -QString FilePath::calcRelativePath(const QString &absolutePath, const QString &absoluteAnchorPath) -{ - if (absolutePath.isEmpty() || absoluteAnchorPath.isEmpty()) - return QString(); - // TODO using split() instead of parsing the strings by char index is slow - // and needs more memory (but the easiest implementation for now) - const QStringList splits1 = absolutePath.split('/'); - const QStringList splits2 = absoluteAnchorPath.split('/'); - int i = 0; - while (i < splits1.count() && i < splits2.count() && splits1.at(i) == splits2.at(i)) - ++i; - QString relativePath; - int j = i; - bool addslash = false; - while (j < splits2.count()) { - if (!splits2.at(j).isEmpty()) { - if (addslash) - relativePath += '/'; - relativePath += ".."; - addslash = true; - } - ++j; - } - while (i < splits1.count()) { - if (!splits1.at(i).isEmpty()) { - if (addslash) - relativePath += '/'; - relativePath += splits1.at(i); - addslash = true; - } - ++i; - } - return relativePath; -} - -/*! - Returns a path corresponding to the current object on the - same device as \a deviceTemplate. - - Example usage: - \code - localDir = FilePath::fromString("/tmp/workingdir"); - executable = FilePath::fromUrl("docker://123/bin/ls") - realDir = localDir.onDevice(executable) - assert(realDir == FilePath::fromUrl("docker://123/tmp/workingdir")) - \endcode -*/ -FilePath FilePath::onDevice(const FilePath &deviceTemplate) const -{ - FilePath res; - res.m_data = m_data; - res.m_host = deviceTemplate.m_host; - res.m_scheme = deviceTemplate.m_scheme; - return res; -} - -/*! - Returns a FilePath with local path \a newPath on the same device - as the current object. - - Example usage: - \code - devicePath = FilePath::fromString("docker://123/tmp"); - newPath = devicePath.withNewPath("/bin/ls"); - assert(realDir == FilePath::fromUrl("docker://123/bin/ls")) - \endcode -*/ -FilePath FilePath::withNewPath(const QString &newPath) const -{ - FilePath res; - res.m_data = newPath; - res.m_host = m_host; - res.m_scheme = m_scheme; - return res; -} - -/*! - Searched a binary corresponding to this object in the PATH of - the device implied by this object's scheme and host. - - Example usage: - \code - binary = FilePath::fromUrl("docker://123/./make); - fullPath = binary.searchOnDevice(); - assert(fullPath == FilePath::fromUrl("docker://123/usr/bin/make")) - \endcode -*/ -FilePath FilePath::searchOnDevice(const FilePaths &dirs) const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.searchInPath, return {}); - return s_deviceHooks.searchInPath(*this, dirs); - } - return Environment::systemEnvironment().searchInPath(path(), dirs); -} - -Environment FilePath::deviceEnvironment() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.environment, return {}); - return s_deviceHooks.environment(*this); - } - return Environment::systemEnvironment(); -} - -QString FilePath::formatFilePaths(const QList &files, const QString &separator) -{ - const QStringList nativeFiles = Utils::transform(files, &FilePath::toUserOutput); - return nativeFiles.join(separator); -} - -void FilePath::removeDuplicates(QList &files) -{ - // FIXME: Improve. - QStringList list = Utils::transform(files, &FilePath::toString); - list.removeDuplicates(); - files = Utils::transform(list, &FilePath::fromString); -} - -void FilePath::sort(QList &files) -{ - // FIXME: Improve. - QStringList list = Utils::transform(files, &FilePath::toString); - list.sort(); - files = Utils::transform(list, &FilePath::fromString); -} - -FilePath FilePath::pathAppended(const QString &path) const -{ - FilePath fn = *this; - if (path.isEmpty()) - return fn; - if (!fn.m_data.isEmpty() && !fn.m_data.endsWith(QLatin1Char('/'))) - fn.m_data.append('/'); - fn.m_data.append(path); - return fn; -} - -FilePath FilePath::stringAppended(const QString &str) const -{ - FilePath fn = *this; - fn.m_data.append(str); - return fn; -} - -uint FilePath::hash(uint seed) const -{ - if (Utils::HostOsInfo::fileNameCaseSensitivity() == Qt::CaseInsensitive) - return qHash(m_data.toUpper(), seed); - return qHash(m_data, seed); -} - -QDateTime FilePath::lastModified() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.lastModified, return {}); - return s_deviceHooks.lastModified(*this); - } - return toFileInfo().lastModified(); -} - -QFile::Permissions FilePath::permissions() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.permissions, return {}); - return s_deviceHooks.permissions(*this); - } - return toFileInfo().permissions(); -} - -OsType FilePath::osType() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.osType, return {}); - return s_deviceHooks.osType(*this); - } - return HostOsInfo::hostOs(); -} - -bool FilePath::removeFile() const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.removeFile, return false); - return s_deviceHooks.removeFile(*this); - } - return QFile::remove(path()); -} - -/*! - Removes the directory this filePath refers too and its subdirectories recursively. - - \note The \a error parameter is optional. - - Returns whether the operation succeeded. -*/ -bool FilePath::removeRecursively(QString *error) const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.removeRecursively, return false); - return s_deviceHooks.removeRecursively(*this); - } - return removeRecursivelyLocal(*this, error); -} - -bool FilePath::copyFile(const FilePath &target) const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.copyFile, return false); - return s_deviceHooks.copyFile(*this, target); - } - return QFile::copy(path(), target.path()); -} - -bool FilePath::renameFile(const FilePath &target) const -{ - if (needsDevice()) { - QTC_ASSERT(s_deviceHooks.renameFile, return false); - return s_deviceHooks.renameFile(*this, target); - } - return QFile::rename(path(), target.path()); -} - -QTextStream &operator<<(QTextStream &s, const FilePath &fn) -{ - return s << fn.toString(); -} - #ifdef QT_GUI_LIB FileUtils::CopyAskingForOverwrite::CopyAskingForOverwrite( QWidget *dialogParent, const std::function &postOperation) @@ -1652,6 +327,74 @@ FilePaths FileUtils::CopyAskingForOverwrite::files() const } #endif // QT_GUI_LIB +// Copied from qfilesystemengine_win.cpp +#ifdef Q_OS_WIN + +// File ID for Windows up to version 7. +static inline QByteArray fileIdWin7(HANDLE handle) +{ + BY_HANDLE_FILE_INFORMATION info; + if (GetFileInformationByHandle(handle, &info)) { + char buffer[sizeof "01234567:0123456701234567\0"]; + qsnprintf(buffer, sizeof(buffer), "%lx:%08lx%08lx", + info.dwVolumeSerialNumber, + info.nFileIndexHigh, + info.nFileIndexLow); + return QByteArray(buffer); + } + return QByteArray(); +} + +// File ID for Windows starting from version 8. +static QByteArray fileIdWin8(HANDLE handle) +{ + QByteArray result; + FILE_ID_INFO infoEx; + if (GetFileInformationByHandleEx(handle, + static_cast(18), // FileIdInfo in Windows 8 + &infoEx, sizeof(FILE_ID_INFO))) { + result = QByteArray::number(infoEx.VolumeSerialNumber, 16); + result += ':'; + // Note: MinGW-64's definition of FILE_ID_128 differs from the MSVC one. + result += QByteArray(reinterpret_cast(&infoEx.FileId), int(sizeof(infoEx.FileId))).toHex(); + } + return result; +} + +static QByteArray fileIdWin(HANDLE fHandle) +{ + return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8 ? + fileIdWin8(HANDLE(fHandle)) : fileIdWin7(HANDLE(fHandle)); +} +#endif + +QByteArray FileUtils::fileId(const FilePath &fileName) +{ + QByteArray result; + +#ifdef Q_OS_WIN + const HANDLE handle = + CreateFile((wchar_t*)fileName.toUserOutput().utf16(), 0, + FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (handle != INVALID_HANDLE_VALUE) { + result = fileIdWin(handle); + CloseHandle(handle); + } +#else // Copied from qfilesystemengine_unix.cpp + if (Q_UNLIKELY(fileName.isEmpty())) + return result; + + QT_STATBUF statResult; + if (QT_STAT(fileName.toString().toLocal8Bit().constData(), &statResult)) + return result; + result = QByteArray::number(quint64(statResult.st_dev), 16); + result += ':'; + result += QByteArray::number(quint64(statResult.st_ino)); +#endif + return result; +} + #ifdef Q_OS_WIN template <> void withNtfsPermissions(const std::function &task) @@ -1664,10 +407,3 @@ void withNtfsPermissions(const std::function &task) } // namespace Utils -std::hash::result_type - std::hash::operator()(const std::hash::argument_type &fn) const -{ - if (fn.caseSensitivity() == Qt::CaseInsensitive) - return hash()(fn.toString().toUpper().toStdString()); - return hash()(fn.toString().toStdString()); -} diff --git a/src/libs/utils/fileutils.h b/src/libs/utils/fileutils.h index 772c246ed71..9901f678763 100644 --- a/src/libs/utils/fileutils.h +++ b/src/libs/utils/fileutils.h @@ -27,6 +27,7 @@ #include "utils_global.h" +#include "filepath.h" #include "hostosinfo.h" #include @@ -40,33 +41,17 @@ #include #include -namespace Utils { -class Environment; -class FilePath; -} // Utils - QT_BEGIN_NAMESPACE class QDataStream; -class QDateTime; -class QDir; -class QFile; -class QFileInfo; -class QTemporaryFile; class QTextStream; class QWidget; -QTCREATOR_UTILS_EXPORT QDebug operator<<(QDebug dbg, const Utils::FilePath &c); - // for withNtfsPermissions #ifdef Q_OS_WIN extern Q_CORE_EXPORT int qt_ntfs_permission_lookup; #endif - QT_END_NAMESPACE -// tst_fileutils becomes a friend of Utils::FilePath for testing private method -class tst_fileutils; - namespace Utils { class DeviceFileHooks @@ -99,152 +84,6 @@ public: std::function environment; }; -class QTCREATOR_UTILS_EXPORT FilePath -{ -public: - FilePath(); - - static FilePath fromString(const QString &filepath); - static FilePath fromFileInfo(const QFileInfo &info); - static FilePath fromStringWithExtension(const QString &filepath, const QString &defaultExtension); - static FilePath fromUserInput(const QString &filepath); - static FilePath fromUtf8(const char *filepath, int filepathSize = -1); - static FilePath fromVariant(const QVariant &variant); - - QString toString() const; - FilePath onDevice(const FilePath &deviceTemplate) const; - FilePath withNewPath(const QString &newPath) const; - - QFileInfo toFileInfo() const; - QVariant toVariant() const; - QDir toDir() const; - - QString toUserOutput() const; - QString shortNativePath() const; - - QString fileName() const; - QString fileNameWithPathComponents(int pathComponents) const; - - QString baseName() const; - QString completeBaseName() const; - QString suffix() const; - QString completeSuffix() const; - - QString scheme() const { return m_scheme; } - void setScheme(const QString &scheme); - - QString host() const { return m_host; } - void setHost(const QString &host); - - QString path() const { return m_data; } - void setPath(const QString &path) { m_data = path; } - - bool needsDevice() const; - bool exists() const; - - bool isWritablePath() const { return isWritableDir(); } // Remove. - bool isWritableDir() const; - bool isWritableFile() const; - bool ensureWritableDir() const; - bool ensureExistingFile() const; - bool isExecutableFile() const; - bool isReadableFile() const; - bool isReadableDir() const; - bool isRelativePath() const; - bool isAbsolutePath() const { return !isRelativePath(); } - bool isFile() const; - bool isDir() const; - - bool createDir() const; - QList dirEntries(const QStringList &nameFilters, - QDir::Filters filters, - QDir::SortFlags sort = QDir::NoSort) const; - QList dirEntries(QDir::Filters filters) const; - QByteArray fileContents(qint64 maxSize = -1, qint64 offset = 0) const; - bool writeFileContents(const QByteArray &data) const; - - FilePath parentDir() const; - FilePath absolutePath() const; - FilePath absoluteFilePath() const; - FilePath absoluteFilePath(const FilePath &tail) const; - FilePath absoluteFromRelativePath(const FilePath &anchor) const; - - bool operator==(const FilePath &other) const; - bool operator!=(const FilePath &other) const; - bool operator<(const FilePath &other) const; - bool operator<=(const FilePath &other) const; - bool operator>(const FilePath &other) const; - bool operator>=(const FilePath &other) const; - FilePath operator+(const QString &s) const; - - bool isChildOf(const FilePath &s) const; - bool isChildOf(const QDir &dir) const; - bool startsWith(const QString &s) const; - bool endsWith(const QString &s) const; - - bool isNewerThan(const QDateTime &timeStamp) const; - QDateTime lastModified() const; - QFile::Permissions permissions() const; - OsType osType() const; - bool removeFile() const; - bool removeRecursively(QString *error = nullptr) const; - bool copyFile(const FilePath &target) const; - bool renameFile(const FilePath &target) const; - - Qt::CaseSensitivity caseSensitivity() const; - - FilePath relativeChildPath(const FilePath &parent) const; - FilePath relativePath(const FilePath &anchor) const; - FilePath pathAppended(const QString &str) const; - FilePath stringAppended(const QString &str) const; - FilePath resolvePath(const QString &fileName) const; - FilePath resolveSymlinkTarget() const; - FilePath cleanPath() const; - - FilePath canonicalPath() const; - FilePath symLinkTarget() const; - FilePath resolveSymlinks() const; - FilePath withExecutableSuffix() const; - - FilePath operator/(const QString &str) const; - - void clear(); - bool isEmpty() const; - - uint hash(uint seed) const; - - // NOTE: Most FilePath operations on FilePath created from URL currently - // do not work. Among the working are .toVariant() and .toUrl(). - static FilePath fromUrl(const QUrl &url); - QUrl toUrl() const; - - static void setDeviceFileHooks(const DeviceFileHooks &hooks); - - FilePath searchOnDevice(const QList &dirs) const; - Environment deviceEnvironment() const; - - static QString formatFilePaths(const QList &files, const QString &separator); - static void removeDuplicates(QList &files); - static void sort(QList &files); - - static QList filterEntriesHelper(const FilePath &base, - const QStringList &entries, - const QStringList &nameFilters, - QDir::Filters filters, - QDir::SortFlags sort); -private: - friend class ::tst_fileutils; - static QString calcRelativePath(const QString &absolutePath, const QString &absoluteAnchorPath); - - QString m_scheme; - QString m_host; - QString m_data; -}; - -QTCREATOR_UTILS_EXPORT QTextStream &operator<<(QTextStream &s, const FilePath &fn); - -using FilePaths = QList; - class QTCREATOR_UTILS_EXPORT FileUtils { public: #ifdef QT_GUI_LIB @@ -287,6 +126,8 @@ public: static QByteArray fileId(const FilePath &fileName); static FilePath homePath(); static bool renameFile(const FilePath &srcFilePath, const FilePath &tgtFilePath); + + static void setDeviceFileHooks(const DeviceFileHooks &hooks); }; template @@ -432,6 +273,8 @@ private: bool m_autoRemove = true; }; +QTCREATOR_UTILS_EXPORT QTextStream &operator<<(QTextStream &s, const FilePath &fn); + inline uint qHash(const Utils::FilePath &a, uint seed = 0) { return a.hash(seed); } } // namespace Utils @@ -445,4 +288,3 @@ template<> struct QTCREATOR_UTILS_EXPORT hash }; } // namespace std -Q_DECLARE_METATYPE(Utils::FilePath) diff --git a/src/libs/utils/qtcprocess.cpp b/src/libs/utils/qtcprocess.cpp index e4869ba55e1..93830de61d6 100644 --- a/src/libs/utils/qtcprocess.cpp +++ b/src/libs/utils/qtcprocess.cpp @@ -554,7 +554,13 @@ void QtcProcess::start() { d->clearForRun(); - QTC_CHECK(d->m_writeData.isEmpty()); // FIXME: Use it. + if (!d->m_writeData.isEmpty()) { + connect(d->m_process, &ProcessInterface::started, this, [this] { + const qint64 bytesWritten = write(d->m_writeData); + QTC_CHECK(bytesWritten == d->m_writeData.size()); + closeWriteChannel(); // FIXME: Is this good? + }); + } if (d->m_commandLine.executable().needsDevice()) { QTC_ASSERT(s_deviceHooks.startProcessHook, return); @@ -1219,6 +1225,7 @@ void QtcProcess::setExitCodeInterpreter(const ExitCodeInterpreter &interpreter) void QtcProcess::setWriteData(const QByteArray &writeData) { d->m_writeData = writeData; + setKeepWriteChannelOpen(); } #ifdef QT_GUI_LIB @@ -1231,11 +1238,9 @@ static bool isGuiThread() void QtcProcess::runBlocking() { // FIXME: Implement properly + if (d->m_commandLine.executable().needsDevice()) { - - // writeData ? QtcProcess::start(); - waitForFinished(); return; }; diff --git a/src/libs/utils/utils-lib.pri b/src/libs/utils/utils-lib.pri index d9a2633deaa..4430c3cee33 100644 --- a/src/libs/utils/utils-lib.pri +++ b/src/libs/utils/utils-lib.pri @@ -62,6 +62,7 @@ SOURCES += \ $$PWD/fancylineedit.cpp \ $$PWD/qtcolorbutton.cpp \ $$PWD/savefile.cpp \ + $$PWD/filepath.cpp \ $$PWD/fileutils.cpp \ $$PWD/textfileformat.cpp \ $$PWD/consoleprocess.cpp \ @@ -196,6 +197,7 @@ HEADERS += \ $$PWD/qtcolorbutton.h \ $$PWD/consoleprocess.h \ $$PWD/savefile.h \ + $$PWD/filepath.h \ $$PWD/fileutils.h \ $$PWD/textfileformat.h \ $$PWD/uncommentselection.h \ diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index fdc625c2412..0ac2c52e294 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -115,6 +115,8 @@ Project { "fileinprojectfinder.h", "filenamevalidatinglineedit.cpp", "filenamevalidatinglineedit.h", + "filepath.cpp", + "filepath.h", "filesearch.cpp", "filesearch.h", "filesystemwatcher.cpp", diff --git a/src/plugins/android/androidconfigurations.cpp b/src/plugins/android/androidconfigurations.cpp index d80529c78bb..7b00fb7f4f2 100644 --- a/src/plugins/android/androidconfigurations.cpp +++ b/src/plugins/android/androidconfigurations.cpp @@ -1066,13 +1066,20 @@ AndroidDeviceInfo AndroidConfigurations::showDeviceDialog(Project *project, if (!serialNumber.isEmpty()) break; } + + const AndroidDeviceInfo defaultDevice = AndroidDeviceDialog::defaultDeviceInfo(serialNumber); + if (defaultDevice.isValid()) + return defaultDevice; + AndroidDeviceDialog dialog(apiLevel, abis, serialNumber, Core::ICore::dialogParent()); - AndroidDeviceInfo info = dialog.device(); + AndroidDeviceInfo info = dialog.showAndGetSelectedDevice(); if (dialog.saveDeviceSelection() && info.isValid()) { - const QString serialNumber = info.type == AndroidDeviceInfo::Hardware ? + const QString newSerialNumber = info.type == AndroidDeviceInfo::Hardware ? info.serialNumber : info.avdname; - if (!serialNumber.isEmpty()) - AndroidConfigurations::setDefaultDevice(project, AndroidManager::devicePreferredAbi(info.cpuAbi, abis), serialNumber); + if (!newSerialNumber.isEmpty()) { + const QString preferredAbi = AndroidManager::devicePreferredAbi(info.cpuAbi, abis); + AndroidConfigurations::setDefaultDevice(project, preferredAbi, newSerialNumber); + } } return info; } diff --git a/src/plugins/android/androiddevicedialog.cpp b/src/plugins/android/androiddevicedialog.cpp index 80121444354..9e33808ebb6 100644 --- a/src/plugins/android/androiddevicedialog.cpp +++ b/src/plugins/android/androiddevicedialog.cpp @@ -44,6 +44,8 @@ using namespace Android::Internal; namespace Android { namespace Internal { +QVector AndroidDeviceDialog::m_connectedDevices = {}; + // yeah, writing tree models is fun! class AndroidDeviceModelNode { @@ -481,26 +483,30 @@ AndroidDeviceDialog::~AndroidDeviceDialog() delete m_ui; } -AndroidDeviceInfo AndroidDeviceDialog::device() +AndroidDeviceInfo AndroidDeviceDialog::defaultDeviceInfo(const QString &serialNumber) { + AndroidDeviceDialog::updateConnectedDevicesList(); + + if (serialNumber.isEmpty()) + return {}; + + return Utils::findOrDefault(m_connectedDevices, [serialNumber](const AndroidDeviceInfo &info) { + return info.serialNumber == serialNumber || info.avdname == serialNumber; + }); +} + +AndroidDeviceInfo AndroidDeviceDialog::showAndGetSelectedDevice() +{ + auto dev = defaultDeviceInfo(m_defaultDevice); + if (dev.isValid()) + return dev; + refreshDeviceList(); - if (!m_defaultDevice.isEmpty()) { - auto device = std::find_if(m_connectedDevices.cbegin(), - m_connectedDevices.cend(), - [this](const AndroidDeviceInfo &info) { - return info.serialNumber == m_defaultDevice || - info.avdname == m_defaultDevice; - }); - - if (device != m_connectedDevices.cend()) - return *device; - m_defaultDevice.clear(); - } - if (exec() == QDialog::Accepted) return m_model->device(m_ui->deviceView->currentIndex()); - return AndroidDeviceInfo(); + + return {}; } bool AndroidDeviceDialog::saveDeviceSelection() const @@ -508,11 +514,16 @@ bool AndroidDeviceDialog::saveDeviceSelection() const return m_ui->defaultDeviceCheckBox->isChecked(); } +void AndroidDeviceDialog::updateConnectedDevicesList() +{ + m_connectedDevices = AndroidConfig::connectedDevices(AndroidConfigurations::currentConfig() + .adbToolPath()); +} + void AndroidDeviceDialog::refreshDeviceList() { m_ui->refreshDevicesButton->setEnabled(false); m_progressIndicator->show(); - m_connectedDevices = AndroidConfig::connectedDevices(AndroidConfigurations::currentConfig().adbToolPath()); m_futureWatcherRefreshDevices.setFuture(m_avdManager->avdList()); } diff --git a/src/plugins/android/androiddevicedialog.h b/src/plugins/android/androiddevicedialog.h index 4bb9a55e470..33bd2871e9c 100644 --- a/src/plugins/android/androiddevicedialog.h +++ b/src/plugins/android/androiddevicedialog.h @@ -56,7 +56,8 @@ public: const QString &serialNumber, QWidget *parent = nullptr); ~AndroidDeviceDialog() override; - AndroidDeviceInfo device(); + AndroidDeviceInfo showAndGetSelectedDevice(); + static AndroidDeviceInfo defaultDeviceInfo(const QString &serialNumber); bool saveDeviceSelection() const; @@ -68,6 +69,7 @@ private: void devicesRefreshed(); void enableOkayButton(); void defaultDeviceClear(); + static void updateConnectedDevicesList(); AndroidDeviceModel *m_model; Ui::AndroidDeviceDialog *m_ui; @@ -76,8 +78,8 @@ private: QStringList m_abis; QString m_avdNameFromAdd; QString m_defaultDevice; + static QVector m_connectedDevices; std::unique_ptr m_avdManager; - QVector m_connectedDevices; QFutureWatcher m_futureWatcherAddDevice; QFutureWatcher m_futureWatcherRefreshDevices; }; diff --git a/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.cpp b/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.cpp index f2e6dbf9e44..2a1a27683b8 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.cpp @@ -515,7 +515,7 @@ void CMakeBuildSettingsWidget::batchEditConfiguration() [expander](const QString &s) { return expander->expand(s); }); - const CMakeConfig config = CMakeConfigItem::itemsFromArguments(expandedLines); + const CMakeConfig config = CMakeConfig::fromArguments(expandedLines); m_configModel->setBatchEditConfiguration(config); }); @@ -1048,7 +1048,7 @@ bool CMakeBuildConfiguration::fromMap(const QVariantMap &map) }(); if (initialCMakeArguments().isEmpty()) { QStringList initialArgs = defaultInitialCMakeArguments(kit(), buildTypeName) - + Utils::transform(conf, [this](const CMakeConfigItem &i) { + + Utils::transform(conf.toList(), [this](const CMakeConfigItem &i) { return i.toArgument(macroExpander()); }); @@ -1110,7 +1110,8 @@ CMakeConfig CMakeBuildConfiguration::configurationChanges() const QStringList CMakeBuildConfiguration::configurationChangesArguments() const { - return Utils::transform(m_configurationChanges, [](const CMakeConfigItem &i) { return i.toArgument(); }); + return Utils::transform(m_configurationChanges.toList(), + [](const CMakeConfigItem &i) { return i.toArgument(); }); } QStringList CMakeBuildConfiguration::initialCMakeArguments() const @@ -1296,9 +1297,9 @@ BuildInfo CMakeBuildConfigurationFactory::createBuildInfo(BuildType buildType) BuildConfiguration::BuildType CMakeBuildConfiguration::buildType() const { - QByteArray cmakeBuildTypeName = CMakeConfigItem::valueOf("CMAKE_BUILD_TYPE", m_configurationFromCMake); + QByteArray cmakeBuildTypeName = m_configurationFromCMake.valueOf("CMAKE_BUILD_TYPE"); if (cmakeBuildTypeName.isEmpty()) { - QByteArray cmakeCfgTypes = CMakeConfigItem::valueOf("CMAKE_CONFIGURATION_TYPES", m_configurationFromCMake); + QByteArray cmakeCfgTypes = m_configurationFromCMake.valueOf("CMAKE_CONFIGURATION_TYPES"); if (!cmakeCfgTypes.isEmpty()) cmakeBuildTypeName = cmakeBuildType().toUtf8(); } @@ -1352,14 +1353,14 @@ QString CMakeBuildConfiguration::cmakeBuildType() const QString errorMessage; config = CMakeBuildSystem::parseCMakeCacheDotTxt(cmakeCacheTxt, &errorMessage); } else { - config = CMakeConfigItem::itemsFromArguments(initialCMakeArguments()); + config = CMakeConfig::fromArguments(initialCMakeArguments()); } } else if (!hasCMakeCache) { - config = CMakeConfigItem::itemsFromArguments(initialCMakeArguments()); + config = CMakeConfig::fromArguments(initialCMakeArguments()); } if (!config.isEmpty() && !isMultiConfig()) { - cmakeBuildType = CMakeConfigItem::stringValueOf("CMAKE_BUILD_TYPE", config); + cmakeBuildType = config.stringValueOf("CMAKE_BUILD_TYPE"); const_cast(this)->setCMakeBuildType(cmakeBuildType); } diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp index aae63f2f41f..463ebc4cb41 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp @@ -838,7 +838,7 @@ void CMakeBuildSystem::wireUpConnections() QString errorMessage; const CMakeConfig config = CMakeBuildSystem::parseCMakeCacheDotTxt(cmakeCacheTxt, &errorMessage); if (!config.isEmpty() && errorMessage.isEmpty()) { - QString cmakeBuildTypeName = CMakeConfigItem::stringValueOf("CMAKE_BUILD_TYPE", config); + QString cmakeBuildTypeName = config.stringValueOf("CMAKE_BUILD_TYPE"); cmakeBuildConfiguration()->setCMakeBuildType(cmakeBuildTypeName, true); } } @@ -1057,7 +1057,7 @@ CMakeConfig CMakeBuildSystem::parseCMakeCacheDotTxt(const Utils::FilePath &cache *errorMessage = tr("CMakeCache.txt file not found."); return {}; } - CMakeConfig result = CMakeConfigItem::itemsFromFile(cacheFile, errorMessage); + CMakeConfig result = CMakeConfig::fromFile(cacheFile, errorMessage); if (!errorMessage->isEmpty()) return {}; return result; @@ -1210,7 +1210,7 @@ void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths, }; const CMakeConfig &cm = cmakeBuildConfiguration()->configurationFromCMake(); - addImports(CMakeConfigItem::stringValueOf("QML_IMPORT_PATH", cm)); + addImports(cm.stringValueOf("QML_IMPORT_PATH")); addImports(kit()->value(QtSupport::KitQmlImportPath::id()).toString()); for (const QString &extraHeaderPath : extraHeaderPaths) @@ -1244,7 +1244,7 @@ void CMakeBuildSystem::updateInitialCMakeExpandableVars() { const CMakeConfig &cm = cmakeBuildConfiguration()->configurationFromCMake(); const CMakeConfig &initialConfig = - CMakeConfigItem::itemsFromArguments(cmakeBuildConfiguration()->initialCMakeArguments()); + CMakeConfig::fromArguments(cmakeBuildConfiguration()->initialCMakeArguments()); CMakeConfig config; @@ -1275,7 +1275,7 @@ void CMakeBuildSystem::updateInitialCMakeExpandableVars() }); if (it != cm.cend()) { - const QByteArray initialValue = CMakeConfigItem::expandedValueOf(kit(), var, initialConfig).toUtf8(); + const QByteArray initialValue = initialConfig.expandedValueOf(kit(), var).toUtf8(); const FilePath initialPath = FilePath::fromString(QString::fromUtf8(initialValue)); const FilePath path = FilePath::fromString(QString::fromUtf8(it->value)); @@ -1299,20 +1299,23 @@ void CMakeBuildSystem::updateInitialCMakeExpandableVars() }); if (it != cm.cend()) { - const QByteArray initialValue = CMakeConfigItem::expandedValueOf(kit(), var, initialConfig).toUtf8(); - const FilePath initialPath = FilePath::fromString(QString::fromUtf8(initialValue)); + const QByteArrayList initialValueList = initialConfig.expandedValueOf(kit(), var).toUtf8().split(';'); - const bool pathIsContained - = Utils::contains(it->value.split(';'), [samePath, initialPath](const QByteArray &p) { - return samePath(FilePath::fromString(QString::fromUtf8(p)), initialPath); - }); - if (!initialValue.isEmpty() && !pathIsContained) { - CMakeConfigItem item(*it); - item.value = initialValue; - item.value.append(";"); - item.value.append(it->value); + for (const auto &initialValue: initialValueList) { + const FilePath initialPath = FilePath::fromString(QString::fromUtf8(initialValue)); - config << item; + const bool pathIsContained + = Utils::contains(it->value.split(';'), [samePath, initialPath](const QByteArray &p) { + return samePath(FilePath::fromString(QString::fromUtf8(p)), initialPath); + }); + if (!initialValue.isEmpty() && !pathIsContained) { + CMakeConfigItem item(*it); + item.value = initialValue; + item.value.append(";"); + item.value.append(it->value); + + config << item; + } } } } diff --git a/src/plugins/cmakeprojectmanager/cmakeconfigitem.cpp b/src/plugins/cmakeprojectmanager/cmakeconfigitem.cpp index 6ccd0ae2368..93e6e6232af 100644 --- a/src/plugins/cmakeprojectmanager/cmakeconfigitem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakeconfigitem.cpp @@ -55,29 +55,28 @@ CMakeConfigItem::CMakeConfigItem(const QByteArray &k, const QByteArray &v) : key(k), value(v) { } -QByteArray CMakeConfigItem::valueOf(const QByteArray &key, const QList &input) +QByteArray CMakeConfig::valueOf(const QByteArray &key) const { - for (auto it = input.constBegin(); it != input.constEnd(); ++it) { + for (auto it = constBegin(); it != constEnd(); ++it) { if (it->key == key) return it->value; } return QByteArray(); } -QString CMakeConfigItem::stringValueOf(const QByteArray &key, const QList &input) +QString CMakeConfig::stringValueOf(const QByteArray &key) const { - return QString::fromUtf8(valueOf(key, input)); + return QString::fromUtf8(valueOf(key)); } -FilePath CMakeConfigItem::filePathValueOf(const QByteArray &key, const QList &input) +FilePath CMakeConfig::filePathValueOf(const QByteArray &key) const { - return FilePath::fromUtf8(valueOf(key, input)); + return FilePath::fromUtf8(valueOf(key)); } -QString CMakeConfigItem::expandedValueOf(const ProjectExplorer::Kit *k, const QByteArray &key, - const QList &input) +QString CMakeConfig::expandedValueOf(const ProjectExplorer::Kit *k, const QByteArray &key) const { - for (auto it = input.constBegin(); it != input.constEnd(); ++it) { + for (auto it = constBegin(); it != constEnd(); ++it) { if (it->key == key) return it->expandedValue(k); } @@ -312,7 +311,7 @@ static CMakeConfigItem unsetItemFromString(const QString &input) return item; } -QList CMakeConfigItem::itemsFromArguments(const QStringList &list) +CMakeConfig CMakeConfig::fromArguments(const QStringList &list) { CMakeConfig result; bool inSet = false; @@ -348,7 +347,7 @@ QList CMakeConfigItem::itemsFromArguments(const QStringList &li return result; } -QList CMakeConfigItem::itemsFromFile(const Utils::FilePath &cacheFile, QString *errorMessage) +CMakeConfig CMakeConfig::fromFile(const Utils::FilePath &cacheFile, QString *errorMessage) { CMakeConfig result; QFile cache(cacheFile.toString()); @@ -473,6 +472,11 @@ bool CMakeConfigItem::operator==(const CMakeConfigItem &o) const return o.key == key && o.value == value && o.isUnset == isUnset; } +uint qHash(const CMakeConfigItem &it) +{ + return ::qHash(it.key) ^ ::qHash(it.value) ^ ::qHash(it.isUnset); +} + #if WITH_TESTS } // namespace CMakeProjectManager diff --git a/src/plugins/cmakeprojectmanager/cmakeconfigitem.h b/src/plugins/cmakeprojectmanager/cmakeconfigitem.h index 2f1d8a1eec8..dae93455d72 100644 --- a/src/plugins/cmakeprojectmanager/cmakeconfigitem.h +++ b/src/plugins/cmakeprojectmanager/cmakeconfigitem.h @@ -43,18 +43,14 @@ class Kit; namespace CMakeProjectManager { -class CMAKE_EXPORT CMakeConfigItem { +class CMAKE_EXPORT CMakeConfigItem +{ public: enum Type { FILEPATH, PATH, BOOL, STRING, INTERNAL, STATIC, UNINITIALIZED }; CMakeConfigItem(); CMakeConfigItem(const QByteArray &k, Type t, const QByteArray &d, const QByteArray &v, const QStringList &s = {}); CMakeConfigItem(const QByteArray &k, const QByteArray &v); - static QByteArray valueOf(const QByteArray &key, const QList &input); - static QString stringValueOf(const QByteArray &key, const QList &input); - static Utils::FilePath filePathValueOf(const QByteArray &key, const QList &input); - static QString expandedValueOf(const ProjectExplorer::Kit *k, const QByteArray &key, - const QList &input); static QStringList cmakeSplitValue(const QString &in, bool keepEmpty = false); static Type typeStringToType(const QByteArray &typeString); static QString typeToTypeString(const Type t); @@ -66,8 +62,6 @@ public: static bool less(const CMakeConfigItem &a, const CMakeConfigItem &b); static CMakeConfigItem fromString(const QString &s); - static QList itemsFromArguments(const QStringList &list); - static QList itemsFromFile(const Utils::FilePath &input, QString *errorMessage); QString toString(const Utils::MacroExpander *expander = nullptr) const; QString toArgument(const Utils::MacroExpander *expander = nullptr) const; QString toCMakeSetLine(const Utils::MacroExpander *expander = nullptr) const; @@ -83,6 +77,25 @@ public: QByteArray documentation; QStringList values; }; -using CMakeConfig = QList; + +uint qHash(const CMakeConfigItem &it); // needed for MSVC + +class CMAKE_EXPORT CMakeConfig : public QList +{ +public: + CMakeConfig() = default; + CMakeConfig(const QList &items) : QList(items) {} + CMakeConfig(std::initializer_list items) : QList(items) {} + + const QList &toList() const { return *this; } + + static CMakeConfig fromArguments(const QStringList &list); + static CMakeConfig fromFile(const Utils::FilePath &input, QString *errorMessage); + + QByteArray valueOf(const QByteArray &key) const; + QString stringValueOf(const QByteArray &key) const; + Utils::FilePath filePathValueOf(const QByteArray &key) const; + QString expandedValueOf(const ProjectExplorer::Kit *k, const QByteArray &key) const; +}; } // namespace CMakeProjectManager diff --git a/src/plugins/cmakeprojectmanager/cmakekitinformation.cpp b/src/plugins/cmakeprojectmanager/cmakekitinformation.cpp index ea9fa55db01..2c6956f99b5 100644 --- a/src/plugins/cmakeprojectmanager/cmakekitinformation.cpp +++ b/src/plugins/cmakeprojectmanager/cmakekitinformation.cpp @@ -996,15 +996,15 @@ void CMakeConfigurationKitAspect::setConfiguration(Kit *k, const CMakeConfig &co { if (!k) return; - const QStringList tmp = Utils::transform(config, [](const CMakeConfigItem &i) { return i.toString(); }); + const QStringList tmp = Utils::transform(config.toList(), + [](const CMakeConfigItem &i) { return i.toString(); }); k->setValue(CONFIGURATION_ID, tmp); } QStringList CMakeConfigurationKitAspect::toStringList(const Kit *k) { - QStringList current - = Utils::transform(CMakeConfigurationKitAspect::configuration(k), - [](const CMakeConfigItem &i) { return i.toString(); }); + QStringList current = Utils::transform(CMakeConfigurationKitAspect::configuration(k).toList(), + [](const CMakeConfigItem &i) { return i.toString(); }); current = Utils::filtered(current, [](const QString &s) { return !s.isEmpty(); }); Utils::sort(current); return current; @@ -1023,7 +1023,7 @@ void CMakeConfigurationKitAspect::fromStringList(Kit *k, const QStringList &in) QStringList CMakeConfigurationKitAspect::toArgumentsList(const Kit *k) { - return Utils::transform(CMakeConfigurationKitAspect::configuration(k), + return Utils::transform(CMakeConfigurationKitAspect::configuration(k).toList(), [](const CMakeConfigItem &i) { return i.toArgument(nullptr); }); } @@ -1046,8 +1046,8 @@ QVariant CMakeConfigurationKitAspect::defaultValue(const Kit *k) const { // FIXME: Convert preload scripts CMakeConfig config = defaultConfiguration(k); - const QStringList tmp - = Utils::transform(config, [](const CMakeConfigItem &i) { return i.toString(); }); + const QStringList tmp = Utils::transform(config.toList(), + [](const CMakeConfigItem &i) { return i.toString(); }); return tmp; } diff --git a/src/plugins/cmakeprojectmanager/cmakeprojectimporter.cpp b/src/plugins/cmakeprojectmanager/cmakeprojectimporter.cpp index c86016da97d..2c4e5434c1d 100644 --- a/src/plugins/cmakeprojectmanager/cmakeprojectimporter.cpp +++ b/src/plugins/cmakeprojectmanager/cmakeprojectimporter.cpp @@ -135,20 +135,16 @@ QStringList CMakeProjectImporter::importCandidates() static FilePath qmakeFromCMakeCache(const CMakeConfig &config) { // Qt4 way to define things (more convenient for us, so try this first;-) - FilePath qmake - = FilePath::fromUtf8(CMakeConfigItem::valueOf(QByteArray("QT_QMAKE_EXECUTABLE"), config)); + const FilePath qmake = config.filePathValueOf("QT_QMAKE_EXECUTABLE"); qCDebug(cmInputLog) << "QT_QMAKE_EXECUTABLE=" << qmake.toUserOutput(); if (!qmake.isEmpty()) return qmake; // Check Qt5 settings: oh, the horror! const FilePath qtCMakeDir = [config] { - FilePath tmp = FilePath::fromUtf8( - CMakeConfigItem::valueOf(QByteArray("Qt5Core_DIR"), config)); - if (tmp.isEmpty()) { - tmp = FilePath::fromUtf8( - CMakeConfigItem::valueOf(QByteArray("Qt6Core_DIR"), config)); - } + FilePath tmp = config.filePathValueOf("Qt5Core_DIR"); + if (tmp.isEmpty()) + tmp = config.filePathValueOf("Qt6Core_DIR"); return tmp; }(); qCDebug(cmInputLog) << "QtXCore_DIR=" << qtCMakeDir.toUserOutput(); @@ -206,11 +202,11 @@ static FilePath qmakeFromCMakeCache(const CMakeConfig &config) cmake.setEnvironment(env); cmake.setTimeOutMessageBoxEnabled(false); - QString cmakeGenerator = CMakeConfigItem::stringValueOf(QByteArray("CMAKE_GENERATOR"), config); - FilePath cmakeExecutable = CMakeConfigItem::filePathValueOf(QByteArray("CMAKE_COMMAND"), config); - FilePath cmakeMakeProgram =CMakeConfigItem::filePathValueOf(QByteArray("CMAKE_MAKE_PROGRAM"), config); - FilePath toolchainFile = CMakeConfigItem::filePathValueOf(QByteArray("CMAKE_TOOLCHAIN_FILE"), config); - FilePath hostPath = CMakeConfigItem::filePathValueOf(QByteArray("QT_HOST_PATH"), config); + QString cmakeGenerator = config.stringValueOf(QByteArray("CMAKE_GENERATOR")); + FilePath cmakeExecutable = config.filePathValueOf(QByteArray("CMAKE_COMMAND")); + FilePath cmakeMakeProgram = config.filePathValueOf(QByteArray("CMAKE_MAKE_PROGRAM")); + FilePath toolchainFile = config.filePathValueOf(QByteArray("CMAKE_TOOLCHAIN_FILE")); + FilePath hostPath = config.filePathValueOf(QByteArray("QT_HOST_PATH")); QStringList args; args.push_back("-S"); @@ -270,7 +266,7 @@ static QVector extractToolChainsFromCache(const CMakeConfi } if (!haveCCxxCompiler) { - const QByteArray generator = CMakeConfigItem::valueOf(QByteArray("CMAKE_GENERATOR"), config); + const QByteArray generator = config.valueOf("CMAKE_GENERATOR"); QString cCompilerName; QString cxxCompilerName; if (generator.contains("Visual Studio")) { @@ -282,8 +278,7 @@ static QVector extractToolChainsFromCache(const CMakeConfi } if (!cCompilerName.isEmpty() && !cxxCompilerName.isEmpty()) { - const FilePath linker = FilePath::fromUtf8( - CMakeConfigItem::valueOf(QByteArray("CMAKE_LINKER"), config)); + const FilePath linker = config.filePathValueOf("CMAKE_LINKER"); if (!linker.isEmpty()) { const FilePath compilerPath = linker.parentDir(); result.append({compilerPath.pathAppended(cCompilerName), @@ -315,13 +310,11 @@ QList CMakeProjectImporter::examineDirectory(const FilePath &importPath, return { }; } - QByteArrayList buildConfigurationTypes = {CMakeConfigItem::valueOf("CMAKE_BUILD_TYPE", config)}; + QByteArrayList buildConfigurationTypes = {config.valueOf("CMAKE_BUILD_TYPE")}; if (buildConfigurationTypes.front().isEmpty()) { - QByteArray buildConfigurationTypesString = - CMakeConfigItem::valueOf("CMAKE_CONFIGURATION_TYPES", config); - if (!buildConfigurationTypesString.isEmpty()) { + QByteArray buildConfigurationTypesString = config.valueOf("CMAKE_CONFIGURATION_TYPES"); + if (!buildConfigurationTypesString.isEmpty()) buildConfigurationTypes = buildConfigurationTypesString.split(';'); - } } QList result; @@ -329,7 +322,7 @@ QList CMakeProjectImporter::examineDirectory(const FilePath &importPath, auto data = std::make_unique(); data->cmakeHomeDirectory = - FilePath::fromUserInput(CMakeConfigItem::stringValueOf("CMAKE_HOME_DIRECTORY", config)) + FilePath::fromUserInput(config.stringValueOf("CMAKE_HOME_DIRECTORY")) .canonicalPath(); const FilePath canonicalProjectDirectory = projectDirectory().canonicalPath(); if (data->cmakeHomeDirectory != canonicalProjectDirectory) { @@ -344,12 +337,12 @@ QList CMakeProjectImporter::examineDirectory(const FilePath &importPath, data->buildDirectory = importPath; data->cmakeBuildType = buildType; - data->cmakeBinary = CMakeConfigItem::filePathValueOf("CMAKE_COMMAND", config); - data->generator = CMakeConfigItem::stringValueOf("CMAKE_GENERATOR", config); - data->extraGenerator = CMakeConfigItem::stringValueOf("CMAKE_EXTRA_GENERATOR", config); - data->platform = CMakeConfigItem::stringValueOf("CMAKE_GENERATOR_PLATFORM", config); - data->toolset = CMakeConfigItem::stringValueOf("CMAKE_GENERATOR_TOOLSET", config); - data->sysroot = CMakeConfigItem::filePathValueOf("CMAKE_SYSROOT", config); + data->cmakeBinary = config.filePathValueOf("CMAKE_COMMAND"); + data->generator = config.stringValueOf("CMAKE_GENERATOR"); + data->extraGenerator = config.stringValueOf("CMAKE_EXTRA_GENERATOR"); + data->platform = config.stringValueOf("CMAKE_GENERATOR_PLATFORM"); + data->toolset = config.stringValueOf("CMAKE_GENERATOR_TOOLSET"); + data->sysroot = config.filePathValueOf("CMAKE_SYSROOT"); // Qt: const FilePath qmake = qmakeFromCMakeCache(config); diff --git a/src/plugins/cmakeprojectmanager/configmodel.cpp b/src/plugins/cmakeprojectmanager/configmodel.cpp index c253ece543e..44d29c404ed 100644 --- a/src/plugins/cmakeprojectmanager/configmodel.cpp +++ b/src/plugins/cmakeprojectmanager/configmodel.cpp @@ -203,7 +203,7 @@ QList ConfigModel::configurationForCMake() const void ConfigModel::setConfiguration(const CMakeConfig &config) { - setConfiguration(Utils::transform(config, [](const CMakeConfigItem &i) { + setConfiguration(Utils::transform(config.toList(), [](const CMakeConfigItem &i) { return DataItem(i); })); } diff --git a/src/plugins/cmakeprojectmanager/fileapireader.cpp b/src/plugins/cmakeprojectmanager/fileapireader.cpp index 4d1e11e55e0..18ec80651c3 100644 --- a/src/plugins/cmakeprojectmanager/fileapireader.cpp +++ b/src/plugins/cmakeprojectmanager/fileapireader.cpp @@ -337,7 +337,7 @@ void FileApiReader::writeConfigurationIntoBuildDirectory(const QStringList &conf QByteArray contents; contents.append("# This file is managed by Qt Creator, do not edit!\n\n"); contents.append( - transform(CMakeConfigItem::itemsFromArguments(configurationArguments), + transform(CMakeConfig::fromArguments(configurationArguments).toList(), [](const CMakeConfigItem &item) { return item.toCMakeSetLine(nullptr); }) diff --git a/src/plugins/docker/dockerdevice.cpp b/src/plugins/docker/dockerdevice.cpp index 6d4c08b5da8..bacbc36204c 100644 --- a/src/plugins/docker/dockerdevice.cpp +++ b/src/plugins/docker/dockerdevice.cpp @@ -69,6 +69,8 @@ #include #include #include +#include +#include #include #include #include @@ -304,9 +306,11 @@ public: }); } - ~DockerDevicePrivate() { delete m_shell; } + ~DockerDevicePrivate() { stopCurrentContainer(); } bool runInContainer(const CommandLine &cmd) const; + bool runInShell(const CommandLine &cmd) const; + QString outputForRunInShell(const CommandLine &cmd) const; void tryCreateLocalFileAccess(); @@ -318,6 +322,7 @@ public: // For local file access QPointer m_shell; + mutable QMutex m_shellMutex; QString m_container; QString m_mergedDir; QFileSystemWatcher m_mergedDirWatcher; @@ -359,12 +364,15 @@ public: auto daemonStateLabel = new QLabel(tr("Daemon state:")); m_daemonReset = new QToolButton; m_daemonReset->setIcon(Icons::INFO.icon()); - m_daemonReset->setToolTip(tr("Daemon state not evaluated.")); + m_daemonReset->setToolTip(tr("Clear detected daemon state. " + "It will be automatically re-evaluated next time an access is needed.")); + + m_daemonState = new QLabel(tr("Daemon state not evaluated.")); connect(m_daemonReset, &QToolButton::clicked, this, [this, dockerDevice] { dockerDevice->resetDaemonState(); m_daemonReset->setIcon(Icons::INFO.icon()); - m_daemonReset->setToolTip(tr("Daemon state not evaluated.")); + m_daemonState->setText(tr("Daemon state not evaluated.")); }); m_runAsOutsideUser = new QCheckBox(tr("Run as outside user")); @@ -403,10 +411,11 @@ public: if (!dockerDevice->isDaemonRunning()) { logView->append(tr("Docker daemon appears to be not running.")); - m_daemonReset->setToolTip(tr("Daemon not running. Push to reset the state.")); + m_daemonState->setText(tr("Docker daemon not running.")); m_daemonReset->setIcon(Icons::CRITICAL.icon()); } else { - m_daemonReset->setToolTip(tr("Docker daemon running.")); + logView->append(tr("Docker daemon appears to be running.")); + m_daemonState->setText(tr("Docker daemon running.")); m_daemonReset->setIcon(Icons::OK.icon()); } @@ -427,7 +436,7 @@ public: Form { idLabel, m_idLineEdit, Break(), repoLabel, m_repoLineEdit, Break(), - daemonStateLabel, m_daemonReset, Break(), + daemonStateLabel, m_daemonReset, m_daemonState, Break(), m_runAsOutsideUser, Break(), tr("Paths to mount:"), m_pathsLineEdit, Break(), Column { @@ -445,6 +454,7 @@ private: QLineEdit *m_idLineEdit; QLineEdit *m_repoLineEdit; QToolButton *m_daemonReset; + QLabel *m_daemonState; QCheckBox *m_runAsOutsideUser; QLineEdit *m_pathsLineEdit; @@ -745,6 +755,20 @@ void DockerDevicePrivate::stopCurrentContainer() if (m_container.isEmpty() || m_accessible == NoDaemon) return; + if (m_shell) { + QMutexLocker l(&m_shellMutex); + m_shell->write("exit\n"); + m_shell->waitForFinished(2000); + if (m_shell->state() == QProcess::NotRunning) { + LOG("Clean exit via shell"); + m_container.clear(); + m_mergedDir.clear(); + delete m_shell; + m_shell = nullptr; + return; + } + } + QtcProcess proc; proc.setCommand({"docker", {"container", "stop", m_container}}); @@ -1019,7 +1043,7 @@ bool DockerDevice::isExecutableFile(const FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"test", {"-x", path}}); + return d->runInShell({"test", {"-x", path}}); } bool DockerDevice::isReadableFile(const FilePath &filePath) const @@ -1033,7 +1057,7 @@ bool DockerDevice::isReadableFile(const FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"test", {"-r", path, "-a", "-f", path}}); + return d->runInShell({"test", {"-r", path, "-a", "-f", path}}); } bool DockerDevice::isWritableFile(const Utils::FilePath &filePath) const @@ -1047,7 +1071,7 @@ bool DockerDevice::isWritableFile(const Utils::FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"test", {"-w", path, "-a", "-f", path}}); + return d->runInShell({"test", {"-w", path, "-a", "-f", path}}); } bool DockerDevice::isReadableDirectory(const FilePath &filePath) const @@ -1061,7 +1085,7 @@ bool DockerDevice::isReadableDirectory(const FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"test", {"-r", path, "-a", "-d", path}}); + return d->runInShell({"test", {"-r", path, "-a", "-d", path}}); } bool DockerDevice::isWritableDirectory(const FilePath &filePath) const @@ -1075,7 +1099,7 @@ bool DockerDevice::isWritableDirectory(const FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"test", {"-w", path, "-a", "-d", path}}); + return d->runInShell({"test", {"-w", path, "-a", "-d", path}}); } bool DockerDevice::isFile(const FilePath &filePath) const @@ -1089,7 +1113,7 @@ bool DockerDevice::isFile(const FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"test", {"-f", path}}); + return d->runInShell({"test", {"-f", path}}); } bool DockerDevice::isDirectory(const FilePath &filePath) const @@ -1103,7 +1127,7 @@ bool DockerDevice::isDirectory(const FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"test", {"-d", path}}); + return d->runInShell({"test", {"-d", path}}); } bool DockerDevice::createDirectory(const FilePath &filePath) const @@ -1131,7 +1155,7 @@ bool DockerDevice::exists(const FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"test", {"-e", path}}); + return d->runInShell({"test", {"-e", path}}); } bool DockerDevice::ensureExistingFile(const FilePath &filePath) const @@ -1145,7 +1169,7 @@ bool DockerDevice::ensureExistingFile(const FilePath &filePath) const return res; } const QString path = filePath.path(); - return d->runInContainer({"touch", {path}}); + return d->runInShell({"touch", {path}}); } bool DockerDevice::removeFile(const FilePath &filePath) const @@ -1172,9 +1196,15 @@ bool DockerDevice::removeRecursively(const FilePath &filePath) const LOG("Remove recursively? " << filePath.toUserOutput() << localAccess.toUserOutput() << res); return res; } -// Open this up only when really needed. -// return d->runInContainer({"rm", "-rf", {filePath.path()}}); - return false; + + const QString path = filePath.cleanPath().path(); + // We are expecting this only to be called in a context of build directories or similar. + // Chicken out in some cases that _might_ be user code errors. + QTC_ASSERT(path.startsWith('/'), return false); + const int levelsNeeded = path.startsWith("/home/") ? 4 : 3; + QTC_ASSERT(path.count('/') >= levelsNeeded, return false); + + return d->runInContainer({"rm", {"-rf", "--", path}}); } bool DockerDevice::copyFile(const FilePath &filePath, const FilePath &target) const @@ -1240,8 +1270,44 @@ FilePath DockerDevice::symLinkTarget(const FilePath &filePath) const return {}; return mapToGlobalPath(target); } - QTC_CHECK(false); - return {}; + + const QString output = d->outputForRunInShell({"readlink", {"-n", "-e", filePath.path()}}); + return output.isEmpty() ? FilePath() : filePath.withNewPath(output); +} + +static FilePaths filterEntriesHelper(const FilePath &base, + const QStringList &entries, + const QStringList &nameFilters, + QDir::Filters filters, + QDir::SortFlags sort) +{ + const QList nameRegexps = transform(nameFilters, [](const QString &filter) { + QRegularExpression re; + re.setPattern(QRegularExpression::wildcardToRegularExpression(filter)); + QTC_CHECK(re.isValid()); + return re; + }); + + const auto nameMatches = [&nameRegexps](const QString &fileName) { + for (const QRegularExpression &re : nameRegexps) { + const QRegularExpressionMatch match = re.match(fileName); + if (match.hasMatch()) + return true; + } + return false; + }; + + // FIXME: Handle sort and filters. For now bark on unsupported options. + QTC_CHECK(filters == QDir::NoFilter); + QTC_CHECK(sort == QDir::NoSort); + + FilePaths result; + for (const QString &entry : entries) { + if (!nameMatches(entry)) + continue; + result.append(base.pathAppended(entry)); + } + return result; } FilePaths DockerDevice::directoryEntries(const FilePath &filePath, @@ -1258,13 +1324,9 @@ FilePaths DockerDevice::directoryEntries(const FilePath &filePath, }); } - QtcProcess proc; - proc.setCommand({"ls", {"-1", "-b", "--", filePath.path()}}); - runProcess(proc); - proc.waitForFinished(); - - QStringList entries = proc.stdOut().split('\n', Qt::SkipEmptyParts); - return FilePath::filterEntriesHelper(filePath, entries, nameFilters, filters, sort); + const QString output = d->outputForRunInShell({"ls", {"-1", "-b", "--", filePath.path()}}); + QStringList entries = output.split('\n', Qt::SkipEmptyParts); + return filterEntriesHelper(filePath, entries, nameFilters, filters, sort); } QByteArray DockerDevice::fileContents(const FilePath &filePath, qint64 limit, qint64 offset) const @@ -1291,15 +1353,35 @@ QByteArray DockerDevice::fileContents(const FilePath &filePath, qint64 limit, qi return output; } -bool DockerDevice::writeFileContents(const Utils::FilePath &filePath, const QByteArray &data) const +bool DockerDevice::writeFileContents(const FilePath &filePath, const QByteArray &data) const { QTC_ASSERT(handlesFile(filePath), return {}); tryCreateLocalFileAccess(); if (hasLocalFileAccess()) return mapToLocalAccess(filePath).writeFileContents(data); - QTC_CHECK(false); // FIXME: Implement - return {}; +// This following would be the generic Unix solution. +// But it doesn't pass input. FIXME: Why? +// QtcProcess proc; +// proc.setCommand({"dd", {"of=" + filePath.path()}}); +// proc.setWriteData(data); +// runProcess(proc); +// proc.waitForFinished(); + + TemporaryFile tempFile("dockertransport-XXXXXX"); + tempFile.open(); + tempFile.write(data); + + const QString tempName = tempFile.fileName(); + tempFile.close(); + + CommandLine cmd{"docker", {"cp", tempName, d->m_container + ':' + filePath.path()}}; + + QtcProcess proc; + proc.setCommand(cmd); + proc.runBlocking(); + + return proc.exitCode() == 0; } void DockerDevice::runProcess(QtcProcess &process) const @@ -1375,6 +1457,48 @@ bool DockerDevicePrivate::runInContainer(const CommandLine &cmd) const return exitCode == 0; } +bool DockerDevicePrivate::runInShell(const CommandLine &cmd) const +{ + if (m_accessible == NoDaemon) + return false; + QTC_ASSERT(m_shell, return false); + QMutexLocker l(&m_shellMutex); + m_shell->readAllStandardOutput(); // clean possible left-overs + m_shell->write(cmd.toUserOutput().toUtf8() + "\necho $?\n"); + m_shell->waitForReadyRead(); + QByteArray output = m_shell->readAllStandardOutput(); + int result = output.toInt(); + LOG("Run command in shell:" << cmd.toUserOutput() << "result: " << output << " ==>" << result); + return result == 0; +} + +// generate hex value +static QByteArray randomHex() +{ + quint32 val = QRandomGenerator::global()->generate(); + return QString::number(val, 16).toUtf8(); +} + +QString DockerDevicePrivate::outputForRunInShell(const CommandLine &cmd) const +{ + if (m_accessible == NoDaemon) + return {}; + QTC_ASSERT(m_shell, return {}); + QMutexLocker l(&m_shellMutex); + m_shell->readAllStandardOutput(); // clean possible left-overs + const QByteArray markerWithNewLine("___QC_DOCKER_" + randomHex() + "_OUTPUT_MARKER___\n"); + m_shell->write(cmd.toUserOutput().toUtf8() + "\necho -n \"" + markerWithNewLine + "\"\n"); + QByteArray output; + while (!output.endsWith(markerWithNewLine)) { + m_shell->waitForReadyRead(); + output.append(m_shell->readAllStandardOutput()); + } + LOG("Run command in shell:" << cmd.toUserOutput() << "output size:" << output.size()); + if (QTC_GUARD(output.endsWith(markerWithNewLine))) + output.chop(markerWithNewLine.size()); + return QString::fromUtf8(output); +} + // Factory DockerDeviceFactory::DockerDeviceFactory() diff --git a/src/plugins/projectexplorer/devicesupport/devicemanager.cpp b/src/plugins/projectexplorer/devicesupport/devicemanager.cpp index 41ca11cd90e..08260700a18 100644 --- a/src/plugins/projectexplorer/devicesupport/devicemanager.cpp +++ b/src/plugins/projectexplorer/devicesupport/devicemanager.cpp @@ -525,7 +525,7 @@ DeviceManager::DeviceManager(bool isInstance) : d(std::make_uniquesystemEnvironment(); }; - FilePath::setDeviceFileHooks(deviceHooks); + FileUtils::setDeviceFileHooks(deviceHooks); DeviceProcessHooks processHooks; diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibrarymodel.cpp b/src/plugins/qmldesigner/components/itemlibrary/itemlibrarymodel.cpp index f233017b706..807821e9789 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibrarymodel.cpp +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibrarymodel.cpp @@ -347,7 +347,7 @@ void ItemLibraryModel::update(ItemLibraryInfo *itemLibraryInfo, Model *model) importSection = importHash[entry.requiredImport()]; } - } else if (catName == "My Quick3D Components") { + } else if (catName == ItemLibraryImport::quick3DAssetsTitle()) { importSection = importHash[ItemLibraryImport::quick3DAssetsTitle()]; } else { if (catName.startsWith("Qt Quick - ")) diff --git a/src/tools/sdktool/CMakeLists.txt b/src/tools/sdktool/CMakeLists.txt index dc2320a92c4..e5197c59526 100644 --- a/src/tools/sdktool/CMakeLists.txt +++ b/src/tools/sdktool/CMakeLists.txt @@ -79,6 +79,7 @@ extend_qtc_executable(sdktool DEFINES QTCREATOR_UTILS_STATIC_LIB SOURCES environment.cpp environment.h + filepath.cpp filepath.h fileutils.cpp fileutils.h hostosinfo.cpp hostosinfo.h namevaluedictionary.cpp namevaluedictionary.h diff --git a/src/tools/sdktool/sdktool.pro b/src/tools/sdktool/sdktool.pro index 0b58755a1e5..aa1edfb504b 100644 --- a/src/tools/sdktool/sdktool.pro +++ b/src/tools/sdktool/sdktool.pro @@ -32,6 +32,7 @@ SOURCES += \ rmtoolchainoperation.cpp \ settings.cpp \ $$UTILS/environment.cpp \ + $$UTILS/filepath.cpp \ $$UTILS/fileutils.cpp \ $$UTILS/hostosinfo.cpp \ $$UTILS/namevaluedictionary.cpp \ @@ -65,6 +66,7 @@ HEADERS += \ rmtoolchainoperation.h \ settings.h \ $$UTILS/environment.h \ + $$UTILS/filepath.h \ $$UTILS/fileutils.h \ $$UTILS/hostosinfo.h \ $$UTILS/namevaluedictionary.h \ diff --git a/src/tools/sdktool/sdktool.qbs b/src/tools/sdktool/sdktool.qbs index a6fd9ed8b84..bf9b42dfb12 100644 --- a/src/tools/sdktool/sdktool.qbs +++ b/src/tools/sdktool/sdktool.qbs @@ -70,6 +70,7 @@ QtcTool { files: [ "commandline.cpp", "commandline.h", "environment.cpp", "environment.h", + "filepath.cpp", "filepath.h", "fileutils.cpp", "fileutils.h", "hostosinfo.cpp", "hostosinfo.h", "namevaluedictionary.cpp", "namevaluedictionary.h", diff --git a/tests/auto/debugger/gdb.pro b/tests/auto/debugger/gdb.pro index fd19feb09b6..802f80b0f17 100644 --- a/tests/auto/debugger/gdb.pro +++ b/tests/auto/debugger/gdb.pro @@ -19,6 +19,7 @@ HEADERS += \ SOURCES += \ $$UTILSDIR/commandline.cpp \ $$UTILSDIR/environment.cpp \ + $$UTILSDIR/filepath.cpp \ $$UTILSDIR/fileutils.cpp \ $$UTILSDIR/hostosinfo.cpp \ $$UTILSDIR/launcherinterface.cpp \ @@ -34,6 +35,7 @@ HEADERS += \ HEADERS += \ $$UTILSDIR/commandline.h \ $$UTILSDIR/environment.h \ + $$UTILSDIR/filepath.h \ $$UTILSDIR/fileutils.h \ $$UTILSDIR/hostosinfo.h \ $$UTILSDIR/launcherinterface.h \