Pages : 1
#1 Le 22/03/2020, à 09:19
- paulwoisard
Menu avec du TTS (QML + Qt/C++).
Bonjour à tous,
Je bosse pour un projet d'accessibilité, et dans ce cadre je veux faire un menu vocal en QML (fork de EQMenu), j'arrive à des premiers résultats de cette manière :
1 module complémentaire au QML en Qt/C++ :
global.h :
#pragma once
#include <string>
#include <QtGlobal>
// Declaration
namespace global
{
extern bool ttsEnCours;
extern qint64 MyPID;
}
myqmllauncher_plugin.h
#pragma once
#include <QQmlExtensionPlugin>
class MyQMLLauncherPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
public:
void registerTypes(const char *uri);
};
qlauncher.h
#ifndef QLAUNCHER_H
#define QLAUNCHER_H
#include <QQuickItem>
#include <QObject>
#include <QProcess>
class QLauncher : public QObject
{
Q_OBJECT
public:
explicit QLauncher(QObject *parent = 0);
~QLauncher();
Q_INVOKABLE QString launch(const QString &program);
protected:
QProcess *m_process = new QProcess;
};
#endif // QLAUNCHER_H
global.cpp
#include "global.h"
#include <QtGlobal>
// Definition
namespace global
{
bool ttsEnCours = false;
qint64 MyPID = Q_UINT64_C(0);
}
myqmllauncher_plugin.cpp
#include "myqmllauncher_plugin.h"
#include "qlauncher.h"
#include "global.h"
#include <qqml.h>
void MyQMLLauncherPlugin::registerTypes(const char *uri)
{
// @uri org.dvkbuntu.dvkmenulauncher
qmlRegisterType<QLauncher>(uri, 1, 0, "QLauncher");
}
qlauncher.cpp
#include "qlauncher.h"
#include "global.h"
QLauncher::QLauncher(QObject *parent):
QObject(parent)//,
//m_process(new QProcess(this))
{
}
QString QLauncher::launch(const QString &program)
{
if(!global::ttsEnCours) {
m_process->start(program);
global::MyPID = m_process->processId();
global::ttsEnCours = true;
QByteArray bytes = m_process->readAllStandardOutput();
QString output = QString::fromLocal8Bit(bytes);
return output;
}
if (program == "endOfHover") {
QProcess::startDetached("kill -9 " + QString::number(global::MyPID));
m_process->kill();
global::ttsEnCours = false;
}
QByteArray bytes = m_process->readAllStandardOutput();
QString output = QString::fromLocal8Bit(bytes);
return output;
}
QLauncher::~QLauncher()
{
}
2- les fichiers de configs de mon menu où se trouve ma problèmatique :
ItemGridDelegate.qml
import QtQuick 2.0
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.kquickcontrolsaddons 2.0
import org.dvkbuntu.dvkmenulauncher 1.0
import "../code/tools.js" as Tools
Item {
id: item
width: GridView.view.cellWidth
height: width
property bool showLabel: true
readonly property int itemIndex: model.index
readonly property url url: model.url != undefined ? model.url : ""
property bool pressed: false
readonly property bool hasActionList: ((model.favoriteId != null)
|| (("hasActionList" in model) && (model.hasActionList == true)))
Accessible.role: Accessible.MenuItem
Accessible.name: model.display
function openActionMenu(x, y) {
var actionList = hasActionList ? model.actionList : [];
Tools.fillActionMenu(i18n, actionMenu, actionList, GridView.view.model.favoritesModel, model.favoriteId);
actionMenu.visualParent = item;
actionMenu.open(x, y);
}
function actionTriggered(actionId, actionArgument) {
Tools.triggerAction(plasmoid, GridView.view.model, model.index, actionId, actionArgument);
}
PlasmaCore.IconItem {
id: icon
y: showLabel ? (2 * highlightItemSvg.margins.top) : 0
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: showLabel ? undefined : parent.verticalCenter
width: mouseArea.containsMouse ? iconSize*1.25 : iconSize
height: width
animated: false
usesPlasmaTheme: item.GridView.view.usesPlasmaTheme
source: model.decoration
}
QLauncher {
id: qprocess
}
PlasmaComponents.Label {
id: label
visible: showLabel
anchors {
top: icon.bottom
topMargin: units.smallSpacing
left: parent.left
leftMargin: highlightItemSvg.margins.left
right: parent.right
rightMargin: highlightItemSvg.margins.right
}
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
wrapMode: Text.NoWrap
font.pointSize: mouseArea.containsMouse ? 26 : 14
text: model.display
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: qprocess.launch('/etc/alternatives/tts "' + model.display + '"')
onExited: qprocess.launch("endOfHover")
}
Keys.onPressed: {
if (event.key == Qt.Key_Menu && hasActionList) {
event.accepted = true;
openActionMenu(item);
} else if ((event.key == Qt.Key_Enter || event.key == Qt.Key_Return)) {
event.accepted = true;
GridView.view.model.trigger(index, "", null);
if ("toggle" in root) {
root.toggle();
} else {
root.visible = false;
}
}
}
}
MenuRepresentation.qml
import QtQuick 2.4
import QtQuick.Layouts 1.1
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.private.kicker 0.1 as Kicker
import org.dvkbuntu.dvkmenulauncher 1.0
PlasmaCore.Dialog {
id: root
objectName: "popupWindow"
flags: Qt.WindowStaysOnTopHint
location: PlasmaCore.Types.LeftEdge
hideOnWindowDeactivate: true
property int iconSize: plasmoid.configuration.gridSize == 0 ? units.iconSizes.huge : units.iconSizes.huge * 1.2
property int cellSize: {
if (plasmoid.configuration.gridSize == 0) {
iconSize + theme.mSize(theme.defaultFont).height
+ (2 * units.smallSpacing)
+ (2 * Math.max(highlightItemSvg.margins.top + highlightItemSvg.margins.bottom,
highlightItemSvg.margins.left + highlightItemSvg.margins.right))
} else if (plasmoid.configuration.gridSize == 1) {
iconSize + theme.mSize(theme.defaultFont).height
+ (10 * units.smallSpacing)
+ (2 * Math.max(highlightItemSvg.margins.top + highlightItemSvg.margins.bottom,
highlightItemSvg.margins.left + highlightItemSvg.margins.right))
} else {
iconSize + theme.mSize(theme.defaultFont).height
+ (14 * units.smallSpacing)
+ (2 * Math.max(highlightItemSvg.margins.top + highlightItemSvg.margins.bottom,
highlightItemSvg.margins.left + highlightItemSvg.margins.right))
}
}
property bool searching: (searchField.text != "")
onVisibleChanged: {
if (!visible) {
reset();
} else {
requestActivate();
}
}
onSearchingChanged: {
if (searching) {
pageList.model = runnerModel;
paginationBar.model = runnerModel;
} else {
reset();
}
}
function reset() {
if (!searching) {
if (filterListScrollArea.visible) {
filterList.currentIndex = 0;
} else {
pageList.model = rootModel.modelForRow(0);
paginationBar.model = rootModel.modelForRow(0);
}
}
searchField.text = "";
pageListScrollArea.focus = true;
pageList.currentIndex = plasmoid.configuration.favoritesFirst ? 0 : 1;
pageList.currentItem.itemGrid.currentIndex = -1;
}
FocusScope {
Layout.minimumWidth: (cellSize * 8) + Math.max(systemFavoritesGrid.width, filterListScrollArea.width) + units.smallSpacing
Layout.maximumWidth: (cellSize * 8) + Math.max(systemFavoritesGrid.width, filterListScrollArea.width) + units.smallSpacing
Layout.minimumHeight: (cellSize * 6) + searchField.height + paginationBar.height + (2 * units.smallSpacing)
Layout.maximumHeight: (cellSize * 6) + searchField.height + paginationBar.height + (2 * units.smallSpacing)
focus: true
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.visible = false;
}
}
PlasmaExtras.Heading {
id: dummyHeading
visible: false
width: 0
level: 5
}
TextMetrics {
id: headingMetrics
font: dummyHeading.font
}
ActionMenu {
id: actionMenu
onActionClicked: visualParent.actionTriggered(actionId, actionArgument)
onClosed: {
if (pageList.currentItem) {
pageList.currentItem.itemGrid.currentIndex = -1;
}
}
}
PlasmaComponents3.TextField {
id: searchField
anchors.top: parent.top
anchors.left: parent.left
anchors.right: systemFavoritesGrid.left
anchors.rightMargin: units.smallSpacing
width: parent.width
placeholderText: i18n("Search...")
onTextChanged: {
runnerModel.query = text;
}
Keys.onPressed: {
if (event.key == Qt.Key_Down) {
event.accepted = true;
pageList.currentItem.itemGrid.tryActivate(0, 0);
} else if (event.key == Qt.Key_Right) {
if (cursorPosition == length) {
event.accepted = true;
if (pageList.currentItem.itemGrid.currentIndex == -1) {
pageList.currentItem.itemGrid.tryActivate(0, 0);
} else {
pageList.currentItem.itemGrid.tryActivate(0, 1);
}
}
} else if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter) {
if (text != "" && pageList.currentItem.itemGrid.count > 0) {
event.accepted = true;
pageList.currentItem.itemGrid.tryActivate(0, 0);
pageList.currentItem.itemGrid.model.trigger(0, "", null);
root.visible = false;
}
} else if (event.key == Qt.Key_Tab) {
event.accepted = true;
systemFavoritesGrid.tryActivate(0, 0);
} else if (event.key == Qt.Key_Backtab) {
event.accepted = true;
if (!searching) {
filterList.forceActiveFocus();
} else {
systemFavoritesGrid.tryActivate(0, 0);
}
}
}
function backspace() {
if (!root.visible) {
return;
}
focus = true;
text = text.slice(0, -1);
}
function appendText(newText) {
if (!root.visible) {
return;
}
focus = true;
text = text + newText;
}
}
PlasmaExtras.ScrollArea {
id: pageListScrollArea
anchors {
left: parent.left
top: searchField.bottom
topMargin: units.smallSpacing
bottom: paginationBar.top
bottomMargin: units.smallSpacing
}
width: (cellSize * 6)
focus: true
horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff
verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff
ListView {
id: pageList
anchors.fill: parent
orientation: Qt.Horizontal
snapMode: ListView.SnapOneItem
currentIndex: 0
model: rootModel.modelForRow(0)
onCurrentIndexChanged: {
positionViewAtIndex(currentIndex, ListView.Contain);
}
onCurrentItemChanged: {
if (!currentItem) {
return;
}
currentItem.itemGrid.focus = true;
}
onModelChanged: {
currentIndex = 0;
}
onFlickingChanged: {
if (!flicking) {
var pos = mapToItem(contentItem, root.width / 2, root.height / 2);
var itemIndex = indexAt(pos.x, pos.y);
currentIndex = itemIndex;
}
}
function cycle() {
enabled = false;
enabled = true;
}
function activateNextPrev(next) {
if (next) {
var newIndex = pageList.currentIndex + 1;
if (newIndex == pageList.count) {
newIndex = 0;
}
pageList.currentIndex = newIndex;
} else {
var newIndex = pageList.currentIndex - 1;
if (newIndex < 0) {
newIndex = (pageList.count - 1);
}
pageList.currentIndex = newIndex;
}
}
delegate: Item {
width: cellSize * 6 + units.smallSpacing * 2
height: parent.height
property Item itemGrid: gridView
ItemGridView {
id: gridView
anchors.fill: parent
cellWidth: cellSize
cellHeight: cellSize
horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff
// verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff
dragEnabled: (index == 0)
model: searching ? runnerModel.modelForRow(index) : rootModel.modelForRow(filterListScrollArea.visible ? filterList.currentIndex : 0).modelForRow(index)
onCurrentIndexChanged: {
if (currentIndex != -1 && !searching) {
pageListScrollArea.focus = true;
focus = true;
}
}
onCountChanged: {
if (searching && index == 0) {
currentIndex = 0;
}
}
onKeyNavUp: {
currentIndex = -1;
searchField.focus = true;
}
onKeyNavRight: {
var newIndex = pageList.currentIndex + 1;
var cRow = currentRow();
if (newIndex == pageList.count) {
newIndex = 0;
}
pageList.currentIndex = newIndex;
pageList.currentItem.itemGrid.tryActivate(cRow, 0);
}
onKeyNavLeft: {
var newIndex = pageList.currentIndex - 1;
var cRow = currentRow();
if (newIndex < 0) {
newIndex = (pageList.count - 1);
}
pageList.currentIndex = newIndex;
pageList.currentItem.itemGrid.tryActivate(cRow, 5);
}
}
}
}
}
ListView {
id: paginationBar
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: model.count * units.iconSizes.small
height: units.iconSizes.small
orientation: Qt.Horizontal
model: rootModel.modelForRow(0)
delegate: Item {
width: units.iconSizes.small
height: width
Rectangle {
id: pageDelegate
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
width: parent.width / 2
height: width
property bool isCurrent: (pageList.currentIndex == index)
radius: width / 2
color: "blue" //theme.textColor
opacity: 0.5
Behavior on width { SmoothedAnimation { duration: units.longDuration; velocity: 0.01 } }
Behavior on opacity { SmoothedAnimation { duration: units.longDuration; velocity: 0.01 } }
states: [
State {
when: pageDelegate.isCurrent
PropertyChanges { target: pageDelegate; width: parent.width - (units.smallSpacing * 2) }
PropertyChanges { target: pageDelegate; opacity: 0.8 }
}
]
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: pageList.currentIndex = index;
property int wheelDelta: 0
function scrollByWheel(wheelDelta, eventDelta) {
// magic number 120 for common "one click"
// See: http://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop
wheelDelta += eventDelta;
var increment = 0;
while (wheelDelta >= 120) {
wheelDelta -= 120;
increment++;
}
while (wheelDelta <= -120) {
wheelDelta += 120;
increment--;
}
while (increment != 0) {
pageList.activateNextPrev(increment < 0);
increment += (increment < 0) ? 1 : -1;
}
return wheelDelta;
}
onWheel: {
wheelDelta = scrollByWheel(wheelDelta, wheel.angleDelta.y);
}
}
}
}
PlasmaExtras.ScrollArea {
id: filterListScrollArea
anchors {
left: pageListScrollArea.right
leftMargin: units.smallSpacing
top: searchField.bottom
topMargin: units.smallSpacing
bottom: paginationBar.top
bottomMargin: units.smallSpacing
}
property int desiredWidth: 0
width: plasmoid.configuration.showFilterList ? desiredWidth : 0
enabled: !searching
visible: plasmoid.configuration.showFilterList
property alias currentIndex: filterList.currentIndex
opacity: root.visible ? (searching ? 0.30 : 1.0) : 0.3
Behavior on opacity { SmoothedAnimation { duration: units.longDuration; velocity: 0.01 } }
verticalScrollBarPolicy: (opacity == 1.0) ? Qt.ScrollBarAsNeeded : Qt.ScrollBarAlwaysOff
onEnabledChanged: {
if (!enabled) {
filterList.currentIndex = -1;
}
}
ListView {
id: filterList
focus: true
property bool allApps: false
property int eligibleWidth: width
property int hItemMargins: highlightItemSvg.margins.left + highlightItemSvg.margins.right
model: filterListScrollArea.visible ? rootModel : null
boundsBehavior: Flickable.StopAtBounds
snapMode: ListView.SnapToItem
spacing: 0
keyNavigationWraps: true
QLauncher {
id: qprocess
}
delegate: MouseArea {
id: item
property int textWidth: label.contentWidth
property int mouseCol
width: parent.width
height: label.paintedHeight + highlightItemSvg.margins.top + highlightItemSvg.margins.bottom
Accessible.role: Accessible.MenuItem
Accessible.name: model.display
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onContainsMouseChanged: {
if (!containsMouse) {
updateCurrentItemTimer.stop();
}
}
onPressed: {
if (!plasmoid.configuration.switchCategoriesOnHover) {
ListView.view.currentIndex = index;
}
}
onPositionChanged: { // Lazy menu implementation.
if (!plasmoid.configuration.switchCategoriesOnHover) {
return;
}
mouseCol = mouse.x;
if (index == ListView.view.currentIndex) {
updateCurrentItem();
} else if ((index == ListView.view.currentIndex - 1) && mouse.y < (item.height - 6)
|| (index == ListView.view.currentIndex + 1) && mouse.y > 5) {
if (mouse.x > ListView.view.eligibleWidth - 5) {
updateCurrentItem();
}
} else if (mouse.x > ListView.view.eligibleWidth) {
updateCurrentItem();
}
updateCurrentItemTimer.start();
}
onEntered: qprocess.launch('/etc/alternatives/tts "' + model.display + '"');
onExited: qprocess.launch("endOfHover");
function updateCurrentItem() {
ListView.view.currentIndex = index;
ListView.view.eligibleWidth = Math.min(width, mouseCol);
}
Timer {
id: updateCurrentItemTimer
interval: 50
repeat: false
onTriggered: {
parent.updateCurrentItem()
}
}
PlasmaExtras.Heading {
id: label
anchors {
fill: parent
leftMargin: highlightItemSvg.margins.left
rightMargin: highlightItemSvg.margins.right
}
elide: Text.ElideRight
wrapMode: Text.NoWrap
opacity: 1.0
level: 5
font.pointSize: item.containsMouse ? 26 : 14
text: model.display
}
}
highlight: PlasmaComponents.Highlight {
anchors {
top: filterList.currentItem ? filterList.currentItem.top : undefined
left: filterList.currentItem ? filterList.currentItem.left : undefined
bottom: filterList.currentItem ? filterList.currentItem.bottom : undefined
}
opacity: filterListScrollArea.focus ? 1.0 : 0.7
width: (highlightItemSvg.margins.left
+ filterList.currentItem.textWidth
+ highlightItemSvg.margins.right
+ units.smallSpacing)
visible: filterList.currentItem
}
highlightFollowsCurrentItem: false
highlightMoveDuration: 0
highlightResizeDuration: 0
onCurrentIndexChanged: applyFilter()
onCountChanged: {
var width = 0;
for (var i = 0; i < rootModel.count; ++i) {
headingMetrics.text = rootModel.labelForRow(i);
if (headingMetrics.width > width) {
width = headingMetrics.width;
}
}
filterListScrollArea.desiredWidth = width + hItemMargins + units.gridUnit;
}
function applyFilter() {
if (filterListScrollArea.visible && !searching && currentIndex >= 0) {
pageList.model = rootModel.modelForRow(currentIndex);
paginationBar.model = pageList.model;
}
}
Keys.onPressed: {
if (event.key == Qt.Key_left) {
event.accepted = true;
var currentRow = Math.max(0, Math.ceil(currentItem.y / cellSize) - 1);
if (pageList.currentItem) {
pageList.currentItem.itemGrid.tryActivate(currentRow, 5);
}
} else if (event.key == Qt.Key_Tab) {
event.accepted = true;
searchField.focus = true;
} else if (event.key == Qt.Key_Backtab) {
event.accepted = true;
systemFavoritesGrid.tryActivate(0, 0);
}
}
}
}
Keys.onPressed: {
if (event.key == Qt.Key_Escape) {
event.accepted = true;
if (searching) {
reset();
} else {
root.visible = false;
}
return;
}
if (searchField.focus) {
return;
}
if (event.key == Qt.Key_Backspace) {
event.accepted = true;
searchField.backspace();
} else if (event.key == Qt.Key_Tab || event.key == Qt.Key_Backtab) {
if (pageListScrollArea.focus == true && pageList.currentItem.itemGrid.currentIndex == -1) {
event.accepted = true;
pageList.currentItem.itemGrid.tryActivate(0, 0);
}
} else if (event.text != "") {
event.accepted = true;
searchField.appendText(event.text);
}
}
}
Component.onCompleted: {
kicker.reset.connect(reset);
dragHelper.dropped.connect(pageList.cycle);
}
}
(pour info /etc/alternatives/tts est une alternatives à la debian pour choisir entre différent service de tts (espeak, google_speech ou autres)
3-Ma problématique dans la config actuelle :
a-Soit je bloque l'exécution avant la fin de la diction avec (m_process->waitForFinished(-1); dans qlancher.cpp) du coup le changement état ne se produit (zoom, grossissement du texte et surlignage par des icônes et textes) qu'après la diction.
b-Soit les voix se superposent, j'ai essaye de limité ça en utilisant un bool (qui évite certaines répétitions mais pas toutes) et en essayant de kill le processus (le kill ne marche, pourquoi ?).
4-Solution possible :
a-Lancer la lecture qu'à la fin du changement d'état d'apparence, du coup cela sera plus simple d'accès.
b-Arriver à kill la voix déjà lancée, ainsi le menu sera hyper fluide, mais j'y arrive pas.
Merci d'avance pour la futur aide apportée.
Dernière modification par paulwoisard (Le 22/03/2020, à 09:32)
Actuellement, j'ai une toute petite équipe de bénévoles et on essaye de dév des deux, trois logiciels plus ou moins utile, en voici le site web : https://bit-scripts.github.io/
Hors ligne
#2 Le 23/03/2020, à 13:27
- paulwoisard
Re : Menu avec du TTS (QML + Qt/C++).
j'ai avancé sur le projet :
https://github.com/handyopensource/dvkbuntu-vocalfly
mais je ne sais pas comment faire pour avoir avoir le mot "ouverture" sur : https://github.com/handyopensource/dvkb … legate.qml sur la ligne 110, onclicked semble pas marcher.
Actuellement, j'ai une toute petite équipe de bénévoles et on essaye de dév des deux, trois logiciels plus ou moins utile, en voici le site web : https://bit-scripts.github.io/
Hors ligne