from __future__ import annotations
from typing import cast, Dict, List, TYPE_CHECKING, Tuple, Union
from ooo.dyn.awt.key_event import KeyEvent
from ooo.dyn.awt.key_modifier import KeyModifierEnum
from com.sun.star.awt import Key
from com.sun.star.container import NoSuchElementException
from ooodev.mock import mock_g
from ooodev.adapter.ui.global_accelerator_configuration_comp import GlobalAcceleratorConfigurationComp
from ooodev.adapter.ui.the_module_ui_configuration_manager_supplier_comp import (
TheModuleUIConfigurationManagerSupplierComp,
)
from ooodev.io.log import logging as logger
from ooodev.io.log.named_logger import NamedLogger
from ooodev.loader import lo as mLo
from ooodev.loader.inst.service import Service
from ooodev.macro.script.macro_script import MacroScript
from ooodev.utils.partial.lo_inst_props_partial import LoInstPropsPartial
from ooodev.utils.cache.lru_cache import LRUCache
from ooodev.utils.string.str_list import StrList
if TYPE_CHECKING:
from ooodev.adapter.ui.accelerator_configuration_comp import AcceleratorConfigurationComp
from ooodev.loader.inst.lo_inst import LoInst
from ooodev.gui.menu.common.command_dict import CommandDict
[docs]class Shortcuts(LoInstPropsPartial):
"""
Class for manager shortcuts.
See Also:
- :ref:`help_working_with_shortcuts`
"""
if mock_g.DOCS_BUILDING:
# When docs are building it may not have access to uno.
# Key is a uno object and it is not available so this will cause errors when building docs.
KEYS: Dict[int, str] = {}
"""
Keys dictionary. This is a dictionary that is built at runtime with the keys and values of the ``Key`` class. The dictionary Keys are the integer values of the keys and the values are the key names.
See `API Key <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1Key.html>`_
"""
else:
KEYS: Dict[int, str] = {getattr(Key, k): k for k in dir(Key)}
"""The dictionary Keys are the integer values of the keys and the values are the key names."""
MODIFIERS = {
"shift": KeyModifierEnum.SHIFT.value,
"ctrl": KeyModifierEnum.MOD1.value,
"alt": KeyModifierEnum.MOD2.value,
"ctrlmac": KeyModifierEnum.MOD3.value,
}
COMBINATIONS = {
0: "",
1: "shift",
2: "ctrl",
4: "alt",
8: "ctrlmac",
3: "shift+ctrl",
5: "shift+alt",
9: "shift+ctrlmac",
6: "ctrl+alt",
10: "ctrl+ctrlmac",
12: "alt+ctrlmac",
7: "shift+ctrl+alt",
11: "shift+ctrl+ctrlmac",
13: "shift+alt+ctrlmac",
14: "ctrl+alt+ctrlmac",
15: "shift+ctrl+alt+ctrlmac",
}
[docs] def __init__(self, app: str | Service = "", lo_inst: LoInst | None = None):
"""
Constructor
Args:
app (str | Service, optional): App Name such as ``Service.CALC``. Defaults to "".
If no app is provided, the global shortcuts will be used.
Hint:
- ``Service`` is an enum and can be imported from ``ooodev.loader.inst.service``
"""
if lo_inst is None:
lo_inst = mLo.Lo.current_lo
LoInstPropsPartial.__init__(self, lo_inst)
self._app = str(app)
self._config = self._get_config()
self._key_events = cast(Tuple[KeyEvent, ...], None)
self._logger = NamedLogger(name="Shortcuts")
self._cache = LRUCache(50)
def _get_config(self) -> Union[AcceleratorConfigurationComp, GlobalAcceleratorConfigurationComp]:
if self._app:
key = f"Shortcuts_get_ui_configuration_manager_{self._app}"
if key in self.lo_inst.cache:
return self.lo_inst.cache[key]
supp = TheModuleUIConfigurationManagerSupplierComp.from_lo(lo_inst=self.lo_inst)
config = supp.get_ui_configuration_manager(self._app)
self.lo_inst.cache[key] = config.get_short_cut_manager()
return cast("AcceleratorConfigurationComp", self.lo_inst.cache[key])
else:
return GlobalAcceleratorConfigurationComp.from_lo(lo_inst=self.lo_inst)
def _get_all_key_events(self) -> Tuple[KeyEvent, ...]:
key = "_get_all_key_events"
if key in self._cache:
return self._cache[key]
key_events = self._config.get_all_key_events()
self._cache[key] = key_events
return key_events
def __getitem__(self, app: str | Service):
return Shortcuts(app)
def __contains__(self, item):
cmd = self.get_by_shortcut(item)
return bool(cmd)
def __iter__(self):
self._i = -1
self._key_events = self._get_all_key_events()
return self
def __next__(self):
if self._key_events is None:
raise StopIteration
self._i += 1
try:
event = self._key_events[self._i]
event = self._get_info(event)
except IndexError:
self._key_events = cast(Tuple[KeyEvent, ...], None)
raise StopIteration
return event
[docs] @classmethod
def to_key_event(cls, shortcut: str) -> KeyEvent | None:
"""Convert from string shortcut (Shift+Ctrl+Alt+LETTER) to KeyEvent"""
key_event = KeyEvent()
keys = shortcut.split("+")
try:
for m in keys[:-1]:
if not m:
# could be empty string
continue
key_event.Modifiers += cls.MODIFIERS[m.lower()]
key_event.KeyCode = getattr(Key, keys[-1].upper())
except Exception:
logger.error("Exception occured", exc_info=True)
key_event = None
return key_event
[docs] @classmethod
def from_key_event(cls, key_event: KeyEvent) -> str:
"""Convert from KeyEvent to string shortcut"""
shortcut = ""
for m in cls.MODIFIERS:
if key_event.Modifiers & cls.MODIFIERS[m]:
shortcut += f"{m.capitalize()}+"
shortcut += cls.KEYS[key_event.KeyCode]
return shortcut
[docs] @classmethod
def get_url_script(cls, command: Union[str, CommandDict]) -> str:
"""
Get uno command or url for macro.
Args:
command (str | CommandDict): Command to search, 'UNOCOMMAND' or dict with macro info.
Returns:
str: Url for macro or uno command or custom command.
Note:
If ``command`` is passed in a a string and it starts with ``.custom:``
then it will be returned with the ``.custom:`` prefix dropped.
The ``.custom:`` prefix is used to indicate that the command is a custom command
and can be used in a menu callback to capture user clicks.
"""
url = command
if isinstance(url, str) and not url.startswith(".uno:"):
if url.startswith(".custom:"):
url = url[8:]
else:
url = f".uno:{command}"
elif isinstance(url, dict):
url = MacroScript.get_url_script(**url)
return url
[docs] def get_shortcut(self, key: KeyEvent) -> str:
"""
Get shortcut for key event.
Args:
key (KeyEvent): Key event
Returns:
str: Shortcut like Shift+Ctrl+Alt+LETTER
"""
# print(k.KeyCode, str(k.KeyChar), k.KeyFunc, k.Modifiers)
shortcut = f"{self.COMBINATIONS[key.Modifiers]}+{self.KEYS[key.KeyCode]}"
return shortcut
def _get_info(self, key: KeyEvent) -> Tuple[str, str]:
"""Get shortcut and command"""
cmd = self._config.get_command_by_key_event(key)
shortcut = self.get_shortcut(key)
return shortcut, cmd
[docs] def get_all(self) -> List[Tuple[str, str]]:
"""
Get all events key.
Returns:
List[Tuple[str, str]]: List of tuples with shortcut and command.
"""
events = [(self._get_info(k)) for k in self._get_all_key_events()]
return events
def _get_by_command_dict(self, url: str) -> StrList:
# for unknown reason LibreOffice does not return the command for most urls.
# This method is a workaround to get the command by url.
key = "_get_by_command_dict"
if key in self._cache:
command_dict = cast(Dict[str, List[str]], self._cache[key])
else:
command_dict: Dict[str, List[str]] = {}
for key in self._get_all_key_events():
try:
cmd = self._config.get_command_by_key_event(key)
except Exception:
continue
if cmd in command_dict:
command_dict[cmd].append(self.get_shortcut(key))
else:
command_dict[cmd] = [self.get_shortcut(key)]
self._cache[key] = command_dict
if url in command_dict:
return StrList(command_dict[url])
return StrList()
[docs] def get_by_command(self, command: Union[str, CommandDict]) -> StrList:
"""
Get shortcuts by command.
Args:
command (str | dict): Command to search, 'UNOCOMMAND' or dict with macro info.
Returns:
List[str]: List of shortcuts or empty list of not found.
"""
url = Shortcuts.get_url_script(command)
key = f"Shortcuts_get_by_command_{url}"
if key in self._cache:
return self._cache[key]
try:
key_events = self._config.get_key_events_by_command(url)
shortcuts = StrList([self.get_shortcut(k) for k in key_events])
except NoSuchElementException:
# fallback on workaround
shortcuts = self._get_by_command_dict(url)
self._cache[key] = shortcuts
return shortcuts
[docs] def get_by_shortcut(self, shortcut: str) -> str:
"""Get command by shortcut"""
key = f"get_by_shortcut_{shortcut}"
if key in self._cache:
return self._cache[key]
key_event = Shortcuts.to_key_event(shortcut)
if key_event is None:
self._logger.warning(f"get_by_shortcut() - Not exists shortcut: {shortcut}")
return ""
try:
command = self._config.get_command_by_key_event(key_event)
except NoSuchElementException:
self._logger.warning(f"Not exists shortcut: {shortcut}")
command = ""
if command:
self._cache[key] = command
return command
[docs] def get_by_key_event(self, key_event: KeyEvent) -> str:
"""Get command by key event"""
sc = self.get_shortcut(key_event)
return self.get_by_shortcut(sc)
[docs] def set(self, shortcut: str, command: Union[str, CommandDict], save: bool = True) -> bool:
"""
Set shortcut to command
Args:
shortcut (str): Shortcut like Shift+Ctrl+Alt+LETTER
command (str | CommandDict): Command to assign, 'UNOCOMMAND' or dict with macro info.
save (bool, optional): Save configuration causing it to persist. Defaults to ``True``.
Returns:
bool: True if set successfully
"""
result = True
url = Shortcuts.get_url_script(command)
key_event = Shortcuts.to_key_event(shortcut)
if key_event is None:
self._logger.warning(f"Not exists shortcut: {shortcut}")
return False
try:
self._config.set_key_event(key_event, url)
if save:
self._config.store()
except Exception as e:
self._logger.error(e)
result = False
return result
[docs] def remove_by_shortcut(self, shortcut: str, save: bool = False) -> bool:
"""
Remove by shortcut
Args:
shortcut (str): Shortcut like Shift+Ctrl+Alt+LETTER
save (bool, optional): Save configuration causing it to persist. Defaults to ``False``.
Returns:
bool: ``True`` if removed successfully
"""
key_event = Shortcuts.to_key_event(shortcut)
if key_event is None:
self._logger.warning(f"Not exists shortcut: {shortcut}")
return False
try:
self._config.remove_key_event(key_event)
if save:
self._config.store()
result = True
except NoSuchElementException:
self._logger.debug(f"No exists: {shortcut}")
result = False
if result:
self._cache.clear()
return result
[docs] def remove_by_command(self, command: Union[str, CommandDict], save: bool = False):
"""
Remove by shortcut.
Args:
command (str | CommandDict): Command to remove, 'UNOCOMMAND' or dict with macro info.
save (bool, optional): Save configuration causing it to persist. Defaults to ``False``.
"""
url = Shortcuts.get_url_script(command)
self._config.remove_command_from_all_key_events(url)
if save:
self._config.store()
self._cache.clear()
return
[docs] def reset(self) -> None:
"""Reset configuration"""
self._config.reset() # type: ignore
self._config.store()
return