Skip to content

Commit

Permalink
feat: V0.2: Fix displaying programs, add different viewing modes for …
Browse files Browse the repository at this point in the history
…Rattrapage and update README
  • Loading branch information
JoshuaVandaele committed May 26, 2023
1 parent 20e63cc commit 1c6c039
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 62 deletions.
63 changes: 50 additions & 13 deletions addon/appModules/captvty.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
from .modules.date_picker import DateRangeDialog
from .modules.helper_functions import (
AppModes,
click_element_with_mouse,
click_position_with_mouse,
fake_typing,
find_element_by_size,
left_click_element_with_mouse,
right_click_element_with_mouse,
scroll_and_click_on_element,
scroll_to_element,
)
Expand Down Expand Up @@ -175,7 +176,7 @@ def _datepick_callback(start_date: DateTime, end_date: DateTime) -> None:
log.debug(f"Start Date and Time: {start_date_str}")
log.debug(f"End Date and Time: {end_date_str}")

click_element_with_mouse(
left_click_element_with_mouse(
element=selectedElement,
y_offset=DIRECT_CHANNEL_LIST_VIEW_BUTTON_OFFSET_Y,
x_offset=DIRECT_CHANNEL_LIST_RECORD_BUTTON_OFFSET_X,
Expand Down Expand Up @@ -276,12 +277,12 @@ def _directSelectViewOptionCallback(
if selectedOption == "Programmer l'enregistrement":
self._directProgrammerEnregistrement(selectedElement)
elif selectedOption == "Visionner en direct avec le lecteur interne":
click_element_with_mouse(
left_click_element_with_mouse(
element=selectedElement,
y_offset=DIRECT_CHANNEL_LIST_VIEW_BUTTON_OFFSET_Y,
)
elif selectedOption == "Visionner en direct avec un lecteur externe":
click_element_with_mouse(
left_click_element_with_mouse(
element=selectedElement,
y_offset=DIRECT_CHANNEL_LIST_VIEW_BUTTON_OFFSET_Y,
x_offset=DIRECT_CHANNEL_LIST_VIDEOPLAYER_BUTTON_OFFSET_X,
Expand Down Expand Up @@ -319,6 +320,27 @@ def _directSelectedChannelCallback(self, selectedElement: NVDAObject):
else:
raise NotImplementedError

def _rattrapageSelectViewOptionCallback(
self, selectedProgramElement: NVDAObject, selectedOption: str
):
scroll_and_click_on_element(
element=selectedProgramElement,
scrollable_container=selectedProgramElement.parent,
max_attempts=10000,
bounds_offset=(0, 0, 450, 450),
)
right_click_element_with_mouse(selectedProgramElement)
if selectedOption == "Télécharger":
left_click_element_with_mouse(selectedProgramElement, 5, 20)
if selectedOption == "Visionner avec le lecteur intégré":
left_click_element_with_mouse(selectedProgramElement, 5, 60)
elif selectedOption == "Visionner sur le site web":
left_click_element_with_mouse(selectedProgramElement, 5, 100)
elif selectedOption == "Copier l'adresse de l'émission":
left_click_element_with_mouse(selectedProgramElement, 5, 120)
else:
raise NotImplementedError

def _rattrapageSelectedChannelCallback(self, selectedElement: NVDAObject):
scroll_area = (
selectedElement.parent.parent.parent # type:ignore - Channels are assumed to always be in the channel list
Expand All @@ -340,7 +362,7 @@ def _rattrapageSelectedChannelCallback(self, selectedElement: NVDAObject):
if not self.window:
raise NotImplementedError

click_element_with_mouse(self.window)
left_click_element_with_mouse(self.window)

def _program_list():
if not mainFrame:
Expand Down Expand Up @@ -373,26 +395,41 @@ def update_program_list(dialog: ElementsListDialog):
def get_program_info(
element: Union[NVDAObject, IAccessible]
) -> Union[str, None]:
if not isinstance(element, (IAccessible, NVDAObject)):
return None

try:
program = Program(element.name)
program_info = f"{program.name} | Durée: {program.duration} | Sommaire: {program.summary}"
return program_info
except (
AttributeError,
IndexError,
): # The element is not a valid program
): # The element is not a program
log.info("Nuh uh, not a program")
return None

def selected_program_callback(
selectedProgramElement: Union[IAccessible, NVDAObject]
) -> None:
scroll_and_click_on_element(
element=selectedProgramElement,
scrollable_container=selectedProgramElement.parent,
max_attempts=10000,
bounds_offset=(0, 0, 450, 450), # TODO: Make this work lol
)
click_element_with_mouse(selectedProgramElement) # Double click
if mainFrame:
mainFrame.prePopup()
dialog = ElementsListDialog(
parent=mainFrame,
elements=[
"Télécharger",
"Visionner avec le lecteur intégré",
"Visionner sur le site web",
"Copier l'adresse de l'émission",
],
callback=lambda option: self._rattrapageSelectViewOptionCallback(
selectedProgramElement, option
),
title="Choisissez une option",
list_label="",
)
dialog.Show()
mainFrame.postPopup()

mainFrame.prePopup()
dialog = ElementsListDialog(
Expand Down
51 changes: 45 additions & 6 deletions addon/appModules/modules/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,30 +110,69 @@ def click_position_with_mouse(position: Tuple[int, int]) -> None:
winUser.mouse_event(winUser.MOUSEEVENTF_LEFTUP, *position, 0, 0)


def click_element_with_mouse(
def hover_element_with_mouse(
element: Union[NVDAObject, IAccessible], x_offset: int = 0, y_offset: int = 0
) -> None:
) -> Tuple[int, int]:
"""
Clicks the specified element using the mouse with the specified x and y offset.
Hovers the specified element using the mouse with the specified x and y offset.
Args:
element (Union[IAccessible, NVDAObject]): The element to click.
element (Union[IAccessible, NVDAObject]): The element to hover.
x_offset (int, optional): The x offset to add to the center of the element. Defaults to 0.
y_offset (int, optional): The y offset to add to the center of the element. Defaults to 0.
Returns:
None
Tuple[int, int]: Position of the cursor after hovering the element.
"""
location = element.location # type: ignore - location is defined for IAccessible
x = location.left + (location.width // 2) + x_offset
y = location.top + (location.height // 2) + y_offset

winUser.setCursorPos(x, y)

return x, y


def left_click_element_with_mouse(
element: Union[NVDAObject, IAccessible], x_offset: int = 0, y_offset: int = 0
) -> None:
"""
Clicks the specified element using the mouse with the specified x and y offset.
Args:
element (Union[IAccessible, NVDAObject]): The element to click.
x_offset (int, optional): The x offset to add to the center of the element. Defaults to 0.
y_offset (int, optional): The y offset to add to the center of the element. Defaults to 0.
Returns:
None
"""
x, y = hover_element_with_mouse(element, x_offset, y_offset)

winUser.mouse_event(winUser.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0)
winUser.mouse_event(winUser.MOUSEEVENTF_LEFTUP, x, y, 0, 0)


def right_click_element_with_mouse(
element: Union[NVDAObject, IAccessible], x_offset: int = 0, y_offset: int = 0
) -> None:
"""
Clicks the specified element using the mouse with the specified x and y offset.
Args:
element (Union[IAccessible, NVDAObject]): The element to click.
x_offset (int, optional): The x offset to add to the center of the element. Defaults to 0.
y_offset (int, optional): The y offset to add to the center of the element. Defaults to 0.
Returns:
None
"""
x, y = hover_element_with_mouse(element, x_offset, y_offset)

winUser.mouse_event(winUser.MOUSEEVENTF_RIGHTDOWN, x, y, 0, 0)
winUser.mouse_event(winUser.MOUSEEVENTF_RIGHTUP, x, y, 0, 0)


def scroll_element_with_mouse(
element: Union[IAccessible, NVDAObject],
delta: Optional[int] = 120,
Expand Down Expand Up @@ -339,4 +378,4 @@ def scroll_and_click_on_element(
scrollable_container=scrollable_container,
bounds_offset=bounds_offset,
)
click_element_with_mouse(element, x_offset, y_offset)
left_click_element_with_mouse(element, x_offset, y_offset)
62 changes: 43 additions & 19 deletions addon/appModules/modules/list_elements.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import threading
from typing import Any, Callable, List, Optional, Union

import winUser
import wx
from gui import guiHelper
from logHandler import log
from NVDAObjects import NVDAObject
from NVDAObjects.IAccessible import IAccessible
from NVDAObjects.IAccessible import IAccessible, getNVDAObjectFromEvent
from NVDAObjects.IAccessible.sysListView32 import ListItem

from .helper_functions import normalize_str

Expand Down Expand Up @@ -77,16 +80,18 @@ def __init__(
"""
super(ElementsListDialog, self).__init__(parent, title=title)

self.lock = threading.Lock()

self.list_label = list_label
self.search_delay = search_delay
self.empty_list = not elements
self.elements = elements or ["Liste vide"]
self.element_name_getter = (
element_name_getter = (
element_name_getter or ElementsListDialog._get_element_name
)
self.element_name_getter = lambda x: element_name_getter(x) or str(x)
self.element_names = [
self.element_name_getter(element) or str(element)
for element in self.elements
self.element_name_getter(element) for element in self.elements
]
self.element_indices = list(range(len(self.element_names)))
self.callback = callback
Expand Down Expand Up @@ -114,8 +119,8 @@ def _get_element_name(element: Any) -> Union[str, None]:
Returns:
Union[str, None]: The name of the element or None if the element does not have a name.
"""
if isinstance(element, (NVDAObject, IAccessible)):
return element.name
if isinstance(element, (NVDAObject, IAccessible, ListItem)):
return element._get_name()

def _createLayout(self) -> None:
"""
Expand Down Expand Up @@ -294,34 +299,53 @@ def appendElement(self, element: Any) -> None:
"""
self.elements.append(element)

element_name = self.element_name_getter(element) or str(element)
element_name = self.element_name_getter(element)

self.element_names.append(element_name)

if self.empty_list:
self.empty_list = False
self.removeElement(0)
self.elements.pop()
self.onSearch()

def appendElements(self, elements: List[Any]) -> None:
"""
Appends a list of elements to the ListBox.
"""Appends a list of elements to the ElementsListDialog.
Args:
elements (List[Any]): The elements to append.
"""
new_element_names = [
self.element_name_getter(element) or str(element) for element in elements
]

self.elements.extend(elements)
self.element_names.extend(new_element_names)
def worker(elements):
"""Worker thread to append elements to the ElementsListDialog."""
with self.lock:
new_element_names = []

if self.empty_list:
self.empty_list = False
self.removeElement(0)
for element in elements:
position_info = getattr(element, "positionInfo", None)
index_in_group = (
position_info.get("indexInGroup") if position_info else None
)

self.onSearch()
if index_in_group:
# get the element by index so we can access the element within this thread.
element = getNVDAObjectFromEvent(
element.windowHandle,
winUser.OBJID_CLIENT,
index_in_group,
)

self.element_names.append(self.element_name_getter(element))

self.elements.extend(elements)
self.element_names.extend(new_element_names)

if self.empty_list:
self.empty_list = False
self.removeElement(0)

wx.CallAfter(self.onSearch)

threading.Thread(target=worker, args=(elements,)).start()

def removeElement(self, index: int) -> None:
"""
Expand Down
54 changes: 31 additions & 23 deletions addon/doc/fr/readme.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
# CaptvtyAddon

CaptvtyAddon est une extension pour l'outil d'accessibilité NVDA qui permet aux utilisateurs aveugles ou malvoyants de naviguer plus facilement dans l'application Captvty v3. Cette extension fournit des scripts personnalisés qui permettent d'effectuer certaines actions de manière plus simple et plus efficace.
CaptvtyAddon est une extension pour l'outil d'accessibilité NVDA qui facilite la navigation dans l'application Captvty v3 pour les utilisateurs aveugles ou malvoyants. Cette extension fournit des scripts personnalisés permettant d'effectuer des actions de manière simple et efficace.

## Raccourcis
## Raccourcis clavier

- <kbd>CTRL</kbd> + <kbd>D</kbd> - Mode Direct
- <kbd>CTRL</kbd> + <kbd>R</kbd> - Mode Rattrapage
- <kbd>NVDA</kbd> + <kbd>L</kbd> - Liste des chaines
- <kbd>CTRL</kbd> + <kbd>D</kbd> : Mode Direct
- <kbd>CTRL</kbd> + <kbd>R</kbd> : Mode Rattrapage
- <kbd>CTRL</kbd> + <kbd>T</kbd> : Mode Téléchargement Manuel
- <kbd>NVDA</kbd> + <kbd>L</kbd> : Liste des chaînes

## Fonctionnalités

- Navigation aisée entre les différents modes de l'application (Direct et Rattrapage)
- Navigation aisée entre les différents modes de l'application (Direct, Rattrapage et Téléchargement Manuel)
- Affichage d'une liste des chaînes pour sélectionner celle que vous souhaitez regarder
- Programmation d'enregistrements pour visionner ultérieurement
- Programmation d'enregistrements pour les visionner ultérieurement
- Téléchargement de programmes en mode rattrapage
- Lecture en direct avec un lecteur interne ou externe

## Comment utiliser CaptvtyAddon

### Installation

- Assurez-vous d'avoir installé l'outil d'accessibilité NVDA
- Telechargez le fichier `CaptvtyAddon-x.y.nvda-addon`
- Démarrez le programme NVDA
- Ouvrez le fichier
- Assurez-vous d'avoir installé l'outil d'accessibilité NVDA.
- Téléchargez le fichier `CaptvtyAddon-x.y.nvda-addon`.
- Démarrez le programme NVDA.
- Ouvrez le fichier `CaptvtyAddon-x.y.nvda-addon` téléchargé.

### Utilisation

- Lancez Captvty
- Appuyez sur la touche CTRL+D pour sélectionner le mode "Direct", ou CTRL+R pour sélectionner le mode "Rattrapage"
- Appuyez sur la touche NVDA+L pour afficher une liste des chaînes
- Lancez Captvty.
- Appuyez sur la combinaison de touches <kbd>CTRL</kbd> + <kbd>D</kbd> pour activer le mode "Direct", <kbd>CTRL</kbd> + <kbd>R</kbd> pour activer le mode "Rattrapage", ou <kbd>CTRL</kbd> + <kbd>T</kbd> pour activer le mode "Téléchargement Direct".
- Appuyez sur la combinaison de touches <kbd>NVDA</kbd> + <kbd>L</kbd> pour afficher la liste des chaînes disponibles.

#### Si vous êtes en mode direct
#### Mode Direct

- Sélectionnez une chaîne dans la liste pour accéder aux options de visionnage
- Utilisez les flèches pour naviguer entre les différentes options
- Appuyez sur Entrée pour sélectionner une option
- Pour visionner en direct avec le lecteur interne, sélectionnez l'option "Visionner en direct avec le lecteur interne" et attendez que le lecteur se charge
- Pour visionner en direct avec un lecteur externe, sélectionnez l'option "Visionner en direct avec un lecteur externe" et attendez que le lecteur se charge. Si c'est votre première utilisation d'un lecteur externe sur Captvty, celui-ci vous demandera ou se trouve l'executable de votre lecteur.
- Pour programmer un enregistrement, sélectionnez l'option "Programmer l'enregistrement" et suivez les instructions à l'écran
- Sélectionnez une chaîne dans la liste pour accéder aux options de visionnage.
- Utilisez les flèches pour naviguer entre les différentes options.
- Appuyez sur Entrée pour sélectionner une option.
- Pour visionner en direct avec le lecteur interne, sélectionnez l'option "Visionner en direct avec le lecteur interne" et attendez que le lecteur se charge.
- Pour visionner en direct avec un lecteur externe, sélectionnez l'option "Visionner en direct avec un lecteur externe" et attendez que le lecteur se charge. Si c'est votre première utilisation d'un lecteur externe sur Captvty, celui-ci vous demandera l'emplacement de l'exécutable de votre lecteur.
- Pour programmer un enregistrement, sélectionnez l'option "Programmer l'enregistrement" et suivez les instructions à l'écran.

#### Si vous êtes en mode rattrapage
#### Mode Rattrapage

- Les fonctionalitées du mode rattrapage sont encore en travail !
- Sélectionnez une chaîne dans la liste pour accéder aux programmes de cette chaîne.
- Utilisez les flèches et/ou la fonction de recherche pour trouver le programme désiré.
- Lors de la procédure de téléchargement, Captvty vous demandera la qualité préférée pour le programme à télécharger.

#### Mode Téléchargement Direct

**En cours de développement!** Veuillez revenir plus tard pour les mises à jour concernant cette fonctionnalité.
Loading

0 comments on commit 1c6c039

Please sign in to comment.