From 89b4e9136f59dfef561a912cbff006e384b5c578 Mon Sep 17 00:00:00 2001 From: VodBox Date: Wed, 25 Nov 2020 22:45:28 +1300 Subject: [PATCH] UI: Various screen reader fixes This commit fixes various issues with screen readers in the main OBS interface. These were tested using NVDA on Windows 10 2004. Audio track selection in Settings now says Track 1, 2, etc, rather than just the number. Various checkboxes that just say "Enable" now have accessible text that says what the enable is for (since it says "checkbox", the fact it's an enable should hopefully be clear). Type in the recording tab of output now has accessible text which says "Recording Type". Items in the Advanced Audio Properties window now have accessible text for what they are for. Currently some do not report correct values, but that will require an accessible interface in Qt to be written specifically for that, which will be done at a later date. Buttons in the filters window now have accessible text for what they do. All the right side buttons in hotkeys now have tooltips, and by extension, accessible text. --- UI/adv-audio-control.cpp | 32 +++++++++++++-- UI/data/locale/en-US.ini | 16 +++++++- UI/forms/OBSBasicFilters.ui | 18 +++++++++ UI/hotkey-edit.cpp | 2 + UI/obs-app.cpp | 15 ++++++++ UI/slider-ignorewheel.cpp | 75 ++++++++++++++++++++++++++++++++++++ UI/slider-ignorewheel.hpp | 34 ++++++++++++++++ UI/source-tree.cpp | 7 ++++ UI/volume-control.cpp | 6 +-- UI/window-basic-settings.cpp | 50 ++++++++++++++++++++++++ 10 files changed, 247 insertions(+), 8 deletions(-) diff --git a/UI/adv-audio-control.cpp b/UI/adv-audio-control.cpp index f26a558c6..862d746f7 100644 --- a/UI/adv-audio-control.cpp +++ b/UI/adv-audio-control.cpp @@ -22,7 +22,7 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) { QHBoxLayout *hlayout; signal_handler_t *handler = obs_source_get_signal_handler(source); - const char *sourceName = obs_source_get_name(source); + QString sourceName = QT_UTF8(obs_source_get_name(source)); float vol = obs_source_get_volume(source); uint32_t flags = obs_source_get_flags(source); uint32_t mixers = obs_source_get_audio_mixers(source); @@ -90,7 +90,7 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) iconLabel->setFixedSize(16, 16); iconLabel->setStyleSheet("background: none"); - nameLabel->setText(QT_UTF8(sourceName)); + nameLabel->setText(sourceName); nameLabel->setAlignment(Qt::AlignVCenter); bool isActive = obs_source_active(source); @@ -109,15 +109,21 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) volume->setSuffix(" dB"); volume->setValue(obs_mul_to_db(vol)); volume->setFixedWidth(100); + volume->setAccessibleName( + QTStr("Basic.AdvAudio.VolumeSource").arg(sourceName)); - if (volume->value() < MIN_DB) + if (volume->value() < MIN_DB) { volume->setSpecialValueText("-inf dB"); + volume->setAccessibleDescription("-inf dB"); + } percent->setMinimum(0); percent->setMaximum(2000); percent->setSuffix("%"); percent->setValue((int)(obs_source_get_volume(source) * 100.0f)); percent->setFixedWidth(100); + percent->setAccessibleName( + QTStr("Basic.AdvAudio.VolumeSource").arg(sourceName)); stackedWidget->addWidget(volume); stackedWidget->addWidget(percent); @@ -128,6 +134,8 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) SetVolumeWidget(volType); forceMono->setChecked((flags & OBS_SOURCE_FLAG_FORCE_MONO) != 0); + forceMono->setAccessibleName( + QTStr("Basic.AdvAudio.MonoSource").arg(sourceName)); forceMonoContainer->layout()->addWidget(forceMono); forceMonoContainer->layout()->setAlignment(forceMono, Qt::AlignVCenter); @@ -138,6 +146,8 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) balance->setMaximum(100); balance->setTickPosition(QSlider::TicksAbove); balance->setTickInterval(50); + balance->setAccessibleName( + QTStr("Basic.AdvAudio.BalanceSource").arg(sourceName)); const char *speakers = config_get_string(main->Config(), "Audio", "ChannelSetup"); @@ -156,6 +166,8 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) syncOffset->setSuffix(" ms"); syncOffset->setValue(int(cur_sync / NSEC_PER_MSEC)); syncOffset->setFixedWidth(100); + syncOffset->setAccessibleName( + QTStr("Basic.AdvAudio.SyncOffsetSource").arg(sourceName)); int idx; #if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO @@ -168,20 +180,34 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) int mt = (int)obs_source_get_monitoring_type(source); idx = monitoringType->findData(mt); monitoringType->setCurrentIndex(idx); + monitoringType->setAccessibleName( + QTStr("Basic.AdvAudio.MonitoringSource").arg(sourceName)); #endif mixer1->setText("1"); mixer1->setChecked(mixers & (1 << 0)); + mixer1->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track1")); mixer2->setText("2"); mixer2->setChecked(mixers & (1 << 1)); + mixer2->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track2")); mixer3->setText("3"); mixer3->setChecked(mixers & (1 << 2)); + mixer3->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track3")); mixer4->setText("4"); mixer4->setChecked(mixers & (1 << 3)); + mixer4->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track4")); mixer5->setText("5"); mixer5->setChecked(mixers & (1 << 4)); + mixer5->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track5")); mixer6->setText("6"); mixer6->setChecked(mixers & (1 << 5)); + mixer6->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track6")); speaker_layout sl = obs_source_get_speaker_layout(source); diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 4c445023e..826b19f1b 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -408,8 +408,8 @@ Deinterlacing.TopFieldFirst="Top Field First" Deinterlacing.BottomFieldFirst="Bottom Field First" # volume control accessibility text -VolControl.SliderUnmuted="Volume slider for '%1': %2" -VolControl.SliderMuted="Volume slider for '%1': %2 (currently muted)" +VolControl.SliderUnmuted="Volume slider for '%1':" +VolControl.SliderMuted="Volume slider for '%1': (currently muted)" VolControl.Mute="Mute '%1'" VolControl.Properties="Properties for '%1'" @@ -449,6 +449,12 @@ Basic.SourceSelect.CreateNew="Create new" Basic.SourceSelect.AddExisting="Add Existing" Basic.SourceSelect.AddVisible="Make source visible" +# source box +Basic.Main.Sources.Visibility="Visibility" +Basic.Main.Sources.VisibilityDescription="Controls the visibility of '%1' in the canvas" +Basic.Main.Sources.Lock="Lock" +Basic.Main.Sources.LockDescription="Locks the position and scale of '%1' in the canvas" + # properties window Basic.PropertiesWindow="Properties for '%1'" Basic.PropertiesWindow.AutoSelectFormat="%1 (autoselect: %2)" @@ -784,6 +790,7 @@ Basic.Settings.Output.Adv.TwitchVodTrack="Twitch VOD Track" # basic mode 'output' settings - advanced section - recording subsection Basic.Settings.Output.Adv.Recording="Recording" +Basic.Settings.Output.Adv.Recording.RecType="Recording Type" Basic.Settings.Output.Adv.Recording.Type="Type" Basic.Settings.Output.Adv.Recording.Type.Standard="Standard" Basic.Settings.Output.Adv.Recording.Type.FFmpegOutput="Custom Output (FFmpeg)" @@ -919,13 +926,18 @@ Basic.AdvAudio="Advanced Audio Properties" Basic.AdvAudio.ActiveOnly="Active Sources Only" Basic.AdvAudio.Name="Name" Basic.AdvAudio.Volume="Volume" +Basic.AdvAudio.VolumeSource="Volume for '%1'" Basic.AdvAudio.Mono="Mono" +Basic.AdvAudio.MonoSource="Mono Downmix for '%1'" Basic.AdvAudio.Balance="Balance" +Basic.AdvAudio.BalanceSource="Balance for '%1'" Basic.AdvAudio.SyncOffset="Sync Offset" +Basic.AdvAudio.SyncOffsetSource="Sync Offset for '%1'" Basic.AdvAudio.Monitoring="Audio Monitoring" Basic.AdvAudio.Monitoring.None="Monitor Off" Basic.AdvAudio.Monitoring.MonitorOnly="Monitor Only (mute output)" Basic.AdvAudio.Monitoring.Both="Monitor and Output" +Basic.AdvAudio.MonitoringSource="Audio Monitoring for '%1'" Basic.AdvAudio.AudioTracks="Tracks" # basic mode 'hotkeys' settings diff --git a/UI/forms/OBSBasicFilters.ui b/UI/forms/OBSBasicFilters.ui index 572a31f9d..272dfc21e 100644 --- a/UI/forms/OBSBasicFilters.ui +++ b/UI/forms/OBSBasicFilters.ui @@ -104,6 +104,9 @@ addIconSmall + + Add + @@ -130,6 +133,9 @@ removeIconSmall + + Remove + @@ -156,6 +162,9 @@ upArrowIconSmall + + MoveUp + @@ -182,6 +191,9 @@ downArrowIconSmall + + MoveDown + @@ -280,6 +292,9 @@ addIconSmall + + Add + @@ -306,6 +321,9 @@ removeIconSmall + + Remove + diff --git a/UI/hotkey-edit.cpp b/UI/hotkey-edit.cpp index 37e5a7d4d..2ee0cc39b 100644 --- a/UI/hotkey-edit.cpp +++ b/UI/hotkey-edit.cpp @@ -293,11 +293,13 @@ void OBSHotkeyWidget::AddEdit(obs_key_combination combo, int idx) auto add = new QPushButton; add->setProperty("themeID", "addIconSmall"); + add->setToolTip(QTStr("Add")); add->setFixedSize(24, 24); add->setFlat(true); auto remove = new QPushButton; remove->setProperty("themeID", "removeIconSmall"); + remove->setToolTip(QTStr("Remove")); remove->setEnabled(removeButtons.size() > 0); remove->setFixedSize(24, 24); remove->setFlat(true); diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 58dcef2f7..ab7ee45ec 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -35,10 +35,12 @@ #include #include #include +#include #include "qt-wrappers.hpp" #include "obs-app.hpp" #include "log-viewer.hpp" +#include "slider-ignorewheel.hpp" #include "window-basic-main.hpp" #include "window-basic-settings.hpp" #include "crash-report.hpp" @@ -1901,6 +1903,17 @@ static auto ProfilerFree = [](void *) { profiler_free(); }; +QAccessibleInterface *accessibleFactory(const QString &classname, + QObject *object) +{ + if (classname == QLatin1String("VolumeSlider") && object && + object->isWidgetType()) + return new VolumeAccessibleInterface( + static_cast(object)); + + return nullptr; +} + static const char *run_program_init = "run_program_init"; static int run_program(fstream &logFile, int argc, char *argv[]) { @@ -1932,6 +1945,8 @@ static int run_program(fstream &logFile, int argc, char *argv[]) OBSApp program(argc, argv, profilerNameStore.get()); try { + QAccessible::installFactory(accessibleFactory); + bool created_log = false; program.AppInit(); diff --git a/UI/slider-ignorewheel.cpp b/UI/slider-ignorewheel.cpp index 8203c81f9..11023d638 100644 --- a/UI/slider-ignorewheel.cpp +++ b/UI/slider-ignorewheel.cpp @@ -1,4 +1,5 @@ #include "slider-ignorewheel.hpp" +#include "volume-control.hpp" SliderIgnoreScroll::SliderIgnoreScroll(QWidget *parent) : QSlider(parent) { @@ -20,3 +21,77 @@ void SliderIgnoreScroll::wheelEvent(QWheelEvent *event) else QSlider::wheelEvent(event); } + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) + : SliderIgnoreScroll(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, + QWidget *parent) + : SliderIgnoreScroll(orientation, parent) +{ + fad = fader; +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) + : QAccessibleWidget(w) +{ +} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/UI/slider-ignorewheel.hpp b/UI/slider-ignorewheel.hpp index f5c7e5d72..40d04487c 100644 --- a/UI/slider-ignorewheel.hpp +++ b/UI/slider-ignorewheel.hpp @@ -1,8 +1,10 @@ #pragma once +#include "obs.hpp" #include #include #include +#include class SliderIgnoreScroll : public QSlider { Q_OBJECT @@ -15,3 +17,35 @@ public: protected: virtual void wheelEvent(QWheelEvent *event) override; }; + +class VolumeSlider : public SliderIgnoreScroll { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, + QWidget *parent = nullptr); +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; diff --git a/UI/source-tree.cpp b/UI/source-tree.cpp index d6ee35b64..34994f9f4 100644 --- a/UI/source-tree.cpp +++ b/UI/source-tree.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -84,12 +85,18 @@ SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) vis->setFixedSize(16, 16); vis->setChecked(obs_sceneitem_visible(sceneitem)); vis->setStyleSheet("background: none"); + vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); + vis->setAccessibleDescription( + QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); lock = new LockedCheckBox(); lock->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); lock->setFixedSize(16, 16); lock->setChecked(obs_sceneitem_locked(sceneitem)); lock->setStyleSheet("background: none"); + lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); + lock->setAccessibleDescription( + QTStr("Basic.Main.Sources.LockDescription").arg(name)); label = new QLabel(QT_UTF8(name)); label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); diff --git a/UI/volume-control.cpp b/UI/volume-control.cpp index 7543d153e..cfc64b6c8 100644 --- a/UI/volume-control.cpp +++ b/UI/volume-control.cpp @@ -89,7 +89,7 @@ void VolControl::updateText() : "VolControl.SliderUnmuted"; QString sourceName = obs_source_get_name(source); - QString accText = QTStr(accTextLookup).arg(sourceName, db); + QString accText = QTStr(accTextLookup).arg(sourceName); slider->setAccessibleName(accText); } @@ -161,7 +161,7 @@ VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) QHBoxLayout *meterLayout = new QHBoxLayout; volMeter = new VolumeMeter(nullptr, obs_volmeter, true); - slider = new SliderIgnoreScroll(Qt::Vertical); + slider = new VolumeSlider(obs_fader, Qt::Vertical); nameLayout->setAlignment(Qt::AlignCenter); meterLayout->setAlignment(Qt::AlignCenter); @@ -205,7 +205,7 @@ VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) QHBoxLayout *botLayout = new QHBoxLayout; volMeter = new VolumeMeter(nullptr, obs_volmeter, false); - slider = new SliderIgnoreScroll(Qt::Horizontal); + slider = new VolumeSlider(obs_fader, Qt::Horizontal); textLayout->setContentsMargins(0, 0, 0, 0); textLayout->addWidget(nameLabel); diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index fd081ce10..70237ed8c 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -817,6 +817,56 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) InitStreamPage(); LoadSettings(false); + ui->advOutTrack1->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track1")); + ui->advOutTrack2->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track2")); + ui->advOutTrack3->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track3")); + ui->advOutTrack4->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track4")); + ui->advOutTrack5->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track5")); + ui->advOutTrack6->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track6")); + + ui->advOutRecTrack1->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track1")); + ui->advOutRecTrack2->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track2")); + ui->advOutRecTrack3->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track3")); + ui->advOutRecTrack4->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track4")); + ui->advOutRecTrack5->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track5")); + ui->advOutRecTrack6->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track6")); + + ui->advOutFFTrack1->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track1")); + ui->advOutFFTrack2->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track2")); + ui->advOutFFTrack3->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track3")); + ui->advOutFFTrack4->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track4")); + ui->advOutFFTrack5->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track5")); + ui->advOutFFTrack6->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Audio.Track6")); + + ui->snappingEnabled->setAccessibleName( + QTStr("Basic.Settings.General.Snapping")); + ui->systemTrayEnabled->setAccessibleName( + QTStr("Basic.Settings.General.SysTray")); + ui->label_31->setAccessibleName( + QTStr("Basic.Settings.Output.Adv.Recording.RecType")); + ui->streamDelayEnable->setAccessibleName( + QTStr("Basic.Settings.Advanced.StreamDelay")); + ui->reconnectEnable->setAccessibleName( + QTStr("Basic.Settings.Output.Reconnect")); + // Add warning checks to advanced output recording section controls connect(ui->advOutRecTrack1, SIGNAL(clicked()), this, SLOT(AdvOutRecCheckWarnings()));