From 88de9142b1111d4d14d40cd7c08dc11561908e58 Mon Sep 17 00:00:00 2001 From: Joris Goosen Date: Tue, 17 Sep 2024 15:24:53 +0200 Subject: [PATCH 01/62] make a fair attempt at making the components a bit more informative add ribbonbutton and fix comboboxes --- .../JASP/Widgets/FileMenu/ListItem.qml | 30 +- Desktop/components/JASP/Widgets/MainPage.qml | 5 + .../JASP/Widgets/Ribbon/MenuArrowButton.qml | 6 +- .../JASP/Widgets/Ribbon/RibbonButton.qml | 5 + Desktop/components/JASP/Widgets/SpinBox.qml | 13 + .../JASP/Controls/AddColumnField.qml | 2 + .../components/JASP/Controls/AssignButton.qml | 3 + .../components/JASP/Controls/Button.qml | 6 +- .../components/JASP/Controls/CheckBox.qml | 6 + .../components/JASP/Controls/ComboBox.qml | 313 ++++++++++++++++++ .../JASP/Controls/ComponentsList.qml | 5 + .../JASP/Controls/ComputedColumnField.qml | 2 + .../components/JASP/Controls/DoubleField.qml | 2 + .../components/JASP/Controls/FormulaField.qml | 2 + .../components/JASP/Controls/Group.qml | 6 + .../components/JASP/Controls/HelpButton.qml | 7 +- .../components/JASP/Controls/IntegerField.qml | 2 + .../components/JASP/Controls/JAGSTextArea.qml | 3 + .../components/JASP/Controls/Label.qml | 5 + .../components/JASP/Controls/MenuButton.qml | 6 + .../components/JASP/Controls/PercentField.qml | 2 + .../components/JASP/Controls/RadioButton.qml | 6 + .../JASP/Controls/RectangularButton.qml | 6 + .../components/JASP/Controls/Section.qml | 6 + .../components/JASP/Controls/Text.qml | 4 + .../components/JASP/Controls/TextArea.qml | 6 + .../components/JASP/Controls/TextField.qml | 5 + .../JASP/Controls/VariablesList.qml | 4 + 28 files changed, 457 insertions(+), 11 deletions(-) create mode 100644 QMLComponents/components/JASP/Controls/ComboBox.qml diff --git a/Desktop/components/JASP/Widgets/FileMenu/ListItem.qml b/Desktop/components/JASP/Widgets/FileMenu/ListItem.qml index bcf6764dde..b0d88b4fb6 100644 --- a/Desktop/components/JASP/Widgets/FileMenu/ListItem.qml +++ b/Desktop/components/JASP/Widgets/FileMenu/ListItem.qml @@ -7,10 +7,17 @@ FocusScope width: 300 //Should be set from ListView height: rectTitle.height + rectDescription.height + 3 + + readonly property int folderModelType: 3 property alias color: rectTitleBackground.color property alias border: rectTitleBackground.border property var datafile: rectTitleAndDataFile.hasDatafile ? rectTitleAndDataFile : null + + Accessible.role: Accessible.Button + Accessible.name: (model.type == folderModelType ? qsTr("Folder %1") : qsTr("File %1")).arg(model.name) + Accessible.description: Accessible.name + Accessible.onPressAction: openStuff(model) Rectangle { @@ -33,17 +40,18 @@ FocusScope onAllHoveredChanged: if(allHovered) { ListView.currentIndex = index; forceActiveFocus(); } - Keys.onEnterPressed: openStuff(model); - Keys.onReturnPressed: (event)=> openStuff(model); - Keys.onSpacePressed: openStuff(model); - Keys.onRightPressed: if(model.type === 3) openStuff(model); + Keys.onEnterPressed: openStuff(model); + Keys.onReturnPressed: openStuff(model); + Keys.onSpacePressed: openStuff(model); + Keys.onRightPressed: if(model.type === folderModelType) openStuff(model); + function openStuff(model) { if (!rectTitleAndDescripton.cppModel.mayOpen()) return; - if (model.type === 3) rectTitleAndDescripton.cppModel.changePath(model.name, model.path); //Folder type - else rectTitleAndDescripton.cppModel.openFile(model.path) + if (model.type === folderModelType) rectTitleAndDescripton.cppModel.changePath(model.name, model.path); //Folder type + else rectTitleAndDescripton.cppModel.openFile(model.path) } Rectangle @@ -66,11 +74,11 @@ FocusScope { id : firstFileOrFolderImage - height: model.type==3 ? 0.75 * rectTitle.height : 0.95 * rectTitle.height //Tune folder image to file image (wtih topmargin in svg) + height: model.type==folderModelType ? 0.75 * rectTitle.height : 0.95 * rectTitle.height //Tune folder image to file image (wtih topmargin in svg) width: height anchors.left: rectTitle.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: model.type==3 ? 5 * preferencesModel.uiScale : 0 + anchors.leftMargin: model.type==folderModelType ? 5 * preferencesModel.uiScale : 0 fillMode: Image.PreserveAspectFit source: model.iconsource @@ -123,6 +131,12 @@ FocusScope Keys.onSpacePressed: (event) => { openDataFile(event); } onActiveFocusChanged: if (!activeFocus) focus = false; + + Accessible.role: Accessible.Button + Accessible.name: qsTr("Datafile %1").arg(model.name) + Accessible.description: qsTr("Datafile %1").arg(model.name) + Accessible.onPressAction: if(hasDataFile) openDataFile(); + function openDataFile(event) { diff --git a/Desktop/components/JASP/Widgets/MainPage.qml b/Desktop/components/JASP/Widgets/MainPage.qml index cfe0ca1705..3db6b13db8 100644 --- a/Desktop/components/JASP/Widgets/MainPage.qml +++ b/Desktop/components/JASP/Widgets/MainPage.qml @@ -315,6 +315,11 @@ Item clip: true anchors.fill: parent anchors.leftMargin: 1 + + Accessible.role: Accessible.WebDocument + Accessible.name: qsTr("Results") + Accessible.description: qsTr("Results") + url: resultsJsInterface.resultsPageUrl diff --git a/Desktop/components/JASP/Widgets/Ribbon/MenuArrowButton.qml b/Desktop/components/JASP/Widgets/Ribbon/MenuArrowButton.qml index ce29c579ec..80bf584fe7 100644 --- a/Desktop/components/JASP/Widgets/Ribbon/MenuArrowButton.qml +++ b/Desktop/components/JASP/Widgets/Ribbon/MenuArrowButton.qml @@ -49,6 +49,10 @@ Rectangle readonly property bool showArrow: buttonType == MenuArrowButton.ButtonType.LeftArrow || buttonType == MenuArrowButton.ButtonType.RightArrow readonly property bool isTools: buttonType == MenuArrowButton.ButtonType.Tools + Accessible.role: Accessible.Button + Accessible.name: hamburger ? qsTr("Main menu") : !isTools ? qsTr("Modules menu") : qsTr("Workspace tools") + Accessible.description: toolTip != "" ? toolTip : Accessible.name + Accessible.onPressAction: clicked() ToolTip.text: toolTip ToolTip.visible: toolTip !== "" && mice.containsMouse @@ -59,7 +63,7 @@ Rectangle { id: hamburgerArrow anchors.centerIn: parent - width: hamburgerArrow.barWidth//parent.width - (2 * jaspTheme.ribbonButtonPadding) + width: hamburgerArrow.barWidth height: baseHeight - 20 scale: baseScale * (mice.containsMouse && !ribbonButton.pressed ? jaspTheme.ribbonScaleHovered : 1) diff --git a/Desktop/components/JASP/Widgets/Ribbon/RibbonButton.qml b/Desktop/components/JASP/Widgets/Ribbon/RibbonButton.qml index e10ecb300e..37ac50d46e 100644 --- a/Desktop/components/JASP/Widgets/Ribbon/RibbonButton.qml +++ b/Desktop/components/JASP/Widgets/Ribbon/RibbonButton.qml @@ -42,6 +42,11 @@ Item property var menu : [] property bool myMenuOpen : false property bool showPressed : ribbonButton.activeFocus || myMenuOpen + + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.description: toolTip + onMyMenuOpenChanged: if(!myMenuOpen) myMenuOpen = false; //Break the binding diff --git a/Desktop/components/JASP/Widgets/SpinBox.qml b/Desktop/components/JASP/Widgets/SpinBox.qml index 65c4b4f38c..d2aa50bd34 100644 --- a/Desktop/components/JASP/Widgets/SpinBox.qml +++ b/Desktop/components/JASP/Widgets/SpinBox.qml @@ -50,6 +50,19 @@ Item Keys.onReturnPressed: (event)=> { valueField.focus = !valueField.focus; } activeFocusOnTab: true + + Accessible.role: Accessible.SpinBox + Accessible.name: text + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Spinbox %1").arg(title) : info + Accessible.value: value + Accessible.minimumValue: min + Accessible.maximumValue: max + Accessible.stepSize: stepSize + Accessible.onIncreaseAction: plus.clicked() + Accessible.onDecreaseAction: minus.clicked() + + + signal editingFinished() diff --git a/QMLComponents/components/JASP/Controls/AddColumnField.qml b/QMLComponents/components/JASP/Controls/AddColumnField.qml index b881b9a5e7..e246333209 100644 --- a/QMLComponents/components/JASP/Controls/AddColumnField.qml +++ b/QMLComponents/components/JASP/Controls/AddColumnField.qml @@ -61,6 +61,8 @@ import JASP TextField { inputType: "addColumn" + + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Add a column textfield %1").arg(title) : info property int columnType: columnTypeScale //Or columnTypeNominal, or columnTypeOrdinal } diff --git a/QMLComponents/components/JASP/Controls/AssignButton.qml b/QMLComponents/components/JASP/Controls/AssignButton.qml index 09d1da2a67..6abe43375c 100644 --- a/QMLComponents/components/JASP/Controls/AssignButton.qml +++ b/QMLComponents/components/JASP/Controls/AssignButton.qml @@ -50,6 +50,9 @@ Button readonly property string iconToLeft: jaspTheme.iconPath + "arrow-left.png" readonly property string iconToRight: jaspTheme.iconPath + "arrow-right.png" + Accessible.name: leftToRight ? qsTr("Assignbutton for variable list %1").arg(!leftSource ? "?" : leftSource.title) : qsTr("Unassignbutton for variablelist %1").arg(!rightSource ? "?" : rightSource.title) + Accessible.description: info === "" ? Accessible.name : info + text: "" visible: sourceM && targetM && sourceM.visible && targetM.visible diff --git a/QMLComponents/components/JASP/Controls/Button.qml b/QMLComponents/components/JASP/Controls/Button.qml index 7490f4a218..662acafa67 100644 --- a/QMLComponents/components/JASP/Controls/Button.qml +++ b/QMLComponents/components/JASP/Controls/Button.qml @@ -66,8 +66,12 @@ JASPControl innerControl: control focusIndicator: focusIndicator title: text - + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Button %1").arg(title) : info + Accessible.onPressAction: clicked() + readonly property alias control: control property alias text: control.text property alias label: control.text diff --git a/QMLComponents/components/JASP/Controls/CheckBox.qml b/QMLComponents/components/JASP/Controls/CheckBox.qml index d7eb3da8d8..e0169e20ea 100644 --- a/QMLComponents/components/JASP/Controls/CheckBox.qml +++ b/QMLComponents/components/JASP/Controls/CheckBox.qml @@ -105,6 +105,12 @@ CheckBoxBase function click() { control.toggle(); } function toggle() { control.toggle(); } + + Accessible.role: Accessible.CheckBox + Accessible.name: control.text + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("A checkbox %1").arg(title) : info + Accessible.onPressAction: click() + Accessible.onToggleAction: toggle() QtC.CheckBox { diff --git a/QMLComponents/components/JASP/Controls/ComboBox.qml b/QMLComponents/components/JASP/Controls/ComboBox.qml new file mode 100644 index 0000000000..e17c648cef --- /dev/null +++ b/QMLComponents/components/JASP/Controls/ComboBox.qml @@ -0,0 +1,313 @@ +import QtQuick +import QtQuick.Controls as QTC +import QtQuick.Layouts +import JASP + + +ComboBoxBase +{ + id: comboBox + implicitHeight: control.height + ((controlLabel.visible && setLabelAbove) ? rectangleLabel.height : 0) + implicitWidth: setLabelAbove ? Math.max(control.width, rectangleLabel.width) : (rectangleLabel.width + jaspTheme.labelSpacing + control.width) + background: useExternalBorder ? externalControlBackground : control.background + innerControl: control + title: label + + property alias control: control + property alias controlLabel: controlLabel + property alias label: controlLabel.text + property alias currentLabel: comboBox.currentText + property alias value: comboBox.currentValue + property alias indexDefaultValue: comboBox.currentIndex + property alias fieldWidth: control.width + property bool showVariableTypeIcon: containsVariables + property var enabledOptions: [] + property bool setLabelAbove: false + property int controlMinWidth: 0 + property bool useExternalBorder: true + property bool showBorder: true + property bool showEmptyValueAsNormal: false + property bool addLineAfterEmptyValue: false + property double controlXOffset: 0 + + onControlMinWidthChanged: _resetWidth(textMetrics.width) + + + + + function resetWidth(values) + { + var maxWidth = 0 + var maxValue = "" + textMetrics.initialized = false; + + if (addEmptyValue) + values.push(placeholderText) + + for (var i = 0; i < values.length; i++) + { + textMetrics.text = values[i] + if (textMetrics.width > maxWidth) + { + maxWidth = textMetrics.width + maxValue = values[i] + } + } + + textMetrics.text = maxValue; + textMetrics.initialized = true; + _resetWidth(maxWidth) + } + + function _resetWidth(maxTextWidth) + { + control.maxTextWidth = maxTextWidth + // The real field width is composed by the type icon (if displayed) + 2-padding + max width + 5-padding + dropdownIcon width + 2-padding + var newFieldWidth = (comboBox.showVariableTypeIcon ? contentIcon.x + contentIcon.width : 0) + maxTextWidth + dropdownIcon.width + 9 * preferencesModel.uiScale + if (newFieldWidth < controlMinWidth) + newFieldWidth = controlMinWidth + + control.realFieldWidth = newFieldWidth + if (!fixedWidth) control.width = newFieldWidth; + } + + Component.onCompleted: control.activated.connect(activated); + + Rectangle + { + id: rectangleLabel + width: controlLabel.width + height: control.height + color: debug ? jaspTheme.debugBackgroundColor : "transparent" + visible: controlLabel.text && comboBox.visible ? true : false + Label + { + id: controlLabel + font: jaspTheme.font + anchors.verticalCenter: parent.verticalCenter + color: enabled ? jaspTheme.textEnabled : jaspTheme.textDisabled + width: implicitWidth + } + } + + QTC.ComboBox + { + id: control + model: comboBox.model + anchors + { + top: rectangleLabel.visible && comboBox.setLabelAbove ? rectangleLabel.bottom: comboBox.top + left: !rectangleLabel.visible || comboBox.setLabelAbove ? comboBox.left : rectangleLabel.right + leftMargin: controlXOffset + (!rectangleLabel.visible || comboBox.setLabelAbove ? 0 : jaspTheme.labelSpacing) + } + + focus: true + padding: 2 * preferencesModel.uiScale + width: 0 + height: jaspTheme.comboBoxHeight + font: jaspTheme.font + property bool isEmptyValue: comboBox.addEmptyValue && comboBox.currentIndex === 0 + property bool showEmptyValueStyle: !comboBox.showEmptyValueAsNormal && isEmptyValue + property double realFieldWidth: width + property double maxTextWidth: 0 + + Accessible.role: Accessible.ButtonDropDown + Accessible.name: label + Accessible.description: comboBox.info === undefined || comboBox.info == "" ? comboBox.toolTip !== undefined && toolTip != "" ? comboBox.toolTip : qsTr("A dropdown %1").arg(comboBox.title) : info + Accessible.onPressAction: click() + Accessible.onToggleAction: toggle() + + TextMetrics + { + id: textMetrics + font: control.font + + property bool initialized: false + + onWidthChanged: + { + if (initialized) + _resetWidth(width) + } + } + + contentItem: Rectangle + { + color: jaspTheme.controlBackgroundColor + Image + { + id: contentIcon + height: 15 * preferencesModel.uiScale + width: 15 * preferencesModel.uiScale // Even if not visible, the width should stay the same: if showVariableTypeIcon is true, a value may have no icon, but an empty icon place should still be displayed + x: 2 * preferencesModel.uiScale + anchors.verticalCenter: parent.verticalCenter + source: !visible ? "" : ((comboBox.currentColumnTypeIcon && comboBox.isBound) ? comboBox.currentColumnTypeIcon : (comboBox.values && comboBox.currentIndex >= 0 && comboBox.currentIndex < comboBox.values.length ? comboBox.values[comboBox.currentIndex].columnTypeIcon : "")) + visible: comboBox.showVariableTypeIcon && !control.isEmptyValue && (comboBox.currentColumnType || !comboBox.isBound) + } + + Text + { + anchors.left: contentIcon.visible ? contentIcon.right : parent.left + anchors.leftMargin: 2 * preferencesModel.uiScale + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: control.showEmptyValueStyle ? parent.horizontalCenter : undefined + text: comboBox.currentText + font: control.font + color: (!enabled || control.showEmptyValueStyle) ? jaspTheme.grayDarker : jaspTheme.black + width: (fixedWidth ? widthWhenContralHasFixedWidth : control.maxTextWidth) + 5 * preferencesModel.uiScale + elide: Text.ElideRight + + property double widthWhenContralHasFixedWidth: control.width - (x + dropdownIcon.width + 4 * preferencesModel.uiScale) // 4 = leftMargin + 2 padding right of dropdownIcon) + + } + } + + indicator: Image + { + id: dropdownIcon + x: control.width - width - 2 * preferencesModel.uiScale + y: control.topPadding + (control.availableHeight - height) / 2 + width: 12 * preferencesModel.uiScale + height: 12 * preferencesModel.uiScale + source: jaspTheme.iconPath + "/toolbutton-menu-indicator.svg" + + } + + background: Rectangle + { + id: comboBoxBackground + border.width: comboBox.showBorder && !control.activeFocus ? 1 : 0 + border.color: comboBox.showBorder ? jaspTheme.borderColor : "transparent" + radius: 2 + color: jaspTheme.controlBackgroundColor + } + + Rectangle + { + id: externalControlBackground + height: parent.height + jaspTheme.jaspControlHighlightWidth + width: parent.width + jaspTheme.jaspControlHighlightWidth + color: "transparent" + border.width: 1 + border.color: "transparent" + anchors.centerIn: parent + opacity: debug ? .3 : 1 + visible: comboBox.useExternalBorder + radius: jaspTheme.jaspControlHighlightWidth + } + + popup: QTC.Popup + { + id: popupRoot + y: control.height + width: Math.max(control.realFieldWidth, fieldWidth) + scrollBar.width + + property real maxHeight: typeof mainWindowRoot !== 'undefined' ? mainWindowRoot.height // Case Dropdowns used in Desktop + : (typeof rcmdRoot !== 'undefined' ? rcmdRoot.height // Case Dropdown used in R Command + : (typeof backgroundForms !== 'undefined' ? backgroundForms.height // Case Dropdowns used in Analysis forms + : Infinity)) + height: Math.min(popupView.contentHeight + (padding*2), maxHeight) + padding: 1 + + enter: Transition { NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 } enabled: preferencesModel.animationsOn } + + JASPScrollBar + { + id: scrollBar + flickable: popupView + manualAnchor: true + vertical: true + z: 1337 + + anchors + { + top: parent.top + right: parent.right + bottom: parent.bottom + } + } + + + contentItem: ListView + { + id: popupView + width: popupRoot.width - scrollBar.width + height: popupRoot.height + model: control.popup.visible ? control.delegateModel : null + currentIndex: control.highlightedIndex + clip: true + + Rectangle + { + anchors.centerIn: parent + width: parent.width + 4 + height: parent.height + 4 + border.color: jaspTheme.focusBorderColor + border.width: 2 + color: "transparent" + } + } + + background: Rectangle + { + border.color: jaspTheme.borderColor + border.width: 1 + color: jaspTheme.white + } + } + + delegate: QTC.ItemDelegate + { + height: jaspTheme.comboBoxHeight + width: popupView.width + enabled: comboBox.enabledOptions.length == 0 || comboBox.enabledOptions.length <= index || comboBox.enabledOptions[index] + + contentItem: Rectangle + { + id: itemRectangle + anchors.fill: parent + color: comboBox.currentIndex === index ? jaspTheme.itemSelectedColor : (control.highlightedIndex === index ? jaspTheme.itemHoverColor : jaspTheme.controlBackgroundColor) + + property bool isEmptyValue: comboBox.addEmptyValue && index === 0 + property bool showEmptyValueStyle: !comboBox.showEmptyValueAsNormal && isEmptyValue + property bool showLine: comboBox.addLineAfterEmptyValue && index === 0 + + + Image + { + id: delegateIcon + x: 1 * preferencesModel.uiScale + height: 15 * preferencesModel.uiScale + width: 15 * preferencesModel.uiScale + source: visible ? (comboBox.isBound ? model.columnTypeIcon : comboBox.values[index].columnTypeIcon) : "" + visible: comboBox.showVariableTypeIcon && !itemRectangle.isEmptyValue + + anchors.verticalCenter: parent.verticalCenter + } + + Text + { + x: (delegateIcon.visible ? 20 : 4) * preferencesModel.uiScale + text: itemRectangle.isEmptyValue ? comboBox.placeholderText : (model && model.name ? model.name : "") + font: jaspTheme.font + color: itemRectangle.showEmptyValueStyle || !enabled ? jaspTheme.grayDarker : (comboBox.currentIndex === index ? jaspTheme.white : jaspTheme.black) + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: itemRectangle.showEmptyValueStyle ? parent.horizontalCenter : undefined + } + + Rectangle + { + anchors + { + left: parent.left + right: parent.right + bottom: parent.bottom + } + visible: itemRectangle.showLine + height: 1 + color: jaspTheme.focusBorderColor + } + } + } + } +} diff --git a/QMLComponents/components/JASP/Controls/ComponentsList.qml b/QMLComponents/components/JASP/Controls/ComponentsList.qml index 6692a6a4ec..8f8ef2a2c0 100644 --- a/QMLComponents/components/JASP/Controls/ComponentsList.qml +++ b/QMLComponents/components/JASP/Controls/ComponentsList.qml @@ -126,6 +126,11 @@ ComponentsListBase Layout.columnSpan : (parent && parent.hasOwnProperty('columns')) ? parent.columns : 1 preferredWidth : parent.width preferredHeight : implicitHeight + + Accessible.role: Accessible.List + Accessible.name: itemTitle.text + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("A list of other components %1").arg(title) : info + property alias label : componentsList.title property alias columns : itemGrid.columns diff --git a/QMLComponents/components/JASP/Controls/ComputedColumnField.qml b/QMLComponents/components/JASP/Controls/ComputedColumnField.qml index 23f2ac5de7..29afe34f37 100644 --- a/QMLComponents/components/JASP/Controls/ComputedColumnField.qml +++ b/QMLComponents/components/JASP/Controls/ComputedColumnField.qml @@ -67,4 +67,6 @@ import JASP.Controls TextField { inputType: "computedColumn" + + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Add a computed column textfield %1").arg(title) : info } diff --git a/QMLComponents/components/JASP/Controls/DoubleField.qml b/QMLComponents/components/JASP/Controls/DoubleField.qml index 3775b212f8..5f73e08056 100644 --- a/QMLComponents/components/JASP/Controls/DoubleField.qml +++ b/QMLComponents/components/JASP/Controls/DoubleField.qml @@ -91,4 +91,6 @@ TextField inputType: "number" validator: JASPDoubleValidator { id: doubleValidator; bottom: min; top: max ; decimals: doubleField.decimals; notation: DoubleValidator.StandardNotation } fieldWidth: jaspTheme.numericFieldWidth + + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Double entry field %1").arg(title) : info } diff --git a/QMLComponents/components/JASP/Controls/FormulaField.qml b/QMLComponents/components/JASP/Controls/FormulaField.qml index 0a55648240..ea30dbfc6e 100644 --- a/QMLComponents/components/JASP/Controls/FormulaField.qml +++ b/QMLComponents/components/JASP/Controls/FormulaField.qml @@ -89,4 +89,6 @@ TextField property bool parseDefaultValue: true inputType: "formula" fieldWidth: jaspTheme.textFieldWidth / 2 + + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("R Formula field %1").arg(title) : info } diff --git a/QMLComponents/components/JASP/Controls/Group.qml b/QMLComponents/components/JASP/Controls/Group.qml index cc11ee034f..6a20d69694 100644 --- a/QMLComponents/components/JASP/Controls/Group.qml +++ b/QMLComponents/components/JASP/Controls/Group.qml @@ -75,6 +75,12 @@ GroupBoxBase ALTNavigation.enabled: true ALTNavigation.onTagMatch: { contentArea.nextItemInFocusChain().forceActiveFocus(); } + + //Accessible.role: Accessible.Section + //Accessible.name: label.text + //Accessible.description: qsTr("A section of an analysis") + + default property alias content: contentArea.children property int rowSpacing: jaspTheme.rowGroupSpacing diff --git a/QMLComponents/components/JASP/Controls/HelpButton.qml b/QMLComponents/components/JASP/Controls/HelpButton.qml index b38d0fa2f2..a60f15fd18 100644 --- a/QMLComponents/components/JASP/Controls/HelpButton.qml +++ b/QMLComponents/components/JASP/Controls/HelpButton.qml @@ -53,7 +53,7 @@ MenuButton implicitHeight: visible ? 22 * preferencesModel.uiScale : 0 implicitWidth: implicitHeight Layout.alignment: Qt.AlignRight - activeFocusOnTab: true +activeFocusOnTab: true visible: helpMD || helpPage toolTip: qsTr("Open Documentation") @@ -65,4 +65,9 @@ MenuButton helpModel.showOrToggleMarkdown(helpMD) onHelpMDChanged: if (helpModel.visible) helpModel.markdown = helpMD + + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Helpbutton %1").arg(title) : info + Accessible.onPressAction: clicked() } diff --git a/QMLComponents/components/JASP/Controls/IntegerField.qml b/QMLComponents/components/JASP/Controls/IntegerField.qml index 816361f348..7e810d8880 100644 --- a/QMLComponents/components/JASP/Controls/IntegerField.qml +++ b/QMLComponents/components/JASP/Controls/IntegerField.qml @@ -89,4 +89,6 @@ TextField validator: JASPDoubleValidator { id: intValidator; bottom: min; top: max; decimals: 0 } cursorShape: Qt.IBeamCursor fieldWidth: jaspTheme.numericFieldWidth + + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Integer entry field %1").arg(title) : info } diff --git a/QMLComponents/components/JASP/Controls/JAGSTextArea.qml b/QMLComponents/components/JASP/Controls/JAGSTextArea.qml index 006ae92039..bf95faae00 100644 --- a/QMLComponents/components/JASP/Controls/JAGSTextArea.qml +++ b/QMLComponents/components/JASP/Controls/JAGSTextArea.qml @@ -47,6 +47,9 @@ TextArea { textType: JASP.TextTypeJAGSmodel showLineNumber: true + + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("A JAGS text area %1").arg(title) : info + RSyntaxHighlighterQuick { textDocument: parent.textDocument diff --git a/QMLComponents/components/JASP/Controls/Label.qml b/QMLComponents/components/JASP/Controls/Label.qml index 83aa89982a..83f09a46c8 100644 --- a/QMLComponents/components/JASP/Controls/Label.qml +++ b/QMLComponents/components/JASP/Controls/Label.qml @@ -45,4 +45,9 @@ QtC.Label { font: jaspTheme.font color: enabled ? jaspTheme.textEnabled : jaspTheme.textDisabled + + Accessible.role: Accessible.StaticText + Accessible.name: text + Accessible.description: qsTr("A label") + } diff --git a/QMLComponents/components/JASP/Controls/MenuButton.qml b/QMLComponents/components/JASP/Controls/MenuButton.qml index dbfa9df5d0..c5bb783682 100644 --- a/QMLComponents/components/JASP/Controls/MenuButton.qml +++ b/QMLComponents/components/JASP/Controls/MenuButton.qml @@ -48,6 +48,12 @@ RoundedButton border.width: 0 centerText: false activeFocusOnTab: true + + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.description: toolTip != "" ? toolTip : qsTr("A menubutton %1").arg(text) + Accessible.onPressAction: clicked() + signal hoverClicked(); onHoverClicked: forceActiveFocus(); diff --git a/QMLComponents/components/JASP/Controls/PercentField.qml b/QMLComponents/components/JASP/Controls/PercentField.qml index 4e2c448b57..a981af6b09 100644 --- a/QMLComponents/components/JASP/Controls/PercentField.qml +++ b/QMLComponents/components/JASP/Controls/PercentField.qml @@ -84,4 +84,6 @@ DoubleField afterLabel: showPercent ? "%" : "" cursorShape: Qt.IBeamCursor + + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Percentage field %1").arg(title) : info } diff --git a/QMLComponents/components/JASP/Controls/RadioButton.qml b/QMLComponents/components/JASP/Controls/RadioButton.qml index 80db56b6b3..8896919fef 100644 --- a/QMLComponents/components/JASP/Controls/RadioButton.qml +++ b/QMLComponents/components/JASP/Controls/RadioButton.qml @@ -95,6 +95,12 @@ RadioButtonBase childControlsArea: childControlsArea innerControl: control title: label.text + + Accessible.role: Accessible.RadioButton + Accessible.name: text + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("A radiobutton %1").arg(title) : info + Accessible.onToggleAction: control.toggle() + Accessible.onPressAction: click() property alias control: control default property alias content: childControlsArea.children diff --git a/QMLComponents/components/JASP/Controls/RectangularButton.qml b/QMLComponents/components/JASP/Controls/RectangularButton.qml index 150329c404..493209b2b9 100644 --- a/QMLComponents/components/JASP/Controls/RectangularButton.qml +++ b/QMLComponents/components/JASP/Controls/RectangularButton.qml @@ -90,6 +90,12 @@ Rectangle : jaspTheme.buttonColor property color defaultBorderColor: enabled && (filterButtonRoot.hovered || selected) ? jaspTheme.buttonBorderColorHovered : jaspTheme.buttonBorderColor + + + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.description: qsTr("A button") + Accessible.onPressAction: clicked() //on_ScaledDimChanged: console.log("Button " + text + ": " + _scaledDim + ", text height: " + buttonText.height + ", content height: " + buttonText.contentHeight + ", padding: " + buttonPadding) diff --git a/QMLComponents/components/JASP/Controls/Section.qml b/QMLComponents/components/JASP/Controls/Section.qml index 07b2816b11..ff31fa9528 100644 --- a/QMLComponents/components/JASP/Controls/Section.qml +++ b/QMLComponents/components/JASP/Controls/Section.qml @@ -63,6 +63,12 @@ FocusScope clip : true L.Layout.columnSpan : (typeof jaspForm !== 'undefined') ? jaspForm.columns : 1 objectName : "Section" + + Accessible.role: Accessible.Section + Accessible.name: title + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("Analysis Section %1").arg(title) : info + Accessible.onPressAction: click() + Accessible.onToggleAction: toggle() ALTNavigation.enabled: visible ALTNavigation.showChildren: true diff --git a/QMLComponents/components/JASP/Controls/Text.qml b/QMLComponents/components/JASP/Controls/Text.qml index 30c821a091..aa291ccab5 100644 --- a/QMLComponents/components/JASP/Controls/Text.qml +++ b/QMLComponents/components/JASP/Controls/Text.qml @@ -38,4 +38,8 @@ Text { font: jaspTheme.font color: enabled ? jaspTheme.textEnabled : jaspTheme.textDisabled + + Accessible.role: Accessible.StaticText + Accessible.name: text + Accessible.description: qsTr("Text %1").arg(text) } diff --git a/QMLComponents/components/JASP/Controls/TextArea.qml b/QMLComponents/components/JASP/Controls/TextArea.qml index f2bbeeca37..49573dab9b 100644 --- a/QMLComponents/components/JASP/Controls/TextArea.qml +++ b/QMLComponents/components/JASP/Controls/TextArea.qml @@ -84,6 +84,12 @@ TextAreaBase property bool showLineNumber : false Component.onCompleted: control.editingFinished.connect(editingFinished) + + Accessible.role: Accessible.EditableText + Accessible.name: title + Accessible.description: info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("A text area %1").arg(title) : info + + function userEnteredInput() { if (textArea.trim) diff --git a/QMLComponents/components/JASP/Controls/TextField.qml b/QMLComponents/components/JASP/Controls/TextField.qml index 7e49f6d969..d8243b2778 100644 --- a/QMLComponents/components/JASP/Controls/TextField.qml +++ b/QMLComponents/components/JASP/Controls/TextField.qml @@ -86,6 +86,7 @@ TextInputBase title: text mouseAreaZone: (control.tooLongText && label !== "") ? beforeLabelRect : textField + property alias control: control property alias text: textField.label property alias displayValue: control.text ///< In onEditingFinished this contains the "value" entered by the user @@ -260,6 +261,10 @@ TextInputBase QTC.ToolTip.text : control.text QTC.ToolTip.visible : tooLongText && (hovered || control.activeFocus) && control.echoMode != TextInput.Password + Accessible.role: Accessible.EditableText + Accessible.name: text + Accessible.description: textField.info === undefined || textField.info == "" ? qsTr("Textfield %1").arg(textField.title) : textField.info + // The acceptableInput is checked even if the user is still typing in the TextField. // In this case, the error should not appear immediately (only when the user is pressing the return key, or going out of focus), // so the checkValue is called with addErrorIfNotFocussed set to true: it should not display an error if in focus. diff --git a/QMLComponents/components/JASP/Controls/VariablesList.qml b/QMLComponents/components/JASP/Controls/VariablesList.qml index 36a9626960..eb682714f7 100644 --- a/QMLComponents/components/JASP/Controls/VariablesList.qml +++ b/QMLComponents/components/JASP/Controls/VariablesList.qml @@ -104,6 +104,10 @@ VariablesListBase allowAnalysisOwnComputedColumns : true minNumericLevels : allowedColumns.length === 1 && allowedColumns[0] === 'scale' ? 1 : -1 sourceWithoutDefaultInteraction : ["randomFactors", "covariates"] + + Accessible.role : Accessible.List + Accessible.name : label + Accessible.description : info === undefined || info == "" ? toolTip !== undefined && toolTip != "" ? toolTip : qsTr("A button %1").arg(title) : info property alias label : variablesList.title property alias itemGridView : itemGridView From dd3725fdce74082a9d81d3d57f6845d48c324149 Mon Sep 17 00:00:00 2001 From: Virtuoos Automatisch Date: Mon, 8 Jun 2026 14:53:41 +0200 Subject: [PATCH 02/62] Add screen reader support for menus, notes, and tables - Added ARIA attributes to tables: role='table', aria-label, tabindex, role='gridcell', role='columnheader'/role='rowheader', role='alert' for errors, role='rowgroup' for footnotes - Added accessibility to notes: role='region', role='textbox', role='button' for close buttons - Added QML accessibility to menus: Accessible.Menu, Accessible.MenuItem, Accessible.Separator with name properties - Added CSS focus indicators and high contrast mode support - Fixed Rcpp compilation with ENABLE_LEGACY_NONAPI flag - Added accessibility unit tests using AT-SPI2 Note: Menu bar menus may need to be accessed via keyboard shortcuts or opened to be visible in AT-SPI2 tree --- .../components/JASP/Widgets/CustomMenu.qml | 16 +- Desktop/html/css/jasp.css | 59 +++++ Desktop/html/js/jaspNotes.js | 7 +- Desktop/html/js/table.js | 109 ++++++--- R-Interface/CMakeLists.txt | 12 +- Tests/test_accessibility.py | 231 ++++++++++++++++++ 6 files changed, 393 insertions(+), 41 deletions(-) create mode 100755 Tests/test_accessibility.py diff --git a/Desktop/components/JASP/Widgets/CustomMenu.qml b/Desktop/components/JASP/Widgets/CustomMenu.qml index 9dba7db149..ffdcb90e38 100644 --- a/Desktop/components/JASP/Widgets/CustomMenu.qml +++ b/Desktop/components/JASP/Widgets/CustomMenu.qml @@ -199,6 +199,9 @@ FocusScope implicitHeight : column.height + 2 * jaspTheme.contentMargin height : (menu.y + implicitHeight) > sceneHeight ? (sceneHeight - menu.y) : implicitHeight // The menu should not exceed the scene + Accessible.role : Accessible.Menu + Accessible.name : "Analysis menu" + MouseArea { anchors.fill : parent @@ -341,6 +344,9 @@ FocusScope property bool itemEnabled : menu.props.hasOwnProperty("enabled") ? menu.props["enabled"][index] : (model.modelData !== undefined || model.isEnabled) + Accessible.role : Accessible.MenuItem + Accessible.name : (model.modelData !== undefined ? model.modelData : displayText) + Image { id : menuItemImage @@ -424,6 +430,9 @@ FocusScope property bool itemEnabled : menu.props.hasOwnProperty("enabled") ? menu.props["enabled"][index] : (model.modelData !== undefined || model.isEnabled) + Accessible.role : Accessible.MenuItem + Accessible.name : (model.modelData !== undefined ? model.modelData.substring(3) : displayText) + Image { id : menuItemImage @@ -496,7 +505,12 @@ FocusScope Component { id : menuSeparator - ToolSeparator { orientation : Qt.Horizontal; width: column.columnWidth } + ToolSeparator { + orientation : Qt.Horizontal; + width: column.columnWidth + Accessible.role : Accessible.Separator + Accessible.name : "Separator" + } } } } diff --git a/Desktop/html/css/jasp.css b/Desktop/html/css/jasp.css index 9313e9cc17..250dde2102 100644 --- a/Desktop/html/css/jasp.css +++ b/Desktop/html/css/jasp.css @@ -636,3 +636,62 @@ iframe.ql-video { transform: translateX(0); } } + +/* Accessibility enhancements */ +[aria-label] { + /* Ensure aria-label elements are properly accessible */ +} + +[aria-live] { + /* Ensure live regions are properly announced */ +} + +[role="table"] { + outline: none; +} + +[role="row"] { + outline: none; +} + +[role="columnheader"], [role="rowheader"] { + outline: none; +} + +[role="gridcell"] { + outline: none; +} + +/* Focus indicators for keyboard navigation */ +*:focus { + outline: 2px solid #0066cc; + outline-offset: 2px; +} + +/* High contrast focus indicators */ +@media (prefers-contrast: high) { + *[role="button"]:focus, + *[role="menuitem"]:focus, + *[role="tab"]:focus { + outline: 3px solid #000; + outline-offset: 2px; + } +} + +/* Screen reader only content */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +/* Ensure table headers are properly labeled */ +th[role="columnheader"], +th[role="rowheader"] { + font-weight: bold; +} diff --git a/Desktop/html/js/jaspNotes.js b/Desktop/html/js/jaspNotes.js index f5536e28f9..33bfd9ed33 100644 --- a/Desktop/html/js/jaspNotes.js +++ b/Desktop/html/js/jaspNotes.js @@ -231,6 +231,9 @@ JASPWidgets.NoteBox = JASPWidgets.View.extend({ var self = this; this.closeButton.actionTargetElement = function () { self.closeButton.$el.attr("title", i18n("Remove this note")) + .attr("role", "button") + .attr("tabindex", "0") + .attr("aria-label", i18n("Remove this note")) .tooltip({position: {my:"center bottom-15", at:"center top"}}); return self.$el; }; @@ -298,11 +301,13 @@ JASPWidgets.NoteBox = JASPWidgets.View.extend({ this.setVisibility(this.visible) var html = this.model.get("text"); + var noteId = 'note-' + Math.random().toString(36).substr(2, 9); this.closeButton.render(); this.$el.append(`
`); - this.$el.append(`
`) + this.$el.attr('role', 'region').attr('aria-label', i18n('Note')).attr('id', noteId) + this.$el.append(`
`) .append(`