diff --git a/.github/ci/after_make.sh b/.github/ci/after_make.sh index e2801d7a5..378cb9518 100644 --- a/.github/ci/after_make.sh +++ b/.github/ci/after_make.sh @@ -10,7 +10,7 @@ export IGN_CONFIG_PATH=/usr/local/share/ignition export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH # For rendering / window tests -Xvfb :1 -screen 0 1280x1024x24 & +Xvfb :1 -ac -noreset -core -screen 0 1280x1024x24 & export DISPLAY=:1.0 export RENDER_ENGINE_VALUES=ogre2 export MESA_GL_VERSION_OVERRIDE=3.3 diff --git a/Changelog.md b/Changelog.md index 569b42f40..dc7f3c72b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -460,6 +460,64 @@ ## Ignition Gui 3 +### Ignition Gui 3.10.0 (2022-07-13) + +1. Add common widget for vector3 + * [Pull request #427](https://github.com/gazebosim/gz-gui/pull/427) + +1. Allow Dialogs to have a MainWindow independent config + * [Pull request #418](https://github.com/gazebosim/gz-gui/pull/418) + +1. Add common widget for pose + * [Pull request #424](https://github.com/gazebosim/gz-gui/pull/424) + * [Pull request #431](https://github.com/gazebosim/gz-gui/pull/431) + +1. Example running a dialog before the main window + * [Pull request #407](https://github.com/gazebosim/gz-gui/pull/407) + +1. Common widget GzColor + * [Pull request #410](https://github.com/gazebosim/gz-gui/pull/410) + +1. Fix ign_TEST + * [Pull request #420](https://github.com/gazebosim/gz-gui/pull/420) + +1. Make display tests more robust + * [Pull request #419](https://github.com/gazebosim/gz-gui/pull/419) + +1. Bash completion for flags + * [Pull request #392](https://github.com/gazebosim/gz-gui/pull/392) + +1. Disable failing test on Citadel + * [Pull request #416](https://github.com/gazebosim/gz-gui/pull/416) + +1. Search menu keyboard control + * [Pull request #403](https://github.com/gazebosim/gz-gui/pull/403) + * [Pull request #405](https://github.com/gazebosim/gz-gui/pull/405) + +1. Add config relative path environment variable + * [Pull request #386](https://github.com/gazebosim/gz-gui/pull/386) + +1. Sort plugin list in alphabetical order (including when filtering) + * [Pull request #387](https://github.com/gazebosim/gz-gui/pull/387) + +1. Added array to snackbar qml + * [Pull request #370](https://github.com/gazebosim/gz-gui/pull/370) + +1. Fix some Qt warnings + * [Pull request #376](https://github.com/gazebosim/gz-gui/pull/376) + +1. Added Snackbar qtquick object + * [Pull request #369](https://github.com/gazebosim/gz-gui/pull/369) + +1. Fix menu scrolling when a new plugin is added + * [Pull request #368](https://github.com/gazebosim/gz-gui/pull/368) + +1. Improve KeyPublisher's usability + * [Pull request #362](https://github.com/gazebosim/gz-gui/pull/362) + +1. Backport GridConfig improvements to Citadel's Grid3D + * [Pull request #363](https://github.com/gazebosim/gz-gui/pull/363) + ### Ignition Gui 3.9.0 (2022-01-14) 1. Added a button that allows shutting down both the client and server. diff --git a/examples/standalone/start_dialog/CMakeLists.txt b/examples/standalone/start_dialog/CMakeLists.txt new file mode 100644 index 000000000..c35f06409 --- /dev/null +++ b/examples/standalone/start_dialog/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.10.2 FATAL_ERROR) + +project(gz-gui-start-dialog) + +if(POLICY CMP0100) + cmake_policy(SET CMP0100 NEW) +endif() + +set(CMAKE_AUTOMOC ON) + +find_package(ignition-gui6 REQUIRED) +set(GZ_GUI_VER ${ignition-gui6_VERSION_MAJOR}) + +set(EXEC_NAME "start_dialog") + +QT5_ADD_RESOURCES(resources_RCC ${EXEC_NAME}.qrc) + +add_executable(${EXEC_NAME} + ${EXEC_NAME}.cc + ${resources_RCC} +) +target_link_libraries(${EXEC_NAME} + ignition-gui${GZ_GUI_VER}::ignition-gui${GZ_GUI_VER} +) + diff --git a/examples/standalone/start_dialog/README.md b/examples/standalone/start_dialog/README.md new file mode 100644 index 000000000..4ab7c686c --- /dev/null +++ b/examples/standalone/start_dialog/README.md @@ -0,0 +1,16 @@ +Example for how to run a start dialog before the main window. + +## Build + + cd + mkdir build + cd build + cmake .. + make + +## Run + + cd /build + ./start_dialog + +First the dialog shows up, and after that's closed, the main window shows up. diff --git a/examples/standalone/start_dialog/start_dialog.cc b/examples/standalone/start_dialog/start_dialog.cc new file mode 100644 index 000000000..3ef1b7d43 --- /dev/null +++ b/examples/standalone/start_dialog/start_dialog.cc @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include + +#include +#include +#include +#include + +////////////////////////////////////////////////// +int main(int _argc, char **_argv) +{ + // Increase verboosity so we see all messages + ignition::common::Console::SetVerbosity(4); + + // Create app + ignition::gui::Application app(_argc, _argv, ignition::gui::WindowType::kDialog); + + igndbg << "Open dialog" << std::endl; + + // Add and display a dialog + auto dialog = new ignition::gui::Dialog(); + dialog->QuickWindow(); + + std::string qmlFile(":start_dialog/start_dialog.qml"); + if (!QFile(QString::fromStdString(qmlFile)).exists()) + { + ignerr << "Can't find [" << qmlFile + << "]. Are you sure it was added to the .qrc file?" << std::endl; + return -1; + } + + QQmlComponent dialogComponent(ignition::gui::App()->Engine(), + QString(QString::fromStdString(qmlFile))); + if (dialogComponent.isError()) + { + std::stringstream errors; + errors << "Failed to instantiate QML file [" << qmlFile << "]." + << std::endl; + for (auto error : dialogComponent.errors()) + { + errors << "* " << error.toString().toStdString() << std::endl; + } + ignerr << errors.str(); + return -1; + } + + auto dialogItem = qobject_cast(dialogComponent.create()); + if (!dialogItem) + { + ignerr << "Failed to instantiate QML file [" << qmlFile << "]." << std::endl + << "Are you sure the file is valid QML? " + << "You can check with the `qmlscene` tool" << std::endl; + return -1; + } + + dialogItem->setParentItem(dialog->RootItem()); + + // Execute start dialog + app.exec(); + + // After dialog is shut, display the main window + igndbg << "Dialog closed, open main window" << std::endl; + + // Create main window + app.CreateMainWindow(); + + // Run main window + app.exec(); + + igndbg << "Main window closed" << std::endl; + + return 0; +} + diff --git a/examples/standalone/start_dialog/start_dialog.qml b/examples/standalone/start_dialog/start_dialog.qml new file mode 100644 index 000000000..a07f4ff52 --- /dev/null +++ b/examples/standalone/start_dialog/start_dialog.qml @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +import QtQuick 2.0 +import QtQuick.Controls 2.0 +Rectangle { + color: "green" + anchors.fill: parent + Text { + text: qsTr("Start\ndialog!") + font.pointSize: 30 + } +} diff --git a/examples/standalone/start_dialog/start_dialog.qrc b/examples/standalone/start_dialog/start_dialog.qrc new file mode 100644 index 000000000..578d9c3c2 --- /dev/null +++ b/examples/standalone/start_dialog/start_dialog.qrc @@ -0,0 +1,5 @@ + + + start_dialog.qml + + diff --git a/include/ignition/gui/Application.hh b/include/ignition/gui/Application.hh index db9fd4513..fb6c25812 100644 --- a/include/ignition/gui/Application.hh +++ b/include/ignition/gui/Application.hh @@ -53,7 +53,8 @@ namespace ignition /// plugins kMainWindow = 0, - /// \brief One independent dialog per plugin + /// \brief One independent dialog per plugin. Also useful to open a + /// startup dialog before the main window. kDialog = 1 }; @@ -96,6 +97,7 @@ namespace ignition /// and plugins. This function doesn't instantiate the plugins, it just /// keeps them in memory and they can be applied later by either /// instantiating a window or several dialogs. + /// and plugins. /// \param[in] _path Full path to configuration file. /// \return True if successful /// \sa InitializeMainWindow @@ -155,6 +157,7 @@ namespace ignition /// \return True if successful public: bool RemovePlugin(const std::string &_pluginName); + /// \brief Get a plugin by its unique name. /// \param[in] _pluginName Plugn instance's unique name. This is the /// plugin card's object name. @@ -169,6 +172,11 @@ namespace ignition /// \brief Callback when user requests to close a plugin public slots: void OnPluginClose(); + /// \brief Create a main window. Just calls InitializeMainWindow. + /// \return True if successful + /// \sa InitializeMainWindow + public: bool CreateMainWindow(); + /// \brief Create a main window, populate with previously loaded plugins /// and apply previously loaded configuration. /// An empty window will be created if no plugins have been loaded. diff --git a/include/ignition/gui/Dialog.hh b/include/ignition/gui/Dialog.hh index 5da748d42..7271c415b 100644 --- a/include/ignition/gui/Dialog.hh +++ b/include/ignition/gui/Dialog.hh @@ -19,6 +19,7 @@ #define IGNITION_GUI_DIALOG_HH_ #include +#include #include "ignition/gui/qt.h" #include "ignition/gui/Export.hh" @@ -55,6 +56,28 @@ namespace ignition /// \return Pointer to the item public: QQuickItem *RootItem() const; + /// \brief Store dialog default config + /// \param[in] _config XML config as string + public: void SetDefaultConfig(const std::string &_config); + + /// \brief Write dialog config + /// \param[in] _path config path + /// \param[in] _attribute XMLElement attribute name + /// \param[in] _value XMLElement attribute value + /// \return true if written to config file + public: bool UpdateConfigAttribute( + const std::string &_path, const std::string &_attribute, + const bool _value) const; + + /// \brief Gets a config attribute value, if not found in config + /// write the default in the config and get it. + /// creates config file if it doesn't exist. + /// \param[in] _path config path + /// \param[in] _attribute attribute name + /// \return attribute value as string + public: std::string ReadConfigAttribute(const std::string &_path, + const std::string &_attribute) const; + /// \internal /// \brief Private data pointer private: std::unique_ptr dataPtr; diff --git a/include/ignition/gui/MainWindow.hh b/include/ignition/gui/MainWindow.hh index 6c5241430..3379534d4 100644 --- a/include/ignition/gui/MainWindow.hh +++ b/include/ignition/gui/MainWindow.hh @@ -675,6 +675,7 @@ namespace ignition /// \brief Concatenation of all plugin configurations. std::string plugins{""}; + }; } } diff --git a/include/ignition/gui/qml/GzColor.qml b/include/ignition/gui/qml/GzColor.qml new file mode 100644 index 000000000..bcf455379 --- /dev/null +++ b/include/ignition/gui/qml/GzColor.qml @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.1 +import QtQuick.Dialogs 1.0 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.1 + + +// RGBA using range 0 - 1.0 +Item { + id: gzColorRoot + + implicitWidth: 40 + implicitHeight: 40 + + property double r: 1.0 + property double g: 0.0 + property double b: 0.0 + property double a: 1.0 + + signal gzColorSet() + + Button { + id: gzColorButton + Layout.leftMargin: 5 + ToolTip.text: "Open color dialog" + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + background: Rectangle { + implicitWidth: 40 + implicitHeight: 40 + radius: 5 + border.color: (Material.theme === Material.Light) ? Qt.rgba(0,0,0,1) : Qt.rgba(1,1,1,1) + border.width: 2 + color: Qt.rgba(r,g,b,a) + } + onClicked: gzColorDialog.open() + } + + ColorDialog { + id: gzColorDialog + title: "Choose a color" + visible: false + showAlphaChannel: true + modality: Qt.ApplicationModal + onAccepted: { + r = gzColorDialog.color.r + g = gzColorDialog.color.g + b = gzColorDialog.color.b + a = gzColorDialog.color.a + gzColorRoot.gzColorSet() + gzColorDialog.close() + } + onRejected: { + gzColorDialog.close() + } + } +} + diff --git a/include/ignition/gui/qml/GzPose.qml b/include/ignition/gui/qml/GzPose.qml new file mode 100644 index 000000000..d863e6bfc --- /dev/null +++ b/include/ignition/gui/qml/GzPose.qml @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Styles 1.4 + +/** + * Item displaying 3D pose information. + * + * Users can set values to xValue, yValue, etc. + * If readOnly == False, + * users can read from signal parameters of gzPoseSet: _x, _y, etc. + * + * Usage example: + * GzPose { + * id: gzPose + * readOnly: false + * xValue: xValueFromCPP + * yValue: yValueFromCPP + * zValue: zValueFromCPP + * rollValue: rollValueFromCPP + * pitchValue: pitchValueFromCPP + * yawValue: yawValueFromCPP + * onGzPoseSet: { + * myFunc(_x, _y, _z, _roll, _pitch, _yaw) + * } + * } +**/ + +Item { + id: gzPoseRoot + + // Read-only / write + property bool readOnly: false + + // User input value. + property double xValue + property double yValue + property double zValue + property double rollValue + property double pitchValue + property double yawValue + + /** + * Used to read spinbox values + * @params: _x, _y, _z, _roll, _pitch, _yaw: corresponding spinBoxes values + * @note: When readOnly == false, user should read spinbox value from its + * parameters. + * When readOnly == true, this signal is unused. + */ + signal gzPoseSet(double _x, double _y, double _z, double _roll, double _pitch, double _yaw) + + // Maximum spinbox value + property double spinMax: Number.MAX_VALUE + + // Expand/Collapse of this widget + property bool expand: true + + + /*** The following are private variables: ***/ + height: gzPoseContent.height + + // local variables to store spinbox values + property var xItem: {} + property var yItem: {} + property var zItem: {} + property var rollItem: {} + property var pitchItem: {} + property var yawItem: {} + + // Dummy component to use its functions. + IgnHelpers { + id: gzHelper + } + /*** Private variables end: ***/ + + /** + * Used to create a spin box + */ + Component { + id: writableNumber + IgnSpinBox { + id: writableSpin + value: numberValue + minimumValue: -spinMax + maximumValue: spinMax + decimals: gzHelper.getDecimals(writableSpin.width) + onEditingFinished: { + gzPoseRoot.gzPoseSet(xItem.value, yItem.value, zItem.value, rollItem.value, pitchItem.value, yawItem.value) + } + } + } + + /** + * Used to create a read-only number + */ + Component { + id: readOnlyNumber + Text { + id: numberText + anchors.fill: parent + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: { + var decimals = gzHelper.getDecimals(numberText.width) + return numberValue.toFixed(decimals) + } + } + } + + Rectangle { + id: gzPoseContent + width: parent.width + height: expand ? gzPoseGrid.height : 0 + clip: true + color: "transparent" + + Behavior on height { + NumberAnimation { + duration: 200; + easing.type: Easing.InOutQuad + } + } + + GridLayout { + id: gzPoseGrid + width: parent.width + columns: 4 + + Text { + text: 'X (m)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: xLoader + anchors.fill: parent + property double numberValue: xValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + xItem = xLoader.item + } + } + } + + Text { + text: 'Roll (rad)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: rollLoader + anchors.fill: parent + property double numberValue: rollValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + rollItem = rollLoader.item + } + } + } + + Text { + text: 'Y (m)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: yLoader + anchors.fill: parent + property double numberValue: yValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + yItem = yLoader.item + } + } + } + + Text { + text: 'Pitch (rad)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: pitchLoader + anchors.fill: parent + property double numberValue: pitchValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + pitchItem = pitchLoader.item + } + } + } + + Text { + text: 'Z (m)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: zLoader + anchors.fill: parent + property double numberValue: zValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + zItem = zLoader.item + } + } + } + + Text { + text: 'Yaw (rad)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: yawLoader + anchors.fill: parent + property double numberValue: yawValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + yawItem = yawLoader.item + } + } + } + } // end of GridLayout + } // end of Rectangle (gzPoseContent) +} // end of Rectangle (gzPoseRoot) diff --git a/include/ignition/gui/qml/GzVector3.qml b/include/ignition/gui/qml/GzVector3.qml new file mode 100644 index 000000000..84b4cfcbb --- /dev/null +++ b/include/ignition/gui/qml/GzVector3.qml @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +import QtQuick 2.9 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.3 + +/** + * Item displaying a 3D vector + * + * Users can set values to xValue, yValue, and zValue. + * If readOnly == False, + * users can read from signal parameters of GzVectorSet: _x, _y, and _z + * + * Usage example: + * GzVector3 { + * id: gzVector + * xName: "Red" + * yName: "Green" + * zName: "Blue" + * gzUnit: "" + * readOnly: false + * xValue: xValueFromCPP + * yValue: yValueFromCPP + * zValue: zValueFromCPP + * onGzVectorSet: { + * myFunc(_x, _y, _z, _roll, _pitch, _yaw) + * } + * } +**/ +Item { + id: gzVectorRoot + + // Read-only / write + property bool readOnly: true + + // User input value + property double xValue + property double yValue + property double zValue + + /** + * Used to read spinbox values + * @params: _x, _y, _z: corresponding spinBoxes values + * @note: When readOnly == false, user should read spinbox value from its + * parameters. + * When readOnly == true, this signal is unused. + */ + signal gzVectorSet(double _x, double _y, double _z) + + // Names for XYZ + property string xName: "X" + property string yName: "Y" + property string zName: "Z" + + // Units, defaults to meters. + // Set to "" to omit units & the parentheses. + property string gzUnit: "m" + + // Expand/Collapse of this widget + property bool expand: true + + // Maximum spinbox value + property double spinMax: Number.MAX_VALUE + + /*** The following are private variables: ***/ + height: gzVectorContent.height + + // local variables to store spinbox values + property var xItem: {} + property var yItem: {} + property var zItem: {} + + // Dummy component to use its functions. + IgnHelpers { + id: gzHelper + } + /*** Private variables end: ***/ + + /** + * Used to create a spin box + */ + Component { + id: writableNumber + IgnSpinBox { + id: writableSpin + value: numberValue + minimumValue: -spinMax + maximumValue: spinMax + decimals: gzHelper.getDecimals(writableSpin.width) + onEditingFinished: { + gzVectorRoot.gzVectorSet(xItem.value, yItem.value, zItem.value) + } + } + } + + /** + * Used to create a read-only number + */ + Component { + id: readOnlyNumber + Text { + id: numberText + anchors.fill: parent + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: { + var decimals = gzHelper.getDecimals(numberText.width) + return numberValue.toFixed(decimals) + } + } + } + + Rectangle { + id: gzVectorContent + width: parent.width + height: expand ? gzVectorGrid.height : 0 + clip: true + color: "transparent" + + Behavior on height { + NumberAnimation { + duration: 200; + easing.type: Easing.InOutQuad + } + } + + GridLayout { + id: gzVectorGrid + width: parent.width + columns: 2 + + Text { + text: gzUnit == "" ? xName : xName + ' (' + gzUnit + ')' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: xLoader + anchors.fill: parent + property double numberValue: xValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + xItem = xLoader.item + } + } + } + + Text { + text: gzUnit == "" ? yName : yName + ' (' + gzUnit + ')' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: yLoader + anchors.fill: parent + property double numberValue: yValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + yItem = yLoader.item + } + } + } + + Text { + text: gzUnit == "" ? zName : zName + ' (' + gzUnit + ')' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: zLoader + anchors.fill: parent + property double numberValue: zValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + zItem = zLoader.item + } + } + } + } + } +} diff --git a/include/ignition/gui/qml/IgnHelpers.qml b/include/ignition/gui/qml/IgnHelpers.qml index 57c0619b3..9938872f8 100644 --- a/include/ignition/gui/qml/IgnHelpers.qml +++ b/include/ignition/gui/qml/IgnHelpers.qml @@ -39,4 +39,21 @@ Item { return result; } + + /** + * Helper function to get number of decimal digits based on a width value. + * @param _width Pixel width. + * @returns Number of decimals that fit with the provided width. + */ + function getDecimals(_width) { + // Use full decimals if the width is <= 0, which allows the value + // to appear correctly. + if (_width <= 0 || _width > 110) + return 6 + + if (_width <= 80) + return 2 + + return 4 + } } diff --git a/include/ignition/gui/resources.qrc b/include/ignition/gui/resources.qrc index 71026a326..067e5d935 100644 --- a/include/ignition/gui/resources.qrc +++ b/include/ignition/gui/resources.qrc @@ -3,6 +3,9 @@ qtquickcontrols2.conf qml/Chart.qml + qml/GzColor.qml + qml/GzPose.qml + qml/GzVector3.qml qml/IgnCard.qml qml/IgnCardSettings.qml qml/IgnHelpers.qml @@ -28,6 +31,9 @@ qml/qmldir + qml/GzColor.qml + qml/GzPose.qml + qml/GzVector3.qml qml/IgnSnackBar.qml qml/IgnSpinBox.qml diff --git a/src/Application.cc b/src/Application.cc index ba7870b4c..978fff126 100644 --- a/src/Application.cc +++ b/src/Application.cc @@ -543,6 +543,12 @@ std::shared_ptr Application::PluginByName( return nullptr; } +///////////////////////////////////////////////// +bool Application::CreateMainWindow() +{ + return this->InitializeMainWindow(); +} + ///////////////////////////////////////////////// bool Application::InitializeMainWindow() { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5f69d3ddf..9751047db 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ set (sources set (gtest_sources Application_TEST Conversions_TEST + Dialog_TEST DragDropModel_TEST Helpers_TEST GuiEvents_TEST @@ -49,7 +50,35 @@ ign_build_tests(TYPE UNIT LIB_DEPS ${IGNITION-MATH_LIBRARIES} TINYXML2::TINYXML2 + TEST_LIST + gtest_targets ) +foreach(test ${gtest_targets}) + target_compile_definitions(${test} PRIVATE + "PROJECT_SOURCE_DIR=\"${PROJECT_SOURCE_DIR}\"") +endforeach() + +if(TARGET UNIT_ign_TEST) + # Running `ign gazebo` on macOS has problems when run with /usr/bin/ruby + # due to System Integrity Protection (SIP). Try to find ruby from + # homebrew as a workaround. + if (APPLE) + find_program(BREW_RUBY ruby HINTS /usr/local/opt/ruby/bin) + endif() + + target_compile_definitions(UNIT_ign_TEST PRIVATE + "BREW_RUBY=\"${BREW_RUBY} \"") + + target_compile_definitions(UNIT_ign_TEST PRIVATE + "IGN_PATH=\"${HAVE_IGN_TOOLS}\"") + + set(_env_vars) + list(APPEND _env_vars "IGN_CONFIG_PATH=${CMAKE_BINARY_DIR}/test/conf") + + set_tests_properties(UNIT_ign_TEST PROPERTIES + ENVIRONMENT "${_env_vars}") +endif() + add_subdirectory(cmd) add_subdirectory(plugins) diff --git a/src/Dialog.cc b/src/Dialog.cc index 7463a7394..78c45157a 100644 --- a/src/Dialog.cc +++ b/src/Dialog.cc @@ -15,6 +15,8 @@ * */ +#include + #include #include "ignition/gui/Application.hh" #include "ignition/gui/Dialog.hh" @@ -25,6 +27,9 @@ namespace ignition { class DialogPrivate { + /// \brief default dialog config + public: std::string config{""}; + /// \brief Pointer to quick window public: QQuickWindow *quickWindow{nullptr}; }; @@ -75,3 +80,141 @@ QQuickItem *Dialog::RootItem() const return dialogItem; } +///////////////////////////////////////////////// +bool Dialog::UpdateConfigAttribute(const std::string &_path, + const std::string &_attribute, const bool _value) const +{ + if (_path.empty()) + { + ignerr << "Missing config file" << std::endl; + return false; + } + + // Use tinyxml to read config + tinyxml2::XMLDocument doc; + auto success = !doc.LoadFile(_path.c_str()); + if (!success) + { + ignerr << "Failed to load file [" << _path << "]: XMLError" + << std::endl; + return false; + } + + // Update attribute value for the correct dialog + for (auto dialogElem = doc.FirstChildElement("dialog"); + dialogElem != nullptr; + dialogElem = dialogElem->NextSiblingElement("dialog")) + { + if(dialogElem->Attribute("name") == this->objectName().toStdString()) + { + dialogElem->SetAttribute(_attribute.c_str(), _value); + } + } + + // Write config file + tinyxml2::XMLPrinter printer; + doc.Print(&printer); + + std::string config = printer.CStr(); + std::ofstream out(_path.c_str(), std::ios::out); + if (!out) + { + ignerr << "Unable to open file: " << _path + << ".\nCheck file permissions.\n"; + } + else + out << config; + + return true; +} + +///////////////////////////////////////////////// +void Dialog::SetDefaultConfig(const std::string &_config) +{ + this->dataPtr->config = _config; +} + +///////////////////////////////////////////////// +std::string Dialog::ReadConfigAttribute(const std::string &_path, + const std::string &_attribute) const +{ + tinyxml2::XMLDocument doc; + std::string value {""}; + std::string config = "\n\n"; + tinyxml2::XMLPrinter defaultPrinter; + bool configExists{true}; + std::string dialogName = this->objectName().toStdString(); + + auto Value = [&_attribute, &doc, &dialogName]() + { + // Process each dialog + // If multiple attributes share the same name, return the first one + for (auto dialogElem = doc.FirstChildElement("dialog"); + dialogElem != nullptr; + dialogElem = dialogElem->NextSiblingElement("dialog")) + { + if (dialogElem->Attribute("name") == dialogName) + { + if (dialogElem->Attribute(_attribute.c_str())) + return dialogElem->Attribute(_attribute.c_str()); + } + } + return ""; + }; + + // Check if the passed in config file exists. + // (If the default config path doesn't exist yet, it's expected behavior. + // It will be created the first time now.) + if (!common::exists(_path)) + { + configExists = false; + doc.Parse(this->dataPtr->config.c_str()); + value = Value(); + } + else + { + auto success = !doc.LoadFile(_path.c_str()); + if (!success) + { + ignerr << "Failed to load file [" << _path << "]: XMLError" + << std::endl; + return ""; + } + value = Value(); + + // config exists but attribute not there read from default config + if (value.empty()) + { + tinyxml2::XMLDocument missingDoc; + missingDoc.Parse(this->dataPtr->config.c_str()); + value = Value(); + missingDoc.Print(&defaultPrinter); + } + } + + // Write config file + tinyxml2::XMLPrinter printer; + doc.Print(&printer); + + // Don't write the xml version decleration if file exists + if (configExists) + { + config = ""; + } + + igndbg << "Setting dialog " << this->objectName().toStdString() + << " default config." << std::endl; + config += printer.CStr(); + config += defaultPrinter.CStr(); + std::ofstream out(_path.c_str(), std::ios::out); + if (!out) + { + ignerr << "Unable to open file: " << _path + << ".\nCheck file permissions.\n"; + return ""; + } + else + out << config; + + return value; +} diff --git a/src/Dialog_TEST.cc b/src/Dialog_TEST.cc new file mode 100644 index 000000000..c52b46a12 --- /dev/null +++ b/src/Dialog_TEST.cc @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include + +#include +#include + +#include "test_config.h" // NOLINT(build/include) +#include "ignition/gui/Application.hh" +#include "ignition/gui/Dialog.hh" + +std::string kTestConfigFile = "/tmp/ign-gui-test.config"; // NOLINT(*) +int g_argc = 1; +char* g_argv[] = +{ + reinterpret_cast(const_cast("./Dialog_TEST")), +}; + +using namespace ignition; +using namespace gui; +using namespace std::chrono_literals; + +///////////////////////////////////////////////// +TEST(DialogTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(UpdateDialogConfig)) +{ + ignition::common::Console::SetVerbosity(4); + Application app(g_argc, g_argv, ignition::gui::WindowType::kDialog); + + // Change default config path + App()->SetDefaultConfigPath(kTestConfigFile); + + auto dialog = new Dialog; + ASSERT_NE(nullptr, dialog); + + // Read attribute value when the default the config is not set + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + EXPECT_EQ(allow, ""); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + // Read a non existing attribute + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + dialog->setObjectName("quick_menu"); + dialog->SetDefaultConfig(std::string( + "")); + std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + EXPECT_EQ(allow, ""); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + // Read an existing attribute + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + std::string show = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "show"); + EXPECT_EQ(show, "true"); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + // Update a non existing attribute + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + + // Call a read to create config file + std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + + // Empty string for a non existing attribute + EXPECT_EQ(allow, ""); + dialog->UpdateConfigAttribute(app.DefaultConfigPath(), "allow", true); + allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + EXPECT_EQ(allow, "true"); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + // Update a existing attribute + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + + // Call a read to create config file + std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + dialog->UpdateConfigAttribute(app.DefaultConfigPath(), "allow", false); + allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + EXPECT_EQ(allow, "false"); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + delete dialog; +} diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt index 45245a12e..25e6c9d58 100644 --- a/src/cmd/CMakeLists.txt +++ b/src/cmd/CMakeLists.txt @@ -1,4 +1,4 @@ -# Generate a the ruby script. +# Generate the ruby script. # Note that the major version of the library is included in the name. if (APPLE) set(IGN_LIBRARY_NAME lib${PROJECT_NAME_LOWER}.dylib) @@ -11,3 +11,13 @@ configure_file( # Install the ruby command line library in an unversioned location. install(FILES ${CMAKE_CURRENT_BINARY_DIR}/cmdgui${PROJECT_VERSION_MAJOR}.rb DESTINATION lib/ruby/ignition) + +# Tack version onto and install the bash completion script +configure_file( + "gui.bash_completion.sh" + "${CMAKE_CURRENT_BINARY_DIR}/gui${PROJECT_VERSION_MAJOR}.bash_completion.sh" @ONLY) +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/gui${PROJECT_VERSION_MAJOR}.bash_completion.sh + DESTINATION + ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/gz/gz${IGN_TOOLS_VER}.completion.d) diff --git a/src/cmd/gui.bash_completion.sh b/src/cmd/gui.bash_completion.sh new file mode 100644 index 000000000..69b68b24d --- /dev/null +++ b/src/cmd/gui.bash_completion.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2022 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# bash tab-completion + +# This is a per-library function definition, used in conjunction with the +# top-level entry point in ign-tools. + +GZ_GUI_COMPLETION_LIST=" + -l --list + -s --standalone + -c --config + -v --verbose + -h --help + --force-version + --versions +" + +function _gz_gui +{ + if [[ ${COMP_WORDS[COMP_CWORD]} == -* ]]; then + # Specify options (-*) word list for this subcommand + COMPREPLY=($(compgen -W "$GZ_GUI_COMPLETION_LIST" \ + -- "${COMP_WORDS[COMP_CWORD]}" )) + return + else + # Just use bash default auto-complete, because we never have two + # subcommands in the same line. If that is ever needed, change here to + # detect subsequent subcommands + COMPREPLY=($(compgen -o default -- "${COMP_WORDS[COMP_CWORD]}")) + return + fi +} + +function _gz_gui_flags +{ + for word in $GZ_GUI_COMPLETION_LIST; do + echo "$word" + done +} diff --git a/src/ign_TEST.cc b/src/ign_TEST.cc index cf3ba5ea7..f1e03ee82 100644 --- a/src/ign_TEST.cc +++ b/src/ign_TEST.cc @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -32,6 +33,9 @@ # define pclose _pclose #endif +static const std::string kIgnCommand( + std::string(BREW_RUBY) + std::string(IGN_PATH)); + ///////////////////////////////////////////////// std::string custom_exec_str(std::string _cmd) { @@ -87,7 +91,8 @@ TEST_F(CmdLine, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(list)) // Clear home if it exists common::removeAll(this->kFakeHome); - EXPECT_FALSE(common::exists(this->kFakeHome)); + // This line is flaky, see https://github.com/gazebosim/gz-gui/issues/415 + // EXPECT_FALSE(common::exists(this->kFakeHome)); std::string output = custom_exec_str("ign gui -l"); EXPECT_NE(output.find("TopicEcho"), std::string::npos) << output; @@ -96,3 +101,34 @@ TEST_F(CmdLine, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(list)) EXPECT_TRUE(common::exists(common::joinPaths(this->kFakeHome, ".ignition", "gui"))); } + +////////////////////////////////////////////////// +/// \brief Check --help message and bash completion script for consistent flags +// See https://github.com/gazebo-tooling/release-tools/issues/398 +TEST(ignTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(GuiHelpVsCompletionFlags)) +{ + // Flags in help message + std::string helpOutput = custom_exec_str(kIgnCommand + " gui --help"); + + // Call the output function in the bash completion script + std::string scriptPath = common::joinPaths(std::string(PROJECT_SOURCE_DIR), + "src", "cmd", "gui.bash_completion.sh"); + + // Equivalent to: + // sh -c "bash -c \". /path/to/gui.bash_completion.sh; _gz_gui_flags\"" + std::string cmd = "bash -c \". " + scriptPath + "; _gz_gui_flags\""; + std::string scriptOutput = custom_exec_str(cmd); + + // Tokenize script output + std::istringstream iss(scriptOutput); + std::vector flags((std::istream_iterator(iss)), + std::istream_iterator()); + + EXPECT_GT(flags.size(), 0u); + + // Match each flag in script output with help message + for (const auto &flag : flags) + { + EXPECT_NE(std::string::npos, helpOutput.find(flag)) << helpOutput; + } +} diff --git a/test/integration/minimal_scene.cc b/test/integration/minimal_scene.cc index a482704ee..dcb6b59fb 100644 --- a/test/integration/minimal_scene.cc +++ b/test/integration/minimal_scene.cc @@ -155,7 +155,7 @@ TEST(MinimalSceneTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) EXPECT_DOUBLE_EQ(0.1, camera->NearClipPlane()); EXPECT_DOUBLE_EQ(5000.0, camera->FarClipPlane()); - EXPECT_DOUBLE_EQ(60, camera->HFOV().Degree()); + EXPECT_NEAR(60, camera->HFOV().Degree(), 1e-4); // Cleanup auto plugins = win->findChildren();