From 4812a8d14fc235fcc023b54b9d4dbd779399528e Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 15 Aug 2022 23:57:31 -0500 Subject: [PATCH] Refactory --- doc/content/es/application/_index.md | 10 + doc/content/es/calc/_index.md | 12 + doc/content/es/dialog/_index.md | 76 + doc/content/es/tools/_index.md | 105 + doc/content/es/tools/dates_and_time/_index.md | 153 + doc/content/es/tools/email/_index.md | 18 + doc/content/es/tools/messages/_index.md | 69 + doc/content/es/tools/paths/_index.md | 569 ++ doc/content/es/tools/tools_01.png | Bin 0 -> 52339 bytes source/easymacro (copy).bk1 | 4332 --------------- source/easymacro (copy).bk2 | 4671 ----------------- source/easymacro.bk2 | 4671 ----------------- source/{easymacro.bk1 => easymacro.py} | 1010 +--- source/easymacro/__init__.py | 7 +- source/easymacro/easydialog.py | 741 +++ source/easymacro/easyevents.py | 40 + source/easymacro/easymain.py | 7 +- source/easymacro/easytools.py | 1018 +++- source/easymacro/messages.py | 13 + 19 files changed, 2836 insertions(+), 14686 deletions(-) create mode 100644 doc/content/es/application/_index.md create mode 100644 doc/content/es/calc/_index.md create mode 100644 doc/content/es/dialog/_index.md create mode 100644 doc/content/es/tools/_index.md create mode 100644 doc/content/es/tools/dates_and_time/_index.md create mode 100644 doc/content/es/tools/email/_index.md create mode 100644 doc/content/es/tools/messages/_index.md create mode 100644 doc/content/es/tools/paths/_index.md create mode 100644 doc/content/es/tools/tools_01.png delete mode 100644 source/easymacro (copy).bk1 delete mode 100644 source/easymacro (copy).bk2 delete mode 100644 source/easymacro.bk2 rename source/{easymacro.bk1 => easymacro.py} (70%) create mode 100644 source/easymacro/easydialog.py create mode 100644 source/easymacro/easyevents.py create mode 100644 source/easymacro/messages.py diff --git a/doc/content/es/application/_index.md b/doc/content/es/application/_index.md new file mode 100644 index 0000000..fc08acb --- /dev/null +++ b/doc/content/es/application/_index.md @@ -0,0 +1,10 @@ ++++ +title = "LibreOffice" +weight = 4 ++++ + +### create_instance + + +```python +``` diff --git a/doc/content/es/calc/_index.md b/doc/content/es/calc/_index.md new file mode 100644 index 0000000..a9c1069 --- /dev/null +++ b/doc/content/es/calc/_index.md @@ -0,0 +1,12 @@ ++++ +title = "Calc" +weight = 5 ++++ + +#### Trabajar con Calc + +### active + + +```python +``` diff --git a/doc/content/es/dialog/_index.md b/doc/content/es/dialog/_index.md new file mode 100644 index 0000000..7934e68 --- /dev/null +++ b/doc/content/es/dialog/_index.md @@ -0,0 +1,76 @@ ++++ +title = "Cuadros de diálogo" +weight = 6 ++++ + +#### Trabajar con cuadros de diálogo + +### create + +##### Desde archivo + +Crear un cuadro de diálogo desde un archivo `xdl` previamente creado desde el editor de cuadros de diálogo dentro de LibreOffice y exportado a un archivo. + +```python +def crear_cuadro_de_dialogo(): + path = '/home/mau/Desktop/Dialog1.xdl' + propiedades = {'Path': path} + + dialog = app.dialog.create(propiedades) + dialog.open() + return +``` + + +##### Desde mis macros + +Si el cuadro de diálogo esta en la librería `Standard`. + +```python +def crear_cuadro_de_dialogo(): + propiedades = { + 'Location': 'user', + 'Name': 'Dialog1', + } + dialog = app.dialog.create(propiedades) + dialog.open() + return +``` + +Si el cuadro de diálogo esta en otra librería. + +```python +def crear_cuadro_de_dialogo(): + propiedades = { + 'Location': 'user', + 'Library': 'MiAplicacion', + 'Name': 'Dialog2', + } + dialog = app.dialog.create(propiedades) + dialog.open() + return +``` + +Si el cuadro de diálogo esta en el documento activo en la librería `Standard`. + +```python +def crear_cuadro_de_dialogo(): + propiedades = { + 'Location': 'document', + 'Name': 'miApp', + } + dialog = app.dialog.create(propiedades) + dialog.open() + return +``` + +##### Crear dinámicamente + +Crear un cuadro de diálogo vacío. + +```python +def crear_cuadro_de_dialogo(): + dialog = app.dialog.create() + dialog.open() + return +``` diff --git a/doc/content/es/tools/_index.md b/doc/content/es/tools/_index.md new file mode 100644 index 0000000..cd82b8d --- /dev/null +++ b/doc/content/es/tools/_index.md @@ -0,0 +1,105 @@ ++++ +title = "Herramientas" +weight = 3 ++++ + +Recuerda, es necesario importar la librería. + +```python +import easymacro as app +``` + +#### Información sobre la PC + +### OS + +Obtener el sistema operativo. + +```python +app.msgbox(app.OS) +``` + + +### DESKTOP + +Obtener el tipo de escritorio, solo en GNU/Linux. + +```python +app.msgbox(app.DESKTOP) +``` + + +### PC + +Obtener el nombre de la computadora. + +```python +app.msgbox(app.PC) +``` + + +### USER + +Obtener el nombre de usuario. + +```python +app.msgbox(app.USER) +``` + + +### IS_WIN + +Saber si estamos en Windows. + +```python +app.msgbox(app.IS_WIN) +``` + + +### IS_MAC + +Saber si estamos en OSX. + +```python +app.msgbox(app.IS_MAC) +``` + +#### Información sobre LibreOffice + +### NAME + +Nombre de la aplicación. + +```python +app.msgbox(app.NAME) +``` + + +### VERSION + +Versión de la aplicación. + +```python +app.msgbox(app.VERSION) +``` + + +### LANG + +Lenguaje de la aplicación. + +```python +app.msgbox(app.LANG) +``` + + +### LANGUAGE + +Lenguaje con variante. + +```python +app.msgbox(app.LANGUAGE) +``` + + +[1]: https://git.cuates.net/elmau/easymacro/issues diff --git a/doc/content/es/tools/dates_and_time/_index.md b/doc/content/es/tools/dates_and_time/_index.md new file mode 100644 index 0000000..5d45bbc --- /dev/null +++ b/doc/content/es/tools/dates_and_time/_index.md @@ -0,0 +1,153 @@ ++++ +title = "Fechas y tiempo" +weight = 2 ++++ + + +{{% notice tip %}} +La fecha inicial en Calc y en Python es diferente. +{{% /notice %}} + + +### today + +Obtener la fecha de hoy. + +```python +d = app.dates +app.msgbox(d.today) +``` + + +### now + +Obtener la fecha y hora actuales. + +```python +d = app.dates +app.msgbox(d.now) +``` + + +### time + +Obtener la hora actual. + +```python +d = app.dates +app.msgbox(d.now.time()) +``` + + +### epoch + +Obtener el [tiempo Unix][1] + +```python +d = app.dates +app.msgbox(d.epoch) +``` + + +### date + +Devolver una fecha + +```python +d = app.dates + +date = d.date(1974, 1, 15) +app.msgbox(date) +``` + + +### time + +Devolver un tiempo + +```python +d = app.dates + +time = d.time(10, 20, 15) +app.msgbox(time) +``` + + +### datetime + +Devolver fecha y hora + +```python +d = app.dates + +dt = d.datetime(1974, 1, 15, 10, 11, 12) +app.msgbox(dt) +``` + +### str_to_date + +Convertir una cadena en fecha. Mira este [excelente recurso][2] + +```python +d = app.dates + +cadena = '1974-01-15' +plantilla = '%Y-%m-%d' +fecha = d.str_to_date(cadena, plantilla) +app.msgbox(fecha) +app.msgbox(type(fecha)) +``` + +Para obtener un valor válido para establecer en una celda de Calc. + +```python +d = app.dates + +cadena = '1974-01-15' +plantilla = '%Y-%m-%d' +fecha = d.str_to_date(cadena, plantilla, True) +app.msgbox(fecha) +app.msgbox(type(fecha)) +``` + +### calc_to_date + +Convierte el valor de una celda en una fecha Python, por ejemplo, la fecha inicial configurada en Calc. + +```python +d = app.dates + +value_from_cell = 1 +fecha = d.calc_to_date(value_from_cell) +app.msgbox(fecha) +app.msgbox(type(fecha)) +``` + + +### start y end + +Medir tiempo en segundos. + +```python +d = app.dates + +d.start() +app.sleep(5) +seconds = d.end() +app.msgbox(seconds) +``` + +Regresar timedelta en vez de segundos. + +```python +d = app.dates + +d.start() +app.sleep(5) +td = d.end(False) +app.msgbox(td) +``` + + +[1]: https://en.wikipedia.org/wiki/Unix_time +[2]: https://strftime.org diff --git a/doc/content/es/tools/email/_index.md b/doc/content/es/tools/email/_index.md new file mode 100644 index 0000000..e5dbdc8 --- /dev/null +++ b/doc/content/es/tools/email/_index.md @@ -0,0 +1,18 @@ ++++ +title = "Correo electrónico" +weight = 3 ++++ + +#### Enviar correo electrónico. + +{{% notice tip %}} +Siempre guarda las contraseñas de la forma más segura posible. +{{% /notice %}} + +### send + +Enviar un correo electrónico. + +```python + +``` diff --git a/doc/content/es/tools/messages/_index.md b/doc/content/es/tools/messages/_index.md new file mode 100644 index 0000000..c4496f5 --- /dev/null +++ b/doc/content/es/tools/messages/_index.md @@ -0,0 +1,69 @@ ++++ +title = "Mensajes" +weight = 1 ++++ + +#### Cuadros de mensaje + +### msgbox + +Mostrar mensaje estandar. + +```python +mensaje = 'Maldito Mundo' +titulo = 'Mi Macro' +app.msgbox(mensaje, titulo) +``` + + +### warning + +Mostrar mensaje con icono de advertencia. + +```python +mensaje = 'Cuidado, esta acción es peligrosa' +titulo = 'Mi Macro' +app.warning(mensaje, titulo) +``` + + +### errorbox + +Mostrar mensaje con icono de error. + +```python +mensaje = 'ERROR: contacte a soporte' +titulo = 'Mi Macro' +app.errorbox(mensaje, titulo) +``` + + +### question + +Hacer una pregunta mostrando el icono de interrogación y mostrando los botones de comando `Si` y `No`. La respuesta siempre es verdadera (`True`) si el usuario selecciona `si` y falsa (`False`) en caso contrario. + +```python +mensaje = '¿Es fácil Python?' +titulo = 'Mi Macro' +resultado = app.question(mensaje, titulo) +app.msgbox(resultado) +``` + +### inputbox + +Muestra un mensaje al usuario, permitiendo capturar una respuesta. + +```python +mensaje = 'Escribe tu nombre' +nombre = app.inputbox(mensaje) +app.msgbox(nombre) +``` + +Para ocultar solo en pantalla lo que captura el usuario, como contraseñas. + +```python +mensaje = 'Captura la contraseña' +echochar = '*' +contraseña = app.inputbox(mensaje, echochar=echochar) +app.msgbox(contraseña) +``` diff --git a/doc/content/es/tools/paths/_index.md b/doc/content/es/tools/paths/_index.md new file mode 100644 index 0000000..419ec49 --- /dev/null +++ b/doc/content/es/tools/paths/_index.md @@ -0,0 +1,569 @@ ++++ +title = "Rutas y archivos" +weight = 2 ++++ + +#### Trabajar con rutas y archivos. + +### path + +Obtener información de una ruta. + +```python +ruta_archivo = '/home/mau/mi_archivo.ods' +p = app.paths(ruta_archivo) + +app.debug(p.path) +app.debug(p.file_name) +app.debug(p.name) +app.debug(p.ext) +app.debug(p.size) +app.debug(p.url) +``` + +``` +15/08/2022 14:40:08 - DEBUG - /home/mau +15/08/2022 14:40:08 - DEBUG - mi_archivo.ods +15/08/2022 14:40:08 - DEBUG - mi_archivo +15/08/2022 14:40:08 - DEBUG - ods +15/08/2022 14:40:08 - DEBUG - 7011 +15/08/2022 14:40:08 - DEBUG - file:///home/mau/mi_archivo.ods +``` + +Obtener la misma información como un tupla. + +```python +ruta_archivo = '/home/mau/mi_archivo.ods' +p = app.paths(ruta_archivo) +app.debug(p.info) +``` + +``` +15/08/2022 14:43:01 - DEBUG - ('/home/mau', 'mi_archivo.ods', 'mi_archivo', 'ods', 7011, 'file:///home/mau/mi_archivo.ods') +``` + +O como diccionario. + +```python +ruta_archivo = '/home/mau/mi_archivo.ods' +p = app.paths(ruta_archivo) +app.debug(p.dict) +``` + +``` +15/08/2022 14:43:01 - DEBUG - {'path': '/home/mau', 'file_name': 'mi_archivo.ods', 'name': 'mi_archivo', 'ext': 'ods', 'size': 7011, 'url': 'file:///home/mau/mi_archivo.ods'} +``` + + +### home + +Obtener la carpeta de inicio del usuario. + +```python +p = app.paths +app.debug(p.home) +``` + + +### documents + +Obtener la carpeta Documentos del usuario. + +```python +p = app.paths +app.debug(p.documents) +``` + + +### user_profile + +Obtener la ruta del perfil de usuario. + +```python +p = app.paths +app.debug(p.user_profile) +``` + + +### user_config + +Obtener la ruta de la carpeta `config` en el perfil de usuario. + +```python +p = app.paths +app.debug(p.user_config) +``` + + +### python + +Obtener la ruta del ejecutable `python` + +```python +p = app.paths +app.debug(p.python) +``` + + +### to_system + +Pasar una ruta en formato URL al formato del sistema de archivos del SO. + +```python +p = app.paths +ruta_url = 'file:///home/mau/mi_archivo.ods' +ruta = p.to_system(ruta_url) +app.debug(ruta) +``` + + +### to_url + +Pasar una ruta del sistema de archivos del SO al formato URL. + +```python +p = app.paths +ruta = '/home/mau/mi_archivo.ods' +ruta_url = p.to_url(ruta) +app.debug(ruta_url) +``` + +### config + +Obtener rutas de la configuración de LibreOffice. Por default obtiene la ruta de `Documentos`, para otras ruta mire [Api XPathSettings][1] + +```python +p = app.paths +ruta = p.config() +app.debug(ruta) + +ruta = p.config('UserConfig') +app.debug(ruta) +``` + +{{% notice tip %}} +Algunas rutas pueden ser más de una, separados por `;`, en este caso, el resultado es una lista con las rutas. +{{% /notice %}} + + +### join + +Concatenar rutas. + +```python +p = app.paths +ruta = p.join('/home/mau', 'pruebas', 'archivo.ods') +app.debug(ruta) +``` + + +### exists + +Verificar si una ruta existe. + +```python +p = app.paths +resultado = p.exists('/home/mau/test/archivo.ods') +app.debug(resultado) +``` + +### exists_app + +Verificar si una aplicación existe. + +```python +p = app.paths + +resultado = p.exists_app('noexiste') +app.debug(resultado) + +resultado = p.exists_app('soffice') +app.debug(resultado) +``` + + +### is_dir + +Verificar si la ruta es un directorio. + +```python +p = app.paths +resultado = p.is_dir('/home/mau') +app.debug(resultado) +``` + + +### is_file + +Verificar si la ruta es un archivo. + +```python +p = app.paths +resultado = p.is_file('/home/mau/mi_archivo.ods') +app.debug(resultado) +``` + + +### temp_file + +Crear un archivo temporal, es borrado automáticamente al cerrarse. + +```python +p = app.paths + +f = p.temp_file() +f.write(app.INFO_DEBUG) +f.close() +``` + +Usado en `contexto`, se cierra automáticamente. + +```python +with p.temp_file() as f: + app.debug(f.name) + f.write('test') +``` + +### temp_dir + +Crear un directorio temporal, al salir del contexto, es eliminado junto con todo su contenido del sistema de archivos. + +```python +p = app.paths + +with p.temp_dir() as d: + app.debug(p.exists(d)) + app.debug(d) + +app.debug(p.exists(d)) +``` + +### get + +Permitir al usuario seleccionar archivos, devuelve solo la ruta. De forma predeterminada abre en la carpeta `Documentos`. + +```python +p = app.paths +ruta = p.get() +app.debug(ruta) +``` + +Establecer que abra en otro directorio. + +```python +ruta = p.get('/tmp') +app.debug(ruta) +``` + +Agregar un filtro. + +```python +ruta = p.get(filters='ods') +app.debug(ruta) +``` + +Agregar multiples filtros. + +```python +ruta = p.get(filters='xml,txt') +app.debug(ruta) +``` + +Permitir selecciones multiples. + +```python +ruta = p.get(filters='xml,txt', multiple=True) +app.debug(ruta) +``` + + +### get_dir + +Permitir al usuario seleccionar un directorio, devuelve solo la ruta. De forma predeterminada abre en la carpeta `Documentos`. + +```python +p = app.paths +ruta = p.get_dir() +app.debug(ruta) +``` + +Establecer que abra en otro directorio. + +```python +ruta = p.get_dir('/tmp') +app.debug(ruta) +``` + +### get_for_save + +Permitir seleccionar un directorio y capturar el nombre del archivo, devuelve la ruta completa. + +```python +p = app.paths +ruta = p.get_for_save() +app.debug(ruta) +``` + +Establecer que inicie en otro directorio. + +```python +ruta = p.get_for_save('/tmp') +app.debug(ruta) +``` + +Si se establece un filtro, automáticamente se agrega la extensión. + +```python +ruta = p.get_for_save(filters='txt') +app.debug(ruta) +``` + + +### files + +Obtener archivos de una ruta, no recursivamente. De forma predeterminada regresa todos, incluyendo archivos ocultos. + +```python +ruta = '/home/mau' + +p = app.paths +archivos = p.files(ruta) +for a in archivos: + app.debug(a) +``` + +Filtrar por tipo de archivo. + +```python +p = app.paths +archivos = p.files(ruta, '*.pdf') +``` + +Obtener archivos de forma recursiva. + +```python +p = app.paths +archivos = p.files(ruta, '**/*.xml') +``` + + +### walk + +Obtener archivos de forma recursiva. + +```python +p = app.paths +ruta = '/home/mau/Documents' +archivos = p.walk(ruta) +for a in archivos: + app.debug(a) +``` + +Con filtro. + +```python +archivos = p.walk(ruta, 'ods') +``` + +Con filtros. + +```python +archivos = p.walk(ruta, 'ods|odt') +``` + + +### dirs + +Obtener los directorios de una ruta, no recursivamente. + +```python +p = app.paths +ruta = '/home/mau/Documents' +folders = p.dirs(ruta) +for f in folders: + app.debug(f) +``` + + +### walk_dirs + +Obtener los directorios de una ruta, no recursivamente. + +```python +p = app.paths +ruta = '/home/mau/Documents' +folders = p.walk_dirs(ruta) +for f in folders: + app.debug(f) +``` + +Obtener información en una lista de tuplas: (ID_FOLDER, ID_PADRE, NOMBRE) + +```python +folders = p.walk_dirs(ruta, True) +``` + + +### extension + +Obtener la ruta de instalación de una extensión a partir de su ID. + +```python +p = app.paths +id_ext = 'net.elmau.zaz.talk' +ruta = p.extension(id_ext) +app.debug(ruta) +``` + +### replace_ext + +Reemplazar extensión de un archivo. + +```python +p = app.paths +ruta = '/home/mau/mi_archivo.ods' +ruta_nueva = p.replace_ext(ruta, 'pdf') +app.debug(ruta_nueva) +``` + + +### open + +Abrir cualquier archivo con el programas predeterminado del SO. + +```python +p = app.paths +ruta = '/home/mau/archivo.pdf' +p.open(ruta) + +ruta = '/home/mau/index.html' +p.open(ruta) +``` + +### save y read + +Guardar y leer datos, el encoding predeterminado es UTF8. + +```python +p = app.paths +datos = """¿Quieres saber quién eres? No preguntes. Actúa. +La acción te delineará y definirá. + +Thomas Jefferson +""" + +ruta = '/home/mau/temp.txt' +p.save(ruta, datos) + +datos = p.read(ruta) +app.msgbox(datos) +``` + +Cambiar el encoding. + +```python +app.paths.save(ruta, datos, 'iso-8859-1') +``` + + +### save_bin y read_bin + +Guardar y leer datos binarios. + +```python +p = app.paths +datos = b'Datos binarios' + +ruta = '/home/mau/temp.bin' +p.save_bin(ruta, datos) + +datos = p.read_bin(ruta) +app.msgbox(datos) +``` + + +### save_json y read_json + +Guardar y leer en formato json. + +```python +p = app.paths + +ruta = '/home/mau/datos.json' +datos = { + 'tipo': 'calc', + 'nombre': 'miarchivo.ods', +} +p.save_json(ruta, datos) + +datos = p.read_json(ruta) +app.msgbox(datos) +``` + + +### save_csv y read_csv + +Exportar e importar datos en formato CSV. Vea la documentación [CSV][2] para los argumentos que puede usar. + +```python +p = app.paths + +ruta = '/home/mau/datos.csv' +ahora = app.dates.now +datos = ( + (1, 'uno', ahora), + (2, 'dos', ahora), + (3, 'tres', ahora), +) +p.save_csv(ruta, datos) + +datos = p.read_csv(ruta) +app.msgbox(datos) +``` + + +### kill + +Borra archivos o directorios con todo su contenido. Devuelve verdadero (True) en caso de exito y falso (Falso) en cualquier otro caso. + +{{% notice warning %}} +Este proceso es destructivo, es buena practica siempre solicitar confirmación al usuario. +{{% /notice %}} + +```python +p = app.paths + +ruta = '/home/mau/temp.bin' +resultado = p.kill(ruta) +app.msgbox(resultado) +``` + + +### copy + +Copiar archivos. Devuelve la ruta completa final destino. + +De un origen a una carpeta destino + +```python +p = app.paths + +ruta_origen = '/home/mau/temp.txt' +ruta_destino = '/home/mau/Desktop' +ruta = p.copy(ruta_origen, ruta_destino) +app.debug(ruta) +``` + +Cambiar el nombre destino. + +```python +ruta_origen = '/home/mau/temp.txt' +ruta_destino = '/home/mau/Desktop' +nuevo_nombre = 'datos.csv' +ruta = p.copy(ruta_origen, ruta_destino, nuevo_nombre) +app.debug(ruta) +``` + + +[1]: http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html +[2]: https://docs.python.org/3.7/library/csv.html diff --git a/doc/content/es/tools/tools_01.png b/doc/content/es/tools/tools_01.png new file mode 100644 index 0000000000000000000000000000000000000000..6395bb46356997dcdc324d976fcadfeee9b6fdb3 GIT binary patch literal 52339 zcmdqJRan(g_ce+lAOa$xG>Aw^cY}nWbR%5?($XCQ(jna-B3%-jl#uS)bl0Z4*~I=9 zzW?`oo^x@&b1u%+@y5;Omusy#=a^%RMTnxj6xI{sCrC(0SRdX?C?g>~v_nFAkp37I zeB-rBtpomhX_?gJ@-A6|iTT@3DLwge>GaFlL6D9{^dlM5IhtIZ- z2guFB;2@^^gT(Dk3?0pFZCibo?G5A-zQUAR(&inzlRd@)5ra(Q(b#_fx4aB6>*ia6>-=-;OD+%W|_6MKB5AAi@`3|;(~Q3`8TeTfo` z^7Cs_VqbjO9fz2ZZza+n0jKS;<)DM69lg1R+OUvC;2Ihs-47NPh3s^6 zha>GD7E2Z@O#0McK7Q{RFZrN08mDD?YHCVVz~}t5tV6I-2Ny3RHYQHOVjE)=3gwu- zzd2<3WDpH5 zY3Y2-iX6U%s`~FFm&~sZsapDxSe0{gdi3)k5ESE{b5au1PZ z9BjMaHF^3&rJThYJWwYZ8k(CXM-&s8#Fa2MDva85v0%hw|L~XcM&P{9V|J?p)Y} z_W+${tF~nZna6p#>y`BQMi5T)>%2d{r~Q*;0&anuT~T~?a5iL*i}vYhw#5s%$48lF zhua&Xdk|x&Ocmf zNsEbvIW}56UyI_4>SW3BLqP!-7thYgzSlneTOFUv{zA5Ffii9jI^{w_cwYhgGunXR zcj{j6iq@N|EcR2)>uaWm`tNSB2h*C7K0VUG+8OWY>x7%x$at%q1VWaoSL_-Nt}aY(F<)Z^&nQ`x%` zTO!-M%sS5j{pT%C2XkMWgz5_?)abMWCC_$P4UZKhBm|Ad;c{Ra&;+zekYZx+)XWE% z`p34@#`(cYm(5#1rqihA)c4|lLXsNZM8hqdp>H~{43E&Z(rgsWd&jV5U-;~mLciwk z0ROH?m5Kl!MCx(+Onv(=i5D#TJzJFGV)_MOy%g92;e!9Tt zG3dzOj0+EEGq~-^`}gmcF1Mgnw)3Iid>GA!zKRz*W=5tye#_?K@+N^Lb*sLPBe%k0 zo@T;&Aru1PVt6s2mLnr0V|;S1t5fTke4S-Tm`bQJ2lc;e^vGXd+p16y3vTVp{-|WL zP~X;04^0v@Bz^MD&o=KyZ20zBXkEib?q;HB+KniE!0kVa1KaA=sEX)x)kignnS3Mzk_umEbQA!$`gmS!QsyA>pkUc&orQ#5JX0|L{ z3x>P44?nm?dW?>N{6lM8I*E|Tvc%zb2Exk5MoCTm?f37X?@v3m){l~tQ;aMu(qQ}% z$NJv#^76|?2R^S^FPkjXjFfRT$`;>7mkwKI20Z&V>9X2&JP@T)M}gbfPLY?TNcZ?D zdPhf3$cI&tp@APwKMl+%FevXhgb}WfOl0C$3Y$g363Slk@qH7~C>IBDGYg6@`R)%i1X@&?sEuT3TA3lk&E==JZWBB3_csR&{pHgmx)pJXCBS z=YrPL;Xj$v(S3}LMRocY8A*6^F>!*)^J??t2&o_L#=ek&Xj0}ca+)b#tcU=wPe??> zmT${qp;0C;>&bZW(Eo^ezu-i;j#psIF7Dg217ayOH1C`BqVTuOoU2i?X0%e?m!_o5 z@^#nVBo9Izi9+$jKCPVHQ5H3cd9CZz&#-$42oO({A^X}wB_)Ks`%@ycYf13iE0aE2 zS(l6-BUlYdU#zbe4w?6I%BS;TzrE&W`dv}|D&0|w%eJ!@vJ>Ky@z&rAE37YGDPk$- zk88Avmt1%R$DOUwv6h$q;$;7Y(2f%kV!`8(6^ZFKWi(gK0Vyqw?C^-WbW*ryzElj~ zJ1|f(i7+)^n1qDL*vcx+gdzty8ILxg+b5$Vw9ux8d4kUNP>3-g4p=NBdDeMuy(UuGOz8EeqdkVk^?nmp~xtfnjYrP$vvLZS* zRi)CcvwO#(KRhC0;bR`(Q2*HJVzWV5ZT;vkL!pS3zJxdETZ;Yj5vet+wUD$Vp4KqD`2&`zrG|tjiD%;)ZbUxjx zj$gpoz`$3zU=3cP%~`0=qw(({{QM49K>-NS_YQ2d?#!RXUJN%rt|EFgkKIG#lAt#~ z_0ne>j0XYycyMSkIXQWGcP;qb#meMHgoY+E{rah5gS(noEBSBdK6Ru*wZhnHK>p*VXZvNXHwz&CIYUEA5R0Wf5_`<#AuzJNh(SpOK5+oIC;gyA}jmG;f!DOLOj^#j+7`Y<2bUG`(=-$PKIzD!j4cK@3vl;|t}fmr_^ez2!x zW^VdXbpJlV2)gW=@@RZ&L9dKn$f4^Udc>-d4sLmfdF^b9k)ee|ypj4PZ?ekVU(B|L zPZ@^v>)Z1cx{mwedN^&SUP2u!*ZWOwbD8>6gd_O5ERlXYw@I1~BzKiEkpL9uqpgOz zw6vUUwNM@5+i>w-P6l9|m^cQNz&kGwV@u1TXklUD47ad^FDq;GNLrUy?Cxua{fV@% z4Gc{Kzl#3tx|O-p8H_RMtCZz{HP{|mPDt$35C8tcZ9Cfu3KA~yi@@k;O5uft(}UR{ zeu%}oRJ;SMfx>5{eT##1s!nbP{$t6&9hQVZ+aNm4?R+&ixjrJ(gbLb^3^2Uo5C-yUZg{ z^?j~|R%v8E&G*7;c?FP(62A_m33PQ)1&%aYnyJD26GXh=n6$J~bMp{1d0TCI3JU3~ z%g>Ao@*fP(|DaTzp0#z(Xj$rk)Uk5qaCAzSTYYo8(0FOk-o^Ow&l=+0GalH6KHWa7 zBL>BRjf;(ba~lL&H<}x9yO8U--(6psoiK0UHnq-Y)ht5igxu+M4o=pVkr~V`_ILT( zxsRiaV|g8!;ugZn6on07|KNc{D?o9sivmJxNm0ssvN^@yCtg)OwlOBA&3#$NYwHmDJ5mJ@EgU+ z3+7jBY&Z@MCpU$rO<^GsFz?TFx7X*2JDwV%I;mp^F@o%`>L4FG`X*e6qBr&84aP5PBUy45`d#3bE4NPfdHPwq8?HZ$T)^Qu)1T&QiH( zX=o&kbrF3fl47QP|zcFaMx%3r)47>BzcL{e8za1VM zR*z0ch$BvPG$w^m;LmrVqjkm}RwdR*@1>=U4u_Y2#mYyO>Xuhk86dLS?%%&Z`AP(Y zDeBw9+iO>RhCnColVgkV{J|b*9y%6Awo}*(UOwK#lW@&TZfn`DYU>BT%OT6>djiCL z5Z&s4@H&%BFTM*JF1epM@`^sywnQA2_ zpJ(KQ>ApztGq|Ls7buwfddxEUVk-RPkVvCr|G z%>Ws}FM8+Y{&l)&KkqZyNqA1d_Ffu+ld|z=gSXI7@+r-fqn)e8)juO4NK z!A#_q+hIk}mt|Ue>FEk%Hc?1pG@B9<5kFL{akkzbE6tHMv$f1~s^j*+CS98Q7RH#u zlPdlE&1bpn9q&J-(y&la@K+B5Id-PadGkI<)UlPYQt~9Boome+Oc+y ze!PhOTDUOiSgaC**u!?;U@mDYFhV(Nv3T1o{83BG{$|WPqDmYMYwV-*Y5Rxm$)fBh zxO(Ovn}r`->=!UcH@Zek+uKW5pRsYXp|-Tn$xiBaPgIjN^M?gZ*btfM5c`+9jctzH1JalAKIL9ZEp^no-p_Ju5n^vtsx+>;6JR*PVY5*8~wEVLo%1n>+rh z=%v3^4aGUT!QtuYXw7zVJq2+B%;EIy#jUGKQNqCe(&|#k%yl|hmynROUtO0A^kRK) z2?s?i`gjE;UEq_Kcw4HT56zr!lh?-@L%i5L`L>Y-VQ6%B+CG#aLHz}ig~RKEM~|N2 z5#%Kd_O^;y+vv;tGt){*NqX`IU#Cq?@SZ$i8L`Q@=D^%%E_@RZgL-*8yd<_ zv*#r7djILHpEQ2Nf{pFG_~)I(r`I;6rO62{0|?Yy8X7lv@OQv0S|MZTUaZP>2no?^ z=W+%`k>-K7Bo%fiMf32mm>BjvPgO4XbTY9wR;Z`Dqniic94W!LhexH2*X&p;_Vo&y z$qfcL8_asD+UlT6CG=-v_@<{bp`)XN>}yVD9v(JCSnk-nvfLPmiaJ)L67&St2d#GC zkx%1i5>0z1hHuaJWaWI`Q)~+Qy0dD?TR4H6I3dZpK&O3d6kPNU$6B!ZEBj348yS-v6O#oh-XGZ_Dv+W z>DZxN&SFJTOM^?-d=H1&nDD_|L*nLUSJmO!I6cCHN$VL7P7hp8yFyk2B2Dd?%5Bd~6dhPD+ zR`d(NHns#GV|2A;6Y|*38$l=bs?5jP`<9lR_GTd_7k9Te{4V5QMMc3PLZhNWF4a5f zf({S(*GDv9?Qf42cJ%W(933D2&`D+| zHYX1Va#){p3&h;b+{&L~;V`p1JX8`U9L~)0N3*AW=_^VR`Qw)YdZ9*!&O+?Mo> z?c7p#Ii6~%>emcij^_TPRIgroz#R!waD>uBbDho5`)` z?QQ~E&k|2xHE+jIg|LkNX7#F(MEgey9H;7urseq)DXcD*xG+n0yjC1&tN&5-k2*L~ z2WO5SS5R}weOQBTOw)Q}yvn5ho|5}3JXJ^jIe6lBpD=KGm|CYv-}g1Q=Q7LXlbY*k*+EBYs_`>qg66$_{ zL>CTOY7)iS$aLjK&m+msL+`zxC|cP6X2b7?%QKBWC@K#LUgWh%cNBbc_i$V>bUIb3 zF5l$qg!uive!g5=68pq2bFHzmeVhM~BqA(QZcW+XHTdG;D8{d+_XGS4g3cr!d3fp9@YZ_*W~KQ}*M(EP7D>6|TNC+t%#GIh?mxvUxy)$q?6xP66Q1UWXMg*X zBYJ;Zgl=7EfcyBDbxE*v$)7y>VfsNM{kS7%Q^e1ik1jMKZ=bb5_X9X!LZJDcs%b~i z){zgwv%JueyoK=Jdn!MXV{reJYIesrPw^rxA$M&m0*$nuxb>GaHPK=PuRYN}A_(E+ zW-oUfbWWYGG#3b(&u7o>;`Xt64b03_cI+v-Xv)vmtjUE#%(i@cS)`U;|3}|E8m?^8PB=ZQ5H^jFv-t2gF|}IU{pu+cdqmpCW;<;+@qpu=>%`I~VXP zP8C=G*Ab_12>67QlIZZMqh)67^tWVIH=V-2rIz0p6wNDVF8w%4Zdv*xDWyV8aHtHW zAPo_TgZLjZCb}&vQ9c)$xe~cG6Ck{SNty1>11AKcE*YBn1*NafP66a{KlC2#`_y0+ z0DA7jztH=C{Ln7&^1me5&+pMLnvKghIWpmJ$Kn}wAf0(i#~BP?dll#u7-d*n?gd8l zr*K5+YICm`*k+o+)*=4~5^7s#aN1!bx$@A3baxk^l=@CMsOA7BB*-L|fmTa8qHvt( zY8h>6X2w+8SymR)r}QS(@{Z^66U^3$!N_g3LRPD8gS7PYih*XMGh!oSqpQ!CLHq{_ zddXa7%sV{Rh7+geV+ER2i#0p|2;Av4L>gaT7#JDF9L##CnC>mfzeNHh05qj*Tsqv@rT^m5m*xAl>?tl0@F}MlSUZIN zyAvC~WUU>qnsn+tzeO7ht9g~`W(N+!!seo1GUEZzo&_{jx_D_Z@wO>Y(Ay*<3~#0s zwx&wc2VnpWAGM8TnnMezv34ehx`;QTPEQ>J!wEyuOPJ1f# zXcSW3gI-iK!R}ff$${Wz@Jc4pH=N}%^{&o7zI?U0bACQOW4<2uIoZIVu1Br=-Wvzg zftDV&6EjTIpa9n0?2)P2X{8!@rW78_&XU=hX^1Ta0O_C3j&lH%GfffUW{!k0XpA&E z(2_+-FWB4J{aqM*N@7{`aQbdGMpxjytSl;6inoVv_6w>9$39*QB%KLe)K_!p_6=Ci zLN5qA+S`X+4xfcbgqttsa!Gb}rU`nb9C)Wmf8In6d`g~Vas?+{y}Wd7x;ytMy;}Ss z+E`y@2~Ao1^}1B&WTRzSJQ>xlo;;Q&f>^nIK+ci~g8(5aYlR81Q12QYjhU)-U1!m2 z?a%b-wMXb(8NBs*qfzV|QUDjGkC&dXnQCvP_5mKTcP;c+-rP*)<|#0gLg^bHBfCJ2VS75}qzU_D*l`mBb?66tquB zNa)O>>dODvJ4m2uZ)4wa!ny7XT>nxwf>(AntbW?~_ z@_u{9uj_Y6=fw1=HL|gJ`gRo+>vAp3?xxO&+E|1rc8eP2{- zJ)_evO_PUhJM5pCo7E?z&Q|9gu%%pF8^xJR-@zSlCRqIMyc^w*Z{rFWwaa5Jv>?Ae zDqYCdWDT)p2=DMb+@CkTv*n=I$PJ&Vud1q&J=t=X1>`8e%u-Z7_R>!(H<*6plu>X` zOuUs;JRkpMTL;jgNnhMx#RAejwI;u(Mcg-OJ=dRxf)=xd-%;pC^8WY@>;<3F*#=0&&|{i=;bJctbDKIoGnT&fLRx6h@fd;EE%% zm|KYS6hO?kTv!ITxpJB|;*-Xt;GWmmWTuzj{}MkS{cfTdGiz+WG;9dq@zzY8zlG*% z;8{7~aIZ_}HP;7vv-9a*Aj;!P(*;JGZ7E6~kbXL~E(;3{H9e{@aon29_wMj@Ijvyr4{w{nS zB%3(`Sre&0qxt*v34xKJO=p%+!`SFzty?Tv!-YLy29VAy#*N0h4T&8bl~q)(96A7){US2(TjFtt6x#OmKx zQ!XYZHeacPIMb6;dGEM8r%z+5-V2S#W`0FUl{50`)3FkjFhhT|f^=kS2dXo^AUDy> z`t@IW^;P~F4+4`5H1}QJ_tg!>I_HX~jxx+!RtQLLZEZ{=UdB}y_#cw$o$I)nUCw-RktB%2H=JioJS=_i7<4)cg3aC=E{Z4#aL>9ssIo1GmJ z&=P;D8pT1!6DXpqG6P8E*G%#)p~ z3Gzo9F8)B}e6Tol8L}9Ykx?ogo2$xh)Sp+ZSCiomu{=3CT;}EHzmGW@&x=OMWZ-Qe z1O$9(DYNJ0-xk@v+l^k6pV<@+x1Dn7k)j~J!2y?Uos7U+@pfom*Qqx#zFR5^NIAJ@ zEE6kwzj$SXNx`1v?zHpU zWY&4cd28@@l%%nVc?amvMoXP6BQphi@>ZK($rY%=79nOV^eoDs7pt+1B_n@;?6Erq zNiC!C%5#3op1s4@j{zU!YTk}NwU^)#eB}o<8?p!b4j?f2-pI_>4TU-9%A}6crVHag zK_P!PU*~#N(OGXclz{`{4<{Nip+D=Q4WbeEMQC0k!TS38uiRXFz)|;Ul;T`hV0)&Q zdId9gt*!6P&i!!;v(Z}5Q!mj;o~q^D4+=D`OYfh=4CVOs%_F>|BtmGdl6WyAW4=Zn zH_K2!Kba(t3p0Xpb{E%?(!8c z)+SdHFk8$`*8=Vs;>I%5dwUF3IJ6v%1H=2N0RiyFdhhto+fM4V=(fPo8jT^V6BF7u zZ=Mhk5R~lbMaT-dAxv$oOO9fr#?o8fw7kz(&wF7MvSvLT*?Gy z(Q3Ln-CX4E$2mcMY%E^$V0stC@(yr9=)Sg{J_Sm8N*AyCbq@s$vB_7I7*FbC$~Ntv zp7=F64Me#&5>vy0=BL^RCW7hKlZeZRP+q=%ajRf&ZsVN*G)HbLR$gD1-}w`GDcRI3 zhFee;1_u*%cx(a1bP)GOGv+rMwyHVgU$o&wEAjI2e_5a7^Uh!k3l4t&a@Auo+tN&! zTo{vzDqFTDJA%3E(`s(XM@`Ln-IR-|zz~;0=m6n?|pvkewb)IoXON7mzm! zg$veJAxNKVaC7;z7 zA_{l%PBoe*rgBq&w(Oy&Pipd1Z5vso1q2wg`imF*wtWq?i8C8BT$aC2%bJ1BNagnB z{`X*X?i-Dcaxk`;l|S4t=r8Sy3)u=R`LcMh;O*VyFi*~=8BZp;}%9)XRGy16^Ly1L`S zlZATeOtaO-Ho4w>h3XO8NCCo+RZ#3^RK59DE?x@C=Dn0pbV$;#D75q*D^l_gR4vg# z*U-=?W@JK-3jVYKs6?%r3}A_QF_2yYz4_Tc=w&mTTxu;n>Bu(%agciTib}K58#%Sc z2ASPk@jesd<;fS==pgMaTI)uEWzI=Y-*dxlOQ|1DBo|+(791633>p`Za~^njc%c6I zVX(7TZ$1$nG zm&|OG(HlqgL${+mw+(MZ;3m-*YnY!_hNF#v=75Qej2v1|kR`h{TCBsZzQ@u6(sA+p z?+^?E=FuPCLPkbLwVr3hv9xmiFMJV=rpg_1&bgm8E4HpbV`vYlv$SeY!tteq>$G zj-JE7G`h_l0OKeCVD|r3DjbLF@5Vp7>39y3hwts}b=*E1@V1`~gcSM&2|do!&h)m} z2_v|8y?N)AUXy(v8+sRguY9`GYGrTOP1wyM$ydgfk&6)?C?ehhO2&_y#jfn}8MX&U zV)KxAosaQNb7?q>%NDU7*1vZ<@*|+9*P0|s1+s4+SM%BC1d1gCgY({eVv)U0S{kTp zuQmJbgdSrLyDm#NFX!_1(gwF9 zQ%6D=9F!o?<8-vn<_x+WAnHGFX)HC3*anq;@VN9;!%>I^)Lfuzsua09mAJGazit$? z<@0*$WLLH#A?z+;p!i+y>Q7@R9_YFD1RhM&4vOB7&w1Y~#QSH7GEAOce0In9;1X0_ z^24>F;t|?oCt~G4-__;hsJI@jpkTMQ9v+`|pIC3V_V?ifVUS^1$=tZji2^)507K9Q4<5+~ zQh8wTpi^#_+*T)uyCcI<6?=X5lGJNJ9Zd;p2 zv4U>!IDiE==n$!0wK`j)?VTMsxWu{9-1Kbd3=Bn96e>T5*!Udw1+iP(wq`zD(~-8E zBvO5ua2FK3a!Q|pyvtH#wLBMWJa)3mWFv51Pvg6axzml;xUJ~c$8Cox_pLYG-{ORT z#cp}!T?E=9F$oFq#QlJ9j7ep~&+e57AdwX@EdmWbq^GCq^khJ(?mtRI+OI^sjK#QI zReRdE*ZOfl(cMr|jhi9;z1s`T%_(DXcCKdBJEtAW6wzR6#(UP1mEE;DRa&BDP;5#= zF(956ool_ZUc>`=Fj1b2m7Cl2?q`R^$!#Y?8`p5c!!UfjF%|0&=Iz((wb_{VG3G^pKLt+?d<{fCVjsVOO$^Eh3o*cVKYDT zar@}43TUg`%YomgQ|>0AG@W0u;urQEkIg-Wzi=?mFzfQfbUiV-lGpW;jskPD>?~f$ z<>ABoCRZ9B_ro1hh;Qd4o?P0F=){U76%d6)=^! zQj{s$^Z2>L=Ir>z;Bpu{iNlAoP0B}XH4uxHmDQkYaK62>^Qfnb!laJOt0@Wu%6+_u zqn-~Pcg71maCtzdt$woHRuA87%c6tES!(gRQxL4O-~KQ@X%x@xiD};55+p3I!1STlCPoj<%sh$u`EF}2%YS=+I_=lL`Q_-#R%m8? z68MlX`cTl@TU6s0tANYVt89^LziE!Vof{*=qkTetdwkTuC*GMH4L^!vsDSONbF^9g zw{4fYo70ql?4ZivJx7=8zyr(*bbT%(lJDL@o3ydLli4-*<|kM7h%N?ROi?sDEi*Q$fcHZ{8d#{7q<9$}4FZk=S)){M12je`y78oS6vUvY?c);uSzQx^ z%>i8+Y|8q%=TU-D|3>VO3gFCgs;W4jk_wB(N@AV4kCeN*fb^a%yQRzsUmfx*m5u~P zMD08tn_xfc4SF-+B-d=T5%TglAG=cCPQ;^8t_fjD_t-vX6o?i8*03xaJpFC%BN%9* zP;}D~`1c9WY$$zWx{`3*AwgmpRFxJW{!;S6-id0wvn?nn;LO)gtS;fCc*L|oW}jg{ z*gEQ`L>08dc`-RL5xAJEpyI_H-9Wn-sw^yQ(KX+Bz9($7(JZ^(4jcW|BVxbZ&v$M$ z|LZLGKPK8griSKR%0DA@;9#Ai=%U<9J~wDRY5&=6B=#QcG6LTO|Nrkptxj-Q7|Z4x zAT-;3tIMS8cOqD6Zpr__a9GOcvxS>2OxDMuLIbTe^p}MWq;MpjrB<_A!a0HE`{;#b z5(g+GnlxqT1h~kfsl*VgpT+pAtc(ERL z$}F1qbPu>~Ur)jo(H|SYQXqh5N8#X50ZVbP8(XAhVuE|N0;3gp4}m5@-cW7T{&zty zPjH&d|7I{V9T+0-Ll!U!A6z9~4-Q6-m8gY@-Yx<`LX-+LH38gwOeec_C*fB{q~fSp z2HhYA%2z4M)tdRUi3o5d0F9(LGcz;b-wlne)rQqZ zJ{TFXBl#~J1ZmhQK%8r407bWPVFVO52KcEPF>v_FPj+&Q+q8_F|k)F z2p*2E>kO3{c*w6`uf3Dt9M*se2Cfg=M)~^Q17T*~*8zqU5uFQzk@Zd%BY!kG?yVK2 zUe~-JClw{Kbc~KFTQ2;+kTVKT>$boUCEgDa2>BnjL*yvBSn1T%)DMUAMTPGDa1Kp; zCj?-JlzVRi^FVlk%Eu;)_z6d-*{x(tqX)1K(!hG-l(Z88`(AV$FHL>#w1T$zUo{W3 z-n8SEa^-rcXWJO0-w*3kznWxn3Ae$WOeHOnB3DttONJOSshkomp6dDY(xZLEGLSzutZ;^_qtILH@&+ zx@x5#zanl=srp_p{Sdu%2Y!eMPT<0>KnxoOViOe~wLQsq8s?)%tZhL9L_E8LC@@uv ze1K^6S+R_~;e3}`m{PRt);)349`l%^xzz6<=!YU5o!Y3BwOeDolOi3yp!=zjuY-u7c)g zczD?f2%qMGT({Y$#4u*i-8!XS#^kC#{B7#h=?t7-ISDogU;Xbd^Hn^^sqmmkzQ2D= zw(Qnwv=#?zd$Nyx0C=C-c(`(MGBfo|cC_xw@$!I6@R##I3h~ZO3O6Ms8#-W(`E5@| zVGN!EZcpZ~n-pRb4n}e`^u3eMiaPnU8Vsp`3z!CeWZaa=9$%f_z-F-7d}1*gC!K(3 z3>9sY9Ia9#;Xm6Ph%<+NaCa-#ud_R$S*7)snPr}bRHNxeimKbE>Q8d7Z)wJXl^sbc zl9jLzk4XKMm!|?Wn(>Ocs*wJ*U;S}$__?yFgTCHghji+y2iA87lZ*Ml86DjH9s*-f zFS{>b>Lt2))q+5AgAb%sn%+vjt<2r2aNxR^PO*S&J}Ise>8`2@9h(G zB^(4g)RvI+C}7r}lJm8)aQI|vK`cI|q~hU`^Z^^D`Se`kdecA!%cJ-1R~qK75|92; zUX)i?bON_lH8;2W!D^~ltJu-$aqHY%8sJ*?g92~~2oyqaJMP(kr>p2FawVYi?Yx(< zW5HG<*4>+_OpGI-ud+<--;3mn9a)G2J`PdO;fOJv)-Sqrf$FXa z{#DnUyu6d?#ML_cJ$^tP{*cK#2SRNC*j~WKp@PUFydLLq+?k8UM#JuQ^618{uPz6! zRHarAED^a>;B)G7+#1gC_4s;o2py`jJt0>sx*=(YNu_pwNcOq8A^jy{JO62pyT)Y6%!BS7KR>?@($YU2=`&@af9Uz{w_2hVFaG%^g!Yux@$fC=>}nc; zuJj#XEHoK{L6uY&!~ctS*gf6^pdp^s=3j`VEXI#jN#(|pCp&3wgd{{K9kiL7I)g>c z5+d+5j&b&HVxlr4`jQcZfw3wRE`BD|uh^f^N}&ycoe@JErZ_k_1%)0_rRi6$m#pF8 z_6uV_fByVpXE##6KTc*}rr+B)2<|fWQGKlOG0>*vr~HFl@f|d@H5#SWn(pk2XDS-e zo_^VHefMG@RW9er^NP8JKl*0M*;;Dj8*^1bH$N;%fT_}WIZa=)b`5*IU%xx;3gf}0 z=P6yi)7CF_Q|~HtuRhFLkHqH5QseXREwD3SV{=NiO!=Nbf&g;0((H2a&j-LV?25Q1 z0g5hvzh4M+Uw}~qWK=0c(8EIjj{O|*UoXJ+cmdYid45Q~N<+egquYq-gzbVDu+86; zf63TBc9@hW1H(Q+=rlgL<8t7#KlODnkBe%^lEMnn!!@eRqJ~( zO^X^BK|BE3w6EdStoBoi@bm401R9ZR5y~eqkL<2m1_tEz9QIPUfCE)OARrL7DK*>I z=1=L5R$(xLyK7}NU5P+yzB?c&KC87X{y$T)>Dg}5NFyU7pe~=x{`uy~X3D8ns7@)& z90{z#74Gw>yL)>?-+LaJ=>B;xH{du5FQTxfTAky9^|JVU0-i6 z(qKmO+npB!XETA^V9=kE@{5%ab8)U?J0HxkO^VJHk!b4?4VPi>a9gim7C;MM%7Y+a zKHK-dnr35VzHU=Q%gWyty;(zjgt zU1yN!`rSyBmIA05FKG29Z*PGia^onL774Wb#IvlNqq}nlL4hV$q<@RoehF<>DAWyP z>XOV>nUDZAWhiPGB z8Sx9r>*@oTn{E{w2S>KV(MmwTx59+2YqAi}(_r*PZw0VGPwN@Z&&*n}fa~6ggqzK6 zo0D{JPmL=5zXQwg8vGT3-&MUyo0g`gKg+%M{obWDxGGs|c07S;visO1z~G>f1!L`S zvCv5BVgz8XAkdiLhF-b@#N+o_6v*^^O&t9lJ;|vx2jA0T{0c(gO(Orp`D!K1mw&9V z;_Fi{{-seO|jS^dk)a5UeT^x=jYTE&MmHXDAIu34mzbD7(3bW%}Xz4mUr zu^Vz5Jn_tQ$T;4s8{#t~8_-StvNv<^1_m~T@;a?Ef(p>mGW`8sgLAhb=H)l|{JA^% zH>(wc#>#0jtEv3;L*Mn%Qbz4%aZgV{5HcNeb2`=u+qylB`)g~TVsP;ss9zw7izA;t zxoK!(VGyAz(`z&u9or%#BxbejbUZy>xXWm;(G^&|KH+ zG{T}991!pbroowv{uG_^7?^i=cgn3dY=oU%@@Aye;;$Faw{BSUbD1xhG9rRt>SbE- z!?Ah6p^-`rdkD1~2H>2ibh2#r&lW;Me)w3(EoHq_PW7A&1G96kKU|CJPS-zar0%^s z-BgtcgOJExZ$1uU!90aK`$DU0BQd_bVr6h%Ky&lm`KOCLjC#_{5keVxc^c+@N@s{= z5t)XfLKGO7-&b(wySr2Z2K#C9li)u**15UK796W7QU48e) z+BkP3_seoCFW=b4=`?Gh+I8@!Z(?qFjwiVhnzf(k^M(TLw)-`8(nvlrJm?ASUt!kH z#cb9`P-z$`(c#a9gtkKLjA7imK9VspF-h!RG#3YpU1nQReD;-JqoZ+H^g7M7m}$Kc z=tsgmRFVs^fIE!=+stzMaNf*Dp`jGT-!3BVR%Wj-HaCyZ!#0Mp{p8%5k_Dnb3QnJe zp1S%0$j5Cn?f3Po@Y(jnhyTUhTL5L5rGj#67Wsy@C{S10A>=1xk|%$Ti9UO5EM(N`82^p9_(G;)L7CGrgJ1P!U`r@d?xDz~ z<0HkysgnRgg4UZiZ(ts7CAN(BlwVw6KCB6jWP9G>?y(*2NqUw>6Yx~2P_KB^*3!B` zcV9v@mj3GmcZH^rR{ikq;_>Xl^P>F8j?0gHH|}l^ZzW<;%6RL?OK5a?OJ=EF-i2+g ztuR~5<5S!@cV2$}uf#awRAI%PwaprK2QF_y*R!8<=9%bAcSX;tcjH9w-POiHY? zI2-#aE6_Rpeh}txO6Sz35XEBiQbHog`4nwyZFth3ct0|KZM4r6_Oy`Ovym6=MxKEL z{Dg;cl}_7VR8J2iyBj@us;pd{UQPe<$dIm}P_|v08}8NZ%T|t7E-@|87a5p{Wz%}$ z3Y2-^V0|;t;eM2%RB#Q*K<(7-Xu~gKA^a+Bg|9g)5G1_5nUGj$IO++RvaOkUk*k43 zvnK((N7JLjEhqU56ju8*D}A!P0s#@H=T{HbMmy!D)4#1*T3XLH%}*>XFLx)rY*e3o zxnh^2w*Cl}l-*bCct;_O+NYr4YYSDY1>d9^H=EfK-Pd&I*WrOAkVNqLojEvEU|ORz zT$uX}L1ji8_KMcrkJ}x0+GwcPFjfr&F(gm(P;I2xM*d9D5@2@G2l;IO}$ ze{d?{eJ1dQ?K${`vdtamv0E)4`%uYcJr%*dsJ*|5ouycT3Kb|Tqoeeu2dqv!>r~oh z+^6SI5u$xjQV?QmD4Q!65EpNwJ)TRbKQi=s-cVL-?+(_~YHqdGrVDQ%GV<<&4YG#k z)IYLSe4b%8O6~^aK3pC6-rlg)Dw7*#9=p81F%i&{nl)5pOS|ea>|GX#Z)0u1sIl}e z^f0R0_`E%e<^A@Vz0pX1fI5pwBQz9*nY4pn%`EJ4_f+SE38-}ODId)1KrF$orZ`&= zllL`E%VmpKIE?yduA!4+zQzq$(59CQbQKhpdg@a#K+5FkL|M2|9!QUT>X4;e^~Jj8 zLRe_&na$>~Ah0tT2a?!4@pR(GL@OK=`7^408Ge05CjDhr>{r7*J19@1Dp;oB7WaKu zR9(zt+ilHos(FWzIgr(m|LS&22utF^iqj_V^Cj4yh()Z!iNC zkJ!=XoV&zc;rG&2>L_}nxzK(=f&1`;?p0HLLT+xZgef z1wUM6%L~h}-NEW^H9scb9MT(THJIUTDf@^CNw>Pq1YVtm1q4laY^ht*8~wr^y-d)= z2x0AZ|E=pUCMwvH^v5sm?IiT&FQ)hZf4?Zb{7`f0auII?!BThhkvI3lKi-zEPMMr~ zZ%^SlS!~=i+#m4d@-*J!GAXrHZ@oLX_x)nXC{t*wq49heaITD|(;7U>JpmHsR+>H~ z5{;wk)vprYR_>PW{2GGPMsN+b!{n7YS%BDEt>MQh#?f5FZmFr{4Zb)HS>F29`c$l9 zrw%M7n`4RUxp7c_q^b4po3-QG`Ki@vFDV4Ea5^tHVjDtm8^eHTKa+rU1gel4p!+JX zBr`f)b7{Pa5Q2TJ^6+io-_eo5sEY@Nqj;O;kmZBR^G#aekf-92404-HIhAz9K-SN> zeyP_7S(x8 zKZX>1vFMY1v!APS+E1;`T~Em&M*x6os<6BNz}tFb6kT!t-^v)fM29`eLtXUbvX*u;4S zkxDld-{FU_xs9GV=UlZvDT1%iljjA&v14i}4NC$ai8YP<2aZ;Ytv8@KQ!TTKJka);A3K9uy7eKDc8FrJX)Zd zMbc5@->KD8H)$X#R2s4Kb0NQzEPR??*MX1!$Yv7)${RS?I#xGoT11_|Ykv+~O2@R=-@r8zd8F{zCEvpefd1Yg) ztaYd1?z6WEAMyji^TpU>NQB@D-z0mglsN>bIv!ZmUZUSTIbpK3)_yTLQC~l%*&0YZ z7T~KByD2=Efs)RfRXAPAlKf>X1NPR^TLyhDlh|1E;kK-}lK) zKYDEMp-zZgLF{1|!Saf;vtmU-->-I+B_rPrqt;$Mh>bzz6ujnjg4j9uE+6xBh>J6s zS;!R2wpD&>T#q_JzHUabMwA&wn}{HjTbBqpwVZNrJ1@oArSEP@Vk<8^S82PrhPVJ%O*e~SszVIPjAZMc8p{_B?NHzv=uRVH?)RX9$Q?uu{JdH7raHrW3}@n z=jQbE^srKa%+NQwMH-cot*H2HQ?d0(P`Pq1wDbIQ?-y7-rnObIpIg}3U!g@dEa{XVc#n3m%+k1GfZrxamOb={QU`d{MmP_ z({*dEAm&2HyR01cc6wR4?-D-|82olYi2~V7GK23%l19>`vT{OMXE#0l8IbR=BTJ8_ z!+&Q=G?3;gCM}Vpl1}k{27=GRhU)%7{;Y|WzlEnHp{k}=R_y902M3uS&m^owvrc&K zEz(gfQgBdsN{l$i=%Yjy-9~rV!D{;Ssq8++&w`rx-J@nqB&_C0UenILdb|&V&CQgf zV*b)4PikCZQ}H~|XZRn!iMfeOqn4DaTu{O@k*z2VRWK_k#z4Q;TUX_;X5Q7lJK)&L1g$*%$IvD!r9%DPK`f_# zJOlHnH6#pwM?5xP9`g&BpH3DuFbvzfi~SHLAP>GooA7vw#zv3jp1f3`!w;lDV9?fUBZo`nAnk}3`JtL6|ohK?4F8s!hP)vEK zp<&jc6`(BlHt;S^>#&xR`qHupPaCWbgqqt3Lek|mHOQnWiAh2m71Qhk}q zUW=Dp@oUDKLc2;$xenDhjmj9N-MK|-)A5Q z+*6bdOMuvbw1$~!qOxQ8cjXdP+&WrI@`qA}_^O(nYbvH9wrAa%AGy5!>Z)Y##%i>; zlqb#X*97JTW9W9qF9t=I$h5%9+|^`BXKgT;rX;KE8xe&8N(Hp?^li|!8KB5r zQs0@ZwB|I)5$iXh5Hjts-7Uk#^eeJv&rz#rgcynb=!ojPNzIrM)uZ@A)25GgUW%~U zl$cDWy?46_TYD3b5Fxat?N5xQ2Tk`k`rY7dK5{xiDYx3I9Vmv3)8J5*Eh#9cwZ#_X zu_L|xpRb&=Tdc9EVLs{2m7x7DlOsA-=Y_Bfu`}u>z^i1rHqv1obuXF)@df}@>|IXS zzF<6RW;h^IcEro5$j%CVBva5DBpP>st;*r>woHl~_9@NI>T{0x=wBXprPFQ1z?0Nm z08s?x5*GNXI=x}W9D^__&aA_%*a2umZ=^JM4u?sun`*A$CGL(8(@%8Tv={3$N3G7kfwiuSsf#=rIc`d$Q!1}aGpdOZ`@H0@= z(e7joUoI?+$z1;xybcgB;b_WPS4ofLGNI-?+?dsShdwahGCf>F3Ufvu`#@FJfe`-uQq)tIl=kGWl`t zhB+GPOm`9IZcXfK%y9orp)=b0&$uV2iNBXrboVIy#}-J<-0=$*ei*h=%o}CF3|!eX zRBx4%<>OfAv2}PU-a6(_M!R>&AFSSCZ6%6wa&iI`z;QJ{;IH7@Z)PT-%I3LKRrtcv zZqJSdihW`YsqdLt4(9#)Tby>@LCO`YR>;NhsfQv1^;JK=%VZp0U`v@a{`D377EcQD zW-}CidJ^hXufQLye43*CQGkNn@_Dd{A;?Z5xf?|^#MK=-~S97+(VcR!V3)tQbK z&#tYcIsIg-{a-MMfpXch@of8>Zu&gx?SuDIKY|O57GFlA#v%qX@%Vea4sFYfy34cd z5%jZ9^v7J!8}9+15q+S_lM3Fuw%D4_=@Rh^3T;t2Rd5Z^ydIk?fdCJKSDH)J;h~wF zss`dgR_IQO(B3`P+$^s_aI-7^k+AzaKiSt66c)bYCg}h8@!n%#6zH9{9+cax{B|p4 ztE3QL*b3I9oOi8paqFI?m| zL3f|2l+q!IsjA>{E?c@xY~96da?s}7h$%ZU z=7M}~f3Y*qx$1|w)NXAE6eh)197;u{{&lY|NQBMq>4y*JjxsIB7nP`!MXR^iabEm% zRg(?Ldiz6r)#l*z6qArpB>16@OkPo{;S)4)PJZaKeivPARA<}35O$h1BRa$mXs@HZ zpQ@-a;f(;T1n+-@P`xGlqs8lM6N^oQz3DR*Hr(3=r=d}*xir;~I2CNd@l#+z$``{zioG3oA=`Vbsqm;88l*+5uEvUZ+2RpbnCz@aITuZNq2SAWrkr%9ju=HCp-7NKg@#-`7C--^PzzTcyV@;! zHLl}Hb*q)F+v}r+&3Sl8IJC-vhM2gO0;uVg*0APH)*f}%bScP5Dn$C?C%CJriC;03J<-y^J5PP6*bvTG zBbT`gW>OT=+1D9TVJO5*FAS?pg@r`t;D=5n?=V2WSh~>1U4NqJhHP#gQkSk#-9|y#(;rEpPit&sGWx9KWqD1IfQ{Y2-}ZRMqc2xC{me+mpfPHH zwaj@DHI!2B`n$ons#mYnv;%@z`+2_Kd9fHsLamS=l09l`!Ldekk7y;cucA?xo%y?b z=Z^`dI)Na#;n+w(Z9v!9ttv;WK^tFYhodbr@JkUKrnW~kG;sGhRF*|Xwy+BWJlx*F z23k1y<+YP#EPkCqG3c_pc*1B-;O^z6Yv`=-KKWM+)bxznCNn(e)SCKWd;i*t?5OXo z7<-$&cVQRoWjCI{r843DEzPuvU+P7JDu^PXp19Le+O3X2mXyxTrLEfybtKGB<7 z^Uui3>)4jddY8O2yd1AZP01lpwmvnD6hlN5a$6+|dU=v^b0vPi>94*5SuSlQQ-V&H z^CN>5ECCc^QSdSiRcaGncwMvFxb~uFbx=vSvZuO*CMEZDQlgqS_X!ODk?JlVtE;QU zo3w2Hy01X=nDKD!7U{o6Yx{>6Ko1|E*$Gb~?kp(o{3c?STXh}(wx5`F<8FrQd>AIZ zo`#mzEAT9M`}(-77T;M}mi(E<1=~xD@dh+8k&;Rb@pe)AxBFrw92zTg(*CfH%(Dnp!;ef93yV6+;7CL74 zA`3X0j1}`A2gU2TEU1nYYzgZ}W{A+jO5!qHh~IhPoS|WuX^FC|Uv7d}j#=N#FqN9o zI~sP6MKNn{9%PFWEP7;Q+~%n^+&U`c1QDtX$BhM=V({?4<+QgNDBsw>qhWMrO3Z91 zlMWE|x@QW{HJYc7u^#bXBVe`Yh)kIL91-%Whmg_5w1<_h{`YU8+3J%1Y(rfbY-bi- zg%&lbMMCB}A&jR((L3fRCYD4k64q82S6}~1r#-ewQ1*LQjVovV#SsL`6s@Eeja9hD=T@Kb z&5GT}@7^zmZkFFQkN=>R{rCR`tqi`mX>a+C1p?pnu^;ZRFIhlZ?iJ?S?5Pe-OD?Xi zB-{>JJIuN|#>S(0BJ;cd!W^ON8_%WHqpc6d?0i+O>E7El zc8aqnbgc67PI8y^efW&a;{}|J0s>%oP+Kn&dG_qt-ZqgaBziLB`okWk>AZq^yQSY4 zv72}LaNJ}wq>zjz%86MA(+pT?Qdk!&j-kK0VWgZ$K37$fDSlhf!Xp+uBs9)^TnnFQ1N= z@X%mEveh#xtY>#1pNL>7q4KGxN<=qyl@Y=jE$-W*mpUx`f&qyEj#QvWG=o?58;BqA zYTPm{t~F*Df`K;}zPa*x+Mzy}j?b06OGqFE$fqNYH&?1!yge$i!(zqZ&l;lsr9Rbi zx`#Z9#U!P;3i7|ysz=vED&>FZtaBi8?53tomBW);$A!5e^B_0tK^Ep`;uCfftol}5 z+Pp&QH#HMgR%(MO&prR4#vH6o^Bs}9DCLP75(B8w8!WyDHqLnnl*$z5y>I)CVYT@^ z${%r2LBAX>vn3V0=Jta|9<}r0d}%N3-40%kMk^*KZA($m3MG8I;$paAK1SHd}?R4 z-f?EC+g;x<&r8ZLV644IamyHCJ&V}>s*rES1CxTT%ZK0Qrj~zh0dFHk+9PeTF+v*m zn#)741sxRjak;l!x7*s=zM1NV+rNilhfmAzvAO9tr7?eL~ zHkljuu(jGs0rEnBxT*yolqDQ0HNmw-R{!~{aV(EzSj~lluW~^eyHq?v`eC48G=C^a zgmSC4uMZ2k>{Jq=24P!hx*>k^70)A9mOq-%ob(syuSd)e{9o$Nk-A3lEllFaNM?F^+1{F3KL;E8bp6S&C4A zX_C%tNJy!8H$lux9HC-?%|SaOu2;9C2R13qMv0!%OIQM&m8G4X~cR{m?(r`qWfaT-$LnjvJ3j1B*)TwCDS4%j_oF z{e$=5ru_`_sYpojss0Y%G2$%zqyO;TK`qxi#g7HjkOYbl+__I0$PM600% z#IOx*$HNU&QYno^mp^u`iYv(u9w$BZWDcQbLLhdV5tY0uv5LdEw~zSo#}py#^B)6cxWn@ zb@Sc^ryLnx`HB=**8nfBCijXBfYhJz_O7V)w%w>bsDDXUoGkUD_J)V$=e=yf#^Hehe?yn);+RzMN3 zy*3%n=g~*&cv6heiTj{a;kCFvZ>p=5O9a|mDh06+IN6-7j#9$+4 zem)+B2%YZjU7T0U{hvS=@@?VHBK@zTtG78Ye0_Xwi=(0%oGLL{lGCw8K!%@y?fWJ! zyeUiwI|zsp4y*g`VWG-*>Nhax&uXqW*IeG#mv8fp(9qLMk=lR>1dpfD4^&7$abliC zNc^(nT&tbYZZ$Wa`84=@8lZ1e)IK5J*#6C%BQj|UlQGJ>SZ5_!4M2+K6(>}dgk2=q;cN$r1QtCOD|Q3QnRy|s^yD61r+7` z!^)9J1%Mlv|ECuxtIGh}IPI3`)_G0pz=^`}+)#ZmFyrf!1v?a^$4TZu4+YM0ec zVdCK}^%lO-6b%T_A9_%Px_C1XWRgC-O_)zJ)H}tGhuix6nO6rbPdkVyWwUP80u2ZI zaSH*d>33%t><{2{Quv|u47yO}tcKVL*!tu%Yy&zi3}kZS-dE{+29WR{rBWe*M}^*E z2;JduDQ6vPb#1!N3(BVq#==?L2yhSSM%NK9lo`BGe`z12a{sONk+vds{a@M#M~Q5a zFm`AsNqk{C$c5>3QlwQNu?V&<4XUs{OdMx{7HbRb+}JCnpy<5Zzdy&RZSD;eo-AES+mY%wjt1c{z;~ zdzv5yc%x;wb+4y-f+mI5RT`H4uGq)s1TkoxPS2GD1n!YGetmM841xztQ*9^etOm{x zOn>sNsOQeV`Y&=vdG^0l3yWw+KO9(zFb_HlR^oV^TN*^d_2(DDvhBn~r7>#LGr?_qOpia%Ll$Mtcg`cdM52r`L?HW6al-x`ueVuPBPm$jl|X_ zOsV+2Bm^6xKZ?xY0&Gp69B$CFT$BVxF@KlNF~YyfFBN|U7wf)naN?B!LiR<`E>j^< z;fKos_%*D{gm{3OQp_huR|bk0UyF)u@A8IZJNy$oQ2hZ9-p>I%$PvP8&qIxvs%Tll zKwZhmM(h2{!iaUpLwp~P5I0Sv3H6BjIG8xvr6EayzEb{JhstwJ>Mv{GNkEgn)_~l< zi4&Tta{IwV;74P_xbMrCI0a^ysB)&pt}Z&kS!8JBp&rbc?7~~Dioi2!QC_r*77!4q zw97chcvNm?RtOAM9JnF-|K)f5ZpbDDk7bwWmr|<^Lq$@#93w>->`JZJ`Ls{XiFF3E zvV4>Yb3(m&t(K{h{`}MQ*cU%9B!^6ey1`yn9OrG-OZNJzG{*UmRxL{gHV4?MAjzm)V;zD=u|ak8{3pfX2=<+h*aF zzcdY=w*OAkkauj50tVBW3mIsBYcfVMhBg1)GiWrtaDYV5_PtaOFMO zU&+YJa%{!PQkeIHHJpI-DcAqz1bYo_U4_NjK5wRfWgV(EGsMff$8a>jb}o~bUEiiJ z4cYwH3)p&ZVaddC=Ec3_7i1%}i9<%acr{szzG|TzImJv6WMkYtI4B>MtDMOKavFr; z`%w&J2eb4sVT@}^dp>%kJ*z`aO|1t$l4<^%y6lmNI@f<0y_)ZWb)!Dn(00L<(~p9N z=JFVi!GJ26^`#yZCbKvC$Jc-H*fsyI&bxaJX2e_Da(|3fQKhUZw_Gnb(ZGFUQJ$SJ zg$cuo6M|$G@ef@31EHlx~V?De$?wD1kLhek&QX5W!p1!B`!UEoJUzq-9)>ayi{iZjw0uH%ZERuuZ z0v12^>E%m&i|%sFu)kVWgM7Ji{~wMj>gY}f$H8;w{S|C!-L|<`dP@j}0EQMkE{(@z ziBIw}rTwPekh{U>LsH)`&1}v9s(Wi;;AHOPJK&MQgRQdeT&|DsiPT4!k5yy7KFm=C z?oPC}I~j#^y&<_Nh?(STwB`#0QTVVE3dkG!uui;MSa=*+XiN6gFEZzAv)E%3Ju9)9*y z*eYds{Ax>^ry)EauR#tj#X1uQ#(4=Rr^*4%)xPh|Xp))|UnGA$JUcJ%R;vU2G z2JiraN{+0(!ZtKmm-?Rn4E1F9Y+j^jmS=Ajlwm&3KPpJ785sKIs=uswk%pUXy+yim zdiX75_uQVi+HerVg8IWJhYWV}e7>Jy*`vdon-WGw)L?d(Y#4yz4K6bqi>p$DC!m+# zo5XHa*eLL2e82cqGKpw+r`^jNig?gU=xc$k(~ZO3Vq!9snoa6C)yH| z1E7BuIL4EzOT_zbj_j-Wcq2-zVXXDPFuG20`S{^vhk7kf%{8K-s%w^vjG-K-X!xG5 z>F;vb7wYbW_g+z$S+9ek-~TS5`CdZ2@bT|V`C;4I_*RTyBql2>t-asxBch`-Lmbaf zn0ZZ)LZ`akCM73BoF^?+*%FYOXWT@MYxr5QF{k6fDZT!h0+`?Vz>fuh$*?MF>}(cC z=e6&*$7;(6wL+i!KdBY{=Wx3i?*CUum`vWo1MAgqA>G>4)F&<`fhxGFO||g2uT4}0 z{Wcu*c0BS1Qkjj_OeI4oC5EIW!hlFfvgn`hlu z5knf~UVu!?`SH}1x+c5rIak;2jJhXM=5oc%blMZZ_1xUEqUv*(^PAa9OzGesYKsew zs%P)d?F`yo+#ZinG@utf(;y)CRi^7J;6>y_rncUviN?Pa7WT~-g@@x-t~tUq;VCW- zCYhL-2EPJu0jhVLDP?yc%vMhRxL{#tu8geCTEyJm ze#wQQp^b+vazFCq!rsubHhp06@$o?+VfQ^hMVkU?Az2V5SyZ3OW%w*wm|eAzFRG-B zMn9k7COp?)BSL;ZSs$5Uwzz&ThG=-v_5*EoR7Ou=jx!eM%CEq~Q|ip>3v|fIz-k>T z35V!u&?ueuuTs(0k-;L(l{p`Fl54exmO9;qd3TXwIqVQ_EQERG-P<26Ymx%S&*Ls* zEe_D9k|~SZluIMdkx^&5tBDV6te>e>vcZD^Gpnj14c!r#1y7iad+?++H3?XRvg^t| zK7#MV+*kp_?8;z)vTQ-Q)x(cRIU=u@kS-yh=tgAl}i~9t3xglo*hvUCgkT_65WQOCS zH6J_zcjLlAY{l7JLCgxN^Ys_ufy8$ElrQH6uvHs>Mdw?nPK1Qg(;2sD70tGWJ!{X$ zrt$$w4tCD`K0(-}M6@Wx-L|&2jI#MDjbtLAUd8OxL?%t!3(jK@*FgFvE4@$g>B89o zntJ$PR)`%1-p#i~H%MYcdhFMy-uD?n6J5Q%$=a6kAXYyjqnG@1bpm2y>6uvu*e86k zjbwN@&v!}9Wx)D0`m^o|oayuQa#x(uYU;(_{{C(Dj1g8OXlxQ)6`LK-B~=%T{pFqL zmLXq)8eL*kcWL&a!?}xMdEan;P1`|V-594w{ydtJwdsWbDk*26=J`p>97TJ2EXOye zjunO9E!OtEqy8UR`!f_VKRD)H7AG5$dZ0g~4Sp={wmTh}%ir?*+jO1X&YbJs8tVtx z!4xl71+vjJpDPHRuNK94zr=*2Zz*NJy^s;zQ!bH(=+7ONd7nTZSrbds?oZGK{OiJA ze7HAVqReiY0S5Mb>|7uH71xto^%^-7XgE#NcuiQ856h-hj;KCZ_T@&?MXL0{Gza1t zCzJ$gfJTP0)Ef;Pkoig5aLM{jq%d-k9Z2Nq#-?&#%a$pREFUAdhS*Wpj&~_9!E}>0 zMoLU9mMV;G^_`bflPr04CoL=RBTW1ejHW!?r}q+6({oUWnQtP!&gWC4p!)L~@O@#s zN2Ag=@vMu#dux`{kPwiVctzmmcf;NM5xJLDpYRAT zl?5Rj@JzlKvd5K zX=zyVwKGY8xjo+fc`X$GJJX?tG^Pe1IN9RG0X&6b4eaD5F`46%cg+m*7vNh{>a=$# zqsM{Q8Em(jJ>k9dR8sD5W}Bv0tNcACoilAzsm_I^(z_zNq2s(ICn+{q&c&dfje z>BV0N%}eHgMrc|G*3wZ3gm(L%2+dggKOnSzq5l=3g)cj#xBUyD^%niZfc9?)?GDoa zWrP+hoCH(U14K0k5N{8d##Ph7oW>N^X+&KazO<;iqKI8ydDhKV&FJwiwr2fnG`+dHvCO@l;;Os0Vd zhBPBWCb62k1)_mA)y+()RHUK5eQG$_>HqhIo&QgwI+>P5n4v}gV+g8oCo;eztE*34 zwduuw2y?X-8yy1uzjHMbG{Rrx?W>=Cv8h^mJb2%NC~P>8quF{8tAIt-2olh@kiRW% z2rDZnvPTXbCD%RNy8shb3IzU^LkUnN=uXoXu~{Fk`$XQ#aAr^f`I}?`lt)uT^b52n z!QQ_O{qfm9Evq}_NGt181QE8vfwxERyYItsD}K6{E^Tq!p$PFudeWq;GBUH#tGVOM zf|B{WtY&{yQPzq6JxSY517mr@ufIGLH!Hpzkd~0m^*zI(-Cux!Cc^v{T<57(#v@d9 z&9?i{GA~=fr{MjfVwKK45WruJ)d_;|)4uci?mwX*1ggHNWgFZq`ffN-_$LQhD68)> zTpBqGYCb?gZC;xCEFp~0e8gC%X<*O-j=Msm34ai3kIW-k(Pl{UZBk1Hn7tzdZm8<7 zn94QQvi7D=_jE<>~798AY zyR^X(Q&G9trPu0{6EMBfwa`{(>5tFrLwf0r^(I&M&q$8vMKP2_{fA85k1z zH?YiT``>|OW+h$F_Z#c~@B*L~h%|y?Q|b7nzADjVUFpbyw!M4NVtyXaB0n4aELQ`G zn;@SQkQ;Wty!GxZ9j6vG;GT`ogoG3CWWR@4knMY+^NSZ-pR_3gWBUwy~ z)B=9Q2hX8K`e3pnIN!9iG`5rqLs=)ge|`i43zO~Om*xFA_2XeYBJhMjw50G0Z?2`y zn)sSc^j9R0G~tzx`J7_VeWp(}U-{zo{oGt(+PB$P3Ji^p+Wv9IX(9 zZ0u^^g0t#kr5SgUriLQS|Fv*1;&6u>3o*y`@X$6erD3TK;_>Vvq@6$EdcK*xfD;I} z`m=@$H+Pp7qx}~a5o{wr9|_#Q?{L3@qX>`34qcVgdJgz+ z@U2=N{mv6zJvu+RClD0vJ($wjKM@?W01ByqKYr6Xr=->9h=)K&$8{p^maT3zE{8LP zS$b1T#CNK6y6`|Rf8;d?GHTd%JoBkxcF~c)JSF!B49Fk#g#_z)m2Dn26sR5dT(NqBek)= zItbrV{u4jHPheo<$Ve2#8C+po+|^zFZ}b<4gIwqZ^=4O z^ur|#1iZzt50$^2`%OKUM3a~BP4gFWnGr_}(oSF5%-eT4JL{T)GBlKZ664|sE=MFL zOJcmI&l=2ZHpd0$iVWdJegzSO7(zCNHp_&Tvg3aXPSO9H(eEX9f6enk)w`-o4#Y^H zuYdt25>}E@utB|sHtj6a&g;3|Fgh)dHTf^~ghMV(x4UQlXniE`b)E+0De4WPhR#cL z=ur-NrW&OqMld^EL+f!T@6nM8I@o;DT z?8e$IXYI#vPC8ab>5PKQ&+iqm3KG5wI#Rf@6MrC|-~nUl&(a?lf@mihA+GYgv|k$o zwP(Yg12gGy^+w@9W^+5Q$$PmJZ{eBHIKy6w$N@~Nhge{a0$)?#L6P2qFr40wff3AP zCNmy?QH~gFhRb8mFD{@zQYyt8U)IQYd8GLo^n+^GOcG%NN@C!?R6LGN2*}aAX?02b zd0^4sWq|4SZSLk2+H}SQ2~)6DMx-9h%+4xKpJM8ExNqyK5QS37))$zPrnxfukyb}% zT8LY*o0h9El3BQcr4^yvHf?106-xj*g;($0H(ghgvC4*yamO-SUK|ippIs_R$UViI zW)1{{->&|mxRu^uAk=suQfk5SxeSNVxw0#j_Ny$9su~gJ=0-6Y{D%G$L<$G_4DEeF zu7st$JTxeaiMDaW0op&uzv&Ge=g3fS0#W_T^0%3kP@-};4b-bzLdcYdkTWjzu50bj zYPB@yRudF3hQ1LMp8INJ45(+iF0w;BKWYdQj>+ONf< zk!+prgWPU?=Pz||S@hHa2b>AL6@sjHuw>$g5RU?aU(gR1FJ&@HD4Jj;e@p>o4nf(R zw&rvXK9I#-n^@S(v_r=g)Tg2f*!0SNr8TokZ_xtTDC(89W2y;3yB$xH45RhYC`7@HZ7exX*yYoHF1bcUxvnO zBp60SfXNv+vXfp<{*Gj|vGns3xyW$2^AkwEb=M|V3s)J+I{+ihjKnok!VfulNPsaw zN}JNPIsCumn6CZ!S6+)!SDeSjwBbj$rowW?P2|l~j{WDH^rDEMRCT_#aDh>Sum(`w zI=L@j;xJ3qH)V zh|qjsCsfI@ z3ag)w+M+`xb*EZuhDzQ;#Hn`R3uDXn;|p1M&h1R`O6N1PIfuKa=Zb296bN|%@vAlG z7G>C&nG?m6o$)8utKVxre{tr~x`l&-1IKZw7}hjMac@;lj4q0gq@1)Ds>QO7YMRt! z($dX6HMvKqcfOpN7fm>5GWOuvvp15X4ENn=m?vCbOjdujDJx9D&LwNytD~8$>a)m5 zToNHC*BprmE`FRTCu@munsDKqS5WZiC}JT(I5Q`RQl{%@I9Xco4lZSVGoz_V)q6M+ zxHTx+d}j}TW@fg39&HiU+j0RG=c+?U;oEM}9IxqOan~8QqI}Je!l>E7nf=3LQ*E|1twtw`r|P z`?6^&N8KOEp?c2XP;aKw87mn7(aqe3QeqWn!)kx)cNd%FNu=5S2y@)oZcyW@y^(Rh z?%aMiv(Ag*?ZiEWYZ2S$L?#XOJDG>VaE@o8n!J>{s1bE6N1?P#d{3peK>Y1s5(b~% z=`#;HrE*q@!_!&J`$9tIHy@GT6``tH<>_gKVpd@J=!|!?vInJT(OUFI_qO4Dv7%Mi zWZR$(C+_!L4Sv{<7$|*je@)lYtHJjfjBv3#*~|D@fQfIcsWU&((sI8*C4=TBeLjsP zqotAYJ&GRgvInH7?L~Ph0r4FQ!T(_LBiz}k!UTFDjYV~(r>6V3i+H;VnRQ|o7HSq z&(;-u;as%0V~G(RuLqh%yIEB4%TmIbGkoz94s;H46%}1i;sb>2DLNn8nn0oM+YA{c zd9NrAHO-SmceiVj-I5N6+Ml zX7A{ZcjuFpLTy(MdRB0il?lsjmqib0V#^#4N!0!0y%#Artt4)RxKL0lesg~5PvhC> zPv7K~sQEm#(c~UJ{hG8~){ai(pm1X#fxeY9xk$)VXXYSwe>#)tyAvMFhum6XDwK<& zKTI-wl=*2q1kITGImqMJIqmUr1>ph3zbDOKA(veWf0=Uex==yPvyn6rClw-Ewdz-) zDAH)H$kc(~aMVVHpR zY~^bEweIDDMK;TWtbum(=hW1Q*S!q29WT7M94uTdK!i9 zjMVKaGy1yKu#^4ub{AgP>PQy14OI}PY#wsW+~Kr6Wq`RdCblJ-PGhMr@7n3v86pMG z`sk7VP{Nfm%V-Q6`k0>$Z7Rl&ukes8iSM*wbxUlWQoTo%{ zb81P!7Y^_F0gT*8iGDUc8GT>pDyz|4_iR&Nj_OzCZhE7=?VnZl()%9N&A?LnNgrFZ zss31Mulgw!i%|f%E{jy(BjP&=aEQnd?sMNvL+1cTuV=ydO|AWggLpf~LU2K(E9As{ zaV=kjraXirhaSBo>-(k_sy1Gc(Yi_??TI)Q*?#7Fn)4xdd=Lr$=s>pJ_Sp?iCucD0 zpq!r`3_Q)vR0sFS67L=(TrTm{pqS?9C#2ID7(`rFLeBlVO8cv=H9eizQ&9r&H3QN3 zo1$_U_j)NL9-hddSS~zIFj{7i`F<2Ua{{wd zwNpCX>6j5K2@kb@TEI9%AsQzkq07EnGefQ{5x4o(NoVA5Qz~(h(&=4k+IQAt`Y z5(oK7mpXx~Zb37+W_X5%R52}~$zdY*tl8lP$8V;IX#>K#?&*&1bjAj)?K%O27LCr? zWyLA)eqIF^G^PmG9~U&FN)bIsmNl1wrfK4XPcv!N!VUV%;5}*yoltEbDh(zyRxZ|R z%2AC$yEnUt-Ri&MmGs(g>y*G05x@kejNQ)OGt`3vCjB`9KEA8q#qu5fMup>S+$}2? zk2nz_(OKDMZKNFz6F^*6O@V+E`5+U5d6fO#le28IwGW6yLN%5WKBBCR3hE9I z{BcOLk`Qw)RVHq=nWod3{(LBxt?1_*{4nIHTH938Q6Iys-SIcR8_-Z$_tDJ@tW!jX z8{-YXdhRqykw>x}h;VTf4wm*v4n{~N+5fRYhu@2KK3We7K-5r$ZICi*4CyL;Ra8Cy z^7-?N=&+`bDS<#nV)+wx`@YAw-55ipt82 zt_RdN(MX$FrPFs?DQO$_hlhvrOp0**!tE^{KfOL#b($er#erE()cz~Am5$ZXc0l~i z1jC3fUxXCRyEzC2auslXKKyx(jJCDZ?RU)Emh|S4w!FkV7JIfa`}2uHEp6qV?^M}O zUM^X#tSL5%9z_9NCS4;=TxW{TG2gcx6G#75nrMr+-e0j`IM3eg#J?1s-6vZ$PBNc@ z#$z{;bc6AprP8Q{Ql)-#OL0VbPQn!W&SI-zY!vf7TpR~yq1j`83`{~l4-bz4?0UYr z#|%CqKT{o;E4jm6J|CVm>l?S~Z>Nz{jcT-%LkZ6|uai37a}iIjDIf=<#;rKM~K z8@xglEZO(e$Z6|OMF;9rr`~1_Av-E@rx!g>Z-@@dGV*)2$^nIhu9xT64;RG5@@rfR z(Db<$U#a^?zbMPs{w7t%0OSc26?wtOd7`exk0;&^CS&ly?>Hj{6T zDd*nGtnCsVY@rX-FV0G3G8@m>C#6%&rq@u#yitPh68@p$Ebz*H{6)oiefir5g8KMt zDOxS>C0FYPdcC7SBm6n1Bdz=4o<3h|w5N3I?%}p2;jtVWH55fwoRD=QA*>()g1A7~ zW=^DK#^hORr|vZx3e)G=D$Fc5e4b9$8~*8599yx|PZlFb-~Z8+2)>W zUF`xhR$AvtT;MowqF~KMTV&1WClYZfWfGdFuLmQJK3Ad5LRmz<u?22mMrxrL!3PaYBT6& zTJyM^_JkH4dwTzm&b~UV%CvhI6N69z2?-IT1?d!&lul7v*n~()H>i}7N;iW@mvpmH zO1itGr5l7zoV9i4oB7U}ncq3bf4txV_TKONyw9`Nz3yNsZ>U4Xx&vIfKvhE2HQmq_ z1JT?5!<+>c*HWpyAycwD_i+bPIwKX_|BF1=M2i@Vg}&Y6pJ@*B>x*xpTX`adK)n>K z+P9f`rkb^z^2je}r`kO|mCzlEn&cosz0a{jS*U5;Y&M24Up4DFrQxot9Vd6{=2omZ zW4NBzmF6NRBX*D*eWiKoYtr(_?<9C@)_?{Ie+-I!+cSOWwd^hWB)^TIKR#AfYo??!r#vY7aS4PE;UG_P4d7llwxJ1At`Zy?1=D>~w zLt3B;7Nl2p04K0mo`(jNx)8$&QA$L9>VB=$&JU~J7n}1Pxr#r5!~A3W*Fj`dG&JWW zBqY?!-DQy4_c+ND3!01=(Xy2#%jqA8EyFKV%py6OPJ5Xx0ZZIX5!|^Ab&N%ZvN1%X z@(N#Bp)G*0w48nuNTa0!2Kf5=1(Mz#mwUEIIx-FVMp1>YiXQv!$D6kqRkP<>qpSrS zw%=T8pWS!)u8`D@!9d22G0*IoZ(YK2^>NONlg}d1FM67IIAKxniDv3RDeN}#={+3H zqT*tTH{IR9?YFHiPe;$pESo(%sWEVCr=*8y6iJcb&R3Uqpk3|KbZISt*LD%h9(*Cw zU3k55zIPO^8E*6hS#wx@ptg`YkhwtFEkc97&Dcp=OR5V_Yizk;Xw)unWu&X*G4xoD z%|%NCdHLOkU&@@<$f+9qll3=(ZB|C#9y)T;J|KR3>gh)QEY=OaNa1srUTn6i^&ghU zOJBXpq;^*)Gb^XuTwI*(>G!)(DYt&2^eBa7di1qa5H-C95mC!97%M z&0TU*?lFIIJ-x&Hc7}>SDSvznH`V?}jmLcU-|v5Yi^sI5E><_&(z?HjBtcbLvIm1G zeN%ET2W=>`=BDiH7evdo%LO4lPmxHWT5lXV`2$Y0c5Vs>hcDrQda-{}xy)n(}B5iZz3*Cj~=U97;*u|I9U-ysxrFt%4{ zKG{B>fWw+m4O9X_GTvxs`?c;QjmOI?epZ4U{%=qARWr$x9i`(Iu?ow7C4phHH1!j4%eUpH z?TAf&FvdrM(wZ)c1a$PDAZ ze%CnPf^&n7M;Z7q|Ahh#TGrdTswBQ{=pc{dQz})Ul#6$?Ucv@zaCNs)e+WOlLP(Lz zOh`of8phlJI}kK#{ElD6`H%U&ePl7RHZOlvZrg`fs-J*M{MyUwoJ5dGF?P-8&!4}! zg9NSjsi2#9gz`8JkoeNPfL%a$bYMc^ru(V6ws@$8yt;P4 z?l4h1j&JdzWMsk@^=XJF1twNX_)| zX+MgZ??2v(rq}DN{{!mbq8YP|0!}PMh3V<7rIi$!cfFPs9O1E^V+t50*_>&@WA@b2 zhg5cA-vNN6E`9^WPTz?6m~WzM1(&+P)?AxN`n7r(`E)tSHo1i)YlfgZMlO^oqCd6_ zpZSTx8nNi@WxwmQU5K$gR_SV_TAqs0qRoyUD?&Owa_~vcL}}udDg)#bE(&pKSJ{CJ z;9=5+6S;;4q;-YP9oq&F|2>GM6oDrFB*=2Q@t4iS4QVTQnK@w4RQjyHO-{~#KdQhALq_~o2~K&49I9Mw8{7S=a^gP zU9+;7SX+`EPxlX&gZeKI5<=rs{|)F7q^t?RR9>)i**OZTiOW3cm#n_pX@VbJKx7mJ}k5^r7KlK*`Ug$i`>ws5jsC0GsNm zUbVIx7anJ-pR?#NIKTApo?#`>a8?c`zK&O$yZ|K#(@%PpHGDA56>~}Xf{oq+v+7GAy(Fb)K`qy^!f)@ezMj~BaMy3N?5&@lNV1Z# z=BaeRKCuDPO>vHBAFXl@Vrh`CvkoiyfNSQ;svjzqd~31)PBf=^R%s5HKnEm1{J@*Q zw~8l{Y-)`5_I3xW4>&CaBwhiAan8YJH z-TkiKUhL0o1G!otnNU+>8%JZ!WX)4_;a|kC*1wY6$3jf3Q66f?R1cv&bgK$7^In8a z;Y_{NH^m7>5HyO(dbM+Rw)a0`CP`E~UlZFTZW^FJUe^5b`BmC?I?&J9S5Hvo($bt4c5O1Sdn=B z8hf?pb0SfZ=JR!+c45`OlNm@jWy|X{<=N2Tj6?cuvR0zd%KoutL`N|hi4Z0!&dm4` zs?fYo7~#AWZb9yBbFj?Y9(ym-)>`xO<;$?0+T@6^i!XnnZvTS|fLG$Jc`4}IvkY5T z!4dmSHs(M$gm)^Hkx<2D4iwUQV}QhJ)P4PBG;?3yNc?}mBPC6VzkoOb^*p6cD@bsOk)5nUl#mm4)drGC6)S8gV$=c$RicuX}9}Sj|Za$|$mdpW9*3_c9&h)sh)b`Ry%#qu% z3%l9*u?mN8%gdq;bp&>+y9W0Y#(DU|xSzYP8{Xoa*?JFOq}pXi5 zr1EGaRpz)geI1lyJc;A8dzz3y;TLgd-cX+!`v%8Ndvo?L%-+i7g;a6Fj2cwot(}p1 zd3lNAI~nL0v^Nnp3X+QpDb zZISXr0LUP&&{;O`f4U2f)IG!6pPrrqL&>+d;~Ae6*pcn-?)Ii@g_KRWHv(#q*>anj z8ja*YWj_B9?6l&MHRIKEXHYu^3dm|=-$Y9)?%`t7ra-L<9tx6%4#w?VF$RE6$c9S{ z8gs^=i;DzY<*s4xL#LhZDT)xr+ypyA}oKgSjgdkr_G5DMlWL9o` zAAB&6?PcV~@8x*;dfxLk^<_W&M9~es^Mv)OhDL~5*x}XYihKbm{Z_GtK@i)*ZoP;P zj$XPBn0ziOiKqSM+9qp0i-7VW(0OlFPuJKBksd5Vin>isHf*A~@LegJk5HIJsjOSV zc=?gWR*LKs0e9vbT&VRCiW~GQX;L{csC_lKJgpk%bZfk2@*Q|WAbA#&| zkqbWZW$hCUjY*XpQrIGh`otBu469?f!oW@ykLa54`o=UQS4VMbw-E0Y%^l?b3;guD zU1&(-{}ZB-*>Jq+dF5c{w0O0{aVvYp8Al4(FffA1t0GTonW8zA?8G$X0x7f0yPd9i z3Z}Nl8$jEy1~kZ>rC!#<^NBsEyEr{m_YgFxJm1{k5EmF0Ahyo$5L*JzSgzR4Q5>W$ z`0YN1C)NxFZ0D5+2=GaTp}k8}E=ZaDBAmOYOL|Ebj#rTBKht`^3;))f9yJMU>l2~9O_YsVac{6gdHamnm zcq|w3L2^AGvZ{q`eu6wf#oQ9Rr2N5CB&A_T&_6p?W)pnUEWT391wks>Ntc8gaV@G- zTiF@dP8j=}bB_wE0F7N9ETrt2KDc5X7e?(4L^y9ihwr7s492auYU*;JoxhplIFoxF7O`5g{&8QiC4#wK`5UR5LD<2Y&xaQ&>yTkTZxD%i zMSphp&bWa#;}A4}WFwHcoeS8)W~j+PE)3tY6<1fQoc)Q|!-MOkyNj%8H|N`o@_(9h zwSP+VpjY_~1DZyIw%nyM{HvdX&utYo8$QxH7(N7}2XCAk53WgWFOQS)^J^TQ#@PbM z5e?eIGCe&mlO`TW*3lO8-laR6F^=En=1kqiaOdn>Q=0~JE_*r{!^O|V-V1yEu>5>$ zG=@9b+6M*_Wgak9$!Nra4%bP!rx!Nrc>8utvgCr0Zr^=V_O%!Ft%j z3P^&8R!1_%@?cR66n9x0n;(_xgD}4L*L$3k4$lWKJ3xq~J<4q%3HWT;gq#bsJgE@< zw^)8@j^#J&3T7}9;e}usv`H}_K71{70c73nJ5&2Y86CZ+RkFUdj(YhG6<7$tfP<(t zv3nH5^l$s}t5(aA?RJY9){8Te>a$YebnR&QO4SizV6xP+%3`}@T40Txu1P{*LP0#3 zXTpu1;hf)D%xEE?SMo%A6mRJ9aB5{RHpAX{IP&$5xXlG@?6%J68#hdD0MT4tfBTMy zfe$uuEIlg*7BhLH>0xr=9imaKH_~c}~H! z@+a+tmcB`lg6%96D6<%KJx zITqZTE1naSVO2%@{8AcC+3*lX=W%j_Ov(scnzo5~h>TJ<0bG)MjEftGV(&1^LEsVx zJs^a(s5#{bS}2`bV~wjyS{^WOUqJ zOyaP+%RDe=Xb7gc(UF7v!?_Dz6IsjNz70rL_OUyUxT`{2lQ&JvLyF%D2yXoGmOs?h zXMU67a6wiH*cD?`%E|N&nj4`Eou`%tNJiKZS(hk9uC8&icB&x*(9rsLLQ?#?2S5MN zjERXtm}F+jHq6ifU%^5R;gGA<2g@k=|{N4Dedf%(W1ofUx%50SuT5Kji-y2Lz zRLm#JYCU{#i~2{O%(Me_o0K6;YKcKe%hb#ajTWKFNvmVdF05|1;xj7=WT&?4-zfsd z58krR`b#Z~d&9!J1CvYP(XS`-H3qBV?BCl}6K5C=qB~a3PvAs!o0Jp+jHC+uy9cpn z`6wwLOwWTL8>r4{Cw62plnpZ}`fsltI_{KzI9d!zQ_9obx_;o&?a@Uwaqk}G{sLKg zM~(puG#^1>k^Ylku$@5$^k72&CcH8AK#O*i;Wkc`wxQxz+vhPd&vx7A9>;?XIeGqBGIW`jilekLp0tT@bXa*~ukXUGC3`1pP2 zu22v*`bDrPCi5v1lUowsch!BHPW1cp7*(nw%%a&_HrG(4#$Ks<(i#f>UIVFw#r^%U zg7$&1SfBqCC7g72K-0t*CTK-t5%1zDA^KvylQNqTDATd+4a$vgfMVqL;odadF2aw`*A6N>{S+^D0&eZyg_ za{G;%p`o|yZy1+HS6X1w1AL-f=<>a>$?FnpB_OVm`uy&i^f_AERUwC(rk+$tp*{}} zl`mo-VoCq+r}$IuBK}QnFRQDD>^p-bl~a4F{4nKaTktIRFIBkg(X|Hz1Ym;?qCrdX zih%1TK4HY-D4ar2!PKMfdMExocLQXmlz1twi}y5`tB+j1&Qt?Zj=VbSb0FoY9UqSe z6EU_u)Y=cjMH|~Pz*C@sc>r4(>BURvUUG1R(BLVu8f$TYTZEOvd`5gE{0^j663&a% z{&1{kt3uKJQ;QXDW}93;+KSat@Am|$OZDor#KvVF|DqxSx!Gn|VczAjaRJy!r(FH0 z0fxYMo>yztXH;G=cYxvevmT`(Vnzi19d*x%0fz$p`b)(tgY}TTYWb-oRg#JuC@r7z z^OZ;|sx@h2z&?D$9(C;sktZTu2{g^`Ub&*^0{bFI2mf`xtZa9ErNUTQt^HB)z}{j=;KB!Jzp&9HF}d$AyI?F?}y;1o^$%pG)UcoARrJD1&Q5I@Ir z@GGe^d^B9w;Sc~QvTap?V8=8WBTA=j12wv&Lr`r<{-3o@Z+^#y?%Ka^M*oygO+eu-HfWc zxbX_zn+$>fm-u^DM$v|^)Xdm;wC*i69peIn&VRHft8qQarW$hV)s3j-A@(_DO&RD# z7J92_VIVhd>b`M0Oe$>h=mv@3UkAy%)j?`iye%`DBzn)X;sk6-1`JmnPV$+@P!0(n z1377SaiCg$D|p7Ivr^W1*mj|z^Qz7#r#2RlN(}1nERqxq-L5!Ta^)AljjPtEViqnL zOAZ@urypUu@=|R@*)us44Y@b=i|$C^bm0Mi!slwOVDj^1snPuHMy2xwCBx0#==N`q--b&+mw<_yd!Kj{S z&<$>jO}IYK2V72s-UNj84DZ4P`|n<GZbP2FB>qJG4N zFJZVBkv`RG8jRV~;>*?Yv+WWbZ9JSQEUsGH5Co%xmoAQ5&rMmLe8S-8GW)uIJh8W% z>QfzSV8@It$vB`3rhm>If^Hhqvbm=`x{HGLWSgI$nP%Y34e(6c_Qg>YHSNx0!(f3K z85mJbyF8(TgbMYb{kQ+`Ls;X&Uqe`fsXMmN57Q8DmD%x@)%*dD8fm7Ar!nH=>}04(nI zR5JlwH+1cVo@z~s_&l4LOFg-xd-A~ja1Zn$O{fT4bXCw*dmNeSG!s%?{qT-r@>B`- z4c=?`!ul%s=Qih4wd01-C{KTX|H5KKZezf7P+QE!pi-{{0t_Ln?>GE)flq==+=odxjVnL) z%iHnVOdH!@`WvFb5j&tcDd$*;q!xFJ@LPpF1qp3NAbhu&1$>I6^uBy-0@jOYNjtgCQukYki@>v^*E}+a zk9Q+xHDpS#S(8fg#VorE+Wk||p0z~l3|Q0OdJXCklhBI)ea1iGa+8rv zM`{aJ&i>~7bcawbLwj2G8q?xHx(IUoUX#J` zYKw*>x$SICBoPc4jUv{_f!y){#g)xDjGFH=kx5`1dK%lrw&YyjIUh0efE`zA_;Pdg zn#|B#AwW$&FtP}wiba|9!14M5BA9Ji9N$TK`T zyztUoOW>y+T(S8)pl%##8WIR3C`Xj`SH74B1(E?176HI>>(MbkNpEH*^W%>XuY$#R zd%fReuT45m&4Cct=6V-ZvDtyhQH%nhB1FftM5<1?i8=F1VrDH5vCtFi;s0%7U9~SK zt-g_BQ!|kOPjvqC{c9PIvoi{0oDK|tst8#aS;xG#?h!x&AGN?W9XQxARzPY;TbloE zue_jnm{E9S9L4Q?UG@g|>EBF@aN+fJVX_B|U9=G@iGfK)uYU8#T#jOT^sSFkZU4Pn zmpT~5Z)^ZQ$i8cjVK`G{ke8QOG^;@&@=wx%wgoX__-r{!MJKE0WDEPGB_RRWdsf>& zEJ;ayyHdn150{vT5KBk|>@hYW6N$Ff1WqIZ*bNC7dDsE8gyhE{iAEd75sAPDLhX zK*O{jgQ()OC~h2ed#v-TzwhJ=8yjp3fDPrDree72ACizye@H?sq@X}oYDpH4)}dKg zAjHIM^V%*y7$N%aPTn^B@$hk>N@hk`nw`Q)H59E^U1Tb%YQ3@ghf{1^l4+5|i61-I zolL1|XxNq-_Y2MXl%_AsO+1PwUSatYkq9ZgXtS4I{bXf%IdXGHwAk#>1DKe>@7AMj zktYulUWq&@w25k{4x5=!z&e20#9!$Lyh-1Wq&6=Y??ogM0VeWn-;y2Z$0C|8mF1dG zUt9np956i|;WO%w@A-Cjur$^3FOWGe991iK!zGP^&fJSZ!{gl3i1{`Q*Q0|Xrx5^z z3M~f2Gfl@U9qAr@;OXjOJ--*zxVqwohsU|(?0la(C5`1Tz=9jRDu-MXNe>r74}-_< z@T`vq1YnT2_qfNWuhAz<$(&SGg*MzkYePmp2GJjgJ$AoA1-~yxHcZcKA>&SOU}h$} zpKnk`kU6HkH9Dt05*o;&JTpKfbs&(`*kEyk+Y)$x;monQJ8oKR!#aNpf&?59_t12X zrF!e#Q7XMlC3NE7A#|fTy(d+zT#o>bT*Z8ci!ez)PuBu#HIxkuwoRkD>*~f8RkS6^ zKbIbN6NvniD8*3ojB-=f8i8wqPB7-tc0g6@k7{->7iv#&ZVzU^*ag%{OyCaM3)vmZ)TK`msJUw98uT2lLtlgEU<)26ii$bn9dQuAM-u6LKGW5e}mo+|{l?Pw*rWYk+gAZeX zWzAb$+}!XkSm5=>Q5c_h#v=yh4E>9zgUFNKYlyrKB(EKA4sI~d@!d;TF(lUs_9;v@ zw=i^_NcMq76&(S9OD@>jN%9ef;Z}g5X0HH1F&Y?9-&dnffe8C^5?{5hLQO30 zk3ZBQ&40N{h>^fJYHI2iokA~xmZ-i43-a?|E;6yyvP4R>^XH83Q{#KO`s#G!%xmJ;N6S_TK zqt~1dWk*S@=mlOUrT3HWn@%*fl=8rN*YPZpWVq;gJ-m^@d^3sYS+rtiYI@3HV-rdo zWSEbH=6MAcJs)n&rvNGeg2CBLei8LL(G>&ZvZtiz1C>jvq-)S)U;OE?Zm&c4A6*Db zPycQ7MQgy;DkCCq2l$SonBo#vH-kwUGBn;?$Vs>$=ReR8qe*V}Mp)neqwaZPUakgr z-e|m6r}$1zxp3)Up*?+OuzCTv3WYJ<;PM;?ryk8vGO}1pFp`P!H)Hv(;{)~M>%kHG zG_|U7rE%IoUxkcqFUDDNtWjCPCts_e3J4khM9|t3oVPdGg7>@-K4CknSlm~fG%IyT+VIc zJ+PP%#fG3cI)Hk1IJ#%P-SVhGHaq`dD(=q#K2RlzUjEx${Z&+yS~xt;XAX7^CmsmV z$i~U-F_ZIzvB=j!5xO~+RC9|De!#L`t`Yi$GMlP179txQUmsC8s~iq`B=& zNL%7)o&)~apI?fETH5HZ|8{C|Q))9o`|qKvdQ}WCNPwh`t4NVkpin0P@tvh@c2IUv z(FXyNMeEzF7ywgTPu-l2ta#cLgYr%0!~4k69=b=Se5)XkrOCr`g>w$ou3{$V_a}ly zJnZw&uu;C(BpFfDKVVMj1B)+>hf_N2ds5Kt(`gam3xaO3usIbpnD9n8p-pcl~0&@Y9SvYZVtP~Qv-vr*`p7B zadBizs?K>1QMAC}SRboM0O>9JWJ?#5W;vF1fjzQ*!Nh%9+`Q0iC>W+VmO$uCc+AwJ zp;N|Y0FI`2AYXZIGO00?QQ4a{NxXY$rJUw=Xkczudv35`M>9fXLNGLN|6d@x$69j6 zG5!HXFt0weIMzJGA*10~zJ-Z%fB?xUI2!x_?UCR@KS2z>jv)FzhBO2rQ06+PPkX#z zdgKc}&TOA7D6*~Ck#S!t&-pzmGQ6`Z=Bx0*ffcA6k3%IyTyr?k4TomrrdvAXp82M+w1!>B)0i(3r$=g^(9bdAvx&;BxSbHAzBkuu-Lo%W}Ai@+4{&Ml@2h-Y(oIU^H$<;#q=fP45DWhFN?q41m5 zTSkANioEa%?^b>MxEu&(n}QBjG{5X&FoW6QbB2U$y#O?FAN?2LQ;UL91ujEVvdeQ3 z!=n+fY9!;c`)CuDA4;d@Tf8#7wNj$&_+vd$b*R=S=pC(vpo^<(kv6~WBL41f{rAzb z%b=u*x|Rts3g-dLq_hepWYNBB)k}y)%JHU7lpw)%mmbmRPiZk;nYF(8;gyNVX&egG z!dxJn_DqMwtK?`#2_63+yL{PCCQutbuS7rm5g8V|4nf(d%{wvN7KZhkv)gME^3z2< zsq*Ykzui38Uac*&HlkPiAZm4g?Rbl;7-&IVWzvr?5i+-`Cfpeu9{l>KNcasYoL2Cs znCU}~2`%^8kXagy-&w@Fa@}>Z2bTIXTgq*b>r=DRDY6MU8|uxa$Bw(WFkSkxG;ncw zusTfG>#~7*YZE$0GrbA!*?WY8vopfj_;aZxVkbOZ2T5)=QvA8mh!HIcak@uI`3kHZ zuedH9Q7 z{LA)2&kI|#cl4|<-cx-K%A+qcY)i$rHZESQblLBI$}8Q%{WxlPXsEiZz7b{?e74JI z@6sQRZ6wD4fwD-Od)+D_x+<)H{pX#-0w0lENw=`WE5L>dgW^%zZ+TV1=ZyMuQC?|j z&knGOAn-vrrH&p*J`d0jOh`*sra&Ac4<1Ap$(YJS&%(3jRTM^$+!ESfE?6kpLvzUC zP7bN-{$xUeJ$kbHz-#%6g-i_fRg>yY9G7aL;Eo!9;Nbb{*v{VNF^0)dxQd6zNDEIcuQ>D5I#B4T>f7H0N9o$xPu8I)X;OAR z8`@=#oUfZXXP6SMe6cZI+4OG>$U^Tiu`XHg{L)AV{~<$*r~dimLx$QbcpmxCU!XU- zGfj8IJ+GwDqW6beTTM~0CMCnN6{6CX+zsuC)&k&vrDEaPWgL+U8RMZ6 z*4Q<{_>szr3jJy{q5SsdR`YIKC|wVlJNdjMVA}r_#-_h^o^~peKXxo&wfvfhkfWF4 z&y6=j^FGV?C`&umU3b)kCjBu|&DX5yMD{3%;~3ThP?apgLmS+cPn45?SZpHe@JFtN bl$;28hqjx_or|)=fPa!=a-uoHPhbBp_rg0= literal 0 HcmV?d00001 diff --git a/source/easymacro (copy).bk1 b/source/easymacro (copy).bk1 deleted file mode 100644 index fb1519f..0000000 --- a/source/easymacro (copy).bk1 +++ /dev/null @@ -1,4332 +0,0 @@ -#!/usr/bin/env python3 - -# == Rapid Develop Macros in LibreOffice == - -# ~ https://git.cuates.net/elmau/easymacro - -# ~ easymacro is free software: you can redistribute it and/or modify -# ~ it under the terms of the GNU General Public License as published by -# ~ the Free Software Foundation, either version 3 of the License, or -# ~ (at your option) any later version. - -# ~ easymacro is distributed in the hope that it will be useful, -# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of -# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# ~ GNU General Public License for more details. - -# ~ You should have received a copy of the GNU General Public License -# ~ along with easymacro. If not, see . - - -import csv -import datetime -import getpass -import hashlib -import io -import json -import logging -import os -import platform -import re -import shlex -import shutil -import socket -import ssl -import subprocess -import sys -import tempfile -import threading -import time -import traceback - -from functools import wraps -from pathlib import Path -from pprint import pprint -from string import Template -from typing import Any, Union - -from socket import timeout -from urllib import parse -from urllib.request import Request, urlopen -from urllib.error import URLError, HTTPError - -import mailbox -import smtplib -from smtplib import SMTPException, SMTPAuthenticationError -from email.mime.multipart import MIMEMultipart -from email.mime.base import MIMEBase -from email.mime.text import MIMEText -from email.utils import formatdate -from email import encoders - -import uno -import unohelper -from com.sun.star.awt import Rectangle, Size, Point -from com.sun.star.awt import Key, KeyEvent, KeyModifier -from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS -from com.sun.star.awt.MessageBoxResults import YES -from com.sun.star.beans import PropertyValue, NamedValue -from com.sun.star.beans.PropertyConcept import ALL -from com.sun.star.datatransfer import XTransferable, DataFlavor -from com.sun.star.io import IOException, XOutputStream -from com.sun.star.ui.dialogs import TemplateDescription - -from com.sun.star.sheet import XRangeSelectionListener -from com.sun.star.lang import XEventListener - -from com.sun.star.container import NoSuchElementException - -# Global variables -OS = platform.system() -DESKTOP = os.environ.get('DESKTOP_SESSION', '') -PC = platform.node() -USER = getpass.getuser() -IS_WIN = OS == 'Windows' -IS_MAC = OS == 'Darwin' - - -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_DATE = '%d/%m/%Y %H:%M:%S' -if IS_WIN: - logging.addLevelName(logging.ERROR, 'ERROR') - logging.addLevelName(logging.DEBUG, 'DEBUG') - logging.addLevelName(logging.INFO, 'INFO') -else: - logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') - logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') - logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') -logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) -log = logging.getLogger(__name__) - - -_info_debug = f"Python: {sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) - -TIMEOUT = 10 -SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc' - -_EVENTS = {} -PYTHON = 'python' -if IS_WIN: - PYTHON = 'python.exe' - -FILES = { - 'CONFIG': 'zaz-{}.json', -} -DIRS = {} - -MESSAGES = { - 'es': { - 'OK': 'Aceptar', - 'Cancel': 'Cancelar', - 'Select path': 'Seleccionar ruta', - 'Select directory': 'Seleccionar directorio', - 'Select file': 'Seleccionar archivo', - 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', - 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', - } -} - - -CTX = uno.getComponentContext() -SM = CTX.getServiceManager() - - -# UNO Enum -class MessageBoxType(): - """Class for import enum - - `See Api MessageBoxType `_ - """ - from com.sun.star.awt.MessageBoxType \ - import MESSAGEBOX, INFOBOX, WARNINGBOX, ERRORBOX, QUERYBOX -MBT = MessageBoxType - - -def create_instance(name: str, with_context: bool=False, argument: Any=None) -> Any: - """Create a service instance - - :param name: Name of service - :type name: str - :param with_context: If used context - :type with_context: bool - :param argument: If needed some argument - :type argument: Any - :return: PyUno instance - :rtype: PyUno Object - """ - - if with_context: - instance = SM.createInstanceWithContext(name, CTX) - elif argument: - instance = SM.createInstanceWithArguments(name, (argument,)) - else: - instance = SM.createInstance(name) - - return instance - - -def get_app_config(node_name: str, key: str='') -> Any: - """Get any key from any node from LibreOffice configuration. - - :param node_name: Name of node - :type name: str - :param key: Name of key - :type key: str - :return: Any value - :rtype: Any - - `See Api ConfigurationProvider `_ - """ - - name = 'com.sun.star.configuration.ConfigurationProvider' - service = 'com.sun.star.configuration.ConfigurationAccess' - cp = create_instance(name, True) - node = PropertyValue(Name='nodepath', Value=node_name) - value = '' - - try: - value = cp.createInstanceWithArguments(service, (node,)) - if value and value.hasByName(key): - value = value.getPropertyValue(key) - except Exception as e: - error(e) - value = '' - - return value - - -# Get info LibO -NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') -VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') -LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') -LANG = LANGUAGE.split('-')[0] - -INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{_info_debug}" - -# Get start date from Calc configuration -node = '/org.openoffice.Office.Calc/Calculate/Other/Date' -year = get_app_config(node, 'YY') -month = get_app_config(node, 'MM') -day = get_app_config(node, 'DD') -DATE_OFFSET = datetime.date(year, month, day).toordinal() - - -def _(msg): - if LANG == 'en': - return msg - - if not LANG in MESSAGES: - return msg - - return MESSAGES[LANG][msg] - - -def set_app_config(node_name: str, key: str, new_value: Any) -> Any: - """Update value for key in node name. - - :param node_name: Name of node - :type name: str - :param key: Name of key - :type key: str - :return: True if update sucesfully - :rtype: bool - - `See Api ConfigurationUpdateAccess `_ - """ - result = True - current_value = '' - name = 'com.sun.star.configuration.ConfigurationProvider' - service = 'com.sun.star.configuration.ConfigurationUpdateAccess' - cp = create_instance(name, True) - node = PropertyValue(Name='nodepath', Value=node_name) - update = cp.createInstanceWithArguments(service, (node,)) - - try: - current_value = update.getPropertyValue(key) - update.setPropertyValue(key, new_value) - update.commitChanges() - except Exception as e: - error(e) - if update.hasByName(key) and current_value: - update.setPropertyValue(key, current_value) - update.commitChanges() - result = False - - return result - - -def debug(*messages) -> None: - """Show messages debug - - :param messages: List of messages to debug - :type messages: list[Any] - """ - - data = [str(m) for m in messages] - log.debug('\t'.join(data)) - return - - -def error(message: Any) -> None: - """Show message error - - :param message: The message error - :type message: Any - """ - - log.error(message) - return - - -def info(*messages) -> None: - """Show messages info - - :param messages: List of messages to debug - :type messages: list[Any] - """ - - data = [str(m) for m in messages] - log.info('\t'.join(data)) - return - - -def save_log(path: str, data: Any) -> None: - """Save data in file, data append to end and automatic add current time. - - :param path: Path to save log - :type path: str - :param data: Data to save in file log - :type data: Any - """ - - with open(path, 'a') as f: - f.write(f'{str(now())[:19]} - ') - pprint(data, stream=f) - return - - -def mri(obj: Any) -> None: - """Inspect object with MRI Extension - - :param obj: Any pyUno object - :type obj: Any - - `See MRI `_ - """ - - m = create_instance('mytools.Mri') - if m is None: - msg = 'Extension MRI not found' - error(msg) - return - - if hasattr(obj, 'obj'): - obj = obj.obj - m.inspect(obj) - return - - -def catch_exception(f): - """Catch exception for any function - - :param f: Any Python function - :type f: Function instance - """ - - @wraps(f) - def func(*args, **kwargs): - try: - return f(*args, **kwargs) - except Exception as e: - name = f.__name__ - if IS_WIN: - msgbox(traceback.format_exc()) - log.error(name, exc_info=True) - return func - - -def msgbox(message: Any, title: str=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, \ - type_message_box=MessageBoxType.INFOBOX) -> int: - """Create message box - - :param message: Any type message, all is converted to string. - :type message: Any - :param title: The title for message box - :type title: str - :param buttons: A combination of `com::sun::star::awt::MessageBoxButtons `_ - :type buttons: long - :param type_message_box: The `message box type `_ - :type type_message_box: enum - :return: `MessageBoxResult `_ - :rtype: int - - `See Api XMessageBoxFactory `_ - """ - - toolkit = create_instance('com.sun.star.awt.Toolkit') - parent = toolkit.getDesktopWindow() - box = toolkit.createMessageBox(parent, type_message_box, buttons, title, str(message)) - return box.execute() - - -def question(message: str, title: str=TITLE) -> bool: - """Create message box question, show buttons YES and NO - - :param message: Message question - :type message: str - :param title: The title for message box - :type title: str - :return: True if user click YES and False if click NO - :rtype: bool - """ - - result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, MessageBoxType.QUERYBOX) - return result == YES - - -def warning(message: Any, title: str=TITLE) -> int: - """Create message box with icon warning - - :param message: Any type message, all is converted to string. - :type message: Any - :param title: The title for message box - :type title: str - :return: MessageBoxResult - :rtype: int - """ - return msgbox(message, title, type_message_box=MessageBoxType.WARNINGBOX) - - -def errorbox(message: Any, title: str=TITLE) -> int: - """Create message box with icon error - - :param message: Any type message, all is converted to string. - :type message: Any - :param title: The title for message box - :type title: str - :return: MessageBoxResult - :rtype: int - """ - return msgbox(message, title, type_message_box=MessageBoxType.ERRORBOX) - - -def sleep(seconds: int): - """Sleep - """ - time.sleep(seconds) - return - - -def run_in_thread(fn): - """Run any function in thread - - :param fn: Any Python function (macro) - :type fn: Function instance - """ - def run(*k, **kw): - t = threading.Thread(target=fn, args=k, kwargs=kw) - t.start() - return t - return run - - -def dict_to_property(values: dict, uno_any: bool=False): - """Convert dictionary to array of PropertyValue - - :param values: Dictionary of values - :type values: dict - :param uno_any: If return like array uno.Any - :type uno_any: bool - :return: Tuple of PropertyValue or array uno.Any - :rtype: tuples or uno.Any - """ - ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) - if uno_any: - ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps) - return ps - - -def _property_to_dict(values): - d = {v.Name: v.Value for v in values} - return d - - -def data_to_dict(data) -> dict: - """Convert tuples, list, PropertyValue, NamedValue to dictionary - - :param data: Dictionary of values - :type data: array of tuples, list, PropertyValue or NamedValue - :return: Dictionary - :rtype: dict - """ - d = {} - if not isinstance(data, (tuple, list)): - return d - - if isinstance(data[0], (tuple, list)): - d = {r[0]: r[1] for r in data} - elif isinstance(data[0], (PropertyValue, NamedValue)): - d = _property_to_dict(data) - - return d - - -def render(template, data): - s = Template(template) - return s.safe_substitute(**data) - - -def _set_properties(model, properties): - # ~ if 'X' in properties: - # ~ properties['PositionX'] = properties.pop('X') - # ~ if 'Y' in properties: - # ~ properties['PositionY'] = properties.pop('Y') - keys = tuple(properties.keys()) - values = tuple(properties.values()) - model.setPropertyValues(keys, values) - return - - -# Classes - -class LOInspect(): - """Classe inspect - - Inspired by `MRI `_ - """ - TYPE_CLASSES = { - 'INTERFACE': '-Interface-', - 'SEQUENCE': '-Sequence-', - 'STRUCT': '-Struct-', - } - - def __init__(self, obj: Any, to_doc: bool=False): - """Introspection objects pyUno - - :param obj: Object to inspect - :type obj: Any pyUno - :param to_doc: If show info in new doc Calc - :type to_doc: bool - """ - self._obj = obj - if hasattr(obj, 'obj'): - self._obj = obj.obj - self._properties = () - self._methods = () - self._interfaces = () - self._services = () - self._listeners = () - - introspection = create_instance('com.sun.star.beans.Introspection') - result = introspection.inspect(self._obj) - if result: - self._properties = self._get_properties(result) - self._methods = self._get_methods(result) - self._interfaces = self._get_interfaces(result) - self._services = self._get_services(self._obj) - self._listeners = self._get_listeners(result) - self._to_doc(to_doc) - - def _to_doc(self, to_doc: bool): - if not to_doc: - return - - doc = LODocuments().new() - sheet = doc[0] - sheet.name = 'Properties' - sheet['A1'].data = self.properties - - sheet = doc.insert('Methods') - sheet['A1'].data = self.methods - - sheet = doc.insert('Interfaces') - sheet['A1'].data = self.interfaces - - sheet = doc.insert('Services') - sheet['A1'].data = self.services - - sheet = doc.insert('Listeners') - sheet['A1'].data = self.listeners - - return - - def _get_value(self, p: Any): - type_class = p.Type.typeClass.value - if type_class in self.TYPE_CLASSES: - return self.TYPE_CLASSES[type_class] - - value = '' - try: - value = getattr(self._obj, p.Name) - if type_class == 'ENUM' and value: - value = value.value - elif type_class == 'TYPE': - value = value.typeName - elif value is None: - value = '-void-' - except: - value = '-error-' - - return str(value) - - def _get_attributes(self, a: Any): - PA = {1 : 'Maybe Void', 16 : 'Read Only'} - attr = ', '.join([PA.get(k, '') for k in PA.keys() if a & k]) - return attr - - def _get_property(self, p: Any): - name = p.Name - tipo = p.Type.typeName - value = self._get_value(p) - attr = self._get_attributes(p.Attributes) - return name, tipo, value, attr - - def _get_properties(self, result: Any): - properties = result.getProperties(ALL) - data = [('Name', 'Type', 'Value', 'Attributes')] - data += [self._get_property(p) for p in properties] - return data - - def _get_arguments(self, m: Any): - arguments = '( {} )'.format(', '.join( - [' '.join(( - f'[{p.aMode.value.lower()}]', - p.aName, - p.aType.Name)) for p in m.ParameterInfos] - )) - return arguments - - def _get_method(self, m: Any): - name = m.Name - arguments = self._get_arguments(m) - return_type = m.ReturnType.Name - class_name = m.DeclaringClass.Name - return name, arguments, return_type, class_name - - def _get_methods(self, result: Any): - methods = result.getMethods(ALL) - data = [('Name', 'Arguments', 'Return Type', 'Class')] - data += [self._get_method(m) for m in methods] - return data - - def _get_interfaces(self, result: Any): - methods = result.getMethods(ALL) - interfaces = {m.DeclaringClass.Name for m in methods} - return tuple(zip(interfaces)) - - def _get_services(self, obj: Any): - try: - data = [str(s) for s in obj.getSupportedServiceNames()] - data = tuple(zip(data)) - except: - data = () - return data - - def _get_listeners(self, result: Any): - data = [l.typeName for l in result.getSupportedListeners()] - return tuple(zip(data)) - - @property - def properties(self): - return self._properties - - @property - def methods(self): - return self._methods - - @property - def interfaces(self): - return self._interfaces - - @property - def services(self): - return self._services - - @property - def listeners(self): - return self._listeners - - -# ~ https://github.com/django/django/blob/main/django/utils/functional.py#L61 -class classproperty: - - def __init__(self, method=None): - self.fget = method - - def __get__(self, instance, cls=None): - return self.fget(cls) - - def getter(self, method): - self.fget = method - return self - - -class Dates(object): - """Class for datetimes - """ - _start = None - - @classproperty - def now(cls): - """Current local date and time - - :return: Return the current local date and time - :rtype: datetime - """ - return datetime.datetime.now().replace(microsecond=0) - - @classproperty - def today(cls): - """Current local date - - :return: Return the current local date - :rtype: date - """ - return datetime.date.today() - - @classproperty - def time(cls): - """Current local time - - :return: Return the current local time - :rtype: datetime.time - """ - t = cls.now.time().replace(microsecond=0) - return t - - @classproperty - def epoch(cls): - """Get unix time - - :return: Return unix time - :rtype: int - - `See Unix Time `_ - """ - n = cls.now - e = int(time.mktime(n.timetuple())) - return e - - @classmethod - def date(cls, year: int, month: int, day: int): - """Get date from year, month, day - - :param year: Year of date - :type year: int - :param month: Month of date - :type month: int - :param day: Day of day - :type day: int - :return: Return the date - :rtype: date - - `See Python date `_ - """ - d = datetime.date(year, month, day) - return d - - @classmethod - def str_to_date(cls, str_date: str, template: str, to_calc: bool=False): - """Get date from string - - :param str_date: Date in string - :type str_date: str - :param template: Formato of date string - :type template: str - :param to_calc: If date is for used in Calc cell - :type to_calc: bool - :return: Return date or int if used in Calc - :rtype: date or int - - `See Python strptime `_ - """ - d = datetime.datetime.strptime(str_date, template).date() - if to_calc: - d = d.toordinal() - DATE_OFFSET - return d - - @classmethod - def calc_to_date(cls, value: float): - """Get date from calc value - - :param value: Float value from cell - :type value: float - :return: Return the current local date - :rtype: date - - `See Python fromordinal `_ - """ - d = datetime.date.fromordinal(int(value) + DATE_OFFSET) - return d - - @classmethod - def start(cls): - """Start counter - """ - cls._start = cls.now - info('Start: ', cls._start) - return - - @classmethod - def end(cls, get_seconds: bool=True): - """End counter - - :param get_seconds: If return value in total seconds - :type get_seconds: bool - :return: Return the timedelta or total seconds - :rtype: timedelta or int - """ - e = cls.now - td = e - cls._start - result = str(td) - if get_seconds: - result = td.total_seconds() - info('End: ', e) - return result - - -class Json(object): - """Class for json data - """ - - @classmethod - def dumps(cls, data: Any) -> str: - """Dumps - - :param data: Any data - :type data: Any - :return: Return string json - :rtype: str - """ - return json.dumps(data, indent=4, sort_keys=True) - - @classmethod - def loads(cls, data: str) -> Any: - """Loads - - :param data: String data - :type data: str - :return: Return any object - :rtype: Any - """ - return json.loads(data) - - -class Macro(object): - """Class for call macro - - `See Scripting Framework `_ - """ - @classmethod - def call(cls, args: dict, in_thread: bool=False): - """Call any macro - - :param args: Dictionary with macro location - :type args: dict - :param in_thread: If execute in thread - :type in_thread: bool - :return: Return None or result of call macro - :rtype: Any - """ - - result = None - if in_thread: - t = threading.Thread(target=cls._call, args=(args,)) - t.start() - else: - result = cls._call(args) - return result - - @classmethod - def get_url_script(cls, args: dict): - library = args['library'] - name = args['name'] - language = args.get('language', 'Python') - location = args.get('location', 'user') - module = args.get('module', '.') - - if language == 'Python': - module = '.py$' - elif language == 'Basic': - module = f".{module}." - if location == 'user': - location = 'application' - - url = 'vnd.sun.star.script' - url = f'{url}:{library}{module}{name}?language={language}&location={location}' - return url - - @classmethod - def _call(cls, args: dict): - url = cls.get_url_script(args) - args = args.get('args', ()) - - service = 'com.sun.star.script.provider.MasterScriptProviderFactory' - factory = create_instance(service) - script = factory.createScriptProvider('').getScript(url) - result = script.invoke(args, None, None)[0] - - return result - - -class Shell(object): - """Class for subprocess - - `See Subprocess `_ - """ - @classmethod - def run(cls, command, capture=False, split=False): - """Execute commands - - :param command: Command to run - :type command: str - :param capture: If capture result of command - :type capture: bool - :param split: Some commands need split. - :type split: bool - :return: Result of command - :rtype: Any - """ - if split: - cmd = shlex.split(command) - result = subprocess.run(cmd, capture_output=capture, text=True, shell=IS_WIN) - if capture: - result = result.stdout - else: - result = result.returncode - else: - if capture: - result = subprocess.check_output(command, shell=True).decode() - else: - result = subprocess.Popen(command) - return result - - @classmethod - def popen(cls, command): - """Execute commands and return line by line - - :param command: Command to run - :type command: str - :return: Result of command - :rtype: Any - """ - try: - proc = subprocess.Popen(shlex.split(command), shell=IS_WIN, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - for line in proc.stdout: - yield line.decode().rstrip() - except Exception as e: - error(e) - yield (e.errno, e.strerror) - - -class Timer(object): - """Class for timer thread""" - - class TimerThread(threading.Thread): - - def __init__(self, event, seconds, macro): - threading.Thread.__init__(self) - self._event = event - self._seconds = seconds - self._macro = macro - - def run(self): - while not self._event.wait(self._seconds): - Macro.call(self._macro) - info('\tTimer stopped... ') - return - - @classmethod - def exists(cls, name): - """Validate in timer **name** exists - - :param name: Timer name, it must be unique - :type name: str - :return: True if exists timer name - :rtype: bool - """ - global _EVENTS - return name in _EVENTS - - @classmethod - def start(cls, name: str, seconds: float, macro: dict): - """Start timer **name** every **seconds** and execute **macro** - - :param name: Timer name, it must be unique - :type name: str - :param seconds: Seconds for wait - :type seconds: float - :param macro: Macro for execute - :type macro: dict - """ - global _EVENTS - - _EVENTS[name] = threading.Event() - info(f"Timer '{name}' started, execute macro: '{macro['name']}'") - thread = cls.TimerThread(_EVENTS[name], seconds, macro) - thread.start() - return - - @classmethod - def stop(cls, name: str): - """Stop timer **name** - - :param name: Timer name - :type name: str - """ - global _EVENTS - _EVENTS[name].set() - del _EVENTS[name] - return - - @classmethod - def once(cls, name: str, seconds: float, macro: dict): - """Start timer **name** only once in **seconds** and execute **macro** - - :param name: Timer name, it must be unique - :type name: str - :param seconds: Seconds for wait before execute macro - :type seconds: float - :param macro: Macro for execute - :type macro: dict - """ - global _EVENTS - - _EVENTS[name] = threading.Timer(seconds, Macro.call, (macro,)) - _EVENTS[name].start() - info(f'Event: "{name}", started... execute in {seconds} seconds') - - return - - @classmethod - def cancel(cls, name: str): - """Cancel timer **name** only once events. - - :param name: Timer name, it must be unique - :type name: str - """ - global _EVENTS - - if name in _EVENTS: - try: - _EVENTS[name].cancel() - del _EVENTS[name] - info(f'Cancel event: "{name}", ok...') - except Exception as e: - error(e) - else: - debug(f'Cancel event: "{name}", not exists...') - return - - -class Hash(object): - """Class for hash - """ - @classmethod - def digest(cls, method: str, data: str, in_hex: bool=True): - """Get digest from data with method - - :param method: Digest method: md5, sha1, sha256, sha512, etc... - :type method: str - :param data: Data for get digest - :type data: str - :param in_hex: If True, get digest in hexadecimal, if False, get bytes - :type in_hex: bool - :return: bytes or hex digest - :rtype: bytes or str - """ - - result = '' - obj = getattr(hashlib, method)(data.encode()) - if in_hex: - result = obj.hexdigest() - else: - result = obj.digest() - return result - - -class Paths(object): - """Class for paths - """ - FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' - FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' - REMOTE_FILE_PICKER = 'com.sun.star.ui.dialogs.RemoteFilePicker' - OFFICE_FILE_PICKER = 'com.sun.star.ui.dialogs.OfficeFilePicker' - - def __init__(self, path=''): - if path.startswith('file://'): - path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) - self._path = Path(path) - - @property - def path(self): - """Get base path""" - return str(self._path.parent) - - @property - def file_name(self): - """Get file name""" - return self._path.name - - @property - def name(self): - """Get name""" - return self._path.stem - - @property - def ext(self): - """Get extension""" - return self._path.suffix[1:] - - @property - def size(self): - """Get size""" - return self._path.stat().st_size - - @property - def url(self): - """Get like URL""" - return self._path.as_uri() - - @property - def info(self): - """Get all info like tuple""" - i = (self.path, self.file_name, self.name, self.ext, self.size, self.url) - return i - - @property - def dict(self): - """Get all info like dict""" - data = { - 'path': self.path, - 'file_name': self.file_name, - 'name': self.name, - 'ext': self.ext, - 'size': self.size, - 'url': self.url, - } - return data - - @classproperty - def home(self): - """Get user home""" - return str(Path.home()) - - @classproperty - def documents(self): - """Get user save documents""" - return self.config() - - @classproperty - def user_profile(self): - """Get path user profile""" - path = self.config('UserConfig') - path = str(Path(path).parent) - return path - - @classproperty - def user_config(self): - """Get path config in user profile""" - path = self.config('UserConfig') - return path - - @classproperty - def python(self): - """Get path executable python""" - if IS_WIN: - path = self.join(self.config('Module'), PYTHON) - elif IS_MAC: - path = self.join(self.config('Module'), '..', 'Resources', PYTHON) - else: - path = sys.executable - return path - - @classmethod - def to_system(cls, path:str) -> str: - """Convert paths in URL to system - - :param path: Path to convert - :type path: str - :return: Path system format - :rtype: str - """ - if path.startswith('file://'): - path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) - return path - - @classmethod - def to_url(cls, path: str) -> str: - """Convert paths in format system to URL - - :param path: Path to convert - :type path: str - :return: Path in URL - :rtype: str - """ - if not path.startswith('file://'): - path = Path(path).as_uri() - return path - - @classmethod - def config(cls, name: str='Work') -> Union[str, list]: - """Return path from config - - :param name: Name in service PathSettings, default get path documents - :type name: str - :return: Path in config, if exists. - :rtype: str or list - - `See Api XPathSettings `_ - """ - path = create_instance('com.sun.star.util.PathSettings') - path = cls.to_system(getattr(path, name)).split(';') - if len(path) == 1: - path = path[0] - return path - - @classmethod - def join(cls, *paths: str) -> str: - """Join paths - - :param paths: Paths to join - :type paths: list - :return: New path with joins - :rtype: str - """ - path = str(Path(paths[0]).joinpath(*paths[1:])) - return path - - @classmethod - def exists(cls, path: str) -> bool: - """If exists path - - :param path: Path for validate - :type path: str - :return: True if path exists, False if not. - :rtype: bool - """ - path = cls.to_system(path) - result = Path(path).exists() - return result - - @classmethod - def exists_app(cls, name_app: str) -> bool: - """If exists app in system - - :param name_app: Name of application - :type name_app: str - :return: True if app exists, False if not. - :rtype: bool - """ - result = bool(shutil.which(name_app)) - return result - - @classmethod - def is_dir(cls, path: str): - """Validate if path is directory - - :param path: Path for validate - :type path: str - :return: True if path is directory, False if not. - :rtype: bool - """ - return Path(path).is_dir() - - @classmethod - def is_file(cls, path: str): - """Validate if path is a file - - :param path: Path for validate - :type path: str - :return: True if path is a file, False if not. - :rtype: bool - """ - return Path(path).is_file() - - @classmethod - def temp_file(self): - """Make temporary file""" - return tempfile.NamedTemporaryFile(mode='w') - - @classmethod - def temp_dir(self): - """Make temporary directory""" - return tempfile.TemporaryDirectory(ignore_cleanup_errors=True) - - @classmethod - def get(cls, init_dir: str='', filters: str='') -> str: - """Get path for save - - :param init_dir: Initial default path - :type init_dir: str - :param filters: Filter for show type files: 'xml' or 'txt,xml' - :type filters: str - :return: Selected path - :rtype: str - - `See API `_ - """ - if not init_dir: - init_dir = cls.documents - init_dir = cls.to_url(init_dir) - file_picker = create_instance(cls.FILE_PICKER) - file_picker.setTitle(_('Select path')) - file_picker.setDisplayDirectory(init_dir) - file_picker.initialize((TemplateDescription.FILEOPEN_SIMPLE,)) - if filters: - for f in filters.split(','): - file_picker.appendFilter(f.upper(), f'*.{f.lower()}') - - path = '' - if file_picker.execute(): - path = cls.to_system(file_picker.getSelectedFiles()[0]) - return path - - @classmethod - def get_dir(cls, init_dir: str='') -> str: - """Get path dir - - :param init_dir: Initial default path - :type init_dir: str - :return: Selected path - :rtype: str - """ - folder_picker = create_instance(cls.FOLDER_PICKER) - if not init_dir: - init_dir = cls.documents - init_dir = cls.to_url(init_dir) - folder_picker.setTitle(_('Select directory')) - folder_picker.setDisplayDirectory(init_dir) - - path = '' - if folder_picker.execute(): - path = cls.to_system(folder_picker.getDirectory()) - return path - - @classmethod - def get_file(cls, init_dir: str='', filters: str='', multiple: bool=False): - """Get path exists file - - :param init_dir: Initial default path - :type init_dir: str - :param filters: Filter for show type files: 'xml' or 'txt,xml' - :type filters: str - :param multiple: If user can selected multiple files - :type multiple: bool - :return: Selected path - :rtype: str - """ - if not init_dir: - init_dir = cls.documents - init_dir = cls.to_url(init_dir) - - file_picker = create_instance(cls.FILE_PICKER) - file_picker.setTitle(_('Select file')) - file_picker.setDisplayDirectory(init_dir) - file_picker.setMultiSelectionMode(multiple) - - if filters: - for f in filters.split(','): - file_picker.appendFilter(f.upper(), f'*.{f.lower()}') - - path = '' - if file_picker.execute(): - files = file_picker.getSelectedFiles() - path = [cls.to_system(f) for f in files] - if not multiple: - path = path[0] - return path - - @classmethod - def files(cls, path: str, pattern: str='*'): - """Get all files in path - - :param path: Path with files - :type path: str - :param pattern: For filter files, default get all. - :type pattern: str - :return: Files in path - :rtype: list - """ - files = [str(p) for p in Path(path).glob(pattern) if p.is_file()] - return files - - @classmethod - def walk(cls, path, filters=''): - """Get all files in path recursively - - :param path: Path with files - :type path: str - :param filters: For filter files, default get all. - :type filters: str - :return: Files in path - :rtype: list - """ - paths = [] - for folder, _, files in os.walk(path): - if filters: - pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) - paths += [cls.join(folder, f) for f in files if pattern.search(f)] - else: - paths += [cls.join(folder, f) for f in files] - return paths - - @classmethod - def dirs(cls, path): - """Get directories in path - - :param path: Path to scan - :type path: str - :return: Directories in path - :rtype: list - """ - dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()] - return dirs - - @classmethod - def walk_dirs(cls, path, tree=False): - """Get directories recursively - - :param path: Path to scan - :type path: str - :param tree: get info in a tuple (ID_FOLDER, ID_PARENT, NAME) - :type tree: bool - :return: Directories in path - :rtype: list - """ - folders = [] - if tree: - i = 0 - parents = {path: 0} - for root, dirs, _ in os.walk(path): - for name in dirs: - i += 1 - rn = cls.join(root, name) - if not rn in parents: - parents[rn] = i - folders.append((i, parents[root], name)) - else: - for root, dirs, _ in os.walk(path): - folders += [cls.join(root, name) for name in dirs] - return folders - - @classmethod - def extension(cls, id_ext: str): - """Get path extension install from id - - :param id_ext: ID extension - :type id_ext: str - :return: Path extension - :rtype: str - """ - pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') - path = Paths.to_system(pip.getPackageLocation(id_ext)) - return path - - @classmethod - def replace_ext(cls, path: str, new_ext: str): - """Replace extension in file path - - :param path: Path to file - :type path: str - :param new_ext: New extension - :type new_ext: str - :return: Path with new extension - :rtype: str - """ - p = Paths(path) - name = f'{p.name}.{new_ext}' - path = cls.join(p.path, name) - return path - - @classmethod - def open(cls, path: str): - """Open any file with default program in systema - - :param path: Path to file - :type path: str - :return: PID file, only Linux - :rtype: int - """ - pid = 0 - if IS_WIN: - os.startfile(path) - else: - pid = subprocess.Popen(['xdg-open', path]).pid - return pid - - # ~ Save/read data - - @classmethod - def save(cls, path: str, data: str, encoding: str='utf-8') -> bool: - """Save data in path with encoding - - :param path: Path to file save - :type path: str - :param data: Data to save - :type data: str - :param encoding: Encoding for save data, default utf-8 - :type encoding: str - :return: True, if save corrrectly - :rtype: bool - """ - result = bool(Path(path).write_text(data, encoding=encoding)) - return result - - @classmethod - def save_bin(cls, path: str, data: bytes) -> bool: - """Save binary data in path - - :param path: Path to file save - :type path: str - :param data: Data to save - :type data: bytes - :return: True, if save corrrectly - :rtype: bool - """ - result = bool(Path(path).write_bytes(data)) - return result - - @classmethod - def read(cls, path: str, get_lines: bool=False, encoding: str='utf-8') -> Union[str, list]: - """Read data in path - - :param path: Path to file read - :type path: str - :param get_lines: If read file line by line - :type get_lines: bool - :return: File content - :rtype: str or list - """ - if get_lines: - with Path(path).open(encoding=encoding) as f: - data = f.readlines() - else: - data = Path(path).read_text(encoding=encoding) - return data - - @classmethod - def read_bin(cls, path: str) -> bytes: - """Read binary data in path - - :param path: Path to file read - :type path: str - :return: File content - :rtype: bytes - """ - data = Path(path).read_bytes() - return data - - # ~ Import/export data - - @classmethod - def from_json(cls, path: str) -> Any: - """Read path file and load json data - - :param path: Path to file - :type path: str - :return: Any data - :rtype: Any - """ - data = json.loads(cls.read(path)) - return data - - @classmethod - def to_json(cls, path: str, data: str): - """Save data in path file like json - - :param path: Path to file - :type path: str - :return: True if save correctly - :rtype: bool - """ - data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) - return cls.save(path, data) - - @classmethod - def from_csv(cls, path: str, args: dict={}) -> tuple: - """Read CSV - - :param path: Path to file csv - :type path: str - :param args: Any argument support for Python library - :type args: dict - :return: Data csv like tuple - :rtype: tuple - - `See CSV Reader `_ - """ - with open(path) as f: - rows = tuple(csv.reader(f, **args)) - return rows - - @classmethod - def to_csv(cls, path: str, data: Any, args: dict={}): - """Write CSV - - :param path: Path to file write csv - :type path: str - :param data: Data to write - :type data: Iterable - :param args: Any argument support for Python library - :type args: dict - - `See CSV Writer `_ - """ - with open(path, 'w') as f: - writer = csv.writer(f, **args) - writer.writerows(data) - return - - @classmethod - def zip(cls, source: Union[str, tuple, list], target='') -> str: - path_zip = target - if not isinstance(source, (tuple, list)): - path, _, name, _ = _P(source).info - start = len(path) + 1 - if not target: - path_zip = f'{path}/{name}.zip' - - if isinstance(source, (tuple, list)): - files = [(f, f[len(_P(f).path)+1:]) for f in source] - elif _P.is_file(source): - files = ((source, source[start:]),) - else: - files = [(f, f[start:]) for f in _P.walk(source)] - - compression = zipfile.ZIP_DEFLATED - with zipfile.ZipFile(path_zip, 'w', compression=compression) as z: - for f in files: - z.write(f[0], f[1]) - return path_zip - - @classmethod - def unzip(cls, source, target='', members=None, pwd=None): - path = target - if not target: - path = _P(source).path - with zipfile.ZipFile(source) as z: - if not pwd is None: - pwd = pwd.encode() - if isinstance(members, str): - members = (members,) - z.extractall(path, members=members, pwd=pwd) - return - - @classmethod - def zip_content(cls, path: str): - with zipfile.ZipFile(path) as z: - names = z.namelist() - return names - - @classmethod - def merge_zip(cls, target, zips): - try: - with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: - for path in zips: - with zipfile.ZipFile(path, compression=zipfile.ZIP_DEFLATED) as s: - for name in s.namelist(): - t.writestr(name, s.open(name).read()) - except Exception as e: - error(e) - return False - - return True - - @classmethod - def kill(cls, path: str): - """Delete path - - :param path: Path to file or directory - :type path: str - :return: True if delete correctly - :rtype: bool - """ - p = Path(path) - try: - if p.is_file(): - p.unlink() - elif p.is_dir(): - shutil.rmtree(path) - result = True - except OSError as e: - log.error(e) - result = False - - return result - - @classmethod - def copy(cls, source: str, target: str='', name: str=''): - p, f, n, e, _, _ = Paths(source).info - if target: - p = target - e = f'.{e}' - if name: - e = '' - n = name - path_new = cls.join(p, f'{n}{e}') - shutil.copy(source, path_new) - return path_new - - -class Config(object): - """Class for set and get configurations - """ - @classmethod - def set(cls, prefix: str, value: Any, key: str='') -> bool: - """Save data config in user config like json - - :param prefix: Unique prefix for this data - :type prefix: str - :param value: Value for save - :type value: Any - :param key: Key for value - :type key: str - :return: True if save correctly - :rtype: bool - """ - name_file = FILES['CONFIG'].format(prefix) - path = Paths.join(Paths.user_config, name_file) - data = value - if key: - data = cls.get(prefix) - data[key] = value - result = Paths.to_json(path, data) - return result - - @classmethod - def get(cls, prefix: str, key: str='', default: Any={}) -> Any: - """Get data config from user config like json - - :param prefix: Unique prefix for this data - :type prefix: str - :param key: Key for value - :type key: str - :param default: Get if not exists key - :type default: Any - :return: data - :rtype: Any - """ - data = {} - name_file = FILES['CONFIG'].format(prefix) - path = Paths.join(Paths.user_config, name_file) - if not Paths.exists(path): - return data - - data = Paths.from_json(path) - if key: - data = data.get(key, default) - - return data - - -class Url(object): - """Class for simple url open - """ - @classmethod - def _open(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ - json: bool=False, timeout: int=TIMEOUT, method: str='GET') -> tuple: - """URL Open""" - - debug(url) - result = None - context = None - rheaders = {} - err = '' - - if verify: - if not data is None: - if isinstance(data, str): - data = data.encode() - elif isinstance(data, dict): - data = parse.urlencode(data).encode('ascii') - else: - context = ssl._create_unverified_context() - - try: - req = Request(url, data=data, headers=headers, method=method) - response = urlopen(req, timeout=timeout, context=context) - except HTTPError as e: - error(e) - err = str(e) - except URLError as e: - error(e.reason) - err = str(e.reason) - # ToDo - # ~ except timeout: - # ~ err = 'timeout' - # ~ error(err) - else: - rheaders = dict(response.info()) - result = response.read().decode() - if json: - result = Json.loads(result) - - return result, rheaders, err - - @classmethod - def get(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ - json: bool=False, timeout: int=TIMEOUT) -> tuple: - """Method GET - - :param url: Url to open - :type url: str - :return: result, headers and error - :rtype: tuple - """ - return cls._open(url, data, headers, verify, json, timeout) - - # ToDo - @classmethod - def post(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ - json: bool=False, timeout: int=TIMEOUT) -> tuple: - """Method POST - """ - data = parse.urlencode(data).encode('ascii') - return cls._open(url, data, headers, verify, json, timeout, 'POST') - - -class Email(object): - """Class for send email - """ - class _SmtpServer(object): - - def __init__(self, config): - self._server = None - self._error = '' - self._sender = '' - self._is_connect = self._login(config) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @property - def is_connect(self): - return self._is_connect - - @property - def error(self): - return self._error - - def _login(self, config): - name = config['server'] - port = config['port'] - is_ssl = config['ssl'] - starttls = config.get('starttls', False) - self._sender = config['user'] - try: - if starttls: - self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) - self._server.ehlo() - self._server.starttls() - self._server.ehlo() - elif is_ssl: - self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) - self._server.ehlo() - else: - self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) - - self._server.login(self._sender, config['password']) - msg = 'Connect to: {}'.format(name) - debug(msg) - return True - except smtplib.SMTPAuthenticationError as e: - if '535' in str(e): - self._error = _('Incorrect user or password') - return False - if '534' in str(e) and 'gmail' in name: - self._error = _('Allow less secure apps in GMail') - return False - except smtplib.SMTPException as e: - self._error = str(e) - return False - except Exception as e: - self._error = str(e) - return False - return False - - def _body(self, msg): - body = msg.replace('\n', '
') - return body - - def send(self, message): - # ~ file_name = 'attachment; filename={}' - email = MIMEMultipart() - email['From'] = self._sender - email['To'] = message['to'] - email['Cc'] = message.get('cc', '') - email['Subject'] = message['subject'] - email['Date'] = formatdate(localtime=True) - if message.get('confirm', False): - email['Disposition-Notification-To'] = email['From'] - email.attach(MIMEText(self._body(message['body']), 'html')) - - paths = message.get('files', ()) - if isinstance(paths, str): - paths = (paths,) - for path in paths: - fn = _P(path).file_name - # ~ print('NAME', fn) - part = MIMEBase('application', 'octet-stream') - part.set_payload(_P.read_bin(path)) - encoders.encode_base64(part) - part.add_header('Content-Disposition', f'attachment; filename="{fn}"') - email.attach(part) - - receivers = ( - email['To'].split(',') + - email['CC'].split(',') + - message.get('bcc', '').split(',')) - try: - self._server.sendmail(self._sender, receivers, email.as_string()) - msg = 'Email sent...' - debug(msg) - if message.get('path', ''): - self.save_message(email, message['path']) - return True - except Exception as e: - self._error = str(e) - return False - return False - - def save_message(self, email, path): - mbox = mailbox.mbox(path, create=True) - mbox.lock() - try: - msg = mailbox.mboxMessage(email) - mbox.add(msg) - mbox.flush() - finally: - mbox.unlock() - return - - def close(self): - try: - self._server.quit() - msg = 'Close connection...' - debug(msg) - except: - pass - return - - @classmethod - def _send_email(cls, server, messages): - with cls._SmtpServer(server) as server: - if server.is_connect: - for msg in messages: - server.send(msg) - else: - error(server.error) - return server.error - - @classmethod - def send(cls, server: dict, messages: Union[dict, tuple, list]): - """Send email with config server, emails send in thread. - - :param server: Configuration for send emails - :type server: dict - :param server: Dictionary con message or list of messages - :type server: dict or iterator - """ - if isinstance(messages, dict): - messages = (messages,) - t = threading.Thread(target=cls._send_email, args=(server, messages)) - t.start() - return - - -class Color(object): - """Class for colors - - `See Web Colors `_ - """ - COLORS = { - 'aliceblue': 15792383, - 'antiquewhite': 16444375, - 'aqua': 65535, - 'aquamarine': 8388564, - 'azure': 15794175, - 'beige': 16119260, - 'bisque': 16770244, - 'black': 0, - 'blanchedalmond': 16772045, - 'blue': 255, - 'blueviolet': 9055202, - 'brown': 10824234, - 'burlywood': 14596231, - 'cadetblue': 6266528, - 'chartreuse': 8388352, - 'chocolate': 13789470, - 'coral': 16744272, - 'cornflowerblue': 6591981, - 'cornsilk': 16775388, - 'crimson': 14423100, - 'cyan': 65535, - 'darkblue': 139, - 'darkcyan': 35723, - 'darkgoldenrod': 12092939, - 'darkgray': 11119017, - 'darkgreen': 25600, - 'darkgrey': 11119017, - 'darkkhaki': 12433259, - 'darkmagenta': 9109643, - 'darkolivegreen': 5597999, - 'darkorange': 16747520, - 'darkorchid': 10040012, - 'darkred': 9109504, - 'darksalmon': 15308410, - 'darkseagreen': 9419919, - 'darkslateblue': 4734347, - 'darkslategray': 3100495, - 'darkslategrey': 3100495, - 'darkturquoise': 52945, - 'darkviolet': 9699539, - 'deeppink': 16716947, - 'deepskyblue': 49151, - 'dimgray': 6908265, - 'dimgrey': 6908265, - 'dodgerblue': 2003199, - 'firebrick': 11674146, - 'floralwhite': 16775920, - 'forestgreen': 2263842, - 'fuchsia': 16711935, - 'gainsboro': 14474460, - 'ghostwhite': 16316671, - 'gold': 16766720, - 'goldenrod': 14329120, - 'gray': 8421504, - 'grey': 8421504, - 'green': 32768, - 'greenyellow': 11403055, - 'honeydew': 15794160, - 'hotpink': 16738740, - 'indianred': 13458524, - 'indigo': 4915330, - 'ivory': 16777200, - 'khaki': 15787660, - 'lavender': 15132410, - 'lavenderblush': 16773365, - 'lawngreen': 8190976, - 'lemonchiffon': 16775885, - 'lightblue': 11393254, - 'lightcoral': 15761536, - 'lightcyan': 14745599, - 'lightgoldenrodyellow': 16448210, - 'lightgray': 13882323, - 'lightgreen': 9498256, - 'lightgrey': 13882323, - 'lightpink': 16758465, - 'lightsalmon': 16752762, - 'lightseagreen': 2142890, - 'lightskyblue': 8900346, - 'lightslategray': 7833753, - 'lightslategrey': 7833753, - 'lightsteelblue': 11584734, - 'lightyellow': 16777184, - 'lime': 65280, - 'limegreen': 3329330, - 'linen': 16445670, - 'magenta': 16711935, - 'maroon': 8388608, - 'mediumaquamarine': 6737322, - 'mediumblue': 205, - 'mediumorchid': 12211667, - 'mediumpurple': 9662683, - 'mediumseagreen': 3978097, - 'mediumslateblue': 8087790, - 'mediumspringgreen': 64154, - 'mediumturquoise': 4772300, - 'mediumvioletred': 13047173, - 'midnightblue': 1644912, - 'mintcream': 16121850, - 'mistyrose': 16770273, - 'moccasin': 16770229, - 'navajowhite': 16768685, - 'navy': 128, - 'oldlace': 16643558, - 'olive': 8421376, - 'olivedrab': 7048739, - 'orange': 16753920, - 'orangered': 16729344, - 'orchid': 14315734, - 'palegoldenrod': 15657130, - 'palegreen': 10025880, - 'paleturquoise': 11529966, - 'palevioletred': 14381203, - 'papayawhip': 16773077, - 'peachpuff': 16767673, - 'peru': 13468991, - 'pink': 16761035, - 'plum': 14524637, - 'powderblue': 11591910, - 'purple': 8388736, - 'red': 16711680, - 'rosybrown': 12357519, - 'royalblue': 4286945, - 'saddlebrown': 9127187, - 'salmon': 16416882, - 'sandybrown': 16032864, - 'seagreen': 3050327, - 'seashell': 16774638, - 'sienna': 10506797, - 'silver': 12632256, - 'skyblue': 8900331, - 'slateblue': 6970061, - 'slategray': 7372944, - 'slategrey': 7372944, - 'snow': 16775930, - 'springgreen': 65407, - 'steelblue': 4620980, - 'tan': 13808780, - 'teal': 32896, - 'thistle': 14204888, - 'tomato': 16737095, - 'turquoise': 4251856, - 'violet': 15631086, - 'wheat': 16113331, - 'white': 16777215, - 'whitesmoke': 16119285, - 'yellow': 16776960, - 'yellowgreen': 10145074, - } - - def _get_color(self, index): - if isinstance(index, tuple): - color = (index[0] << 16) + (index[1] << 8) + index[2] - else: - if index[0] == '#': - r, g, b = bytes.fromhex(index[1:]) - color = (r << 16) + (g << 8) + b - else: - color = self.COLORS.get(index.lower(), -1) - return color - - def __call__(self, index): - return self._get_color(index) - - def __getitem__(self, index): - return self._get_color(index) - - -COLOR_ON_FOCUS = Color()('LightYellow') - - -class ClipBoard(object): - SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard' - CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' - - class TextTransferable(unohelper.Base, XTransferable): - - def __init__(self, text): - df = DataFlavor() - df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT - df.HumanPresentableName = 'encoded text utf-16' - self.flavors = (df,) - self._data = text - - def getTransferData(self, flavor): - return self._data - - def getTransferDataFlavors(self): - return self.flavors - - @classmethod - def set(cls, value): - ts = cls.TextTransferable(value) - sc = create_instance(cls.SERVICE) - sc.setContents(ts, None) - return - - @classproperty - def contents(cls): - df = None - text = '' - sc = create_instance(cls.SERVICE) - transferable = sc.getContents() - data = transferable.getTransferDataFlavors() - for df in data: - if df.MimeType == cls.CLIPBOARD_FORMAT_TEXT: - break - if df: - text = transferable.getTransferData(df) - return text - - -class IOStream(object): - """Classe for input/output stream""" - - class OutputStream(unohelper.Base, XOutputStream): - - def __init__(self): - self._buffer = b'' - self.closed = 0 - - @property - def buffer(self): - return self._buffer - - def closeOutput(self): - self.closed = 1 - - def writeBytes(self, seq): - if seq.value: - self._buffer = seq.value - - def flush(self): - pass - - @classmethod - def buffer(cls): - return io.BytesIO() - - @classmethod - def input(cls, buffer): - service = 'com.sun.star.io.SequenceInputStream' - stream = create_instance(service, True) - stream.initialize((uno.ByteSequence(buffer.getvalue()),)) - return stream - - @classmethod - def output(cls): - return cls.OutputStream() - - -class EventsListenerBase(unohelper.Base, XEventListener): - - def __init__(self, controller, name, window=None): - self._controller = controller - self._name = name - self._window = window - - @property - def name(self): - return self._name - - def disposing(self, event): - self._controller = None - if not self._window is None: - self._window.setMenuBar(None) - - -class EventsRangeSelectionListener(EventsListenerBase, XRangeSelectionListener): - - def __init__(self, controller): - super().__init__(controller, '') - - def done(self, event): - range_selection = event.RangeDescriptor - event_name = 'range_selection_done' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(range_selection) - return - - def aborted(self, event): - range_selection = event.RangeDescriptor - event_name = 'range_selection_aborted' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)() - return - - -class LOShapes(object): - _type = 'ShapeCollection' - - def __init__(self, obj): - self._obj = obj - - def __len__(self): - return self.obj.Count - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - try: - s = self.obj[self._index] - shape = LOShape(s) - except IndexError: - raise StopIteration - - self._index += 1 - return shape - - def __str__(self): - return 'Shapes' - - @property - def obj(self): - return self._obj - - -class LOShape(object): - IMAGE = 'com.sun.star.drawing.GraphicObjectShape' - - def __init__(self, obj): - self._obj = obj - - def __str__(self): - return f'Shape: {self.name}' - - @property - def obj(self): - return self._obj - - @property - def properties(self): - # ~ properties = self.obj.PropertySetInfo.Properties - # ~ data = {p.Name: getattr(self.obj, p.Name) for p in properties} - data = self.obj.PropertySetInfo.Properties - keys = [p.Name for p in data] - values = self.obj.getPropertyValues(keys) - data = dict(zip(keys, values)) - return data - @properties.setter - def properties(self, values): - _set_properties(self.obj, values) - - @property - def shape_type(self): - return self.obj.ShapeType - - @property - def name(self): - return self.obj.Name - @name.setter - def name(self, value): - self.obj.Name = value - - @property - def is_image(self): - return self.shape_type == self.IMAGE - - @property - def is_shape(self): - return self.shape_type != self.IMAGE - - @property - def size(self): - s = self.obj.Size - return s - @size.setter - def size(self, value): - self.obj.Size = value - - @property - def width(self): - s = self.obj.Size - return s.Width - @width.setter - def width(self, value): - s = self.size - s.Width = value - self.size = s - - @property - def height(self): - s = self.obj.Size - return s.Height - @height.setter - def height(self, value): - s = self.size - s.Height = value - self.size = s - - @property - def position(self): - return self.obj.Position - @property - def x(self): - return self.position.X - @property - def y(self): - return self.position.Y - - @property - def string(self): - return self.obj.String - @string.setter - def string(self, value): - self.obj.String = value - - @property - def title(self): - return self.obj.Title - @title.setter - def title(self, value): - self.obj.Title = value - - @property - def description(self): - return self.obj.Description - @description.setter - def description(self, value): - self.obj.Description = value - - - -class LOShortCuts(object): - """Classe for manager shortcuts""" - KEYS = {getattr(Key, k): k for k in dir(Key)} - MODIFIERS = { - 'shift': KeyModifier.SHIFT, - 'ctrl': KeyModifier.MOD1, - 'alt': KeyModifier.MOD2, - 'ctrlmac': KeyModifier.MOD3, - } - 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', - } - - def __init__(self, app: str=''): - self._app = app - service = 'com.sun.star.ui.GlobalAcceleratorConfiguration' - if app: - service = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' - type_app = LODocuments.TYPES[app] - manager = create_instance(service, True) - uicm = manager.getUIConfigurationManager(type_app) - self._config = uicm.ShortCutManager - else: - self._config = create_instance(service) - - def __getitem__(self, index): - return LOShortCuts(index) - - def __contains__(self, item): - cmd = self.get_by_shortcut(item) - return bool(cmd) - - def __iter__(self): - self._i = -1 - return self - - def __next__(self): - self._i += 1 - try: - event = self._config.AllKeyEvents[self._i] - event = self._get_info(event) - except IndexError: - raise StopIteration - - return event - - @classmethod - def to_key_event(cls, shortcut: str): - """Convert from string shortcut (Shift+Ctrl+Alt+LETTER) to KeyEvent""" - key_event = KeyEvent() - keys = shortcut.split('+') - try: - for m in keys[:-1]: - key_event.Modifiers += cls.MODIFIERS[m.lower()] - key_event.KeyCode = getattr(Key, keys[-1].upper()) - except Exception as e: - error(e) - key_event = None - return key_event - - @classmethod - def get_url_script(cls, command: Union[str, dict]) -> str: - """Get uno command or url for macro""" - url = command - if isinstance(url, str) and not url.startswith('.uno:'): - url = f'.uno:{command}' - elif isinstance(url, dict): - url = Macro.get_url_script(command) - return url - - def _get_shortcut(self, k): - """Get shortcut for key event""" - # ~ print(k.KeyCode, str(k.KeyChar), k.KeyFunc, k.Modifiers) - shortcut = f'{self.COMBINATIONS[k.Modifiers]}+{self.KEYS[k.KeyCode]}' - return shortcut - - def _get_info(self, key): - """Get shortcut and command""" - cmd = self._config.getCommandByKeyEvent(key) - shortcut = self._get_shortcut(key) - return shortcut, cmd - - def get_all(self): - """Get all events key""" - events = [(self._get_info(k)) for k in self._config.AllKeyEvents] - return events - - def get_by_command(self, command: Union[str, dict]): - """Get shortcuts by command""" - url = LOShortCuts.get_url_script(command) - key_events = self._config.getKeyEventsByCommand(url) - shortcuts = [self._get_shortcut(k) for k in key_events] - return shortcuts - - def get_by_shortcut(self, shortcut: str): - """Get command by shortcut""" - command = '' - key_event = LOShortCuts.to_key_event(shortcut) - if key_event: - command = self._config.getCommandByKeyEvent(key_event) - return command - - def set(self, shortcut: str, command: Union[str, dict]) -> bool: - """Set shortcut to command - - :param shortcut: Shortcut like Shift+Ctrl+Alt+LETTER - :type shortcut: str - :param command: Command tu assign, 'UNOCOMMAND' or dict with macro info - :type command: str or dict - :return: True if set sucesfully - :rtype: bool - """ - result = True - url = LOShortCuts.get_url_script(command) - key_event = LOShortCuts.to_key_event(shortcut) - try: - self._config.setKeyEvent(key_event, url) - self._config.store() - except Exception as e: - error(e) - result = False - - return result - - def remove_by_shortcut(self, shortcut: str): - """Remove by shortcut""" - key_event = LOShortCuts.to_key_event(shortcut) - try: - self._config.removeKeyEvent(key_event) - result = True - except NoSuchElementException: - debug(f'No exists: {shortcut}') - result = False - return result - - def remove_by_command(self, command: Union[str, dict]): - """Remove by shortcut""" - url = LOShortCuts.get_url_script(command) - self._config.removeCommandFromAllKeyEvents(url) - return - - def reset(self): - """Reset configuration""" - self._config.reset() - self._config.store() - return - - -class LOMenuDebug(): - """Classe for debug info menu""" - - @classmethod - def _get_info(cls, menu, index): - """Get every option menu""" - line = f"({index}) {menu.get('CommandURL', '----------')}" - submenu = menu.get('ItemDescriptorContainer', None) - if not submenu is None: - line += cls._get_submenus(submenu) - return line - - @classmethod - def _get_submenus(cls, menu, level=1): - """Get submenus""" - line = '' - for i, v in enumerate(menu): - data = data_to_dict(v) - cmd = data.get('CommandURL', '----------') - line += f'\n{" " * level}├─ ({i}) {cmd}' - submenu = data.get('ItemDescriptorContainer', None) - if not submenu is None: - line += cls._get_submenus(submenu, level + 1) - return line - - def __call__(cls, menu): - for i, m in enumerate(menu): - data = data_to_dict(m) - print(cls._get_info(data, i)) - return - - -class LOMenuBase(): - """Classe base for menus""" - NODE = 'private:resource/menubar/menubar' - config = None - menus = None - app = '' - - @classmethod - def _get_index(cls, parent: Any, name: Union[int, str]=''): - """Get index menu from name - - :param parent: Menu parent - :type parent: pyUno - :param name: Menu name for search if is str - :type name: int or str - :return: Index of menu - :rtype: int - """ - index = None - if isinstance(name, str) and name: - for i, m in enumerate(parent): - menu = data_to_dict(m) - if menu.get('CommandURL', '') == name: - index = i - break - elif isinstance(name, str): - index = len(parent) - 1 - elif isinstance(name, int): - index = name - return index - - @classmethod - def _get_command_url(cls, menu: dict): - """Get url from command and set shortcut - - :param menu: Menu data - :type menu: dict - :return: URL command - :rtype: str - """ - shortcut = menu.pop('ShortCut', '') - command = menu['CommandURL'] - url = LOShortCuts.get_url_script(command) - if shortcut: - LOShortCuts(cls.app).set(shortcut, command) - return url - - @classmethod - def _save(cls, parent: Any, menu: dict, index: int): - """Insert menu - - :param parent: Menu parent - :type parent: pyUno - :param menu: New menu data - :type menu: dict - :param index: Position to insert - :type index: int - """ - # ~ Some day - # ~ self._menus.insertByIndex(index, new_menu) - properties = dict_to_property(menu, True) - uno.invoke(parent, 'insertByIndex', (index, properties)) - cls.config.replaceSettings(cls.NODE, cls.menus) - return - - @classmethod - def _insert_submenu(cls, parent: Any, menus: list): - """Insert submenus recursively - - :param parent: Menu parent - :type parent: pyUno - :param menus: List of menus - :type menus: list - """ - for i, menu in enumerate(menus): - submenu = menu.pop('Submenu', False) - if submenu: - idc = cls.config.createSettings() - menu['ItemDescriptorContainer'] = idc - menu['Type'] = 0 - if menu['Label'][0] == '-': - menu['Type'] = 1 - else: - menu['CommandURL'] = cls._get_command_url(menu) - cls._save(parent, menu, i) - if submenu: - cls._insert_submenu(idc, submenu) - return - - @classmethod - def _get_first_command(cls, command): - url = command - if isinstance(command, dict): - url = Macro.get_url_script(command) - return url - - @classmethod - def insert(cls, parent: Any, menu: dict, after: Union[int, str]=''): - """Insert new menu - - :param parent: Menu parent - :type parent: pyUno - :param menu: New menu data - :type menu: dict - :param after: After menu insert - :type after: int or str - """ - index = cls._get_index(parent, after) + 1 - submenu = menu.pop('Submenu', False) - menu['Type'] = 0 - idc = cls.config.createSettings() - menu['ItemDescriptorContainer'] = idc - menu['CommandURL'] = cls._get_first_command(menu['CommandURL']) - cls._save(parent, menu, index) - if submenu: - cls._insert_submenu(idc, submenu) - return - - @classmethod - def remove(cls, parent: Any, name: Union[str, dict]): - """Remove name in parent - - :param parent: Menu parent - :type parent: pyUno - :param menu: Menu name - :type menu: str - """ - if isinstance(name, dict): - name = Macro.get_url_script(name) - index = cls._get_index(parent, name) - if index is None: - debug(f'Not found: {name}') - return - uno.invoke(parent, 'removeByIndex', (index,)) - cls.config.replaceSettings(cls.NODE, cls.menus) - cls.config.store() - return - - -class LOMenu(object): - """Classe for individual menu""" - - def __init__(self, config: Any, menus: Any, app: str, menu: Any): - """ - :param config: Configuration Mananer - :type config: pyUno - :param menus: Menu bar main - :type menus: pyUno - :param app: Name LibreOffice module - :type app: str - :para menu: Particular menu - :type menu: pyUno - """ - self._config = config - self._menus = menus - self._app = app - self._parent = menu - - def __contains__(self, name): - """If exists name in menu""" - exists = False - for m in self._parent: - menu = data_to_dict(m) - cmd = menu.get('CommandURL', '') - if name == cmd: - exists = True - break - return exists - - def __getitem__(self, index): - """Index access""" - if isinstance(index, int): - menu = data_to_dict(self._parent[index]) - else: - for m in self._parent: - menu = data_to_dict(m) - cmd = menu.get('CommandURL', '') - if cmd == index: - break - - obj = LOMenu(self._config, self._menus, self._app, - menu['ItemDescriptorContainer']) - return obj - - def debug(self): - """Debug menu""" - LOMenuDebug()(self._parent) - return - - def insert(self, menu: dict, after: Union[int, str]='', save: bool=True): - """Insert new menu - - :param menu: New menu data - :type menu: dict - :param after: Insert in after menu - :type after: int or str - :param save: For persistente save - :type save: bool - """ - LOMenuBase.config = self._config - LOMenuBase.menus = self._menus - LOMenuBase.app = self._app - LOMenuBase.insert(self._parent, menu, after) - if save: - self._config.store() - return - - def remove(self, menu: str): - """Remove menu - - :param menu: Menu name - :type menu: str - """ - LOMenuBase.config = self._config - LOMenuBase.menus = self._menus - LOMenuBase.remove(self._parent, menu) - return - - -class LOMenuApp(object): - """Classe for manager menu by LibreOffice module""" - NODE = 'private:resource/menubar/menubar' - MENUS = { - 'file': '.uno:PickList', - 'picklist': '.uno:PickList', - 'tools': '.uno:ToolsMenu', - 'help': '.uno:HelpMenu', - 'window': '.uno:WindowList', - 'edit': '.uno:EditMenu', - 'view': '.uno:ViewMenu', - 'insert': '.uno:InsertMenu', - 'format': '.uno:FormatMenu', - 'styles': '.uno:FormatStylesMenu', - 'formatstyles': '.uno:FormatStylesMenu', - 'sheet': '.uno:SheetMenu', - 'data': '.uno:DataMenu', - 'table': '.uno:TableMenu', - 'formatform': '.uno:FormatFormMenu', - 'page': '.uno:PageMenu', - 'shape': '.uno:ShapeMenu', - 'slide': '.uno:SlideMenu', - 'slideshow': '.uno:SlideShowMenu', - } - - def __init__(self, app: str): - """ - :param app: LibreOffice Module: calc, writer, draw, impress, math, main - :type app: str - """ - self._app = app - self._config = self._get_config() - self._menus = self._config.getSettings(self.NODE, True) - - def _get_config(self): - """Get config manager""" - service = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' - type_app = LODocuments.TYPES[self._app] - manager = create_instance(service, True) - config = manager.getUIConfigurationManager(type_app) - return config - - def debug(self): - """Debug menu""" - LOMenuDebug()(self._menus) - return - - def __contains__(self, name): - """If exists name in menu""" - exists = False - for m in self._menus: - menu = data_to_dict(m) - cmd = menu.get('CommandURL', '') - if name == cmd: - exists = True - break - return exists - - def __getitem__(self, index): - """Index access""" - if isinstance(index, int): - menu = data_to_dict(self._menus[index]) - else: - for m in self._menus: - menu = data_to_dict(m) - cmd = menu.get('CommandURL', '') - if cmd == index or cmd == self.MENUS[index.lower()]: - break - - obj = LOMenu(self._config, self._menus, self._app, - menu['ItemDescriptorContainer']) - return obj - - def insert(self, menu: dict, after: Union[int, str]='', save: bool=True): - """Insert new menu - - :param menu: New menu data - :type menu: dict - :param after: Insert in after menu - :type after: int or str - :param save: For persistente save - :type save: bool - """ - LOMenuBase.config = self._config - LOMenuBase.menus = self._menus - LOMenuBase.app = self._app - LOMenuBase.insert(self._menus, menu, after) - if save: - self._config.store() - return - - def remove(self, menu: str): - """Remove menu - - :param menu: Menu name - :type menu: str - """ - LOMenuBase.config = self._config - LOMenuBase.menus = self._menus - LOMenuBase.remove(self._menus, menu) - return - - -class LOMenus(object): - """Classe for manager menus""" - - def __getitem__(self, index): - """Index access""" - return LOMenuApp(index) - - -class LOEvents(): - - def __init__(self, obj): - self._obj = obj - - def __contains__(self, item): - return self.obj.hasByName(item) - - def __getitem__(self, index): - """Index access""" - return self.obj.getByName(index) - - def __setitem__(self, name: str, macro: dict): - """Set macro to event - - :param name: Event name - :type name: str - :param macro: Macro execute in event - :type name: dict - """ - pv = '[]com.sun.star.beans.PropertyValue' - args = () - if macro: - url = Macro.get_url_script(macro) - args = dict_to_property(dict(EventType='Script', Script=url)) - uno.invoke(self.obj, 'replaceByName', (name, uno.Any(pv, args))) - - @property - def obj(self): - return self._obj - - @property - def names(self): - return self.obj.ElementNames - - def remove(self, name): - pv = '[]com.sun.star.beans.PropertyValue' - uno.invoke(self.obj, 'replaceByName', (name, uno.Any(pv, ()))) - return - - -class LOMain(): - """Classe for LibreOffice""" - - class commands(): - """Class for disable and enable commands - - `See DispatchCommands `_ - """ - @classmethod - def _set_app_command(cls, command: str, disable: bool): - """Disable or enabled UNO command - - :param command: UNO command to disable or enabled - :type command: str - :param disable: True if disable, False if active - :type disable: bool - :return: True if correctly update, False if not. - :rtype: bool - """ - NEW_NODE_NAME = f'zaz_disable_command_{command.lower()}' - name = 'com.sun.star.configuration.ConfigurationProvider' - service = 'com.sun.star.configuration.ConfigurationUpdateAccess' - node_name = '/org.openoffice.Office.Commands/Execute/Disabled' - - cp = create_instance(name, True) - node = PropertyValue(Name='nodepath', Value=node_name) - update = cp.createInstanceWithArguments(service, (node,)) - - result = True - try: - if disable: - new_node = update.createInstanceWithArguments(()) - new_node.setPropertyValue('Command', command) - update.insertByName(NEW_NODE_NAME, new_node) - else: - update.removeByName(NEW_NODE_NAME) - update.commitChanges() - except Exception as e: - result = False - - return result - - @classmethod - def disable(cls, command: str): - """Disable UNO command - - :param command: UNO command to disable - :type command: str - :return: True if correctly disable, False if not. - :rtype: bool - """ - return cls._set_app_command(command, True) - - @classmethod - def enabled(cls, command): - """Enabled UNO command - - :param command: UNO command to enabled - :type command: str - :return: True if correctly disable, False if not. - :rtype: bool - """ - return cls._set_app_command(command, False) - - @classproperty - def cmd(cls): - """Disable or enable commands""" - return cls.commands - - @classproperty - def desktop(cls): - """Create desktop instance - - :return: Desktop instance - :rtype: pyUno - """ - obj = create_instance('com.sun.star.frame.Desktop', True) - return obj - - @classmethod - def dispatch(cls, frame: Any, command: str, args: dict={}) -> None: - """Call dispatch, used only if not exists directly in API - - :param frame: doc or frame instance - :type frame: pyUno - :param command: Command to execute - :type command: str - :param args: Extra argument for command - :type args: dict - - `See DispatchCommands <`See DispatchCommands `_>`_ - """ - dispatch = create_instance('com.sun.star.frame.DispatchHelper') - if hasattr(frame, 'frame'): - frame = frame.frame - url = command - if not command.startswith('.uno:'): - url = f'.uno:{command}' - opt = dict_to_property(args) - dispatch.executeDispatch(frame, url, '', 0, opt) - return - - @classmethod - def fonts(cls): - """Get all font visibles in LibreOffice - - :return: tuple of FontDescriptors - :rtype: tuple - - `See API FontDescriptor `_ - """ - toolkit = create_instance('com.sun.star.awt.Toolkit') - device = toolkit.createScreenCompatibleDevice(0, 0) - return device.FontDescriptors - - @classmethod - def filters(cls): - """Get all support filters - - `See Help ConvertFilters `_ - `See API FilterFactory `_ - """ - factory = create_instance('com.sun.star.document.FilterFactory') - rows = [data_to_dict(factory[name]) for name in factory] - for row in rows: - row['UINames'] = data_to_dict(row['UINames']) - return rows - - -class LODocument(): - - def __init__(self, obj): - self._obj = obj - self._cc = obj.getCurrentController() - self._undo = True - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @property - def obj(self): - """Return original pyUno object""" - return self._obj - - @property - def type(self): - """Get type document""" - return self._type - - @property - def frame(self): - """Get frame document""" - return self._cc.getFrame() - - @property - def title(self): - """Get title document""" - return self.obj.getTitle() - @title.setter - def title(self, value): - self.obj.setTitle(value) - - @property - def uid(self): - """Get Runtime UID""" - return self.obj.RuntimeUID - - @property - def is_saved(self): - """Get is saved""" - return self.obj.hasLocation() - - @property - def is_modified(self): - """Get is modified""" - return self.obj.isModified() - - @property - def is_read_only(self): - """Get is read only""" - return self.obj.isReadonly() - - @property - def path(self): - """Get path in system files""" - return Paths.to_system(self.obj.URL) - - @property - def dir(self): - """Get directory from path""" - return Paths(self.path).path - - @property - def file_name(self): - """Get only file name""" - return Paths(self.path).file_name - - @property - def name(self): - """Get name without extension""" - return Paths(self.path).name - - @property - def visible(self): - """Get windows visible""" - w = self.frame.ContainerWindow - return w.isVisible() - @visible.setter - def visible(self, value): - w = self.frame.ContainerWindow - w.setVisible(value) - - @property - def zoom(self): - """Get current zoom value""" - return self._cc.ZoomValue - @zoom.setter - def zoom(self, value): - self._cc.ZoomValue = value - - @property - def status_bar(self): - """Get status bar""" - bar = self._cc.getStatusIndicator() - return bar - - @property - def selection(self): - """Get current selecction""" - sel = self.obj.CurrentSelection - return sel - - @property - def table_auto_formats(self): - taf = create_instance('com.sun.star.sheet.TableAutoFormats') - return taf.ElementNames - - def save(self, path: str='', args: dict={}) -> bool: - """Save document - - :param path: Path to save document - :type path: str - :param args: Optional: Extra argument for save - :type args: dict - :return: True if save correctly, False if not - :rtype: bool - """ - if not path: - self.obj.store() - return True - - path_save = Paths.to_url(path) - opt = dict_to_property(args) - - try: - self.obj.storeAsURL(path_save, opt) - except Exception as e: - error(e) - return False - - return True - - def close(self): - """Close document""" - self.obj.close(True) - return - - def to_pdf(self, path: str='', args: dict={}): - """Export to PDF - - :param path: Path to export document - :type path: str - :param args: Optional: Extra argument for export - :type args: dict - :return: None if path or stream in memory - :rtype: bytes or None - - `See PDF Export `_ - """ - stream = None - path_pdf = 'private:stream' - - filter_name = f'{self.type}_pdf_Export' - filter_data = dict_to_property(args, True) - filters = { - 'FilterName': filter_name, - 'FilterData': filter_data, - } - if path: - path_pdf = Paths.to_url(path) - else: - stream = IOStream.output() - filters['OutputStream'] = stream - - opt = dict_to_property(filters) - try: - self.obj.storeToURL(path_pdf, opt) - except Exception as e: - error(e) - - if not stream is None: - stream = stream.buffer - - return stream - - def export(self, path: str='', filter_name: str='', args: dict={}): - """Export to others formats - - :param path: Path to export document - :type path: str - :param filter_name: Filter name to export - :type filter_name: str - :param args: Optional: Extra argument for export - :type args: dict - :return: None if path or stream in memory - :rtype: bytes or None - """ - FILTERS = { - 'xlsx': 'Calc MS Excel 2007 XML', - 'xls': 'MS Excel 97', - 'docx': 'MS Word 2007 XML', - 'doc': 'MS Word 97', - 'rtf': 'Rich Text Format', - } - - stream = None - path_target = 'private:stream' - - filter_name = FILTERS.get(filter_name, filter_name) - filter_data = dict_to_property(args, True) - filters = { - 'FilterName': filter_name, - 'FilterData': filter_data, - } - if path: - path_target = Paths.to_url(path) - else: - stream = IOStream.output() - filters['OutputStream'] = stream - - opt = dict_to_property(filters) - try: - self.obj.storeToURL(path_target, opt) - except Exception as e: - error(e) - - if not stream is None: - stream = stream.buffer - - return stream - - def _create_instance(self, name): - obj = self.obj.createInstance(name) - return obj - - def set_focus(self): - """Send focus to windows""" - w = self.frame.ComponentWindow - w.setFocus() - return - - def copy(self): - """Copy current selection""" - LOMain.dispatch(self.frame, 'Copy') - return - - def paste(self): - """Paste current content in clipboard""" - sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') - transferable = sc.getContents() - self._cc.insertTransferable(transferable) - return - - def paste_special(self): - """Insert contents, show dialog box Paste Special""" - LOMain.dispatch(self.frame, 'InsertContents') - return - - def paste_values(self): - """Paste only values""" - args = { - 'Flags': 'SVDT', - # ~ 'FormulaCommand': 0, - # ~ 'SkipEmptyCells': False, - # ~ 'Transpose': False, - # ~ 'AsLink': False, - # ~ 'MoveMode': 4, - } - LOMain.dispatch(self.frame, 'InsertContents', args) - return - - def clear_undo(self): - """Clear history undo""" - self.obj.getUndoManager().clear() - return - - -class LODocMain(): - """Classe for start module""" - _type = 'main' - - def __init__(self, obj): - self._obj = obj - - @property - def obj(self): - return self._obj - - @property - def type(self): - return self._type - - -class LOCellStyle(): - - def __init__(self, obj): - self._obj = obj - - def __str__(self): - return f'CellStyle: {self.name}' - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self.obj.Name - - @property - def properties(self): - properties = self.obj.PropertySetInfo.Properties - data = {p.Name: getattr(self.obj, p.Name) for p in properties} - return data - @properties.setter - def properties(self, values): - _set_properties(self.obj, values) - - -class LOCellStyles(): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - - def __len__(self): - return len(self.obj) - - def __getitem__(self, index): - return LOCellStyle(self.obj[index]) - - def __setitem__(self, key, value): - self.obj[key] = value - - def __delitem__(self, key): - if not isinstance(key, str): - key = key.Name - del self.obj[key] - - def __contains__(self, item): - return item in self.obj - - @property - def obj(self): - return self._obj - - @property - def names(self): - return self.obj.ElementNames - - def new(self, name: str): - obj = self._doc.create_instance('com.sun.star.style.CellStyle') - self.obj[name] = obj - return LOCellStyle(obj) - - -class LODocCalc(LODocument): - """Classe for Calc module""" - TYPE_RANGES = ('ScCellObj', 'ScCellRangeObj') - RANGES = 'ScCellRangesObj' - SHAPE = 'com.sun.star.drawing.SvxShapeCollection' - _type = 'calc' - - def __init__(self, obj): - super().__init__(obj) - self._sheets = obj.Sheets - self._listener_range_selection = None - - def __getitem__(self, index): - """Index access""" - return LOCalcSheet(self._sheets[index]) - - def __setitem__(self, key: str, value: Any): - """Insert new sheet""" - self._sheets[key] = value - - def __len__(self): - return self._sheets.Count - - def __contains__(self, item): - return item in self._sheets - - def __iter__(self): - self._i = 0 - return self - - def __next__(self): - try: - sheet = LOCalcSheet(self._sheets[self._i]) - except Exception as e: - raise StopIteration - self._i += 1 - return sheet - - def __str__(self): - return f'Calc: {self.title}' - - @property - def selection(self): - sel = self.obj.CurrentSelection - type_obj = sel.ImplementationName - if type_obj in self.TYPE_RANGES: - sel = LOCalcRange(sel) - elif type_obj == self.RANGES: - sel = LOCalcRanges(sel) - elif type_obj == self.SHAPE: - if len(sel) == 1: - sel = LOShape(sel[0]) - else: - sel = LOShapes(sel) - else: - debug(type_obj) - return sel - - @property - def headers(self): - """Get true if is visible columns/rows headers""" - return self._cc.ColumnRowHeaders - @headers.setter - def headers(self, value): - """Set visible columns/rows headers""" - self._cc.ColumnRowHeaders = value - - @property - def tabs(self): - """Get true if is visible tab sheets""" - return self._cc.SheetTabs - @tabs.setter - def tabs(self, value): - """Set visible tab sheets""" - self._cc.SheetTabs = value - - @property - def names(self): - """Get all sheet names""" - names = self.obj.Sheets.ElementNames - return names - - @property - def active(self): - """Get active sheet""" - return LOCalcSheet(self._cc.ActiveSheet) - - @property - def new_sheet(self): - sheet = self._create_instance('com.sun.star.sheet.Spreadsheet') - return sheet - - @property - def events(self): - return LOEvents(self.obj.Events) - - @property - def cs(self): - return self.cell_styles - @property - def cell_styles(self): - obj = self.obj.StyleFamilies['CellStyles'] - return LOCellStyles(obj, self) - - def activate(self, sheet: Any): - """Activate sheet - - :param sheet: Sheet to activate - :type sheet: str, pyUno or LOCalcSheet - """ - obj = sheet - if isinstance(sheet, LOCalcSheet): - obj = sheet.obj - elif isinstance(sheet, str): - obj = self._sheets[sheet] - self._cc.setActiveSheet(obj) - return - - def insert(self, name: Union[str, list, tuple]): - """Insert new sheet - - :param name: Name new sheet, or iterable with names. - :type name: str, list or tuple - :return: New last instance sheet. - :rtype: LOCalcSheet - """ - names = name - if isinstance(name, str): - names = (name,) - for n in names: - self._sheets[n] = self._create_instance('com.sun.star.sheet.Spreadsheet') - return LOCalcSheet(self._sheets[n]) - - def remove(self, name: str): - """Remove sheet by name - - :param name: Name sheet will remove - :type name: str - """ - if isinstance(name, LOCalcSheet): - name = name.name - self._sheets.removeByName(name) - return - - def move(self, name:str, pos: int=-1): - """Move sheet name to position - - :param name: Name sheet to move - :type name: str - :param pos: New position, if pos=-1 move to end - :type pos: int - """ - index = pos - if pos < 0: - index = len(self) - if isinstance(name, LOCalcSheet): - name = name.name - self._sheets.moveByName(name, index) - return - - def _get_new_name_sheet(self, name): - i = 1 - new_name = f'{name}_{i}' - while new_name in self: - i += 1 - new_name = f'{name}_{i}' - return new_name - - def copy_sheet(self, name: Any, new_name: str='', pos: int=-1): - """Copy sheet by name - - """ - if isinstance(name, LOCalcSheet): - name = name.name - index = pos - if pos < 0: - index = len(self) - if not new_name: - new_name = self._get_new_name_sheet(name) - self._sheets.copyByName(name, new_name, index) - return LOCalcSheet(self._sheets[new_name]) - - def copy_from(self, doc: Any, source: Any=None, target: Any=None, pos: int=-1): - """Copy sheet from document - - """ - index = pos - if pos < 0: - index = len(self) - - names = source - if not source: - names = doc.names - elif isinstance(source, str): - names = (source,) - elif isinstance(source, LOCalcSheet): - names = (source.name,) - - new_names = target - if not target: - new_names = names - elif isinstance(target, str): - new_names = (target,) - - for i, name in enumerate(names): - self._sheets.importSheet(doc.obj, name, index + i) - self[index + i].name = new_names[i] - - return LOCalcSheet(self._sheets[index]) - - def sort(self, reverse=False): - """Sort sheets by name - - :param reverse: For order in reverse - :type reverse: bool - """ - names = sorted(self.names, reverse=reverse) - for i, n in enumerate(names): - self.move(n, i) - return - - @run_in_thread - def start_range_selection(self, controllers: Any, args: dict={}): - """Start select range selection by user - - `See Api RangeSelectionArguments `_ - """ - if args: - args['CloseOnMouseRelease'] = args.get('CloseOnMouseRelease', True) - else: - args = dict( - Title = 'Please select a range', - CloseOnMouseRelease = True) - properties = dict_to_property(args) - - self._listener_range_selection = EventsRangeSelectionListener(controllers(self)) - self._cc.addRangeSelectionListener(self._listener_range_selection) - self._cc.startRangeSelection(properties) - return - - def remove_range_selection_listener(self): - if not self._listener_range_selection is None: - self._cc.removeRangeSelectionListener(self._listener_range_selection) - return - - def select(self, rango: Any): - obj = rango - if hasattr(rango, 'obj'): - obj = rango.obj - self._cc.select(obj) - return - - @property - def ranges(self): - obj = self._create_instance('com.sun.star.sheet.SheetCellRanges') - return LOCalcRanges(obj) - - def get_ranges(self, address: str): - ranges = self.ranges - ranges.add([sheet[address] for sheet in self]) - return ranges - - -class LOCalcSheet(object): - - def __init__(self, obj): - self._obj = obj - - def __getitem__(self, index): - return LOCalcRange(self.obj[index]) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __str__(self): - return f'Sheet: {self.name}' - - @property - def obj(self): - return self._obj - - @property - def doc(self): - return LODocCalc(self.obj.DrawPage.Forms.Parent) - - @property - def name(self): - return self._obj.Name - @name.setter - def name(self, value): - self._obj.Name = value - - @property - def code_name(self): - return self._obj.CodeName - @code_name.setter - def code_name(self, value): - self._obj.CodeName = value - - @property - def visible(self): - return self._obj.IsVisible - @visible.setter - def visible(self, value): - self._obj.IsVisible = value - - @property - def color(self): - return self._obj.TabColor - @color.setter - def color(self, value): - self._obj.TabColor = Color()(value) - - @property - def events(self): - return LOEvents(self.obj.Events) - - @property - def used_area(self): - cursor = self.create_cursor() - cursor.gotoEndOfUsedArea(True) - return self[cursor.AbsoluteName] - - @property - def is_protected(self): - return self._obj.isProtected() - - @property - def password(self): - return '' - @password.setter - def password(self, value): - self.obj.protect(value) - - def unprotect(self, value): - try: - self.obj.unprotect(value) - return True - except: - pass - return False - - def move(self, pos: int=-1): - index = pos - if pos < 0: - index = len(self.doc) - self.doc.move(self.name, index) - return - - def remove(self): - self.doc.remove(self.name) - return - - def copy(self, new_name: str='', pos: int=-1): - index = pos - if pos < 0: - index = len(self.doc) - new_sheet = self.doc.copy_sheet(self.name, new_name, index) - return new_sheet - - def copy_to(self, doc: Any, target: str='', pos: int=-1): - index = pos - if pos < 0: - index = len(doc) - - new_name = target or self.name - sheet = doc.copy_from(self.doc, self.name, new_name, index) - return sheet - - def activate(self): - self.doc.activate(self.obj) - return - - def create_cursor(self, rango: Any=None): - if rango is None: - cursor = self.obj.createCursor() - else: - obj = rango - if hasattr(rango, 'obj'): - obj = rango.obj - cursor = self.obj.createCursorByRange(obj) - return cursor - - -class LOCalcRanges(object): - - def __init__(self, obj): - self._obj = obj - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __len__(self): - return self._obj.Count - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - try: - r = self.obj[self._index] - rango = LOCalcRange(r) - except IndexError: - raise StopIteration - - self._index += 1 - return rango - - def __contains__(self, item): - return self._obj.hasByName(item.name) - - def __getitem__(self, index): - r = self.obj[index] - rango = LOCalcRange(r) - return rango - - def __str__(self): - s = f'Ranges: {self.names}' - return s - - @property - def obj(self): - return self._obj - - @property - def names(self): - return self.obj.ElementNames - - @property - def data(self): - rows = [r.data for r in self] - return rows - @data.setter - def data(self, values): - for i, data in enumerate(values): - self[i].data = data - - @property - def style(self): - return '' - @style.setter - def style(self, value): - for r in self: - r.style = value - - def add(self, rangos: Any): - if isinstance(rangos, LOCalcRange): - rangos = (rangos,) - for r in rangos: - self.obj.addRangeAddress(r.range_address, False) - return - - def remove(self, rangos: Any): - if isinstance(rangos, LOCalcRange): - rangos = (rangos,) - for r in rangos: - self.obj.removeRangeAddress(r.range_address) - return - - -class LOCalcRange(object): - CELL = 'ScCellObj' - - def __init__(self, obj): - self._obj = obj - self._is_cell = obj.ImplementationName == self.CELL - - def __getitem__(self, index): - return LOCalcRange(self.obj[index]) - - def __iter__(self): - self._r = 0 - self._c = 0 - return self - - def __next__(self): - try: - rango = self[self._r, self._c] - except Exception as e: - raise StopIteration - self._c += 1 - if self._c == self.obj.Columns.Count: - self._c = 0 - self._r +=1 - return rango - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __len__(self): - ra = self.range_address - rows = ra.EndRow - ra.StartRow + 1 - cols = ra.EndColumn - ra.StartColumn + 1 - return rows, cols - - def __str__(self): - s = f'Range: {self.name}' - if self.is_cell: - s = f'Cell: {self.name}' - return s - - @property - def obj(self): - return self._obj - - @property - def is_cell(self): - return self._is_cell - - @property - def name(self): - return self.obj.AbsoluteName - - @property - def address(self): - return self.obj.CellAddress - - @property - def range_address(self): - return self.obj.RangeAddress - - @property - def sheet(self): - return LOCalcSheet(self.obj.Spreadsheet) - - @property - def doc(self): - doc = self.obj.Spreadsheet.DrawPage.Forms.Parent - return LODocCalc(doc) - - @property - def cursor(self): - cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) - return cursor - - def offset(self, rows=0, cols=1): - ra = self.range_address - col = ra.EndColumn + cols - row = ra.EndRow + rows - return LOCalcRange(self.sheet[row, col].obj) - - @property - def style(self): - return self.obj.CellStyle - @style.setter - def style(self, value): - self.obj.CellStyle = value - - @property - def string(self): - return self.obj.String - @string.setter - def string(self, value): - self.obj.setString(value) - - @property - def data(self): - return self.obj.getDataArray() - @data.setter - def data(self, values): - if self._is_cell: - self.to_size(len(values[0]), len(values)).data = values - else: - self.obj.setDataArray(values) - - @property - def current_region(self): - cursor = self.cursor - cursor.collapseToCurrentRegion() - rango = self.obj.Spreadsheet[cursor.AbsoluteName] - return LOCalcRange(rango) - - def to_size(self, cols: int, rows: int): - cursor = self.cursor - cursor.collapseToSize(cols, rows) - rango = self.obj.Spreadsheet[cursor.AbsoluteName] - return LOCalcRange(rango) - - -class LOWriterTextRange(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - - @property - def obj(self): - return self._obj - - @property - def text(self): - return self.obj.Text - - @property - def cursor(self): - return self.text.createTextCursorByRange(self.obj) - - def insert_comment(self, content: str, author: str='', dt: Any=None): - # ~ range.Text.insertTextContent(cursor, comment, False) - comment = self._doc._create_instance('com.sun.star.text.textfield.Annotation') - comment.Content = content - comment.Author = author - comment.attach(self.cursor.End) - return - - -class LODocWriter(LODocument): - _type = 'writer' - TEXT_RANGES = 'SwXTextRanges' - - def __init__(self, obj): - super().__init__(obj) - self._view_settings = self._cc.ViewSettings - - @property - def selection(self): - sel = self.obj.CurrentSelection - type_obj = sel.ImplementationName - if type_obj == self.TEXT_RANGES: - if len(sel) == 1: - sel = LOWriterTextRange(sel[0], self) - - return sel - - @property - def zoom(self): - return self._view_settings.ZoomValue - @zoom.setter - def zoom(self, value): - self._view_settings.ZoomValue = value - - -class LODocDrawImpress(LODocument): - - def __init__(self, obj): - super().__init__(obj) - - -class LODocDraw(LODocDrawImpress): - _type = 'draw' - - def __init__(self, obj): - super().__init__(obj) - - -class LODocImpress(LODocDrawImpress): - _type = 'impress' - - def __init__(self, obj): - super().__init__(obj) - - -class LODocMath(LODocDrawImpress): - _type = 'math' - - def __init__(self, obj): - super().__init__(obj) - - -class LODocBase(LODocument): - _type = 'base' - - def __init__(self, obj): - super().__init__(obj) - - -class LODocIDE(LODocument): - _type = 'basicide' - - def __init__(self, obj): - super().__init__(obj) - - -class LODocuments(): - """Classe for documents - """ - TYPES = { - 'calc': 'com.sun.star.sheet.SpreadsheetDocument', - 'writerr': 'com.sun.star.text.TextDocument', - 'draw': 'com.sun.star.drawing.DrawingDocument', - 'impress': 'com.sun.star.presentation.PresentationDocument', - 'math': 'com.sun.star.formula.FormulaProperties', - 'ide': 'com.sun.star.script.BasicIDE', - 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', - 'main': 'com.sun.star.frame.StartModule', - } - _classes = { - 'com.sun.star.sheet.SpreadsheetDocument': LODocCalc, - 'com.sun.star.text.TextDocument': LODocWriter, - 'com.sun.star.drawing.DrawingDocument': LODocDraw, - 'com.sun.star.presentation.PresentationDocument': LODocImpress, - 'com.sun.star.formula.FormulaProperties': LODocMath, - 'com.sun.star.script.BasicIDE': LODocIDE, - 'com.sun.star.sdb.OfficeDatabaseDocument': LODocBase, - 'com.sun.star.frame.StartModule': LODocMain - } - # ~ BASE: 'com.sun.star.sdb.DocumentDataSource', - - def __init__(self): - self._desktop = LOMain.desktop - - def __len__(self): - # ~ len(self._desktop.Components) - for i, _ in enumerate(self._desktop.Components): - pass - return i + 1 - - def __getitem__(self, index): - # ~ self._desktop.Components[index] - obj = None - - for i, doc in enumerate(self._desktop.Components): - if isinstance(index, int) and i == index: - obj = self._get_class_doc(doc) - break - elif isinstance(index, str) and doc.Title == index: - obj = self._get_class_doc(doc) - break - - return obj - - def __contains__(self, item): - doc = self[item] - return not doc is None - - def __iter__(self): - self._i = -1 - return self - - def __next__(self): - self._i += 1 - doc = self[self._i] - if doc is None: - raise StopIteration - else: - return doc - - def _get_class_doc(self, doc): - """Identify type doc""" - main = 'com.sun.star.frame.StartModule' - if doc.supportsService(main): - return self._classes[main](doc) - - mm = create_instance('com.sun.star.frame.ModuleManager') - type_module = mm.identify(doc) - return self._classes[type_module](doc) - - @property - def active(self): - """Get active doc""" - doc = self._desktop.getCurrentComponent() - obj = self._get_class_doc(doc) - return obj - - def new(self, type_doc: str='calc', args: dict={}): - """Create new document - - :param type_doc: The type doc to create, default is Calc - :type type_doc: str - :param args: Extra argument - :type args: dict - :return: New document - :rtype: Custom classe - """ - url = f'private:factory/s{type_doc}' - opt = dict_to_property(args) - doc = self._desktop.loadComponentFromURL(url, '_default', 0, opt) - obj = self._get_class_doc(doc) - return obj - - def open(self, path: str, args: dict={}): - """ Open document from path - - :param path: Path to document - :type path: str - :param args: Extra argument - Usually options: - Hidden: True or False - AsTemplate: True or False - ReadOnly: True or False - Password: super_secret - MacroExecutionMode: 4 = Activate macros - Preview: True or False - :type args: dict - - `See API XComponentLoader `_ - `See API MediaDescriptor `_ - """ - url = Paths.to_url(path) - opt = dict_to_property(args) - doc = self._desktop.loadComponentFromURL(url, '_default', 0, opt) - if doc is None: - return - - obj = self._get_class_doc(doc) - return obj - - -def __getattr__(name): - classes = { - 'inspect': LOInspect, - 'dates': Dates, - 'json': Json, - 'macro': Macro, - 'shell': Shell, - 'timer': Timer, - 'hash': Hash, - 'path': Paths, - 'config': Config, - 'url': Url, - 'email': Email, - 'color': Color(), - 'io': IOStream, - 'clipboard': ClipBoard, - 'shortcuts': LOShortCuts(), - 'menus': LOMenus(), - 'lo': LOMain, - 'command': LOMain.cmd, - 'docs': LODocuments(), - 'active': LODocuments().active, - } - if name in classes: - return classes[name] - - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -class LOServer(object): - """Started LibeOffice like server - """ - HOST = 'localhost' - PORT = '8100' - ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext' - CMD = ['soffice', - '-env:SingleAppInstance=false', - '-env:UserInstallation=file:///tmp/LO_Process8100', - '--headless', '--norestore', '--invisible', - f'--accept={ARG}'] - - def __init__(self): - self._server = None - self._ctx = None - self._sm = None - self._start_server() - self._init_values() - - def _init_values(self): - global CTX - global SM - - if not self.is_running: - return - - ctx = uno.getComponentContext() - service = 'com.sun.star.bridge.UnoUrlResolver' - resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx) - self._ctx = resolver.resolve('uno:{}'.format(self.ARG)) - self._sm = self._ctx.getServiceManager() - CTX = self._ctx - SM = self._sm - return - - @property - def is_running(self): - try: - s = socket.create_connection((self.HOST, self.PORT), 5.0) - s.close() - debug('LibreOffice is running...') - return True - except ConnectionRefusedError: - return False - - def _start_server(self): - if self.is_running: - return - - for i in range(3): - self._server = subprocess.Popen(self.CMD, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - time.sleep(3) - if self.is_running: - break - return - - def stop(self): - """Stop server - """ - if self._server is None: - print('Search pgrep soffice') - else: - self._server.terminate() - debug('LibreOffice is stop...') - return - - def _create_instance(self, name, with_context=True): - if with_context: - instance = self._sm.createInstanceWithContext(name, self._ctx) - else: - instance = self._sm.createInstance(name) - return instance diff --git a/source/easymacro (copy).bk2 b/source/easymacro (copy).bk2 deleted file mode 100644 index 0d78e21..0000000 --- a/source/easymacro (copy).bk2 +++ /dev/null @@ -1,4671 +0,0 @@ -#!/usr/bin/env python3 - -# == Rapid Develop Macros in LibreOffice == - -import base64 -import ctypes -import gettext -import zipfile - -from collections import OrderedDict -from collections.abc import MutableMapping -from decimal import Decimal -from enum import IntEnum - -import imaplib - -from com.sun.star.awt.PosSize import POSSIZE, SIZE - -from com.sun.star.sheet import TableFilterField -from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA -from com.sun.star.util import Time, Date, DateTime - -from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK - -from com.sun.star.lang import Locale -from com.sun.star.awt import XActionListener -from com.sun.star.awt import XMenuListener -from com.sun.star.awt import XMouseListener -from com.sun.star.awt import XMouseMotionListener -from com.sun.star.awt import XFocusListener -from com.sun.star.awt import XKeyListener -from com.sun.star.awt import XItemListener -from com.sun.star.awt import XTabListener -from com.sun.star.awt import XSpinListener -from com.sun.star.awt import XWindowListener -from com.sun.star.awt import XTopWindowListener -from com.sun.star.awt.grid import XGridDataListener -from com.sun.star.awt.grid import XGridSelectionListener -from com.sun.star.script import ScriptEventDescriptor - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1FontUnderline.html -from com.sun.star.awt import FontUnderline -from com.sun.star.style.VerticalAlignment import TOP, MIDDLE, BOTTOM - -from com.sun.star.view.SelectionType import SINGLE, MULTI, RANGE - -from com.sun.star.sdb.CommandType import TABLE, QUERY, COMMAND - -try: - from peewee import Database, DateTimeField, DateField, TimeField, \ - __exception_wrapper__ -except ImportError as e: - Database = DateField = TimeField = DateTimeField = object - print('You need install peewee, only if you will develop with Base') - - -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_DATE = '%d/%m/%Y %H:%M:%S' -logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') -logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') -logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') -logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) -log = logging.getLogger(__name__) - - -# ~ You can get custom salt -# ~ codecs.encode(os.urandom(16), 'hex') -# ~ but, not modify this file, modify in import file -SALT = b'c9548699d4e432dfd2b46adddafbb06d' - -LOG_NAME = 'ZAZ' - -LEFT = 0 -CENTER = 1 -RIGHT = 2 - -CALC = 'calc' -WRITER = 'writer' -DRAW = 'draw' -IMPRESS = 'impress' -BASE = 'base' -MATH = 'math' -BASIC = 'basic' -MAIN = 'main' -TYPE_DOC = { - CALC: 'com.sun.star.sheet.SpreadsheetDocument', - WRITER: 'com.sun.star.text.TextDocument', - DRAW: 'com.sun.star.drawing.DrawingDocument', - IMPRESS: 'com.sun.star.presentation.PresentationDocument', - MATH: 'com.sun.star.formula.FormulaProperties', - BASE: 'com.sun.star.sdb.DocumentDataSource', - BASIC: 'com.sun.star.script.BasicIDE', - MAIN: 'com.sun.star.frame.StartModule', -} - -OBJ_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' -OBJ_GRAPHIC = 'SwXTextGraphicObject' - -OBJ_TEXTS = 'SwXTextRanges' -OBJ_TEXT = 'SwXTextRange' - -CLSID = { - 'FORMULA': '078B7ABA-54FC-457F-8551-6147e776a997', -} - -SERVICES = { - 'TEXT_EMBEDDED': 'com.sun.star.text.TextEmbeddedObject', - 'TEXT_TABLE': 'com.sun.star.text.TextTable', - 'GRAPHIC': 'com.sun.star.text.GraphicObject', -} - - -# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL -class FilterOperator(IntEnum): - EMPTY = 0 - NO_EMPTY = 1 - EQUAL = 2 - NOT_EQUAL = 3 - -# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 -class Border(IntEnum): - NO_BORDER = 0 - BORDER = 1 - SIMPLE = 2 - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a -class ValidationType(): - from com.sun.star.sheet.ValidationType \ - import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM -VT = ValidationType - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7 -class ValidationAlertStyle(): - from com.sun.star.sheet.ValidationAlertStyle \ - import STOP, WARNING, INFO, MACRO -VAS = ValidationAlertStyle - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html -class ConditionOperator(): - from com.sun.star.sheet.ConditionOperator2 \ - import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \ - LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE -CO = ConditionOperator - - -class DataPilotFieldOrientation(): - from com.sun.star.sheet.DataPilotFieldOrientation \ - import HIDDEN, COLUMN, ROW, PAGE, DATA -DPFO = DataPilotFieldOrientation - - -class CellInsertMode(): - from com.sun.star.sheet.CellInsertMode import DOWN, RIGHT, ROWS, COLUMNS -CIM = CellInsertMode - - -class CellDeleteMode(): - from com.sun.star.sheet.CellDeleteMode import UP, LEFT, ROWS, COLUMNS -CDM = CellDeleteMode - - -class FormButtonType(): - from com.sun.star.form.FormButtonType import PUSH, SUBMIT, RESET, URL -FBT = FormButtonType - - -class TextContentAnchorType(): - from com.sun.star.text.TextContentAnchorType \ - import AT_PARAGRAPH, AS_CHARACTER, AT_PAGE, AT_FRAME, AT_CHARACTER -TCAT = TextContentAnchorType - - -SECONDS_DAY = 60 * 60 * 24 -DIR = { - 'images': 'images', - 'locales': 'locales', -} - -KEY = { - 'enter': 1280, -} - -DEFAULT_MIME_TYPE = 'png' -MIME_TYPE = { - 'png': 'image/png', - 'jpg': 'image/jpeg', -} - - -try: - COUNTRY = LANGUAGE.split('-')[1] -except: - COUNTRY = '' -LOCALE = Locale(LANG, COUNTRY, '') - - -def inspect(obj: Any, to_sheet: bool=True) -> None: - if hasattr(obj, 'obj'): - obj = obj.obj - - if to_sheet: - _inspect_to_sheet(obj) - else: - zaz = create_instance('net.elmau.zaz.inspect') - zaz.inspect(obj) - return - - -def get_type_doc(obj: Any) -> str: - for k, v in TYPE_DOC.items(): - if obj.supportsService(v): - return k - return '' - - -def _get_class_doc(obj: Any) -> Any: - classes = { - CALC: LOCalc, - WRITER: LOWriter, - DRAW: LODraw, - IMPRESS: LOImpress, - BASE: LOBase, - MATH: LOMath, - BASIC: LOBasic, - } - type_doc = get_type_doc(obj) - return classes[type_doc](obj) - - -def _date_to_struct(value): - if isinstance(value, datetime.datetime): - d = DateTime() - d.Year = value.year - d.Month = value.month - d.Day = value.day - d.Hours = value.hour - d.Minutes = value.minute - d.Seconds = value.second - elif isinstance(value, datetime.date): - d = Date() - d.Day = value.day - d.Month = value.month - d.Year = value.year - elif isinstance(value, datetime.time): - d = Time() - d.Hours = value.hour - d.Minutes = value.minute - d.Seconds = value.second - return d - - -def _struct_to_date(value): - d = None - if isinstance(value, Time): - d = datetime.time(value.Hours, value.Minutes, value.Seconds) - elif isinstance(value, Date): - if value != Date(): - d = datetime.date(value.Year, value.Month, value.Day) - elif isinstance(value, DateTime): - if value.Year > 0: - d = datetime.datetime( - value.Year, value.Month, value.Day, - value.Hours, value.Minutes, value.Seconds) - return d - - -def install_locales(path: str, domain: str='base', dir_locales=DIR['locales']): - path_locales = _P.join(_P(path).path, dir_locales) - try: - lang = gettext.translation(domain, path_locales, languages=[LANG]) - lang.install() - _ = lang.gettext - except Exception as e: - from gettext import gettext as _ - error(e) - return _ - - -def _export_image(obj, args): - name = 'com.sun.star.drawing.GraphicExportFilter' - exporter = create_instance(name) - path = _P.to_system(args['URL']) - args = dict_to_property(args) - exporter.setSourceDocument(obj) - exporter.filter(args) - return _P.exists(path) - - -def get_size_screen(): - res = '' - if IS_WIN: - user32 = ctypes.windll.user32 - res = f'{user32.GetSystemMetrics(0)}x{user32.GetSystemMetrics(1)}' - else: - try: - args = 'xrandr | grep "*" | cut -d " " -f4' - res = run(args, split=False) - except Exception as e: - error(e) - return res.strip() - - -def _get_key(password): - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - - kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=SALT, - iterations=100000) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - return key - - -def encrypt(data, password): - from cryptography.fernet import Fernet - - f = Fernet(_get_key(password)) - if isinstance(data, str): - data = data.encode() - token = f.encrypt(data).decode() - return token - - -def decrypt(token, password): - from cryptography.fernet import Fernet, InvalidToken - - data = '' - f = Fernet(_get_key(password)) - try: - data = f.decrypt(token.encode()).decode() - except InvalidToken as e: - error('Invalid Token') - return data - - -def switch_design_mode(doc): - call_dispatch(doc.frame, '.uno:SwitchControlDesignMode') - return - - -class ImapServer(object): - - def __init__(self, config): - self._server = None - self._error = '' - self._is_connect = self._login(config) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @property - def is_connect(self): - return self._is_connect - - @property - def error(self): - return self._error - - def _login(self, config): - try: - # ~ hosts = 'gmail' in config['server'] - if config['ssl']: - self._server = imaplib.IMAP4_SSL(config['server'], config['port']) - else: - self._server = imaplib.IMAP4(config['server'], config['port']) - self._server.login(config['user'], config['password']) - self._server.select() - return True - except imaplib.IMAP4.error as e: - self._error = str(e) - return False - except Exception as e: - self._error = str(e) - return False - return False - - def get_folders(self, exclude=()): - folders = {} - result, subdir = self._server.list() - for s in subdir: - print(s.decode('utf-8')) - return folders - - def close(self): - try: - self._server.close() - self._server.logout() - msg = 'Close connection...' - debug(msg) - except: - pass - return - - -# ~ Classes - -class LOBaseObject(object): - - def __init__(self, obj): - self._obj = obj - - def __setattr__(self, name, value): - exists = hasattr(self, name) - if not exists and not name in ('_obj', '_index', '_view'): - setattr(self._obj, name, value) - else: - super().__setattr__(name, value) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - -class LOCalc(LODocument): - - @property - def db_ranges(self): - # ~ return LOCalcDataBaseRanges(self.obj.DataBaseRanges) - return self.obj.DatabaseRanges - - def render(self, data, sheet=None, clean=True): - if sheet is None: - sheet = self.active - return sheet.render(data, clean=clean) - - -class LOChart(object): - - def __init__(self, name, obj, draw_page): - self._name = name - self._obj = obj - self._eobj = self._obj.EmbeddedObject - self._type = 'Column' - self._cell = None - self._shape = self._get_shape(draw_page) - self._pos = self._shape.Position - - def __getitem__(self, index): - return LOBaseObject(self.diagram.getDataRowProperties(index)) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self._name - - @property - def diagram(self): - return self._eobj.Diagram - - @property - def type(self): - return self._type - @type.setter - def type(self, value): - self._type = value - if value == 'Bar': - self.diagram.Vertical = True - return - type_chart = f'com.sun.star.chart.{value}Diagram' - self._eobj.setDiagram(self._eobj.createInstance(type_chart)) - - @property - def cell(self): - return self._cell - @cell.setter - def cell(self, value): - self._cell = value - self._shape.Anchor = value.obj - - @property - def position(self): - return self._pos - @position.setter - def position(self, value): - self._pos = value - self._shape.Position = value - - def _get_shape(self, draw_page): - for shape in draw_page: - if shape.PersistName == self.name: - break - return shape - - -class LOSheetCharts(object): - - def __init__(self, obj, sheet): - self._obj = obj - self._sheet = sheet - - def __getitem__(self, index): - return LOChart(index, self.obj[index], self._sheet.draw_page) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __contains__(self, item): - return item in self.obj - - def __len__(self): - return len(self.obj) - - @property - def obj(self): - return self._obj - - def new(self, name, pos_size, data): - self.obj.addNewByName(name, pos_size, data, True, True) - return LOChart(name, self.obj[name], self._sheet.draw_page) - - -class LOSheetTableField(object): - - def __init__(self, obj): - self._obj = obj - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self.obj.Name - - @property - def orientation(self): - return self.obj.Orientation - @orientation.setter - def orientation(self, value): - self.obj.Orientation = value - - -# ~ com.sun.star.sheet.DataPilotFieldOrientation.ROW -class LOSheetTable(object): - - def __init__(self, obj): - self._obj = obj - self._source = None - - def __getitem__(self, index): - field = self.obj.DataPilotFields[index] - return LOSheetTableField(field) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def filter(self): - return self.obj.ShowFilterButton - @filter.setter - def filter(self, value): - self.obj.ShowFilterButton = value - - @property - def source(self): - return self._source - @source.setter - def source(self, value): - self._source = value - self.obj.SourceRange = value.range_address - - @property - def rows(self): - return self.obj.RowFields - @rows.setter - def rows(self, values): - if not isinstance(values, tuple): - values = (values,) - for v in values: - with self[v] as f: - f.orientation = DPFO.ROW - @property - def columns(self): - return self.obj.ColumnFields - @columns.setter - def columns(self, values): - if not isinstance(values, tuple): - values = (values,) - for v in values: - with self[v] as f: - f.orientation = DPFO.COLUMN - - @property - def data(self): - return self.obj.DataFields - @data.setter - def data(self, values): - if not isinstance(values, tuple): - values = (values,) - for v in values: - with self[v] as f: - f.orientation = DPFO.DATA - - -class LOSheetTables(object): - - def __init__(self, obj, sheet): - self._obj = obj - self._sheet = sheet - - def __getitem__(self, index): - return LOSheetTable(self.obj[index]) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __contains__(self, item): - return item in self.obj - - @property - def obj(self): - return self._obj - - @property - def count(self): - return self.obj.Count - - @property - def names(self): - return self.obj.ElementNames - - def new(self, name, target): - table = self.obj.createDataPilotDescriptor() - self.obj.insertNewByName(name, target.address, table) - return LOSheetTable(self.obj[name]) - - def remove(self, name): - self.obj.removeByName(name) - return - - -# ~ class LOFormControl(LOBaseObject): -class LOFormControl(): - EVENTS = { - 'action': 'actionPerformed', - 'click': 'mousePressed', - } - TYPES = { - 'actionPerformed': 'XActionListener', - 'mousePressed': 'XMouseListener', - } - - def __init__(self, obj, view, form): - self._obj = obj - self._view = view - self._form = form - self._m = view.Model - self._index = -1 - - # ~ def __setattr__(self, name, value): - # ~ if name in ('_form', '_view', '_m', '_index'): - # ~ self.__dict__[name] = value - # ~ else: - # ~ super().__setattr__(name, value) - - def __str__(self): - return f'{self.name} ({self.type}) {[self.index]}' - - @property - def obj(self): - return self._obj - - @property - def form(self): - return self._form - - @property - def doc(self): - return self.obj.Parent.Forms.Parent - - @property - def name(self): - return self._m.Name - @name.setter - def name(self, value): - self._m.Name = value - - @property - def tag(self): - return self._m.Tag - @tag.setter - def tag(self, value): - self._m.Tag = value - - @property - def index(self): - return self._index - @index.setter - def index(self, value): - self._index = value - - @property - def enabled(self): - return self._m.Enabled - @enabled.setter - def enabled(self, value): - self._m.Enabled = value - - @property - def anchor(self): - return self.obj.Anchor - @anchor.setter - def anchor(self, value): - size = None - if hasattr(value, 'obj'): - size = getattr(value, 'size', None) - value = value.obj - self.obj.Anchor = value - if not size is None: - self.size = size - try: - self.obj.ResizeWithCell = True - except: - pass - - @property - def size(self): - return self.obj.Size - @size.setter - def size(self, value): - self.obj.Size = value - - @property - def events(self): - return self.form.getScriptEvents(self.index) - def add_event(self, name, macro): - if not 'name' in macro: - macro['name'] = '{}_{}'.format(self.name, name) - - event = ScriptEventDescriptor() - event.AddListenerParam = '' - event.EventMethod = self.EVENTS[name] - event.ListenerType = self.TYPES[event.EventMethod] - event.ScriptCode = _get_url_script(macro) - event.ScriptType = 'Script' - - for ev in self.events: - if ev.EventMethod == event.EventMethod and \ - ev.ListenerType == event.ListenerType: - self.form.revokeScriptEvent(self.index, - event.ListenerType, event.EventMethod, event.AddListenerParam) - break - - self.form.registerScriptEvent(self.index, event) - return - - def set_focus(self): - self._view.setFocus() - return - - -class LOFormControlLabel(LOFormControl): - - def __init__(self, obj, view, form): - super().__init__(obj, view, form) - - @property - def type(self): - return 'label' - - @property - def value(self): - return self._m.Label - @value.setter - def value(self, value): - self._m.Label = value - - -class LOFormControlText(LOFormControl): - - def __init__(self, obj, view, form): - super().__init__(obj, view, form) - - @property - def type(self): - return 'text' - - @property - def value(self): - return self._m.Text - @value.setter - def value(self, value): - self._m.Text = value - - -class LOFormControlButton(LOFormControl): - - def __init__(self, obj, view, form): - super().__init__(obj, view, form) - - @property - def type(self): - return 'button' - - @property - def value(self): - return self._m.Label - @value.setter - def value(self, value): - self._m.Text = Label - - @property - def url(self): - return self._m.TargetURL - @url.setter - def url(self, value): - self._m.TargetURL = value - self._m.ButtonType = FormButtonType.URL - - -FORM_CONTROL_CLASS = { - 'label': LOFormControlLabel, - 'text': LOFormControlText, - 'button': LOFormControlButton, -} - - -class LOForm(object): - MODELS = { - 'label': 'com.sun.star.form.component.FixedText', - 'text': 'com.sun.star.form.component.TextField', - 'button': 'com.sun.star.form.component.CommandButton', - } - - def __init__(self, obj, draw_page): - self._obj = obj - self._dp = draw_page - self._controls = {} - self._init_controls() - - def __getitem__(self, index): - control = self.obj[index] - return self._controls[control.Name] - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __contains__(self, item): - return item in self.obj - - def __len__(self): - return len(self.obj) - - def __str__(self): - return f'Form: {self.name}' - - def _init_controls(self): - types = { - 'com.sun.star.form.OFixedTextModel': 'label', - 'com.sun.star.form.OEditModel': 'text', - 'com.sun.star.form.OButtonModel': 'button', - } - for i, control in enumerate(self.obj): - name = control.Name - tipo = types[control.ImplementationName] - view = self.doc.CurrentController.getControl(control) - control = FORM_CONTROL_CLASS[tipo](control, view, self._obj) - control.index = i - setattr(self, name, control) - self._controls[name] = control - return - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self.obj.Name - @name.setter - def name(self, value): - self.obj.Name = value - - @property - def source(self): - return self.obj.DataSourceName - @source.setter - def source(self, value): - self.obj.DataSourceName = value - - @property - def type(self): - return self.obj.CommandType - @type.setter - def type(self, value): - self.obj.CommandType = value - - @property - def command(self): - return self.obj.Command - @command.setter - def command(self, value): - self.obj.Command = value - - @property - def doc(self): - return self.obj.Parent.Parent - - def _special_properties(self, tipo, args): - if tipo == 'button': - # ~ if 'ImageURL' in args: - # ~ args['ImageURL'] = self._set_image_url(args['ImageURL']) - args['FocusOnClick'] = args.get('FocusOnClick', False) - return args - return args - - def add(self, args): - name = args['Name'] - tipo = args.pop('Type').lower() - w = args.pop('Width', 1000) - h = args.pop('Height', 200) - x = args.pop('X', 0) - y = args.pop('Y', 0) - control = self.doc.createInstance('com.sun.star.drawing.ControlShape') - control.setSize(Size(w, h)) - control.setPosition(Point(x, y)) - model = self.doc.createInstance(self.MODELS[tipo]) - args = self._special_properties(tipo, args) - _set_properties(model, args) - control.Control = model - index = len(self) - self.obj.insertByIndex(index, model) - self._dp.add(control) - view = self.doc.CurrentController.getControl(self.obj.getByName(name)) - control = FORM_CONTROL_CLASS[tipo](control, view, self.obj) - control.index = index - setattr(self, name, control) - self._controls[name] = control - return control - - -class LOSheetForms(object): - - def __init__(self, draw_page): - self._dp = draw_page - self._obj = draw_page.Forms - - def __getitem__(self, index): - return LOForm(self.obj[index], self._dp) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __contains__(self, item): - return item in self.obj - - def __len__(self): - return len(self.obj) - - @property - def obj(self): - return self._obj - - @property - def doc(self): - return self.obj.Parent - - @property - def count(self): - return len(self) - - @property - def names(self): - return self.obj.ElementNames - - def insert(self, name=''): - if not name: - name = f'form{self.count + 1}' - form = self.doc.createInstance('com.sun.star.form.component.Form') - self.obj.insertByName(name, form) - return LOForm(form, self._dp) - - def remove(self, index): - if isinstance(index, int): - self.obj.removeByIndex(index) - else: - self.obj.removeByName(index) - return - - -# ~ IsFiltered, -# ~ IsManualPageBreak, -# ~ IsStartOfNewPage -class LOSheetRows(object): - - def __init__(self, sheet, obj): - self._sheet = sheet - self._obj = obj - - def __getitem__(self, index): - if isinstance(index, int): - rows = LOSheetRows(self._sheet, self.obj[index]) - else: - rango = self._sheet[index.start:index.stop,0:] - rows = LOSheetRows(self._sheet, rango.obj.Rows) - return rows - - def __len__(self): - return self.obj.Count - - @property - def obj(self): - return self._obj - - @property - def visible(self): - return self._obj.IsVisible - @visible.setter - def visible(self, value): - self._obj.IsVisible = value - - @property - def color(self): - return self.obj.CellBackColor - @color.setter - def color(self, value): - self.obj.CellBackColor = value - - @property - def is_transparent(self): - return self.obj.IsCellBackgroundTransparent - @is_transparent.setter - def is_transparent(self, value): - self.obj.IsCellBackgroundTransparent = value - - @property - def height(self): - return self.obj.Height - @height.setter - def height(self, value): - self.obj.Height = value - - def optimal(self): - self.obj.OptimalHeight = True - return - - def insert(self, index, count): - self.obj.insertByIndex(index, count) - return - - def remove(self, index, count): - self.obj.removeByIndex(index, count) - return - - -# ~ IsManualPageBreak, -# ~ IsStartOfNewPage -class LOSheetColumns(object): - - def __init__(self, sheet, obj): - self._sheet = sheet - self._obj = obj - - def __getitem__(self, index): - if isinstance(index, (int, str)): - rows = LOSheetColumns(self._sheet, self.obj[index]) - else: - rango = self._sheet[0,index.start:index.stop] - rows = LOSheetColumns(self._sheet, rango.obj.Columns) - return rows - - def __len__(self): - return self.obj.Count - - @property - def obj(self): - return self._obj - - @property - def visible(self): - return self._obj.IsVisible - @visible.setter - def visible(self, value): - self._obj.IsVisible = value - - @property - def width(self): - return self.obj.Width - @width.setter - def width(self, value): - self.obj.Width = value - - def optimal(self): - self.obj.OptimalWidth = True - return - - def insert(self, index, count): - self.obj.insertByIndex(index, count) - return - - def remove(self, index, count): - self.obj.removeByIndex(index, count) - return - - -class LOCalcSheet(object): - - @property - def draw_page(self): - return LODrawPage(self.obj.DrawPage) - @property - def dp(self): - return self.draw_page - - @property - def shapes(self): - return self.draw_page - - @property - def charts(self): - return LOSheetCharts(self.obj.Charts, self) - - @property - def tables(self): - return LOSheetTables(self.obj.DataPilotTables, self) - - @property - def rows(self): - return LOSheetRows(self, self.obj.Rows) - - @property - def columns(self): - return LOSheetColumns(self, self.obj.Columns) - - @property - def forms(self): - return LOSheetForms(self.obj.DrawPage) - - @property - def search_descriptor(self): - return self.obj.createSearchDescriptor() - - @property - def replace_descriptor(self): - return self.obj.createReplaceDescriptor() - - def render(self, data, rango=None, clean=True): - if rango is None: - rango = self.used_area - return rango.render(data, clean) - - def find(self, search_string, rango=None): - if rango is None: - rango = self.used_area - return rango.find(search_string) - - -class LOCalcRange(object): - - def __contains__(self, item): - return item.in_range(self) - - @property - def back_color(self): - return self._obj.CellBackColor - @back_color.setter - def back_color(self, value): - self._obj.CellBackColor = get_color(value) - - @property - def columns(self): - return self.obj.Columns.Count - - @property - def column(self): - c1 = self.address.Column - c2 = c1 + 1 - ra = self.current_region.range_address - r1 = ra.StartRow - r2 = ra.EndRow + 1 - return LOCalcRange(self.sheet[r1:r2, c1:c2].obj) - - @property - def rows(self): - return LOSheetRows(self.sheet, self.obj.Rows) - - @property - def row(self): - r1 = self.address.Row - r2 = r1 + 1 - ra = self.current_region.range_address - c1 = ra.StartColumn - c2 = ra.EndColumn + 1 - return LOCalcRange(self.sheet[r1:r2, c1:c2].obj) - - @property - def type(self): - return self.obj.Type - - @property - def error(self): - return self.obj.getError() - - # ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html - @property - def value(self): - v = None - if self.type == VALUE: - v = self.obj.getValue() - elif self.type == TEXT: - v = self.obj.getString() - elif self.type == FORMULA: - v = self.obj.getFormula() - return v - @value.setter - def value(self, data): - if isinstance(data, str): - if data[0] in '=': - self.obj.setFormula(data) - else: - self.obj.setString(data) - elif isinstance(data, Decimal): - self.obj.setValue(float(data)) - elif isinstance(data, (int, float, bool)): - self.obj.setValue(data) - elif isinstance(data, datetime.datetime): - d = data.toordinal() - t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY - self.obj.setValue(d - DATE_OFFSET + t) - elif isinstance(data, datetime.date): - d = data.toordinal() - self.obj.setValue(d - DATE_OFFSET) - elif isinstance(data, datetime.time): - d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY - self.obj.setValue(d) - - @property - def date(self): - value = int(self.obj.Value) - date = datetime.date.fromordinal(value + DATE_OFFSET) - return date - - @property - def time(self): - seconds = self.obj.Value * SECONDS_DAY - time_delta = datetime.timedelta(seconds=seconds) - time = (datetime.datetime.min + time_delta).time() - return time - - @property - def datetime(self): - return datetime.datetime.combine(self.date, self.time) - - @property - def dict(self): - rows = self.data - k = rows[0] - data = [dict(zip(k, r)) for r in rows[1:]] - return data - @dict.setter - def dict(self, values): - data = [tuple(values[0].keys())] - data += [tuple(d.values()) for d in values] - self.data = data - - @property - def formula(self): - return self.obj.getFormulaArray() - @formula.setter - def formula(self, values): - self.obj.setFormulaArray(values) - - @property - def array_formula(self): - return self.obj.ArrayFormula - @array_formula.setter - def array_formula(self, value): - self.obj.ArrayFormula = value - - @property - def next_cell(self): - a = self.current_region.range_address - col = a.StartColumn - row = a.EndRow + 1 - return LOCalcRange(self.sheet[row, col].obj) - - @property - def position(self): - return self.obj.Position - - @property - def size(self): - return self.obj.Size - - @property - def possize(self): - data = { - 'Width': self.size.Width, - 'Height': self.size.Height, - 'X': self.position.X, - 'Y': self.position.Y, - } - return data - - @property - def visible(self): - cursor = self.cursor - rangos = cursor.queryVisibleCells() - rangos = LOCalcRanges(rangos) - return rangos - - @property - def merged_area(self): - cursor = self.cursor - cursor.collapseToMergedArea() - rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj) - return rango - - @property - def empty(self): - cursor = self.cursor - rangos = cursor.queryEmptyCells() - rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] - return tuple(rangos) - - def query_content(self, type_content=1023): - cursor = self.cursor - rangos = cursor.queryContentCells(type_content) - rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] - return tuple(rangos) - - @property - def merge(self): - return self.obj.IsMerged - @merge.setter - def merge(self, value): - self.obj.merge(value) - - @property - def auto_format(self): - return '' - @auto_format.setter - def auto_format(self, value): - self.obj.autoFormat(value) - - @property - def validation(self): - return self.obj.Validation - @validation.setter - def validation(self, values): - current = self.validation - if not values: - current.Type = ValidationType.ANY - current.ShowInputMessage = False - else: - is_list = False - for k, v in values.items(): - if k == 'Type' and v == VT.LIST: - is_list = True - if k == 'Formula1' and is_list: - if isinstance(v, (tuple, list)): - v = ';'.join(['"{}"'.format(i) for i in v]) - setattr(current, k, v) - self.obj.Validation = current - - def select(self): - self.doc._cc.select(self.obj) - return - - def search(self, options, find_all=True): - rangos = None - - descriptor = self.sheet.search_descriptor - descriptor.setSearchString(options['Search']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - - if find_all: - found = self.obj.findAll(descriptor) - else: - found = self.obj.findFirst(descriptor) - - if found: - if found.ImplementationName == OBJ_CELL: - rangos = LOCalcRange(found) - else: - rangos = [LOCalcRange(f) for f in found] - - return rangos - - def replace(self, options): - descriptor = self.sheet.replace_descriptor - descriptor.setSearchString(options['Search']) - descriptor.setReplaceString(options['Replace']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - count = self.obj.replaceAll(descriptor) - return count - - def in_range(self, rango): - if isinstance(rango, LOCalcRange): - address = rango.range_address - else: - address = rango.RangeAddress - result = self.cursor.queryIntersection(address) - return bool(result.Count) - - def move(self, target): - sheet = self.sheet.obj - sheet.moveRange(target.address, self.range_address) - return - - def insert(self, insert_mode=CIM.DOWN): - sheet = self.sheet.obj - sheet.insertCells(self.range_address, insert_mode) - return - - def delete(self, delete_mode=CDM.UP): - sheet = self.sheet.obj - sheet.removeRange(self.range_address, delete_mode) - return - - def copy_from(self, source): - self.sheet.obj.copyRange(self.address, source.range_address) - return - - def copy_to(self, target): - self.sheet.obj.copyRange(target.address, self.range_address) - return - - # ~ def copy_to(self, cell, formula=False): - # ~ rango = cell.to_size(self.rows, self.columns) - # ~ if formula: - # ~ rango.formula = self.formula - # ~ else: - # ~ rango.data = self.data - # ~ return - - # ~ def copy_from(self, rango, formula=False): - # ~ data = rango - # ~ if isinstance(rango, LOCalcRange): - # ~ if formula: - # ~ data = rango.formula - # ~ else: - # ~ data = rango.data - # ~ rows = len(data) - # ~ cols = len(data[0]) - # ~ if formula: - # ~ self.to_size(rows, cols).formula = data - # ~ else: - # ~ self.to_size(rows, cols).data = data - # ~ return - - def optimal_width(self): - self.obj.Columns.OptimalWidth = True - return - - def clean_render(self, template='\{(\w.+)\}'): - self._sd.SearchRegularExpression = True - self._sd.setSearchString(template) - self.obj.replaceAll(self._sd) - return - - def render(self, data, clean=True): - self._sd = self.sheet.obj.createSearchDescriptor() - self._sd.SearchCaseSensitive = False - for k, v in data.items(): - cell = self._render_value(k, v) - return cell - - def _render_value(self, key, value, parent=''): - cell = None - if isinstance(value, dict): - for k, v in value.items(): - # ~ print(1, 'RENDER', k, v) - cell = self._render_value(k, v, key) - return cell - elif isinstance(value, (list, tuple)): - self._render_list(key, value) - return - - search = f'{{{key}}}' - if parent: - search = f'{{{parent}.{key}}}' - ranges = self.find_all(search) - - if ranges is None: - return - - # ~ for cell in ranges or range(0): - for cell in ranges: - self._set_new_value(cell, search, value) - return LOCalcRange(cell) - - def _set_new_value(self, cell, search, value): - if not cell.ImplementationName == 'ScCellObj': - return - - if isinstance(value, str): - pattern = re.compile(search, re.IGNORECASE) - new_value = pattern.sub(value, cell.String) - cell.String = new_value - else: - LOCalcRange(cell).value = value - return - - def _render_list(self, key, rows): - for row in rows: - for k, v in row.items(): - self._render_value(k, v) - return - - def find(self, search_string): - if self._sd is None: - self._sd = self.sheet.obj.createSearchDescriptor() - self._sd.SearchCaseSensitive = False - - self._sd.setSearchString(search_string) - cell = self.obj.findFirst(self._sd) - if cell: - cell = LOCalcRange(cell) - return cell - - def find_all(self, search_string): - if self._sd is None: - self._sd = self.sheet.obj.createSearchDescriptor() - self._sd.SearchCaseSensitive = False - - self._sd.setSearchString(search_string) - ranges = self.obj.findAll(self._sd) - return ranges - - def filter(self, args, with_headers=True): - ff = TableFilterField() - ff.Field = args['Field'] - ff.Operator = args['Operator'] - if isinstance(args['Value'], str): - ff.IsNumeric = False - ff.StringValue = args['Value'] - else: - ff.IsNumeric = True - ff.NumericValue = args['Value'] - - fd = self.obj.createFilterDescriptor(True) - fd.ContainsHeader = with_headers - fd.FilterFields = ((ff,)) - # ~ self.obj.AutoFilter = True - self.obj.filter(fd) - return - - def copy_format_from(self, rango): - rango.select() - self.doc.copy() - self.select() - args = { - 'Flags': 'T', - 'MoveMode': 4, - } - url = '.uno:InsertContents' - call_dispatch(self.doc.frame, url, args) - return - - def to_image(self): - self.select() - self.doc.copy() - args = {'SelectedFormat': 141} - url = '.uno:ClipboardFormatItems' - call_dispatch(self.doc.frame, url, args) - return self.sheet.shapes[-1] - - def insert_image(self, path, options={}): - args = options.copy() - ps = self.possize - args['Width'] = args.get('Width', ps['Width']) - args['Height'] = args.get('Height', ps['Height']) - args['X'] = args.get('X', ps['X']) - args['Y'] = args.get('Y', ps['Y']) - # ~ img.ResizeWithCell = True - img = self.sheet.dp.insert_image(path, args) - img.anchor = self.obj - args.clear() - return img - - def insert_shape(self, tipo, args={}): - ps = self.possize - args['Width'] = args.get('Width', ps['Width']) - args['Height'] = args.get('Height', ps['Height']) - args['X'] = args.get('X', ps['X']) - args['Y'] = args.get('Y', ps['Y']) - - shape = self.sheet.dp.add(tipo, args) - shape.anchor = self.obj - args.clear() - return - - def filter_by_color(self, cell): - rangos = cell.column[1:,:].visible - for r in rangos: - for c in r: - if c.back_color != cell.back_color: - c.rows.visible = False - return - - def clear(self, what=1023): - # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html - self.obj.clearContents(what) - return - - def transpose(self): - # ~ 'Flags': 'A', - # ~ 'FormulaCommand': 0, - # ~ 'SkipEmptyCells': False, - # ~ 'AsLink': False, - # ~ 'MoveMode': 4, - self.select() - self.doc.copy() - self.clear(1023) - self[0,0].select() - self.doc.insert_contents({'Transpose': True}) - _CB.set('') - return - - def transpose_data(self, formula=False): - data = self.data - if formula: - data = self.formula - data = tuple(zip(*data)) - self.clear(1023) - self[0,0].copy_from(data, formula=formula) - return - - def merge_by_row(self): - for r in range(len(self.rows)): - self[r].merge = True - return - - def fill(self, source=1): - self.obj.fillAuto(0, source) - return - - def _cast(self, t, v): - if not t: - return v - - if t == datetime.date: - nv = datetime.date.fromordinal(int(v) + DATE_OFFSET) - else: - nv = t(v) - return nv - - def get_data(self, types): - values = [ - [self._cast(types[i], v) for i, v in enumerate(row)] - for row in self.data - ] - return values - - -class LOWriterStyles(object): - - def __init__(self, styles): - self._styles = styles - - @property - def names(self): - return {s.DisplayName: s.Name for s in self._styles} - - def __str__(self): - return '\n'.join(tuple(self.names.values())) - - -class LOWriterStylesFamilies(object): - - def __init__(self, styles): - self._styles = styles - - def __getitem__(self, index): - styles = { - 'Character': 'CharacterStyles', - 'Paragraph': 'ParagraphStyles', - 'Page': 'PageStyles', - 'Frame': 'FrameStyles', - 'Numbering': 'NumberingStyles', - 'Table': 'TableStyles', - 'Cell': 'CellStyles', - } - name = styles.get(index, index) - return LOWriterStyles(self._styles[name]) - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - obj = LOWriterStyles(self._styles[self._index]) - self._index += 1 - return obj - # ~ raise StopIteration - - @property - def names(self): - return self._styles.ElementNames - - def __str__(self): - return '\n'.join(self.names) - - -class LOWriterPageStyle(LOBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - def __str__(self): - return f'Page Style: {self.name}' - - @property - def name(self): - return self._obj.Name - - -class LOWriterPageStyles(object): - - def __init__(self, styles): - self._styles = styles - - def __getitem__(self, index): - return LOWriterPageStyle(self._styles[index]) - - @property - def names(self): - return self._styles.ElementNames - - def __str__(self): - return '\n'.join(self.names) - - -class LOWriterTextRange(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph' - self._is_table = self.obj.ImplementationName == 'SwXTextTable' - self._is_text = self.obj.ImplementationName == 'SwXTextPortion' - self._is_section = not self.obj.TextSection is None - self._parts = [] - if self._is_paragraph: - self._parts = [LOWriterTextRange(p, doc) for p in obj] - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - try: - obj = self._parts[self._index] - except IndexError: - raise StopIteration - - self._index += 1 - return obj - - @property - def string(self): - s = '' - if not self._is_table: - s = self.obj.String - return s - @string.setter - def string(self, value): - self.obj.String = value - - @property - def value(self): - return self.string - @value.setter - def value(self, value): - self.string = value - - @property - def style(self): - s = '' - if self.is_paragraph: - s = self.obj.ParaStyleName - elif self.is_text: - s = self.obj.CharStyleName - return s - @style.setter - def style(self, value): - if self.is_paragraph: - self.obj.ParaStyleName = value - elif self.is_text: - self.obj.CharStyleName = value - - @property - def is_paragraph(self): - return self._is_paragraph - - @property - def is_table(self): - return self._is_table - - @property - def is_text(self): - return self._is_text - - @property - def is_section(self): - return self._is_section - - @property - def text_cursor(self): - return self.text.createTextCursor() - - @property - def dp(self): - return self._doc.dp - - @property - def paragraph(self): - cursor = self.cursor - cursor.gotoStartOfParagraph(False) - cursor.gotoNextParagraph(True) - return LOWriterTextRange(cursor, self._doc) - - def goto_start(self): - if self.is_section: - rango = self.obj.TextSection.Anchor.Start - else: - rango = self.obj.Start - return LOWriterTextRange(rango, self._doc) - - def goto_end(self): - if self.is_section: - rango = self.obj.TextSection.Anchor.End - else: - rango = self.obj.End - return LOWriterTextRange(rango, self._doc) - - def goto_previous(self, expand=True): - cursor = self.cursor - cursor.gotoPreviousParagraph(expand) - return LOWriterTextRange(cursor, self._doc) - - def goto_next(self, expand=True): - cursor = self.cursor - cursor.gotoNextParagraph(expand) - return LOWriterTextRange(cursor, self._doc) - - def go_left(self, from_self=True, count=1, expand=False): - cursor = self.cursor - if not from_self: - cursor = self.text_cursor - cursor.gotoRange(self.obj, False) - cursor.goLeft(count, expand) - return LOWriterTextRange(cursor, self._doc) - - def go_right(self, from_self=True, count=1, expand=False): - cursor = self.cursor - if not from_self: - cursor = self.text_cursor - cursor.gotoRange(self.obj, False) - cursor.goRight(count, expand) - return LOWriterTextRange(cursor, self._doc) - - def delete(self): - self.value = '' - return - - def offset(self): - cursor = self.cursor.getEnd() - return LOWriterTextRange(cursor, self._doc) - - def insert_content(self, data, cursor=None, replace=False): - if cursor is None: - cursor = self.cursor - self.text.insertTextContent(cursor, data, replace) - return - - def insert_math(self, formula, - anchor_type=TextContentAnchorType.AS_CHARACTER, - cursor=None, replace=False): - - math = self._doc.create_instance(SERVICES['TEXT_EMBEDDED']) - math.CLSID = CLSID['FORMULA'] - math.AnchorType = anchor_type - self.insert_content(math, cursor, replace) - math.EmbeddedObject.Component.Formula = formula - return math - - def new_line(self, count=1): - cursor = self.cursor - for i in range(count): - self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) - return LOWriterTextRange(cursor, self._doc) - - def insert_table(self, data): - table = self._doc.create_instance(SERVICES['TEXT_TABLE']) - rows = len(data) - cols = len(data[0]) - table.initialize(rows, cols) - self.insert_content(table) - table.DataArray = data - name = table.Name - table = LOWriterTextTable(self._doc.tables[name], self._doc) - return table - - def insert_shape(self, tipo, args={}): - # ~ args['Width'] = args.get('Width', 1000) - # ~ args['Height'] = args.get('Height', 1000) - # ~ args['X'] = args.get('X', 0) - # ~ args['Y'] = args.get('Y', 0) - shape = self._doc.dp.add(tipo, args) - # ~ shape.anchor = self.obj - return shape - - def insert_image(self, path, args={}): - w = args.get('Width', 1000) - h = args.get('Height', 1000) - - image = self._doc.create_instance(SERVICES['GRAPHIC']) - image.GraphicURL = _P.to_url(path) - image.AnchorType = TextContentAnchorType.AS_CHARACTER - image.Width = w - image.Height = h - self.insert_content(image) - return self._doc.dp.last - - -class LOWriterTextRanges(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - self._paragraphs = [LOWriterTextRange(p, doc) for p in obj] - - def __len__(self): - return len(self._paragraphs) - - def __getitem__(self, index): - return self._paragraphs[index] - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - try: - obj = self._paragraphs[self._index] - except IndexError: - raise StopIteration - - self._index += 1 - return obj - - @property - def obj(self): - return self._obj - - -class LOWriterTextTable(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self._obj.Name - - @property - def data(self): - return self.obj.DataArray - @data.setter - def data(self, values): - self.obj.DataArray = values - - @property - def style(self): - return self.obj.TableTemplateName - @style.setter - def style(self, value): - self.obj.autoFormat(value) - - -class LOWriterTextTables(object): - - def __init__(self, doc): - self._doc = doc - self._obj = doc.obj.TextTables - - def __getitem__(self, key): - return LOWriterTextTable(self._obj[key], self._doc) - - def __len__(self): - return self._obj.Count - - def insert(self, data, text_range=None): - if text_range is None: - text_range = self._doc.selection - text_range.insert_table(data) - return - - -class LOWriter(LODocument): - - def __init__(self, obj): - super().__init__(obj) - self._type = WRITER - self._settings = self._cc.ViewSettings - - @property - def text(self): - return self.paragraphs - - @property - def paragraphs(self): - return LOWriterTextRanges(self.obj.Text, self) - - @property - def tables(self): - return LOWriterTextTables(self) - - @property - def selection(self): - sel = self.obj.CurrentSelection - if sel.ImplementationName == OBJ_TEXTS: - if len(sel) == 1: - sel = LOWriterTextRanges(sel, self)[0] - else: - sel = LOWriterTextRanges(sel, self) - return sel - - if sel.ImplementationName == OBJ_SHAPES: - if len(sel) == 1: - sel = sel[0] - sel = LODrawPage(sel.Parent)[sel.Name] - return sel - - if sel.ImplementationName == OBJ_GRAPHIC: - sel = self.dp[sel.Name] - else: - debug(sel.ImplementationName) - - return sel - - @property - def dp(self): - return self.draw_page - @property - def shapes(self): - return self.draw_page - @property - def draw_page(self): - return LODrawPage(self.obj.DrawPage) - - @property - def cursor(self): - return self.obj.Text.createTextCursor() - - @property - def view_cursor(self): - return self._cc.ViewCursor - - @property - def page_styles(self): - ps = self.obj.StyleFamilies['PageStyles'] - return LOWriterPageStyles(ps) - - @property - def styles(self): - return LOWriterStylesFamilies(self.obj.StyleFamilies) - - @property - def search_descriptor(self): - return self.obj.createSearchDescriptor() - - @property - def replace_descriptor(self): - return self.obj.createReplaceDescriptor() - - @property - def view_web(self): - return self._settings.ShowOnlineLayout - @view_web.setter - def view_web(self, value): - self._settings.ShowOnlineLayout = value - - def goto_start(self): - self.view_cursor.gotoStart(False) - return self.selection - - def goto_end(self): - self.view_cursor.gotoEnd(False) - return self.selection - - def search(self, options, find_all=True): - descriptor = self.search_descriptor - descriptor.setSearchString(options.get('Search', '')) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if 'Attributes' in options: - attr = dict_to_property(options['Attributes']) - descriptor.setSearchAttributes(attr) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - - result = False - if find_all: - found = self.obj.findAll(descriptor) - if len(found): - result = [LOWriterTextRange(f, self) for f in found] - else: - found = self.obj.findFirst(descriptor) - if found: - result = LOWriterTextRange(found, self) - - return result - - def replace(self, options): - descriptor = self.replace_descriptor - descriptor.setSearchString(options['Search']) - descriptor.setReplaceString(options['Replace']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if 'Attributes' in options: - attr = dict_to_property(options['Attributes']) - descriptor.setSearchAttributes(attr) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - found = self.obj.replaceAll(descriptor) - return found - - def select(self, text): - if hasattr(text, 'obj'): - text = text.obj - self._cc.select(text) - return - - -class LOShape(LOBaseObject): - - @property - def cell(self): - return self.anchor - - @property - def anchor(self): - obj = self.obj.Anchor - if obj.ImplementationName == OBJ_CELL: - obj = LOCalcRange(obj) - elif obj.ImplementationName == OBJ_TEXT: - obj = LOWriterTextRange(obj, LODocs().active) - else: - debug('Anchor', obj.ImplementationName) - return obj - @anchor.setter - def anchor(self, value): - if hasattr(value, 'obj'): - value = value.obj - try: - self.obj.Anchor = value - except Exception as e: - self.obj.AnchorType = value - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.Visible = value - - @property - def path(self): - return self.url - @property - def url(self): - url = '' - if self.is_image: - url = _P.to_system(self.obj.GraphicURL.OriginURL) - return url - - @property - def mimetype(self): - mt = '' - if self.is_image: - mt = self.obj.GraphicURL.MimeType - return mt - - @property - def linked(self): - l = False - if self.is_image: - l = self.obj.GraphicURL.Linked - return l - - def delete(self): - self.remove() - return - def remove(self): - self.obj.Parent.remove(self.obj) - return - - def save(self, path: str, mimetype=DEFAULT_MIME_TYPE): - if _P.is_dir(path): - name = self.name - ext = mimetype.lower() - else: - p = _P(path) - path = p.path - name = p.name - ext = p.ext.lower() - - path = _P.join(path, f'{name}.{ext}') - args = dict( - URL = _P.to_url(path), - MimeType = MIME_TYPE[ext], - ) - if not _export_image(self.obj, args): - path = '' - return path - - # ~ def save2(self, path: str): - # ~ size = len(self.obj.Bitmap.DIB) - # ~ data = self.obj.GraphicStream.readBytes((), size) - # ~ data = data[-1].value - # ~ path = _P.join(path, f'{self.name}.png') - # ~ _P.save_bin(path, b'') - # ~ return - - -class LODrawPage(LOBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - def __getitem__(self, index): - if isinstance(index, int): - shape = LOShape(self.obj[index], index) - else: - for i, o in enumerate(self.obj): - shape = self.obj[i] - name = shape.Name or f'shape{i}' - if name == index: - shape = LOShape(shape, i) - break - return shape - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - if self._index == self.count: - raise StopIteration - shape = self[self._index] - self._index += 1 - return shape - - - @property - def name(self): - return self.obj.Name - - @property - def doc(self): - return self.obj.Forms.Parent - - @property - def width(self): - return self.obj.Width - - @property - def height(self): - return self.obj.Height - - @property - def count(self): - return self.obj.Count - - @property - def last(self): - return self[self.count - 1] - - def create_instance(self, name): - return self.doc.createInstance(name) - - def add(self, type_shape, options={}): - args = options.copy() - """Insert a shape in page, type shapes: - Line - Rectangle - Ellipse - Text - Connector - """ - index = self.count - default_height = 3000 - if type_shape == 'Line': - default_height = 0 - w = args.pop('Width', 3000) - h = args.pop('Height', default_height) - x = args.pop('X', 1000) - y = args.pop('Y', 1000) - name = args.pop('Name', f'{type_shape.lower()}{index}') - - service = f'com.sun.star.drawing.{type_shape}Shape' - shape = self.create_instance(service) - shape.Size = Size(w, h) - shape.Position = Point(x, y) - shape.Name = name - self.obj.add(shape) - - if args: - _set_properties(shape, args) - - return LOShape(self.obj[index], index) - - def remove(self, shape): - if hasattr(shape, 'obj'): - shape = shape.obj - return self.obj.remove(shape) - - def remove_all(self): - while self.count: - self.obj.remove(self.obj[0]) - return - - def insert_image(self, path, options={}): - args = options.copy() - index = self.count - w = args.get('Width', 3000) - h = args.get('Height', 3000) - x = args.get('X', 1000) - y = args.get('Y', 1000) - name = args.get('Name', f'image{index}') - - image = self.create_instance('com.sun.star.drawing.GraphicObjectShape') - if isinstance(path, str): - image.GraphicURL = _P.to_url(path) - else: - gp = create_instance('com.sun.star.graphic.GraphicProvider') - properties = dict_to_property({'InputStream': path}) - image.Graphic = gp.queryGraphic(properties) - - self.obj.add(image) - image.Size = Size(w, h) - image.Position = Point(x, y) - image.Name = name - return LOShape(self.obj[index], index) - - -class LODrawImpress(LODocument): - - def __init__(self, obj): - super().__init__(obj) - - def __getitem__(self, index): - if isinstance(index, int): - page = self.obj.DrawPages[index] - else: - page = self.obj.DrawPages.getByName(index) - return LODrawPage(page) - - @property - def selection(self): - sel = self.obj.CurrentSelection[0] - # ~ return _get_class_uno(sel) - return sel - - @property - def current_page(self): - return LODrawPage(self._cc.getCurrentPage()) - - def paste(self): - call_dispatch(self.frame, '.uno:Paste') - return self.current_page[-1] - - def add(self, type_shape, args={}): - return self.current_page.add(type_shape, args) - - def insert_image(self, path, args={}): - self.current_page.insert_image(path, args) - return - - # ~ def export(self, path, mimetype='png'): - # ~ args = dict( - # ~ URL = _P.to_url(path), - # ~ MimeType = MIME_TYPE[mimetype], - # ~ ) - # ~ result = _export_image(self.obj, args) - # ~ return result - - -class BaseRow: - pass - - -class BaseQuery(object): - PY_TYPES = { - 'VARCHAR': 'getString', - 'INTEGER': 'getLong', - 'DATE': 'getDate', - # ~ 'SQL_LONG': 'getLong', - # ~ 'SQL_VARYING': 'getString', - # ~ 'SQL_FLOAT': 'getFloat', - # ~ 'SQL_BOOLEAN': 'getBoolean', - # ~ 'SQL_TYPE_DATE': 'getDate', - # ~ 'SQL_TYPE_TIME': 'getTime', - # ~ 'SQL_TIMESTAMP': 'getTimestamp', - } - # ~ TYPES_DATE = ('SQL_TYPE_DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP') - TYPES_DATE = ('DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP') - - def __init__(self, query): - self._query = query - self._meta = query.MetaData - self._cols = self._meta.ColumnCount - self._names = query.Columns.ElementNames - self._data = self._get_data() - - def __getitem__(self, index): - return self._data[index] - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - try: - row = self._data[self._index] - except IndexError: - raise StopIteration - self._index += 1 - return row - - def _to_python(self, index): - type_field = self._meta.getColumnTypeName(index) - # ~ print('TF', type_field) - value = getattr(self._query, self.PY_TYPES[type_field])(index) - if type_field in self.TYPES_DATE: - value = _struct_to_date(value) - return value - - def _get_row(self): - row = BaseRow() - for i in range(1, self._cols + 1): - column_name = self._meta.getColumnName(i) - value = self._to_python(i) - setattr(row, column_name, value) - return row - - def _get_data(self): - data = [] - while self._query.next(): - row = self._get_row() - data.append(row) - return data - - @property - def tuples(self): - data = [tuple(r.__dict__.values()) for r in self._data] - return tuple(data) - - @property - def dicts(self): - data = [r.__dict__ for r in self._data] - return tuple(data) - - -def _add_listeners(events, control, name=''): - listeners = { - 'addActionListener': EventsButton, - 'addMouseListener': EventsMouse, - 'addFocusListener': EventsFocus, - 'addItemListener': EventsItem, - 'addKeyListener': EventsKey, - 'addTabListener': EventsTab, - 'addSpinListener': EventsSpin, - } - if hasattr(control, 'obj'): - control = control.obj - # ~ debug(control.ImplementationName) - is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' - is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' - is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' - is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl' - - for key, value in listeners.items(): - if hasattr(control, key): - if is_grid and key == 'addMouseListener': - control.addMouseListener(EventsMouseGrid(events, name)) - continue - if is_link and key == 'addMouseListener': - control.addMouseListener(EventsMouseLink(events, name)) - continue - if is_roadmap and key == 'addItemListener': - control.addItemListener(EventsItemRoadmap(events, name)) - continue - - getattr(control, key)(listeners[key](events, name)) - - if is_grid: - controllers = EventsGrid(events, name) - control.addSelectionListener(controllers) - control.Model.GridDataModel.addGridDataListener(controllers) - return - - -class EventsSpin(EventsListenerBase, XSpinListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def up(self, event): - event_name = f'{self.name}_up' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def down(self, event): - event_name = f'{self.name}_up' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def first(self, event): - event_name = f'{self.name}_first' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def last(self, event): - event_name = f'{self.name}_last' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def mousePressed(self, event): - event_name = '{}_click'.format(self._name) - if event.ClickCount == 2: - event_name = '{}_double_click'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def mouseReleased(self, event): - event_name = '{}_after_click'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def mouseEntered(self, event): - pass - - def mouseExited(self, event): - pass - - # ~ XMouseMotionListener - def mouseMoved(self, event): - pass - - def mouseDragged(self, event): - pass - - -class EventsMouseLink(EventsMouse): - - def __init__(self, controller, name): - super().__init__(controller, name) - self._text_color = 0 - - def mouseEntered(self, event): - model = event.Source.Model - self._text_color = model.TextColor or 0 - model.TextColor = get_color('blue') - return - - def mouseExited(self, event): - model = event.Source.Model - model.TextColor = self._text_color - return - - -class EventsButton(EventsListenerBase, XActionListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def actionPerformed(self, event): - event_name = f'{self.name}_action' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsFocus(EventsListenerBase, XFocusListener): - CONTROLS = ( - 'stardiv.Toolkit.UnoControlEditModel', - ) - - def __init__(self, controller, name): - super().__init__(controller, name) - - def focusGained(self, event): - service = event.Source.Model.ImplementationName - # ~ print('Focus enter', service) - if service in self.CONTROLS: - obj = event.Source.Model - obj.BackgroundColor = COLOR_ON_FOCUS - return - - def focusLost(self, event): - service = event.Source.Model.ImplementationName - if service in self.CONTROLS: - obj = event.Source.Model - obj.BackgroundColor = -1 - return - - -class EventsKey(EventsListenerBase, XKeyListener): - """ - event.KeyChar - event.KeyCode - event.KeyFunc - event.Modifiers - """ - - def __init__(self, controller, name): - super().__init__(controller, name) - - def keyPressed(self, event): - pass - - def keyReleased(self, event): - event_name = '{}_key_released'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - # ~ else: - # ~ if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): - # ~ self._cls.close() - return - - -class EventsItem(EventsListenerBase, XItemListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def disposing(self, event): - pass - - def itemStateChanged(self, event): - event_name = '{}_item_changed'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsItemRoadmap(EventsItem): - - def itemStateChanged(self, event): - dialog = event.Source.Context.Model - dialog.Step = event.ItemId + 1 - return - - -class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def dataChanged(self, event): - event_name = '{}_data_changed'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def rowHeadingChanged(self, event): - pass - - def rowsInserted(self, event): - pass - - def rowsRemoved(self, evemt): - pass - - def selectionChanged(self, event): - event_name = '{}_selection_changed'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsMouseGrid(EventsMouse): - selected = False - - def mousePressed(self, event): - super().mousePressed(event) - # ~ obj = event.Source - # ~ col = obj.getColumnAtPoint(event.X, event.Y) - # ~ row = obj.getRowAtPoint(event.X, event.Y) - # ~ print(col, row) - # ~ if col == -1 and row == -1: - # ~ if self.selected: - # ~ obj.deselectAllRows() - # ~ else: - # ~ obj.selectAllRows() - # ~ self.selected = not self.selected - return - - def mouseReleased(self, event): - # ~ obj = event.Source - # ~ col = obj.getColumnAtPoint(event.X, event.Y) - # ~ row = obj.getRowAtPoint(event.X, event.Y) - # ~ if row == -1 and col > -1: - # ~ gdm = obj.Model.GridDataModel - # ~ for i in range(gdm.RowCount): - # ~ gdm.updateRowHeading(i, i + 1) - return - - -class EventsTab(EventsListenerBase, XTabListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def activated(self, id): - event_name = '{}_activated'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(id) - return - - -class EventsMenu(EventsListenerBase, XMenuListener): - - def __init__(self, controller): - super().__init__(controller, '') - - def itemHighlighted(self, event): - pass - - def itemSelected(self, event): - name = event.Source.getCommand(event.MenuId) - if name.startswith('menu'): - event_name = '{}_selected'.format(name) - else: - event_name = 'menu_{}_selected'.format(name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def itemActivated(self, event): - return - - def itemDeactivated(self, event): - return - - -class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): - - def __init__(self, cls): - self._cls = cls - super().__init__(cls.events, cls.name, cls._window) - - def windowOpened(self, event): - event_name = '{}_opened'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def windowActivated(self, event): - control_name = '{}_activated'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - def windowDeactivated(self, event): - control_name = '{}_deactivated'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - def windowMinimized(self, event): - pass - - def windowNormalized(self, event): - pass - - def windowClosing(self, event): - if self._window: - control_name = 'window_closing' - else: - control_name = '{}_closing'.format(event.Source.Model.Name) - - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - # ~ else: - # ~ if not self._modal and not self._block: - # ~ event.Source.Visible = False - return - - def windowClosed(self, event): - control_name = '{}_closed'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - # ~ XWindowListener - def windowResized(self, event): - sb = self._cls._subcont - sb.setPosSize(0, 0, event.Width, event.Height, SIZE) - event_name = '{}_resized'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def windowMoved(self, event): - pass - - def windowShown(self, event): - pass - - def windowHidden(self, event): - pass - - -# ~ BorderColor = ? -# ~ FontStyleName = ? -# ~ HelpURL = ? -class UnoBaseObject(object): - - def __init__(self, obj, path=''): - self._obj = obj - self._model = obj.Model - - def __setattr__(self, name, value): - exists = hasattr(self, name) - if not exists and not name in ('_obj', '_model'): - setattr(self._model, name, value) - else: - super().__setattr__(name, value) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def model(self): - return self._model - @property - def m(self): - return self._model - - @property - def properties(self): - return {} - @properties.setter - def properties(self, values): - _set_properties(self.model, values) - - @property - def name(self): - return self.model.Name - - @property - def parent(self): - return self.obj.Context - - @property - def tag(self): - return self.model.Tag - @tag.setter - def tag(self, value): - self.model.Tag = value - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.setVisible(value) - - @property - def enabled(self): - return self.model.Enabled - @enabled.setter - def enabled(self, value): - self.model.Enabled = value - - @property - def step(self): - return self.model.Step - @step.setter - def step(self, value): - self.model.Step = value - - @property - def align(self): - return self.model.Align - @align.setter - def align(self, value): - self.model.Align = value - - @property - def valign(self): - return self.model.VerticalAlign - @valign.setter - def valign(self, value): - self.model.VerticalAlign = value - - @property - def font_weight(self): - return self.model.FontWeight - @font_weight.setter - def font_weight(self, value): - self.model.FontWeight = value - - @property - def font_height(self): - return self.model.FontHeight - @font_height.setter - def font_height(self, value): - self.model.FontHeight = value - - @property - def font_name(self): - return self.model.FontName - @font_name.setter - def font_name(self, value): - self.model.FontName = value - - @property - def font_underline(self): - return self.model.FontUnderline - @font_underline.setter - def font_underline(self, value): - self.model.FontUnderline = value - - @property - def text_color(self): - return self.model.TextColor - @text_color.setter - def text_color(self, value): - self.model.TextColor = value - - @property - def back_color(self): - return self.model.BackgroundColor - @back_color.setter - def back_color(self, value): - self.model.BackgroundColor = value - - @property - def multi_line(self): - return self.model.MultiLine - @multi_line.setter - def multi_line(self, value): - self.model.MultiLine = value - - @property - def help_text(self): - return self.model.HelpText - @help_text.setter - def help_text(self, value): - self.model.HelpText = value - - @property - def border(self): - return self.model.Border - @border.setter - def border(self, value): - # ~ Bug for report - self.model.Border = value - - @property - def width(self): - return self._model.Width - @width.setter - def width(self, value): - self.model.Width = value - - @property - def height(self): - return self.model.Height - @height.setter - def height(self, value): - self.model.Height = value - - def _get_possize(self, name): - ps = self.obj.getPosSize() - return getattr(ps, name) - - def _set_possize(self, name, value): - ps = self.obj.getPosSize() - setattr(ps, name, value) - self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) - return - - @property - def x(self): - if hasattr(self.model, 'PositionX'): - return self.model.PositionX - return self._get_possize('X') - @x.setter - def x(self, value): - if hasattr(self.model, 'PositionX'): - self.model.PositionX = value - else: - self._set_possize('X', value) - - @property - def y(self): - if hasattr(self.model, 'PositionY'): - return self.model.PositionY - return self._get_possize('Y') - @y.setter - def y(self, value): - if hasattr(self.model, 'PositionY'): - self.model.PositionY = value - else: - self._set_possize('Y', value) - - @property - def tab_index(self): - return self._model.TabIndex - @tab_index.setter - def tab_index(self, value): - self.model.TabIndex = value - - @property - def tab_stop(self): - return self._model.Tabstop - @tab_stop.setter - def tab_stop(self, value): - self.model.Tabstop = value - - @property - def ps(self): - ps = self.obj.getPosSize() - return ps - @ps.setter - def ps(self, ps): - self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) - - def set_focus(self): - self.obj.setFocus() - return - - def ps_from(self, source): - self.ps = source.ps - return - - def center(self, horizontal=True, vertical=False): - p = self.parent.Model - w = p.Width - h = p.Height - if horizontal: - x = w / 2 - self.width / 2 - self.x = x - if vertical: - y = h / 2 - self.height / 2 - self.y = y - return - - def move(self, origin, x=0, y=5, center=False): - if x: - self.x = origin.x + origin.width + x - else: - self.x = origin.x - if y: - h = origin.height - if y < 0: - h = 0 - self.y = origin.y + h + y - else: - self.y = origin.y - - if center: - self.center() - return - - -class UnoLabel(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'label' - - @property - def value(self): - return self.model.Label - @value.setter - def value(self, value): - self.model.Label = value - - -class UnoLabelLink(UnoLabel): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'link' - - -class UnoButton(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'button' - - @property - def value(self): - return self.model.Label - @value.setter - def value(self, value): - self.model.Label = value - - @property - def image(self): - return self.model.ImageURL - @image.setter - def image(self, value): - self.model.ImageURL = _P.to_url(value) - - -class UnoRadio(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'radio' - - @property - def value(self): - return self.model.Label - @value.setter - def value(self, value): - self.model.Label = value - - -class UnoCheckBox(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'checkbox' - - @property - def value(self): - return self.model.State - @value.setter - def value(self, value): - self.model.State = value - - @property - def label(self): - return self.model.Label - @label.setter - def label(self, value): - self.model.Label = value - - @property - def tri_state(self): - return self.model.TriState - @tri_state.setter - def tri_state(self, value): - self.model.TriState = value - - -# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html -class UnoText(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'text' - - @property - def value(self): - return self.model.Text - @value.setter - def value(self, value): - self.model.Text = value - - @property - def echochar(self): - return chr(self.model.EchoChar) - @echochar.setter - def echochar(self, value): - if value: - self.model.EchoChar = ord(value[0]) - else: - self.model.EchoChar = 0 - - def validate(self): - return - - -class UnoImage(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'image' - - @property - def value(self): - return self.url - @value.setter - def value(self, value): - self.url = value - - @property - def url(self): - return self.m.ImageURL - @url.setter - def url(self, value): - self.m.ImageURL = None - self.m.ImageURL = _P.to_url(value) - - -class UnoListBox(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._path = '' - - def __setattr__(self, name, value): - if name in ('_path',): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - @property - def type(self): - return 'listbox' - - @property - def value(self): - return self.obj.getSelectedItem() - - @property - def count(self): - return len(self.data) - - @property - def data(self): - return self.model.StringItemList - @data.setter - def data(self, values): - self.model.StringItemList = list(sorted(values)) - - @property - def path(self): - return self._path - @path.setter - def path(self, value): - self._path = value - - def unselect(self): - self.obj.selectItem(self.value, False) - return - - def select(self, pos=0): - if isinstance(pos, str): - self.obj.selectItem(pos, True) - else: - self.obj.selectItemPos(pos, True) - return - - def clear(self): - self.model.removeAllItems() - return - - def _set_image_url(self, image): - if _P.exists(image): - return _P.to_url(image) - - path = _P.join(self._path, DIR['images'], image) - return _P.to_url(path) - - def insert(self, value, path='', pos=-1, show=True): - if pos < 0: - pos = self.count - if path: - self.model.insertItem(pos, value, self._set_image_url(path)) - else: - self.model.insertItemText(pos, value) - if show: - self.select(pos) - return - - -class UnoRoadmap(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._options = () - - def __setattr__(self, name, value): - if name in ('_options',): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - @property - def options(self): - return self._options - @options.setter - def options(self, values): - self._options = values - for i, v in enumerate(values): - opt = self.model.createInstance() - opt.ID = i - opt.Label = v - self.model.insertByIndex(i, opt) - return - - @property - def enabled(self): - return True - @enabled.setter - def enabled(self, value): - for m in self.model: - m.Enabled = value - return - - def set_enabled(self, index, value): - self.model.getByIndex(index).Enabled = value - return - - -class UnoTree(UnoBaseObject): - - def __init__(self, obj, ): - super().__init__(obj) - self._tdm = None - self._data = [] - - def __setattr__(self, name, value): - if name in ('_tdm', '_data'): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - @property - def selection(self): - sel = self.obj.Selection - return sel.DataValue, sel.DisplayValue - - @property - def parent(self): - parent = self.obj.Selection.Parent - if parent is None: - return () - return parent.DataValue, parent.DisplayValue - - def _get_parents(self, node): - value = (node.DisplayValue,) - parent = node.Parent - if parent is None: - return value - return self._get_parents(parent) + value - - @property - def parents(self): - values = self._get_parents(self.obj.Selection) - return values - - @property - def root(self): - if self._tdm is None: - return '' - return self._tdm.Root.DisplayValue - @root.setter - def root(self, value): - self._add_data_model(value) - - def _add_data_model(self, name): - tdm = create_instance('com.sun.star.awt.tree.MutableTreeDataModel') - root = tdm.createNode(name, True) - root.DataValue = 0 - tdm.setRoot(root) - self.model.DataModel = tdm - self._tdm = self.model.DataModel - return - - @property - def path(self): - return self.root - @path.setter - def path(self, value): - self.data = _P.walk_dir(value, True) - - @property - def data(self): - return self._data - @data.setter - def data(self, values): - self._data = list(values) - self._add_data() - - def _add_data(self): - if not self.data: - return - - parents = {} - for node in self.data: - parent = parents.get(node[1], self._tdm.Root) - child = self._tdm.createNode(node[2], False) - child.DataValue = node[0] - parent.appendChild(child) - parents[node[0]] = child - self.obj.expandNode(self._tdm.Root) - return - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html -class UnoGrid(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._gdm = self.model.GridDataModel - self._data = [] - self._formats = () - - def __setattr__(self, name, value): - if name in ('_gdm', '_data', '_formats'): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - def __getitem__(self, key): - value = self._gdm.getCellData(key[0], key[1]) - return value - - def __setitem__(self, key, value): - self._gdm.updateCellData(key[0], key[1], value) - return - - @property - def type(self): - return 'grid' - - @property - def columns(self): - return {} - @columns.setter - def columns(self, values): - # ~ self._columns = values - #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html - model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) - for properties in values: - column = create_instance('com.sun.star.awt.grid.GridColumn', True) - for k, v in properties.items(): - setattr(column, k, v) - model.addColumn(column) - self.model.ColumnModel = model - return - - @property - def data(self): - return self._data - @data.setter - def data(self, values): - self._data = values - self.clear() - headings = tuple(range(1, len(values) + 1)) - self._gdm.addRows(headings, values) - # ~ rows = range(grid_dm.RowCount) - # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows] - # ~ grid.Model.RowBackgroundColors = tuple(colors) - return - - @property - def value(self): - if self.column == -1 or self.row == -1: - return '' - return self[self.column, self.row] - @value.setter - def value(self, value): - if self.column > -1 and self.row > -1: - self[self.column, self.row] = value - - @property - def row(self): - return self.obj.CurrentRow - - @property - def row_count(self): - return self._gdm.RowCount - - @property - def column(self): - return self.obj.CurrentColumn - - @property - def is_valid(self): - return not (self.row == -1 or self.column == -1) - - @property - def selected_rows(self): - value = self.obj.SelectedRows - return value - - @property - def row_background_colors(self): - value = self.m.RowBackgroundColors - return value - @row_background_colors.setter - def row_background_colors(self, colors): - c = Color() - self.m.RowBackgroundColors = (c(colors[0]), c(colors[1])) - - @property - def formats(self): - return self._formats - @formats.setter - def formats(self, values): - self._formats = values - - def clear(self): - self._gdm.removeAllRows() - return - - def _format_columns(self, data): - row = data - if self.formats: - for i, f in enumerate(formats): - if f: - row[i] = f.format(data[i]) - return row - - def add_row(self, data): - self._data.append(data) - row = self._format_columns(data) - self._gdm.addRow(self.row_count + 1, row) - return - - def set_cell_tooltip(self, col, row, value): - self._gdm.updateCellToolTip(col, row, value) - return - - def get_cell_tooltip(self, col, row): - value = self._gdm.getCellToolTip(col, row) - return value - - def sort(self, column, asc=True): - self._gdm.sortByColumn(column, asc) - self.update_row_heading() - return - - def update_row_heading(self): - for i in range(self.row_count): - self._gdm.updateRowHeading(i, i + 1) - return - - def remove_row(self, row): - self._gdm.removeRow(row) - del self._data[row] - self.update_row_heading() - return - - -class UnoPage(object): - - def __init__(self, obj): - self._obj = obj - self._events = None - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def model(self): - return self._obj.Model - - # ~ @property - # ~ def id(self): - # ~ return self.m.TabPageID - - @property - def parent(self): - return self.obj.Context - - def _set_image_url(self, image): - if _P.exists(image): - return _P.to_url(image) - - path = _P.join(self._path, DIR['images'], image) - return _P.to_url(path) - - def _special_properties(self, tipo, args): - if tipo == 'link' and not 'Label' in args: - args['Label'] = args['URL'] - return args - - if tipo == 'button': - if 'ImageURL' in args: - args['ImageURL'] = self._set_image_url(args['ImageURL']) - args['FocusOnClick'] = args.get('FocusOnClick', False) - return args - - if tipo == 'roadmap': - args['Height'] = args.get('Height', self.height) - if 'Title' in args: - args['Text'] = args.pop('Title') - return args - - if tipo == 'tree': - args['SelectionType'] = args.get('SelectionType', SINGLE) - return args - - if tipo == 'grid': - args['ShowRowHeader'] = args.get('ShowRowHeader', True) - return args - - if tipo == 'pages': - args['Width'] = args.get('Width', self.width) - args['Height'] = args.get('Height', self.height) - - return args - - def add_control(self, args): - tipo = args.pop('Type').lower() - root = args.pop('Root', '') - sheets = args.pop('Sheets', ()) - columns = args.pop('Columns', ()) - - args = self._special_properties(tipo, args) - model = self.model.createInstance(UNO_MODELS[tipo]) - _set_properties(model, args) - name = args['Name'] - self.model.insertByName(name, model) - control = self.obj.getControl(name) - _add_listeners(self._events, control, name) - control = UNO_CLASSES[tipo](control) - - if tipo in ('listbox',): - control.path = self.path - - if tipo == 'tree' and root: - control.root = root - elif tipo == 'grid' and columns: - control.columns = columns - elif tipo == 'pages' and sheets: - control.sheets = sheets - control.events = self.events - - setattr(self, name, control) - return control - - -class UnoPages(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._sheets = [] - self._events = None - - def __setattr__(self, name, value): - if name in ('_sheets', '_events'): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - def __getitem__(self, index): - name = index - if isinstance(index, int): - name = f'sheet{index}' - sheet = self.obj.getControl(name) - page = UnoPage(sheet) - page._events = self._events - return page - - @property - def type(self): - return 'pages' - - @property - def current(self): - return self.obj.ActiveTabID - @property - def active(self): - return self.current - - @property - def sheets(self): - return self._sheets - @sheets.setter - def sheets(self, values): - self._sheets = values - for i, title in enumerate(values): - sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') - sheet.Title = title - self.m.insertByName(f'sheet{i + 1}', sheet) - return - - @property - def events(self): - return self._events - @events.setter - def events(self, controllers): - self._events = controllers - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.Visible = value - - def insert(self, title): - self._sheets.append(title) - id = len(self._sheets) - sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') - sheet.Title = title - self.m.insertByName(f'sheet{id}', sheet) - return self[id] - - def remove(self, id): - self.obj.removeTab(id) - return - - def activate(self, id): - self.obj.activateTab(id) - return - - -class UnoSpinButton(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'spinbutton' - - @property - def value(self): - return self.model.Label - @value.setter - def value(self, value): - self.model.Label = value - - -class UnoNumericField(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'numeric' - - @property - def int(self): - return int(self.value) - - @property - def value(self): - return self.model.Value - @value.setter - def value(self, value): - self.model.Value = value - - -UNO_CLASSES = { - 'label': UnoLabel, - 'link': UnoLabelLink, - 'button': UnoButton, - 'radio': UnoRadio, - 'checkbox': UnoCheckBox, - 'text': UnoText, - 'image': UnoImage, - 'listbox': UnoListBox, - 'roadmap': UnoRoadmap, - 'tree': UnoTree, - 'grid': UnoGrid, - 'pages': UnoPages, - 'spinbutton': UnoSpinButton, - 'numeric': UnoNumericField, -} - -UNO_MODELS = { - 'label': 'com.sun.star.awt.UnoControlFixedTextModel', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', - 'button': 'com.sun.star.awt.UnoControlButtonModel', - 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', - 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', - 'text': 'com.sun.star.awt.UnoControlEditModel', - 'image': 'com.sun.star.awt.UnoControlImageControlModel', - 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', - 'tree': 'com.sun.star.awt.tree.TreeControlModel', - 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', - 'pages': 'com.sun.star.awt.UnoMultiPageModel', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', - 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', - 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', -} -# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', -# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', -# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', -# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', -# ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', -# ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', -# ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', -# ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', -# ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', -# ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', - - -class LODialog(object): - SEPARATION = 5 - MODELS = { - 'label': 'com.sun.star.awt.UnoControlFixedTextModel', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', - 'button': 'com.sun.star.awt.UnoControlButtonModel', - 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', - 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', - 'text': 'com.sun.star.awt.UnoControlEditModel', - 'image': 'com.sun.star.awt.UnoControlImageControlModel', - 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', - 'tree': 'com.sun.star.awt.tree.TreeControlModel', - 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', - 'pages': 'com.sun.star.awt.UnoMultiPageModel', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', - 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', - 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', - } - - def __init__(self, args={}): - self._obj = self._create(args) - self._model = self.obj.Model - self._events = None - self._modal = True - self._controls = {} - self._color_on_focus = COLOR_ON_FOCUS - self._id = '' - self._path = '' - self._init_controls() - - def _create(self, args): - service = 'com.sun.star.awt.DialogProvider' - path = args.pop('Path', '') - if path: - dp = create_instance(service, True) - dlg = dp.createDialog(_P.to_url(path)) - return dlg - - if 'Location' in args: - name = args['Name'] - library = args.get('Library', 'Standard') - location = args.get('Location', 'application').lower() - if location == 'user': - location = 'application' - url = f'vnd.sun.star.script:{library}.{name}?location={location}' - if location == 'document': - dp = create_instance(service, args=docs.active.obj) - else: - dp = create_instance(service, True) - # ~ uid = docs.active.uid - # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' - dlg = dp.createDialog(url) - return dlg - - dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) - model = create_instance('com.sun.star.awt.UnoControlDialogModel', True) - toolkit = create_instance('com.sun.star.awt.Toolkit', True) - - args['Title'] = args.get('Title', TITLE) - args['Width'] = args.get('Width', 200) - args['Height'] = args.get('Height', 150) - - _set_properties(model, args) - dlg.setModel(model) - dlg.setVisible(False) - dlg.createPeer(toolkit, None) - return dlg - - def _get_type_control(self, name): - name = name.split('.')[2] - types = { - 'UnoFixedTextControl': 'label', - 'UnoEditControl': 'text', - 'UnoButtonControl': 'button', - } - return types[name] - - def _init_controls(self): - for control in self.obj.getControls(): - tipo = self._get_type_control(control.ImplementationName) - name = control.Model.Name - control = UNO_CLASSES[tipo](control) - setattr(self, name, control) - return - - @property - def obj(self): - return self._obj - - @property - def model(self): - return self._model - - @property - def controls(self): - return self._controls - - @property - def path(self): - return self._path - @property - def path_images(self): - return _P.join(self.path, DIR['images']) - @property - def id(self): - return self._id - @id.setter - def id(self, value): - self._id = value - self._path = _P.from_id(value) - - @property - def height(self): - return self.model.Height - @height.setter - def height(self, value): - self.model.Height = value - - @property - def width(self): - return self.model.Width - @width.setter - def width(self, value): - self.model.Width = value - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.Visible = value - - @property - def step(self): - return self.model.Step - @step.setter - def step(self, value): - self.model.Step = value - - @property - def events(self): - return self._events - @events.setter - def events(self, controllers): - self._events = controllers(self) - self._connect_listeners() - - @property - def color_on_focus(self): - return self._color_on_focus - @color_on_focus.setter - def color_on_focus(self, value): - self._color_on_focus = get_color(value) - - def _connect_listeners(self): - for control in self.obj.Controls: - _add_listeners(self.events, control, control.Model.Name) - return - - def _set_image_url(self, image): - if _P.exists(image): - return _P.to_url(image) - - path = _P.join(self._path, DIR['images'], image) - return _P.to_url(path) - - def _special_properties(self, tipo, args): - if tipo == 'link' and not 'Label' in args: - args['Label'] = args['URL'] - return args - - if tipo == 'button': - if 'ImageURL' in args: - args['ImageURL'] = self._set_image_url(args['ImageURL']) - args['FocusOnClick'] = args.get('FocusOnClick', False) - return args - - if tipo == 'roadmap': - args['Height'] = args.get('Height', self.height) - if 'Title' in args: - args['Text'] = args.pop('Title') - return args - - if tipo == 'tree': - args['SelectionType'] = args.get('SelectionType', SINGLE) - return args - - if tipo == 'grid': - args['X'] = args.get('X', self.SEPARATION) - args['Y'] = args.get('Y', self.SEPARATION) - args['Width'] = args.get('Width', self.width - self.SEPARATION * 2) - args['Height'] = args.get('Height', self.height - self.SEPARATION * 2) - args['ShowRowHeader'] = args.get('ShowRowHeader', True) - return args - - if tipo == 'pages': - args['Width'] = args.get('Width', self.width) - args['Height'] = args.get('Height', self.height) - - return args - - def add_control(self, args): - tipo = args.pop('Type').lower() - root = args.pop('Root', '') - sheets = args.pop('Sheets', ()) - columns = args.pop('Columns', ()) - - args = self._special_properties(tipo, args) - model = self.model.createInstance(self.MODELS[tipo]) - - _set_properties(model, args) - name = args['Name'] - self.model.insertByName(name, model) - control = self.obj.getControl(name) - _add_listeners(self.events, control, name) - control = UNO_CLASSES[tipo](control) - - if tipo in ('listbox',): - control.path = self.path - - if tipo == 'tree' and root: - control.root = root - elif tipo == 'grid' and columns: - control.columns = columns - elif tipo == 'pages' and sheets: - control.sheets = sheets - control.events = self.events - - setattr(self, name, control) - self._controls[name] = control - return control - - def center(self, control, x=0, y=0): - w = self.width - h = self.height - - if isinstance(control, tuple): - wt = self.SEPARATION * -1 - for c in control: - wt += c.width + self.SEPARATION - x = w / 2 - wt / 2 - for c in control: - c.x = x - x = c.x + c.width + self.SEPARATION - return - - if x < 0: - x = w + x - control.width - elif x == 0: - x = w / 2 - control.width / 2 - if y < 0: - y = h + y - control.height - elif y == 0: - y = h / 2 - control.height / 2 - control.x = x - control.y = y - return - - def open(self, modal=True): - self._modal = modal - if modal: - return self.obj.execute() - else: - self.visible = True - return - - def close(self, value=0): - if self._modal: - value = self.obj.endDialog(value) - else: - self.visible = False - self.obj.dispose() - return value - - def set_values(self, data): - for k, v in data.items(): - self._controls[k].value = v - return - - -class LOCells(object): - - def __getitem__(self, index): - return LODocs().active.active[index] - - -class LOWindow(object): - EMPTY = """ - -""" - MODELS = { - 'label': 'com.sun.star.awt.UnoControlFixedTextModel', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', - 'button': 'com.sun.star.awt.UnoControlButtonModel', - 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', - 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', - 'text': 'com.sun.star.awt.UnoControlEditModel', - 'image': 'com.sun.star.awt.UnoControlImageControlModel', - 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', - 'tree': 'com.sun.star.awt.tree.TreeControlModel', - 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', - 'pages': 'com.sun.star.awt.UnoMultiPageModel', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', - } - - def __init__(self, args): - self._events = None - self._menu = None - self._container = None - self._model = None - self._id = '' - self._path = '' - self._obj = self._create(args) - - def _create(self, properties): - ps = ( - properties.get('X', 0), - properties.get('Y', 0), - properties.get('Width', 500), - properties.get('Height', 500), - ) - self._title = properties.get('Title', TITLE) - self._create_frame(ps) - self._create_container(ps) - self._create_subcontainer(ps) - # ~ self._create_splitter(ps) - return - - def _create_frame(self, ps): - service = 'com.sun.star.frame.TaskCreator' - tc = create_instance(service, True) - self._frame = tc.createInstanceWithArguments(( - NamedValue('FrameName', 'EasyMacroWin'), - NamedValue('PosSize', Rectangle(*ps)), - )) - self._window = self._frame.getContainerWindow() - self._toolkit = self._window.getToolkit() - desktop = get_desktop() - self._frame.setCreator(desktop) - desktop.getFrames().append(self._frame) - self._frame.Title = self._title - return - - def _create_container(self, ps): - service = 'com.sun.star.awt.UnoControlContainer' - self._container = create_instance(service, True) - service = 'com.sun.star.awt.UnoControlContainerModel' - model = create_instance(service, True) - model.BackgroundColor = get_color((225, 225, 225)) - self._container.setModel(model) - self._container.createPeer(self._toolkit, self._window) - self._container.setPosSize(*ps, POSSIZE) - self._frame.setComponent(self._container, None) - return - - def _create_subcontainer(self, ps): - service = 'com.sun.star.awt.ContainerWindowProvider' - cwp = create_instance(service, True) - - path_tmp = _P.save_tmp(self.EMPTY) - subcont = cwp.createContainerWindow( - _P.to_url(path_tmp), '', self._container.getPeer(), None) - _P.kill(path_tmp) - - subcont.setPosSize(0, 0, 500, 500, POSSIZE) - subcont.setVisible(True) - self._container.addControl('subcont', subcont) - self._subcont = subcont - self._model = subcont.Model - return - - def _create_popupmenu(self, menus): - menu = create_instance('com.sun.star.awt.PopupMenu', True) - for i, m in enumerate(menus): - label = m['label'] - cmd = m.get('event', '') - if not cmd: - cmd = label.lower().replace(' ', '_') - if label == '-': - menu.insertSeparator(i) - else: - menu.insertItem(i, label, m.get('style', 0), i) - menu.setCommand(i, cmd) - # ~ menu.setItemImage(i, path?, True) - menu.addMenuListener(EventsMenu(self.events)) - return menu - - def _create_menu(self, menus): - #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMenu.html - #~ nItemId specifies the ID of the menu item to be inserted. - #~ aText specifies the label of the menu item. - #~ nItemStyle 0 = Standard, CHECKABLE = 1, RADIOCHECK = 2, AUTOCHECK = 4 - #~ nItemPos specifies the position where the menu item will be inserted. - self._menu = create_instance('com.sun.star.awt.MenuBar', True) - for i, m in enumerate(menus): - self._menu.insertItem(i, m['label'], m.get('style', 0), i) - cmd = m['label'].lower().replace(' ', '_') - self._menu.setCommand(i, cmd) - submenu = self._create_popupmenu(m['submenu']) - self._menu.setPopupMenu(i, submenu) - - self._window.setMenuBar(self._menu) - return - - def _add_listeners(self, control=None): - if self.events is None: - return - controller = EventsWindow(self) - self._window.addTopWindowListener(controller) - self._window.addWindowListener(controller) - # ~ self._container.addKeyListener(EventsKeyWindow(self)) - return - - def _set_image_url(self, image): - if _P.exists(image): - return _P.to_url(image) - - path = _P.join(self._path, DIR['images'], image) - return _P.to_url(path) - - def _special_properties(self, tipo, args): - if tipo == 'link' and not 'Label' in args: - args['Label'] = args['URL'] - return args - - if tipo == 'button': - if 'ImageURL' in args: - args['ImageURL'] = self._set_image_url(args['ImageURL']) - args['FocusOnClick'] = args.get('FocusOnClick', False) - return args - - if tipo == 'roadmap': - args['Height'] = args.get('Height', self.height) - if 'Title' in args: - args['Text'] = args.pop('Title') - return args - - if tipo == 'tree': - args['SelectionType'] = args.get('SelectionType', SINGLE) - return args - - if tipo == 'grid': - args['ShowRowHeader'] = args.get('ShowRowHeader', True) - return args - - if tipo == 'pages': - args['Width'] = args.get('Width', self.width) - args['Height'] = args.get('Height', self.height) - - return args - - def add_control(self, args): - tipo = args.pop('Type').lower() - root = args.pop('Root', '') - sheets = args.pop('Sheets', ()) - columns = args.pop('Columns', ()) - - args = self._special_properties(tipo, args) - model = self.model.createInstance(self.MODELS[tipo]) - _set_properties(model, args) - name = args['Name'] - self.model.insertByName(name, model) - control = self._subcont.getControl(name) - _add_listeners(self.events, control, name) - control = UNO_CLASSES[tipo](control) - - # ~ if tipo in ('listbox',): - # ~ control.path = self.path - - if tipo == 'tree' and root: - control.root = root - elif tipo == 'grid' and columns: - control.columns = columns - elif tipo == 'pages' and sheets: - control.sheets = sheets - control.events = self.events - - setattr(self, name, control) - return control - - @property - def events(self): - return self._events - @events.setter - def events(self, controllers): - self._events = controllers(self) - self._add_listeners() - - @property - def model(self): - return self._model - - @property - def width(self): - return self._container.Size.Width - - @property - def height(self): - return self._container.Size.Height - - @property - def name(self): - return self._title.lower().replace(' ', '_') - - def add_menu(self, menus): - self._create_menu(menus) - return - - def open(self): - self._window.setVisible(True) - return - - def close(self): - self._window.setMenuBar(None) - self._window.dispose() - self._frame.close(True) - return - - -class LODBServer(object): - DRIVERS = { - 'mysql': 'mysqlc', - 'mariadb': 'mysqlc', - 'postgres': 'postgresql:postgresql', - } - PORTS = { - 'mysql': 3306, - 'mariadb': 3306, - 'postgres': 5432, - } - - def __init__(self): - self._conn = None - self._error = 'Not connected' - self._type = '' - self._drivers = [] - - def __str__(self): - return f'DB type {self._type}' - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.disconnet() - - @property - def is_connected(self): - return not self._conn is None - - @property - def error(self): - return self._error - - @property - def drivers(self): - return self._drivers - - def disconnet(self): - if not self._conn is None: - if not self._conn.isClosed(): - self._conn.close() - self._conn.dispose() - return - - def connect(self, options={}): - args = options.copy() - self._error = '' - self._type = args.get('type', 'postgres') - driver = self.DRIVERS[self._type] - server = args.get('server', 'localhost') - port = args.get('port', self.PORTS[self._type]) - dbname = args.get('dbname', '') - user = args['user'] - password = args['password'] - - data = {'user': user, 'password': password} - url = f'sdbc:{driver}:{server}:{port}/{dbname}' - - # ~ https://downloads.mariadb.com/Connectors/java/ - # ~ data['JavaDriverClass'] = 'org.mariadb.jdbc.Driver' - # ~ url = f'jdbc:mysql://{server}:{port}/{dbname}' - - args = dict_to_property(data) - manager = create_instance('com.sun.star.sdbc.DriverManager') - self._drivers = [d.ImplementationName for d in manager] - - try: - self._conn = manager.getConnectionWithInfo(url, args) - except Exception as e: - error(e) - self._error = str(e) - - return self - - def execute(self, sql): - query = self._conn.createStatement() - try: - query.execute(sql) - result = True - except Exception as e: - error(e) - self._error = str(e) - result = False - - return result - - -def create_window(args): - return LOWindow(args) - - -class Paths(object): - - @classmethod - def image(cls, path): - # ~ sfa = create_instance('com.sun.star.ucb.SimpleFileAccess') - # ~ stream = sfa.openFileRead(cls.to_url(path)) - gp = create_instance('com.sun.star.graphic.GraphicProvider') - if isinstance(path, str): - properties = (PropertyValue(Name='URL', Value=cls.to_url(path)),) - else: - properties = (PropertyValue(Name='InputStream', Value=path),) - image = gp.queryGraphic(properties) - return image - - -class IOStream(object): - - @classmethod - def qr(cls, data, **kwargs): - import segno - - kwargs['kind'] = kwargs.get('kind', 'svg') - kwargs['scale'] = kwargs.get('scale', 8) - kwargs['border'] = kwargs.get('border', 2) - buffer = cls.buffer() - segno.make(data).save(buffer, **kwargs) - stream = cls.input(buffer) - return stream - - -class SpellChecker(object): - - def __init__(self): - service = 'com.sun.star.linguistic2.SpellChecker' - self._spellchecker = create_instance(service, True) - self._locale = LOCALE - - @property - def locale(self): - slocal = f'{self._locale.Language}-{self._locale.Country}' - return slocale - @locale.setter - def locale(self, value): - lang = value.split('-') - self._locale = Locale(lang[0], lang[1], '') - - def is_valid(self, word): - result = self._spellchecker.isValid(word, self._locale, ()) - return result - - def spell(self, word): - result = self._spellchecker.spell(word, self._locale, ()) - if result: - result = result.getAlternatives() - if not isinstance(result, tuple): - result = () - return result - - -def spell(word, locale=''): - sc = SpellChecker() - if locale: - sc.locale = locale - return sc.spell(word) - - -def __getattr__(name): - if name == 'current_region': - return LODocs().active.selection.current_region - if name in ('rectangle', 'pos_size'): - return Rectangle() - if name == 'db': - return LODBServer() - if name == 'cells': - return LOCells() - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -def create_dialog(args={}): - return LODialog(args) - - -def inputbox(message, default='', title=TITLE, echochar=''): - - class ControllersInput(object): - - def __init__(self, dlg): - self.d = dlg - - def cmd_ok_action(self, event): - self.d.close(1) - return - - args = { - 'Title': title, - 'Width': 200, - 'Height': 80, - } - dlg = LODialog(args) - dlg.events = ControllersInput - - args = { - 'Type': 'Label', - 'Name': 'lbl_msg', - 'Label': message, - 'Width': 140, - 'Height': 50, - 'X': 5, - 'Y': 5, - 'MultiLine': True, - 'Border': 1, - } - dlg.add_control(args) - - args = { - 'Type': 'Text', - 'Name': 'txt_value', - 'Text': default, - 'Width': 190, - 'Height': 15, - } - if echochar: - args['EchoChar'] = ord(echochar[0]) - dlg.add_control(args) - dlg.txt_value.move(dlg.lbl_msg) - - args = { - 'Type': 'button', - 'Name': 'cmd_ok', - 'Label': _('OK'), - 'Width': 40, - 'Height': 15, - 'DefaultButton': True, - 'PushButtonType': 1, - } - dlg.add_control(args) - dlg.cmd_ok.move(dlg.lbl_msg, 10, 0) - - args = { - 'Type': 'button', - 'Name': 'cmd_cancel', - 'Label': _('Cancel'), - 'Width': 40, - 'Height': 15, - 'PushButtonType': 2, - } - dlg.add_control(args) - dlg.cmd_cancel.move(dlg.cmd_ok) - - if dlg.open(): - return dlg.txt_value.value - - return '' diff --git a/source/easymacro.bk2 b/source/easymacro.bk2 deleted file mode 100644 index 0d78e21..0000000 --- a/source/easymacro.bk2 +++ /dev/null @@ -1,4671 +0,0 @@ -#!/usr/bin/env python3 - -# == Rapid Develop Macros in LibreOffice == - -import base64 -import ctypes -import gettext -import zipfile - -from collections import OrderedDict -from collections.abc import MutableMapping -from decimal import Decimal -from enum import IntEnum - -import imaplib - -from com.sun.star.awt.PosSize import POSSIZE, SIZE - -from com.sun.star.sheet import TableFilterField -from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA -from com.sun.star.util import Time, Date, DateTime - -from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK - -from com.sun.star.lang import Locale -from com.sun.star.awt import XActionListener -from com.sun.star.awt import XMenuListener -from com.sun.star.awt import XMouseListener -from com.sun.star.awt import XMouseMotionListener -from com.sun.star.awt import XFocusListener -from com.sun.star.awt import XKeyListener -from com.sun.star.awt import XItemListener -from com.sun.star.awt import XTabListener -from com.sun.star.awt import XSpinListener -from com.sun.star.awt import XWindowListener -from com.sun.star.awt import XTopWindowListener -from com.sun.star.awt.grid import XGridDataListener -from com.sun.star.awt.grid import XGridSelectionListener -from com.sun.star.script import ScriptEventDescriptor - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1FontUnderline.html -from com.sun.star.awt import FontUnderline -from com.sun.star.style.VerticalAlignment import TOP, MIDDLE, BOTTOM - -from com.sun.star.view.SelectionType import SINGLE, MULTI, RANGE - -from com.sun.star.sdb.CommandType import TABLE, QUERY, COMMAND - -try: - from peewee import Database, DateTimeField, DateField, TimeField, \ - __exception_wrapper__ -except ImportError as e: - Database = DateField = TimeField = DateTimeField = object - print('You need install peewee, only if you will develop with Base') - - -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_DATE = '%d/%m/%Y %H:%M:%S' -logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') -logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') -logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') -logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) -log = logging.getLogger(__name__) - - -# ~ You can get custom salt -# ~ codecs.encode(os.urandom(16), 'hex') -# ~ but, not modify this file, modify in import file -SALT = b'c9548699d4e432dfd2b46adddafbb06d' - -LOG_NAME = 'ZAZ' - -LEFT = 0 -CENTER = 1 -RIGHT = 2 - -CALC = 'calc' -WRITER = 'writer' -DRAW = 'draw' -IMPRESS = 'impress' -BASE = 'base' -MATH = 'math' -BASIC = 'basic' -MAIN = 'main' -TYPE_DOC = { - CALC: 'com.sun.star.sheet.SpreadsheetDocument', - WRITER: 'com.sun.star.text.TextDocument', - DRAW: 'com.sun.star.drawing.DrawingDocument', - IMPRESS: 'com.sun.star.presentation.PresentationDocument', - MATH: 'com.sun.star.formula.FormulaProperties', - BASE: 'com.sun.star.sdb.DocumentDataSource', - BASIC: 'com.sun.star.script.BasicIDE', - MAIN: 'com.sun.star.frame.StartModule', -} - -OBJ_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' -OBJ_GRAPHIC = 'SwXTextGraphicObject' - -OBJ_TEXTS = 'SwXTextRanges' -OBJ_TEXT = 'SwXTextRange' - -CLSID = { - 'FORMULA': '078B7ABA-54FC-457F-8551-6147e776a997', -} - -SERVICES = { - 'TEXT_EMBEDDED': 'com.sun.star.text.TextEmbeddedObject', - 'TEXT_TABLE': 'com.sun.star.text.TextTable', - 'GRAPHIC': 'com.sun.star.text.GraphicObject', -} - - -# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL -class FilterOperator(IntEnum): - EMPTY = 0 - NO_EMPTY = 1 - EQUAL = 2 - NOT_EQUAL = 3 - -# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 -class Border(IntEnum): - NO_BORDER = 0 - BORDER = 1 - SIMPLE = 2 - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a -class ValidationType(): - from com.sun.star.sheet.ValidationType \ - import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM -VT = ValidationType - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7 -class ValidationAlertStyle(): - from com.sun.star.sheet.ValidationAlertStyle \ - import STOP, WARNING, INFO, MACRO -VAS = ValidationAlertStyle - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html -class ConditionOperator(): - from com.sun.star.sheet.ConditionOperator2 \ - import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \ - LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE -CO = ConditionOperator - - -class DataPilotFieldOrientation(): - from com.sun.star.sheet.DataPilotFieldOrientation \ - import HIDDEN, COLUMN, ROW, PAGE, DATA -DPFO = DataPilotFieldOrientation - - -class CellInsertMode(): - from com.sun.star.sheet.CellInsertMode import DOWN, RIGHT, ROWS, COLUMNS -CIM = CellInsertMode - - -class CellDeleteMode(): - from com.sun.star.sheet.CellDeleteMode import UP, LEFT, ROWS, COLUMNS -CDM = CellDeleteMode - - -class FormButtonType(): - from com.sun.star.form.FormButtonType import PUSH, SUBMIT, RESET, URL -FBT = FormButtonType - - -class TextContentAnchorType(): - from com.sun.star.text.TextContentAnchorType \ - import AT_PARAGRAPH, AS_CHARACTER, AT_PAGE, AT_FRAME, AT_CHARACTER -TCAT = TextContentAnchorType - - -SECONDS_DAY = 60 * 60 * 24 -DIR = { - 'images': 'images', - 'locales': 'locales', -} - -KEY = { - 'enter': 1280, -} - -DEFAULT_MIME_TYPE = 'png' -MIME_TYPE = { - 'png': 'image/png', - 'jpg': 'image/jpeg', -} - - -try: - COUNTRY = LANGUAGE.split('-')[1] -except: - COUNTRY = '' -LOCALE = Locale(LANG, COUNTRY, '') - - -def inspect(obj: Any, to_sheet: bool=True) -> None: - if hasattr(obj, 'obj'): - obj = obj.obj - - if to_sheet: - _inspect_to_sheet(obj) - else: - zaz = create_instance('net.elmau.zaz.inspect') - zaz.inspect(obj) - return - - -def get_type_doc(obj: Any) -> str: - for k, v in TYPE_DOC.items(): - if obj.supportsService(v): - return k - return '' - - -def _get_class_doc(obj: Any) -> Any: - classes = { - CALC: LOCalc, - WRITER: LOWriter, - DRAW: LODraw, - IMPRESS: LOImpress, - BASE: LOBase, - MATH: LOMath, - BASIC: LOBasic, - } - type_doc = get_type_doc(obj) - return classes[type_doc](obj) - - -def _date_to_struct(value): - if isinstance(value, datetime.datetime): - d = DateTime() - d.Year = value.year - d.Month = value.month - d.Day = value.day - d.Hours = value.hour - d.Minutes = value.minute - d.Seconds = value.second - elif isinstance(value, datetime.date): - d = Date() - d.Day = value.day - d.Month = value.month - d.Year = value.year - elif isinstance(value, datetime.time): - d = Time() - d.Hours = value.hour - d.Minutes = value.minute - d.Seconds = value.second - return d - - -def _struct_to_date(value): - d = None - if isinstance(value, Time): - d = datetime.time(value.Hours, value.Minutes, value.Seconds) - elif isinstance(value, Date): - if value != Date(): - d = datetime.date(value.Year, value.Month, value.Day) - elif isinstance(value, DateTime): - if value.Year > 0: - d = datetime.datetime( - value.Year, value.Month, value.Day, - value.Hours, value.Minutes, value.Seconds) - return d - - -def install_locales(path: str, domain: str='base', dir_locales=DIR['locales']): - path_locales = _P.join(_P(path).path, dir_locales) - try: - lang = gettext.translation(domain, path_locales, languages=[LANG]) - lang.install() - _ = lang.gettext - except Exception as e: - from gettext import gettext as _ - error(e) - return _ - - -def _export_image(obj, args): - name = 'com.sun.star.drawing.GraphicExportFilter' - exporter = create_instance(name) - path = _P.to_system(args['URL']) - args = dict_to_property(args) - exporter.setSourceDocument(obj) - exporter.filter(args) - return _P.exists(path) - - -def get_size_screen(): - res = '' - if IS_WIN: - user32 = ctypes.windll.user32 - res = f'{user32.GetSystemMetrics(0)}x{user32.GetSystemMetrics(1)}' - else: - try: - args = 'xrandr | grep "*" | cut -d " " -f4' - res = run(args, split=False) - except Exception as e: - error(e) - return res.strip() - - -def _get_key(password): - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - - kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=SALT, - iterations=100000) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - return key - - -def encrypt(data, password): - from cryptography.fernet import Fernet - - f = Fernet(_get_key(password)) - if isinstance(data, str): - data = data.encode() - token = f.encrypt(data).decode() - return token - - -def decrypt(token, password): - from cryptography.fernet import Fernet, InvalidToken - - data = '' - f = Fernet(_get_key(password)) - try: - data = f.decrypt(token.encode()).decode() - except InvalidToken as e: - error('Invalid Token') - return data - - -def switch_design_mode(doc): - call_dispatch(doc.frame, '.uno:SwitchControlDesignMode') - return - - -class ImapServer(object): - - def __init__(self, config): - self._server = None - self._error = '' - self._is_connect = self._login(config) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @property - def is_connect(self): - return self._is_connect - - @property - def error(self): - return self._error - - def _login(self, config): - try: - # ~ hosts = 'gmail' in config['server'] - if config['ssl']: - self._server = imaplib.IMAP4_SSL(config['server'], config['port']) - else: - self._server = imaplib.IMAP4(config['server'], config['port']) - self._server.login(config['user'], config['password']) - self._server.select() - return True - except imaplib.IMAP4.error as e: - self._error = str(e) - return False - except Exception as e: - self._error = str(e) - return False - return False - - def get_folders(self, exclude=()): - folders = {} - result, subdir = self._server.list() - for s in subdir: - print(s.decode('utf-8')) - return folders - - def close(self): - try: - self._server.close() - self._server.logout() - msg = 'Close connection...' - debug(msg) - except: - pass - return - - -# ~ Classes - -class LOBaseObject(object): - - def __init__(self, obj): - self._obj = obj - - def __setattr__(self, name, value): - exists = hasattr(self, name) - if not exists and not name in ('_obj', '_index', '_view'): - setattr(self._obj, name, value) - else: - super().__setattr__(name, value) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - -class LOCalc(LODocument): - - @property - def db_ranges(self): - # ~ return LOCalcDataBaseRanges(self.obj.DataBaseRanges) - return self.obj.DatabaseRanges - - def render(self, data, sheet=None, clean=True): - if sheet is None: - sheet = self.active - return sheet.render(data, clean=clean) - - -class LOChart(object): - - def __init__(self, name, obj, draw_page): - self._name = name - self._obj = obj - self._eobj = self._obj.EmbeddedObject - self._type = 'Column' - self._cell = None - self._shape = self._get_shape(draw_page) - self._pos = self._shape.Position - - def __getitem__(self, index): - return LOBaseObject(self.diagram.getDataRowProperties(index)) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self._name - - @property - def diagram(self): - return self._eobj.Diagram - - @property - def type(self): - return self._type - @type.setter - def type(self, value): - self._type = value - if value == 'Bar': - self.diagram.Vertical = True - return - type_chart = f'com.sun.star.chart.{value}Diagram' - self._eobj.setDiagram(self._eobj.createInstance(type_chart)) - - @property - def cell(self): - return self._cell - @cell.setter - def cell(self, value): - self._cell = value - self._shape.Anchor = value.obj - - @property - def position(self): - return self._pos - @position.setter - def position(self, value): - self._pos = value - self._shape.Position = value - - def _get_shape(self, draw_page): - for shape in draw_page: - if shape.PersistName == self.name: - break - return shape - - -class LOSheetCharts(object): - - def __init__(self, obj, sheet): - self._obj = obj - self._sheet = sheet - - def __getitem__(self, index): - return LOChart(index, self.obj[index], self._sheet.draw_page) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __contains__(self, item): - return item in self.obj - - def __len__(self): - return len(self.obj) - - @property - def obj(self): - return self._obj - - def new(self, name, pos_size, data): - self.obj.addNewByName(name, pos_size, data, True, True) - return LOChart(name, self.obj[name], self._sheet.draw_page) - - -class LOSheetTableField(object): - - def __init__(self, obj): - self._obj = obj - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self.obj.Name - - @property - def orientation(self): - return self.obj.Orientation - @orientation.setter - def orientation(self, value): - self.obj.Orientation = value - - -# ~ com.sun.star.sheet.DataPilotFieldOrientation.ROW -class LOSheetTable(object): - - def __init__(self, obj): - self._obj = obj - self._source = None - - def __getitem__(self, index): - field = self.obj.DataPilotFields[index] - return LOSheetTableField(field) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def filter(self): - return self.obj.ShowFilterButton - @filter.setter - def filter(self, value): - self.obj.ShowFilterButton = value - - @property - def source(self): - return self._source - @source.setter - def source(self, value): - self._source = value - self.obj.SourceRange = value.range_address - - @property - def rows(self): - return self.obj.RowFields - @rows.setter - def rows(self, values): - if not isinstance(values, tuple): - values = (values,) - for v in values: - with self[v] as f: - f.orientation = DPFO.ROW - @property - def columns(self): - return self.obj.ColumnFields - @columns.setter - def columns(self, values): - if not isinstance(values, tuple): - values = (values,) - for v in values: - with self[v] as f: - f.orientation = DPFO.COLUMN - - @property - def data(self): - return self.obj.DataFields - @data.setter - def data(self, values): - if not isinstance(values, tuple): - values = (values,) - for v in values: - with self[v] as f: - f.orientation = DPFO.DATA - - -class LOSheetTables(object): - - def __init__(self, obj, sheet): - self._obj = obj - self._sheet = sheet - - def __getitem__(self, index): - return LOSheetTable(self.obj[index]) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __contains__(self, item): - return item in self.obj - - @property - def obj(self): - return self._obj - - @property - def count(self): - return self.obj.Count - - @property - def names(self): - return self.obj.ElementNames - - def new(self, name, target): - table = self.obj.createDataPilotDescriptor() - self.obj.insertNewByName(name, target.address, table) - return LOSheetTable(self.obj[name]) - - def remove(self, name): - self.obj.removeByName(name) - return - - -# ~ class LOFormControl(LOBaseObject): -class LOFormControl(): - EVENTS = { - 'action': 'actionPerformed', - 'click': 'mousePressed', - } - TYPES = { - 'actionPerformed': 'XActionListener', - 'mousePressed': 'XMouseListener', - } - - def __init__(self, obj, view, form): - self._obj = obj - self._view = view - self._form = form - self._m = view.Model - self._index = -1 - - # ~ def __setattr__(self, name, value): - # ~ if name in ('_form', '_view', '_m', '_index'): - # ~ self.__dict__[name] = value - # ~ else: - # ~ super().__setattr__(name, value) - - def __str__(self): - return f'{self.name} ({self.type}) {[self.index]}' - - @property - def obj(self): - return self._obj - - @property - def form(self): - return self._form - - @property - def doc(self): - return self.obj.Parent.Forms.Parent - - @property - def name(self): - return self._m.Name - @name.setter - def name(self, value): - self._m.Name = value - - @property - def tag(self): - return self._m.Tag - @tag.setter - def tag(self, value): - self._m.Tag = value - - @property - def index(self): - return self._index - @index.setter - def index(self, value): - self._index = value - - @property - def enabled(self): - return self._m.Enabled - @enabled.setter - def enabled(self, value): - self._m.Enabled = value - - @property - def anchor(self): - return self.obj.Anchor - @anchor.setter - def anchor(self, value): - size = None - if hasattr(value, 'obj'): - size = getattr(value, 'size', None) - value = value.obj - self.obj.Anchor = value - if not size is None: - self.size = size - try: - self.obj.ResizeWithCell = True - except: - pass - - @property - def size(self): - return self.obj.Size - @size.setter - def size(self, value): - self.obj.Size = value - - @property - def events(self): - return self.form.getScriptEvents(self.index) - def add_event(self, name, macro): - if not 'name' in macro: - macro['name'] = '{}_{}'.format(self.name, name) - - event = ScriptEventDescriptor() - event.AddListenerParam = '' - event.EventMethod = self.EVENTS[name] - event.ListenerType = self.TYPES[event.EventMethod] - event.ScriptCode = _get_url_script(macro) - event.ScriptType = 'Script' - - for ev in self.events: - if ev.EventMethod == event.EventMethod and \ - ev.ListenerType == event.ListenerType: - self.form.revokeScriptEvent(self.index, - event.ListenerType, event.EventMethod, event.AddListenerParam) - break - - self.form.registerScriptEvent(self.index, event) - return - - def set_focus(self): - self._view.setFocus() - return - - -class LOFormControlLabel(LOFormControl): - - def __init__(self, obj, view, form): - super().__init__(obj, view, form) - - @property - def type(self): - return 'label' - - @property - def value(self): - return self._m.Label - @value.setter - def value(self, value): - self._m.Label = value - - -class LOFormControlText(LOFormControl): - - def __init__(self, obj, view, form): - super().__init__(obj, view, form) - - @property - def type(self): - return 'text' - - @property - def value(self): - return self._m.Text - @value.setter - def value(self, value): - self._m.Text = value - - -class LOFormControlButton(LOFormControl): - - def __init__(self, obj, view, form): - super().__init__(obj, view, form) - - @property - def type(self): - return 'button' - - @property - def value(self): - return self._m.Label - @value.setter - def value(self, value): - self._m.Text = Label - - @property - def url(self): - return self._m.TargetURL - @url.setter - def url(self, value): - self._m.TargetURL = value - self._m.ButtonType = FormButtonType.URL - - -FORM_CONTROL_CLASS = { - 'label': LOFormControlLabel, - 'text': LOFormControlText, - 'button': LOFormControlButton, -} - - -class LOForm(object): - MODELS = { - 'label': 'com.sun.star.form.component.FixedText', - 'text': 'com.sun.star.form.component.TextField', - 'button': 'com.sun.star.form.component.CommandButton', - } - - def __init__(self, obj, draw_page): - self._obj = obj - self._dp = draw_page - self._controls = {} - self._init_controls() - - def __getitem__(self, index): - control = self.obj[index] - return self._controls[control.Name] - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __contains__(self, item): - return item in self.obj - - def __len__(self): - return len(self.obj) - - def __str__(self): - return f'Form: {self.name}' - - def _init_controls(self): - types = { - 'com.sun.star.form.OFixedTextModel': 'label', - 'com.sun.star.form.OEditModel': 'text', - 'com.sun.star.form.OButtonModel': 'button', - } - for i, control in enumerate(self.obj): - name = control.Name - tipo = types[control.ImplementationName] - view = self.doc.CurrentController.getControl(control) - control = FORM_CONTROL_CLASS[tipo](control, view, self._obj) - control.index = i - setattr(self, name, control) - self._controls[name] = control - return - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self.obj.Name - @name.setter - def name(self, value): - self.obj.Name = value - - @property - def source(self): - return self.obj.DataSourceName - @source.setter - def source(self, value): - self.obj.DataSourceName = value - - @property - def type(self): - return self.obj.CommandType - @type.setter - def type(self, value): - self.obj.CommandType = value - - @property - def command(self): - return self.obj.Command - @command.setter - def command(self, value): - self.obj.Command = value - - @property - def doc(self): - return self.obj.Parent.Parent - - def _special_properties(self, tipo, args): - if tipo == 'button': - # ~ if 'ImageURL' in args: - # ~ args['ImageURL'] = self._set_image_url(args['ImageURL']) - args['FocusOnClick'] = args.get('FocusOnClick', False) - return args - return args - - def add(self, args): - name = args['Name'] - tipo = args.pop('Type').lower() - w = args.pop('Width', 1000) - h = args.pop('Height', 200) - x = args.pop('X', 0) - y = args.pop('Y', 0) - control = self.doc.createInstance('com.sun.star.drawing.ControlShape') - control.setSize(Size(w, h)) - control.setPosition(Point(x, y)) - model = self.doc.createInstance(self.MODELS[tipo]) - args = self._special_properties(tipo, args) - _set_properties(model, args) - control.Control = model - index = len(self) - self.obj.insertByIndex(index, model) - self._dp.add(control) - view = self.doc.CurrentController.getControl(self.obj.getByName(name)) - control = FORM_CONTROL_CLASS[tipo](control, view, self.obj) - control.index = index - setattr(self, name, control) - self._controls[name] = control - return control - - -class LOSheetForms(object): - - def __init__(self, draw_page): - self._dp = draw_page - self._obj = draw_page.Forms - - def __getitem__(self, index): - return LOForm(self.obj[index], self._dp) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - def __contains__(self, item): - return item in self.obj - - def __len__(self): - return len(self.obj) - - @property - def obj(self): - return self._obj - - @property - def doc(self): - return self.obj.Parent - - @property - def count(self): - return len(self) - - @property - def names(self): - return self.obj.ElementNames - - def insert(self, name=''): - if not name: - name = f'form{self.count + 1}' - form = self.doc.createInstance('com.sun.star.form.component.Form') - self.obj.insertByName(name, form) - return LOForm(form, self._dp) - - def remove(self, index): - if isinstance(index, int): - self.obj.removeByIndex(index) - else: - self.obj.removeByName(index) - return - - -# ~ IsFiltered, -# ~ IsManualPageBreak, -# ~ IsStartOfNewPage -class LOSheetRows(object): - - def __init__(self, sheet, obj): - self._sheet = sheet - self._obj = obj - - def __getitem__(self, index): - if isinstance(index, int): - rows = LOSheetRows(self._sheet, self.obj[index]) - else: - rango = self._sheet[index.start:index.stop,0:] - rows = LOSheetRows(self._sheet, rango.obj.Rows) - return rows - - def __len__(self): - return self.obj.Count - - @property - def obj(self): - return self._obj - - @property - def visible(self): - return self._obj.IsVisible - @visible.setter - def visible(self, value): - self._obj.IsVisible = value - - @property - def color(self): - return self.obj.CellBackColor - @color.setter - def color(self, value): - self.obj.CellBackColor = value - - @property - def is_transparent(self): - return self.obj.IsCellBackgroundTransparent - @is_transparent.setter - def is_transparent(self, value): - self.obj.IsCellBackgroundTransparent = value - - @property - def height(self): - return self.obj.Height - @height.setter - def height(self, value): - self.obj.Height = value - - def optimal(self): - self.obj.OptimalHeight = True - return - - def insert(self, index, count): - self.obj.insertByIndex(index, count) - return - - def remove(self, index, count): - self.obj.removeByIndex(index, count) - return - - -# ~ IsManualPageBreak, -# ~ IsStartOfNewPage -class LOSheetColumns(object): - - def __init__(self, sheet, obj): - self._sheet = sheet - self._obj = obj - - def __getitem__(self, index): - if isinstance(index, (int, str)): - rows = LOSheetColumns(self._sheet, self.obj[index]) - else: - rango = self._sheet[0,index.start:index.stop] - rows = LOSheetColumns(self._sheet, rango.obj.Columns) - return rows - - def __len__(self): - return self.obj.Count - - @property - def obj(self): - return self._obj - - @property - def visible(self): - return self._obj.IsVisible - @visible.setter - def visible(self, value): - self._obj.IsVisible = value - - @property - def width(self): - return self.obj.Width - @width.setter - def width(self, value): - self.obj.Width = value - - def optimal(self): - self.obj.OptimalWidth = True - return - - def insert(self, index, count): - self.obj.insertByIndex(index, count) - return - - def remove(self, index, count): - self.obj.removeByIndex(index, count) - return - - -class LOCalcSheet(object): - - @property - def draw_page(self): - return LODrawPage(self.obj.DrawPage) - @property - def dp(self): - return self.draw_page - - @property - def shapes(self): - return self.draw_page - - @property - def charts(self): - return LOSheetCharts(self.obj.Charts, self) - - @property - def tables(self): - return LOSheetTables(self.obj.DataPilotTables, self) - - @property - def rows(self): - return LOSheetRows(self, self.obj.Rows) - - @property - def columns(self): - return LOSheetColumns(self, self.obj.Columns) - - @property - def forms(self): - return LOSheetForms(self.obj.DrawPage) - - @property - def search_descriptor(self): - return self.obj.createSearchDescriptor() - - @property - def replace_descriptor(self): - return self.obj.createReplaceDescriptor() - - def render(self, data, rango=None, clean=True): - if rango is None: - rango = self.used_area - return rango.render(data, clean) - - def find(self, search_string, rango=None): - if rango is None: - rango = self.used_area - return rango.find(search_string) - - -class LOCalcRange(object): - - def __contains__(self, item): - return item.in_range(self) - - @property - def back_color(self): - return self._obj.CellBackColor - @back_color.setter - def back_color(self, value): - self._obj.CellBackColor = get_color(value) - - @property - def columns(self): - return self.obj.Columns.Count - - @property - def column(self): - c1 = self.address.Column - c2 = c1 + 1 - ra = self.current_region.range_address - r1 = ra.StartRow - r2 = ra.EndRow + 1 - return LOCalcRange(self.sheet[r1:r2, c1:c2].obj) - - @property - def rows(self): - return LOSheetRows(self.sheet, self.obj.Rows) - - @property - def row(self): - r1 = self.address.Row - r2 = r1 + 1 - ra = self.current_region.range_address - c1 = ra.StartColumn - c2 = ra.EndColumn + 1 - return LOCalcRange(self.sheet[r1:r2, c1:c2].obj) - - @property - def type(self): - return self.obj.Type - - @property - def error(self): - return self.obj.getError() - - # ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html - @property - def value(self): - v = None - if self.type == VALUE: - v = self.obj.getValue() - elif self.type == TEXT: - v = self.obj.getString() - elif self.type == FORMULA: - v = self.obj.getFormula() - return v - @value.setter - def value(self, data): - if isinstance(data, str): - if data[0] in '=': - self.obj.setFormula(data) - else: - self.obj.setString(data) - elif isinstance(data, Decimal): - self.obj.setValue(float(data)) - elif isinstance(data, (int, float, bool)): - self.obj.setValue(data) - elif isinstance(data, datetime.datetime): - d = data.toordinal() - t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY - self.obj.setValue(d - DATE_OFFSET + t) - elif isinstance(data, datetime.date): - d = data.toordinal() - self.obj.setValue(d - DATE_OFFSET) - elif isinstance(data, datetime.time): - d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY - self.obj.setValue(d) - - @property - def date(self): - value = int(self.obj.Value) - date = datetime.date.fromordinal(value + DATE_OFFSET) - return date - - @property - def time(self): - seconds = self.obj.Value * SECONDS_DAY - time_delta = datetime.timedelta(seconds=seconds) - time = (datetime.datetime.min + time_delta).time() - return time - - @property - def datetime(self): - return datetime.datetime.combine(self.date, self.time) - - @property - def dict(self): - rows = self.data - k = rows[0] - data = [dict(zip(k, r)) for r in rows[1:]] - return data - @dict.setter - def dict(self, values): - data = [tuple(values[0].keys())] - data += [tuple(d.values()) for d in values] - self.data = data - - @property - def formula(self): - return self.obj.getFormulaArray() - @formula.setter - def formula(self, values): - self.obj.setFormulaArray(values) - - @property - def array_formula(self): - return self.obj.ArrayFormula - @array_formula.setter - def array_formula(self, value): - self.obj.ArrayFormula = value - - @property - def next_cell(self): - a = self.current_region.range_address - col = a.StartColumn - row = a.EndRow + 1 - return LOCalcRange(self.sheet[row, col].obj) - - @property - def position(self): - return self.obj.Position - - @property - def size(self): - return self.obj.Size - - @property - def possize(self): - data = { - 'Width': self.size.Width, - 'Height': self.size.Height, - 'X': self.position.X, - 'Y': self.position.Y, - } - return data - - @property - def visible(self): - cursor = self.cursor - rangos = cursor.queryVisibleCells() - rangos = LOCalcRanges(rangos) - return rangos - - @property - def merged_area(self): - cursor = self.cursor - cursor.collapseToMergedArea() - rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj) - return rango - - @property - def empty(self): - cursor = self.cursor - rangos = cursor.queryEmptyCells() - rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] - return tuple(rangos) - - def query_content(self, type_content=1023): - cursor = self.cursor - rangos = cursor.queryContentCells(type_content) - rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] - return tuple(rangos) - - @property - def merge(self): - return self.obj.IsMerged - @merge.setter - def merge(self, value): - self.obj.merge(value) - - @property - def auto_format(self): - return '' - @auto_format.setter - def auto_format(self, value): - self.obj.autoFormat(value) - - @property - def validation(self): - return self.obj.Validation - @validation.setter - def validation(self, values): - current = self.validation - if not values: - current.Type = ValidationType.ANY - current.ShowInputMessage = False - else: - is_list = False - for k, v in values.items(): - if k == 'Type' and v == VT.LIST: - is_list = True - if k == 'Formula1' and is_list: - if isinstance(v, (tuple, list)): - v = ';'.join(['"{}"'.format(i) for i in v]) - setattr(current, k, v) - self.obj.Validation = current - - def select(self): - self.doc._cc.select(self.obj) - return - - def search(self, options, find_all=True): - rangos = None - - descriptor = self.sheet.search_descriptor - descriptor.setSearchString(options['Search']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - - if find_all: - found = self.obj.findAll(descriptor) - else: - found = self.obj.findFirst(descriptor) - - if found: - if found.ImplementationName == OBJ_CELL: - rangos = LOCalcRange(found) - else: - rangos = [LOCalcRange(f) for f in found] - - return rangos - - def replace(self, options): - descriptor = self.sheet.replace_descriptor - descriptor.setSearchString(options['Search']) - descriptor.setReplaceString(options['Replace']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - count = self.obj.replaceAll(descriptor) - return count - - def in_range(self, rango): - if isinstance(rango, LOCalcRange): - address = rango.range_address - else: - address = rango.RangeAddress - result = self.cursor.queryIntersection(address) - return bool(result.Count) - - def move(self, target): - sheet = self.sheet.obj - sheet.moveRange(target.address, self.range_address) - return - - def insert(self, insert_mode=CIM.DOWN): - sheet = self.sheet.obj - sheet.insertCells(self.range_address, insert_mode) - return - - def delete(self, delete_mode=CDM.UP): - sheet = self.sheet.obj - sheet.removeRange(self.range_address, delete_mode) - return - - def copy_from(self, source): - self.sheet.obj.copyRange(self.address, source.range_address) - return - - def copy_to(self, target): - self.sheet.obj.copyRange(target.address, self.range_address) - return - - # ~ def copy_to(self, cell, formula=False): - # ~ rango = cell.to_size(self.rows, self.columns) - # ~ if formula: - # ~ rango.formula = self.formula - # ~ else: - # ~ rango.data = self.data - # ~ return - - # ~ def copy_from(self, rango, formula=False): - # ~ data = rango - # ~ if isinstance(rango, LOCalcRange): - # ~ if formula: - # ~ data = rango.formula - # ~ else: - # ~ data = rango.data - # ~ rows = len(data) - # ~ cols = len(data[0]) - # ~ if formula: - # ~ self.to_size(rows, cols).formula = data - # ~ else: - # ~ self.to_size(rows, cols).data = data - # ~ return - - def optimal_width(self): - self.obj.Columns.OptimalWidth = True - return - - def clean_render(self, template='\{(\w.+)\}'): - self._sd.SearchRegularExpression = True - self._sd.setSearchString(template) - self.obj.replaceAll(self._sd) - return - - def render(self, data, clean=True): - self._sd = self.sheet.obj.createSearchDescriptor() - self._sd.SearchCaseSensitive = False - for k, v in data.items(): - cell = self._render_value(k, v) - return cell - - def _render_value(self, key, value, parent=''): - cell = None - if isinstance(value, dict): - for k, v in value.items(): - # ~ print(1, 'RENDER', k, v) - cell = self._render_value(k, v, key) - return cell - elif isinstance(value, (list, tuple)): - self._render_list(key, value) - return - - search = f'{{{key}}}' - if parent: - search = f'{{{parent}.{key}}}' - ranges = self.find_all(search) - - if ranges is None: - return - - # ~ for cell in ranges or range(0): - for cell in ranges: - self._set_new_value(cell, search, value) - return LOCalcRange(cell) - - def _set_new_value(self, cell, search, value): - if not cell.ImplementationName == 'ScCellObj': - return - - if isinstance(value, str): - pattern = re.compile(search, re.IGNORECASE) - new_value = pattern.sub(value, cell.String) - cell.String = new_value - else: - LOCalcRange(cell).value = value - return - - def _render_list(self, key, rows): - for row in rows: - for k, v in row.items(): - self._render_value(k, v) - return - - def find(self, search_string): - if self._sd is None: - self._sd = self.sheet.obj.createSearchDescriptor() - self._sd.SearchCaseSensitive = False - - self._sd.setSearchString(search_string) - cell = self.obj.findFirst(self._sd) - if cell: - cell = LOCalcRange(cell) - return cell - - def find_all(self, search_string): - if self._sd is None: - self._sd = self.sheet.obj.createSearchDescriptor() - self._sd.SearchCaseSensitive = False - - self._sd.setSearchString(search_string) - ranges = self.obj.findAll(self._sd) - return ranges - - def filter(self, args, with_headers=True): - ff = TableFilterField() - ff.Field = args['Field'] - ff.Operator = args['Operator'] - if isinstance(args['Value'], str): - ff.IsNumeric = False - ff.StringValue = args['Value'] - else: - ff.IsNumeric = True - ff.NumericValue = args['Value'] - - fd = self.obj.createFilterDescriptor(True) - fd.ContainsHeader = with_headers - fd.FilterFields = ((ff,)) - # ~ self.obj.AutoFilter = True - self.obj.filter(fd) - return - - def copy_format_from(self, rango): - rango.select() - self.doc.copy() - self.select() - args = { - 'Flags': 'T', - 'MoveMode': 4, - } - url = '.uno:InsertContents' - call_dispatch(self.doc.frame, url, args) - return - - def to_image(self): - self.select() - self.doc.copy() - args = {'SelectedFormat': 141} - url = '.uno:ClipboardFormatItems' - call_dispatch(self.doc.frame, url, args) - return self.sheet.shapes[-1] - - def insert_image(self, path, options={}): - args = options.copy() - ps = self.possize - args['Width'] = args.get('Width', ps['Width']) - args['Height'] = args.get('Height', ps['Height']) - args['X'] = args.get('X', ps['X']) - args['Y'] = args.get('Y', ps['Y']) - # ~ img.ResizeWithCell = True - img = self.sheet.dp.insert_image(path, args) - img.anchor = self.obj - args.clear() - return img - - def insert_shape(self, tipo, args={}): - ps = self.possize - args['Width'] = args.get('Width', ps['Width']) - args['Height'] = args.get('Height', ps['Height']) - args['X'] = args.get('X', ps['X']) - args['Y'] = args.get('Y', ps['Y']) - - shape = self.sheet.dp.add(tipo, args) - shape.anchor = self.obj - args.clear() - return - - def filter_by_color(self, cell): - rangos = cell.column[1:,:].visible - for r in rangos: - for c in r: - if c.back_color != cell.back_color: - c.rows.visible = False - return - - def clear(self, what=1023): - # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html - self.obj.clearContents(what) - return - - def transpose(self): - # ~ 'Flags': 'A', - # ~ 'FormulaCommand': 0, - # ~ 'SkipEmptyCells': False, - # ~ 'AsLink': False, - # ~ 'MoveMode': 4, - self.select() - self.doc.copy() - self.clear(1023) - self[0,0].select() - self.doc.insert_contents({'Transpose': True}) - _CB.set('') - return - - def transpose_data(self, formula=False): - data = self.data - if formula: - data = self.formula - data = tuple(zip(*data)) - self.clear(1023) - self[0,0].copy_from(data, formula=formula) - return - - def merge_by_row(self): - for r in range(len(self.rows)): - self[r].merge = True - return - - def fill(self, source=1): - self.obj.fillAuto(0, source) - return - - def _cast(self, t, v): - if not t: - return v - - if t == datetime.date: - nv = datetime.date.fromordinal(int(v) + DATE_OFFSET) - else: - nv = t(v) - return nv - - def get_data(self, types): - values = [ - [self._cast(types[i], v) for i, v in enumerate(row)] - for row in self.data - ] - return values - - -class LOWriterStyles(object): - - def __init__(self, styles): - self._styles = styles - - @property - def names(self): - return {s.DisplayName: s.Name for s in self._styles} - - def __str__(self): - return '\n'.join(tuple(self.names.values())) - - -class LOWriterStylesFamilies(object): - - def __init__(self, styles): - self._styles = styles - - def __getitem__(self, index): - styles = { - 'Character': 'CharacterStyles', - 'Paragraph': 'ParagraphStyles', - 'Page': 'PageStyles', - 'Frame': 'FrameStyles', - 'Numbering': 'NumberingStyles', - 'Table': 'TableStyles', - 'Cell': 'CellStyles', - } - name = styles.get(index, index) - return LOWriterStyles(self._styles[name]) - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - obj = LOWriterStyles(self._styles[self._index]) - self._index += 1 - return obj - # ~ raise StopIteration - - @property - def names(self): - return self._styles.ElementNames - - def __str__(self): - return '\n'.join(self.names) - - -class LOWriterPageStyle(LOBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - def __str__(self): - return f'Page Style: {self.name}' - - @property - def name(self): - return self._obj.Name - - -class LOWriterPageStyles(object): - - def __init__(self, styles): - self._styles = styles - - def __getitem__(self, index): - return LOWriterPageStyle(self._styles[index]) - - @property - def names(self): - return self._styles.ElementNames - - def __str__(self): - return '\n'.join(self.names) - - -class LOWriterTextRange(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph' - self._is_table = self.obj.ImplementationName == 'SwXTextTable' - self._is_text = self.obj.ImplementationName == 'SwXTextPortion' - self._is_section = not self.obj.TextSection is None - self._parts = [] - if self._is_paragraph: - self._parts = [LOWriterTextRange(p, doc) for p in obj] - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - try: - obj = self._parts[self._index] - except IndexError: - raise StopIteration - - self._index += 1 - return obj - - @property - def string(self): - s = '' - if not self._is_table: - s = self.obj.String - return s - @string.setter - def string(self, value): - self.obj.String = value - - @property - def value(self): - return self.string - @value.setter - def value(self, value): - self.string = value - - @property - def style(self): - s = '' - if self.is_paragraph: - s = self.obj.ParaStyleName - elif self.is_text: - s = self.obj.CharStyleName - return s - @style.setter - def style(self, value): - if self.is_paragraph: - self.obj.ParaStyleName = value - elif self.is_text: - self.obj.CharStyleName = value - - @property - def is_paragraph(self): - return self._is_paragraph - - @property - def is_table(self): - return self._is_table - - @property - def is_text(self): - return self._is_text - - @property - def is_section(self): - return self._is_section - - @property - def text_cursor(self): - return self.text.createTextCursor() - - @property - def dp(self): - return self._doc.dp - - @property - def paragraph(self): - cursor = self.cursor - cursor.gotoStartOfParagraph(False) - cursor.gotoNextParagraph(True) - return LOWriterTextRange(cursor, self._doc) - - def goto_start(self): - if self.is_section: - rango = self.obj.TextSection.Anchor.Start - else: - rango = self.obj.Start - return LOWriterTextRange(rango, self._doc) - - def goto_end(self): - if self.is_section: - rango = self.obj.TextSection.Anchor.End - else: - rango = self.obj.End - return LOWriterTextRange(rango, self._doc) - - def goto_previous(self, expand=True): - cursor = self.cursor - cursor.gotoPreviousParagraph(expand) - return LOWriterTextRange(cursor, self._doc) - - def goto_next(self, expand=True): - cursor = self.cursor - cursor.gotoNextParagraph(expand) - return LOWriterTextRange(cursor, self._doc) - - def go_left(self, from_self=True, count=1, expand=False): - cursor = self.cursor - if not from_self: - cursor = self.text_cursor - cursor.gotoRange(self.obj, False) - cursor.goLeft(count, expand) - return LOWriterTextRange(cursor, self._doc) - - def go_right(self, from_self=True, count=1, expand=False): - cursor = self.cursor - if not from_self: - cursor = self.text_cursor - cursor.gotoRange(self.obj, False) - cursor.goRight(count, expand) - return LOWriterTextRange(cursor, self._doc) - - def delete(self): - self.value = '' - return - - def offset(self): - cursor = self.cursor.getEnd() - return LOWriterTextRange(cursor, self._doc) - - def insert_content(self, data, cursor=None, replace=False): - if cursor is None: - cursor = self.cursor - self.text.insertTextContent(cursor, data, replace) - return - - def insert_math(self, formula, - anchor_type=TextContentAnchorType.AS_CHARACTER, - cursor=None, replace=False): - - math = self._doc.create_instance(SERVICES['TEXT_EMBEDDED']) - math.CLSID = CLSID['FORMULA'] - math.AnchorType = anchor_type - self.insert_content(math, cursor, replace) - math.EmbeddedObject.Component.Formula = formula - return math - - def new_line(self, count=1): - cursor = self.cursor - for i in range(count): - self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) - return LOWriterTextRange(cursor, self._doc) - - def insert_table(self, data): - table = self._doc.create_instance(SERVICES['TEXT_TABLE']) - rows = len(data) - cols = len(data[0]) - table.initialize(rows, cols) - self.insert_content(table) - table.DataArray = data - name = table.Name - table = LOWriterTextTable(self._doc.tables[name], self._doc) - return table - - def insert_shape(self, tipo, args={}): - # ~ args['Width'] = args.get('Width', 1000) - # ~ args['Height'] = args.get('Height', 1000) - # ~ args['X'] = args.get('X', 0) - # ~ args['Y'] = args.get('Y', 0) - shape = self._doc.dp.add(tipo, args) - # ~ shape.anchor = self.obj - return shape - - def insert_image(self, path, args={}): - w = args.get('Width', 1000) - h = args.get('Height', 1000) - - image = self._doc.create_instance(SERVICES['GRAPHIC']) - image.GraphicURL = _P.to_url(path) - image.AnchorType = TextContentAnchorType.AS_CHARACTER - image.Width = w - image.Height = h - self.insert_content(image) - return self._doc.dp.last - - -class LOWriterTextRanges(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - self._paragraphs = [LOWriterTextRange(p, doc) for p in obj] - - def __len__(self): - return len(self._paragraphs) - - def __getitem__(self, index): - return self._paragraphs[index] - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - try: - obj = self._paragraphs[self._index] - except IndexError: - raise StopIteration - - self._index += 1 - return obj - - @property - def obj(self): - return self._obj - - -class LOWriterTextTable(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self._obj.Name - - @property - def data(self): - return self.obj.DataArray - @data.setter - def data(self, values): - self.obj.DataArray = values - - @property - def style(self): - return self.obj.TableTemplateName - @style.setter - def style(self, value): - self.obj.autoFormat(value) - - -class LOWriterTextTables(object): - - def __init__(self, doc): - self._doc = doc - self._obj = doc.obj.TextTables - - def __getitem__(self, key): - return LOWriterTextTable(self._obj[key], self._doc) - - def __len__(self): - return self._obj.Count - - def insert(self, data, text_range=None): - if text_range is None: - text_range = self._doc.selection - text_range.insert_table(data) - return - - -class LOWriter(LODocument): - - def __init__(self, obj): - super().__init__(obj) - self._type = WRITER - self._settings = self._cc.ViewSettings - - @property - def text(self): - return self.paragraphs - - @property - def paragraphs(self): - return LOWriterTextRanges(self.obj.Text, self) - - @property - def tables(self): - return LOWriterTextTables(self) - - @property - def selection(self): - sel = self.obj.CurrentSelection - if sel.ImplementationName == OBJ_TEXTS: - if len(sel) == 1: - sel = LOWriterTextRanges(sel, self)[0] - else: - sel = LOWriterTextRanges(sel, self) - return sel - - if sel.ImplementationName == OBJ_SHAPES: - if len(sel) == 1: - sel = sel[0] - sel = LODrawPage(sel.Parent)[sel.Name] - return sel - - if sel.ImplementationName == OBJ_GRAPHIC: - sel = self.dp[sel.Name] - else: - debug(sel.ImplementationName) - - return sel - - @property - def dp(self): - return self.draw_page - @property - def shapes(self): - return self.draw_page - @property - def draw_page(self): - return LODrawPage(self.obj.DrawPage) - - @property - def cursor(self): - return self.obj.Text.createTextCursor() - - @property - def view_cursor(self): - return self._cc.ViewCursor - - @property - def page_styles(self): - ps = self.obj.StyleFamilies['PageStyles'] - return LOWriterPageStyles(ps) - - @property - def styles(self): - return LOWriterStylesFamilies(self.obj.StyleFamilies) - - @property - def search_descriptor(self): - return self.obj.createSearchDescriptor() - - @property - def replace_descriptor(self): - return self.obj.createReplaceDescriptor() - - @property - def view_web(self): - return self._settings.ShowOnlineLayout - @view_web.setter - def view_web(self, value): - self._settings.ShowOnlineLayout = value - - def goto_start(self): - self.view_cursor.gotoStart(False) - return self.selection - - def goto_end(self): - self.view_cursor.gotoEnd(False) - return self.selection - - def search(self, options, find_all=True): - descriptor = self.search_descriptor - descriptor.setSearchString(options.get('Search', '')) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if 'Attributes' in options: - attr = dict_to_property(options['Attributes']) - descriptor.setSearchAttributes(attr) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - - result = False - if find_all: - found = self.obj.findAll(descriptor) - if len(found): - result = [LOWriterTextRange(f, self) for f in found] - else: - found = self.obj.findFirst(descriptor) - if found: - result = LOWriterTextRange(found, self) - - return result - - def replace(self, options): - descriptor = self.replace_descriptor - descriptor.setSearchString(options['Search']) - descriptor.setReplaceString(options['Replace']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if 'Attributes' in options: - attr = dict_to_property(options['Attributes']) - descriptor.setSearchAttributes(attr) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - found = self.obj.replaceAll(descriptor) - return found - - def select(self, text): - if hasattr(text, 'obj'): - text = text.obj - self._cc.select(text) - return - - -class LOShape(LOBaseObject): - - @property - def cell(self): - return self.anchor - - @property - def anchor(self): - obj = self.obj.Anchor - if obj.ImplementationName == OBJ_CELL: - obj = LOCalcRange(obj) - elif obj.ImplementationName == OBJ_TEXT: - obj = LOWriterTextRange(obj, LODocs().active) - else: - debug('Anchor', obj.ImplementationName) - return obj - @anchor.setter - def anchor(self, value): - if hasattr(value, 'obj'): - value = value.obj - try: - self.obj.Anchor = value - except Exception as e: - self.obj.AnchorType = value - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.Visible = value - - @property - def path(self): - return self.url - @property - def url(self): - url = '' - if self.is_image: - url = _P.to_system(self.obj.GraphicURL.OriginURL) - return url - - @property - def mimetype(self): - mt = '' - if self.is_image: - mt = self.obj.GraphicURL.MimeType - return mt - - @property - def linked(self): - l = False - if self.is_image: - l = self.obj.GraphicURL.Linked - return l - - def delete(self): - self.remove() - return - def remove(self): - self.obj.Parent.remove(self.obj) - return - - def save(self, path: str, mimetype=DEFAULT_MIME_TYPE): - if _P.is_dir(path): - name = self.name - ext = mimetype.lower() - else: - p = _P(path) - path = p.path - name = p.name - ext = p.ext.lower() - - path = _P.join(path, f'{name}.{ext}') - args = dict( - URL = _P.to_url(path), - MimeType = MIME_TYPE[ext], - ) - if not _export_image(self.obj, args): - path = '' - return path - - # ~ def save2(self, path: str): - # ~ size = len(self.obj.Bitmap.DIB) - # ~ data = self.obj.GraphicStream.readBytes((), size) - # ~ data = data[-1].value - # ~ path = _P.join(path, f'{self.name}.png') - # ~ _P.save_bin(path, b'') - # ~ return - - -class LODrawPage(LOBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - def __getitem__(self, index): - if isinstance(index, int): - shape = LOShape(self.obj[index], index) - else: - for i, o in enumerate(self.obj): - shape = self.obj[i] - name = shape.Name or f'shape{i}' - if name == index: - shape = LOShape(shape, i) - break - return shape - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - if self._index == self.count: - raise StopIteration - shape = self[self._index] - self._index += 1 - return shape - - - @property - def name(self): - return self.obj.Name - - @property - def doc(self): - return self.obj.Forms.Parent - - @property - def width(self): - return self.obj.Width - - @property - def height(self): - return self.obj.Height - - @property - def count(self): - return self.obj.Count - - @property - def last(self): - return self[self.count - 1] - - def create_instance(self, name): - return self.doc.createInstance(name) - - def add(self, type_shape, options={}): - args = options.copy() - """Insert a shape in page, type shapes: - Line - Rectangle - Ellipse - Text - Connector - """ - index = self.count - default_height = 3000 - if type_shape == 'Line': - default_height = 0 - w = args.pop('Width', 3000) - h = args.pop('Height', default_height) - x = args.pop('X', 1000) - y = args.pop('Y', 1000) - name = args.pop('Name', f'{type_shape.lower()}{index}') - - service = f'com.sun.star.drawing.{type_shape}Shape' - shape = self.create_instance(service) - shape.Size = Size(w, h) - shape.Position = Point(x, y) - shape.Name = name - self.obj.add(shape) - - if args: - _set_properties(shape, args) - - return LOShape(self.obj[index], index) - - def remove(self, shape): - if hasattr(shape, 'obj'): - shape = shape.obj - return self.obj.remove(shape) - - def remove_all(self): - while self.count: - self.obj.remove(self.obj[0]) - return - - def insert_image(self, path, options={}): - args = options.copy() - index = self.count - w = args.get('Width', 3000) - h = args.get('Height', 3000) - x = args.get('X', 1000) - y = args.get('Y', 1000) - name = args.get('Name', f'image{index}') - - image = self.create_instance('com.sun.star.drawing.GraphicObjectShape') - if isinstance(path, str): - image.GraphicURL = _P.to_url(path) - else: - gp = create_instance('com.sun.star.graphic.GraphicProvider') - properties = dict_to_property({'InputStream': path}) - image.Graphic = gp.queryGraphic(properties) - - self.obj.add(image) - image.Size = Size(w, h) - image.Position = Point(x, y) - image.Name = name - return LOShape(self.obj[index], index) - - -class LODrawImpress(LODocument): - - def __init__(self, obj): - super().__init__(obj) - - def __getitem__(self, index): - if isinstance(index, int): - page = self.obj.DrawPages[index] - else: - page = self.obj.DrawPages.getByName(index) - return LODrawPage(page) - - @property - def selection(self): - sel = self.obj.CurrentSelection[0] - # ~ return _get_class_uno(sel) - return sel - - @property - def current_page(self): - return LODrawPage(self._cc.getCurrentPage()) - - def paste(self): - call_dispatch(self.frame, '.uno:Paste') - return self.current_page[-1] - - def add(self, type_shape, args={}): - return self.current_page.add(type_shape, args) - - def insert_image(self, path, args={}): - self.current_page.insert_image(path, args) - return - - # ~ def export(self, path, mimetype='png'): - # ~ args = dict( - # ~ URL = _P.to_url(path), - # ~ MimeType = MIME_TYPE[mimetype], - # ~ ) - # ~ result = _export_image(self.obj, args) - # ~ return result - - -class BaseRow: - pass - - -class BaseQuery(object): - PY_TYPES = { - 'VARCHAR': 'getString', - 'INTEGER': 'getLong', - 'DATE': 'getDate', - # ~ 'SQL_LONG': 'getLong', - # ~ 'SQL_VARYING': 'getString', - # ~ 'SQL_FLOAT': 'getFloat', - # ~ 'SQL_BOOLEAN': 'getBoolean', - # ~ 'SQL_TYPE_DATE': 'getDate', - # ~ 'SQL_TYPE_TIME': 'getTime', - # ~ 'SQL_TIMESTAMP': 'getTimestamp', - } - # ~ TYPES_DATE = ('SQL_TYPE_DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP') - TYPES_DATE = ('DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP') - - def __init__(self, query): - self._query = query - self._meta = query.MetaData - self._cols = self._meta.ColumnCount - self._names = query.Columns.ElementNames - self._data = self._get_data() - - def __getitem__(self, index): - return self._data[index] - - def __iter__(self): - self._index = 0 - return self - - def __next__(self): - try: - row = self._data[self._index] - except IndexError: - raise StopIteration - self._index += 1 - return row - - def _to_python(self, index): - type_field = self._meta.getColumnTypeName(index) - # ~ print('TF', type_field) - value = getattr(self._query, self.PY_TYPES[type_field])(index) - if type_field in self.TYPES_DATE: - value = _struct_to_date(value) - return value - - def _get_row(self): - row = BaseRow() - for i in range(1, self._cols + 1): - column_name = self._meta.getColumnName(i) - value = self._to_python(i) - setattr(row, column_name, value) - return row - - def _get_data(self): - data = [] - while self._query.next(): - row = self._get_row() - data.append(row) - return data - - @property - def tuples(self): - data = [tuple(r.__dict__.values()) for r in self._data] - return tuple(data) - - @property - def dicts(self): - data = [r.__dict__ for r in self._data] - return tuple(data) - - -def _add_listeners(events, control, name=''): - listeners = { - 'addActionListener': EventsButton, - 'addMouseListener': EventsMouse, - 'addFocusListener': EventsFocus, - 'addItemListener': EventsItem, - 'addKeyListener': EventsKey, - 'addTabListener': EventsTab, - 'addSpinListener': EventsSpin, - } - if hasattr(control, 'obj'): - control = control.obj - # ~ debug(control.ImplementationName) - is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' - is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' - is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' - is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl' - - for key, value in listeners.items(): - if hasattr(control, key): - if is_grid and key == 'addMouseListener': - control.addMouseListener(EventsMouseGrid(events, name)) - continue - if is_link and key == 'addMouseListener': - control.addMouseListener(EventsMouseLink(events, name)) - continue - if is_roadmap and key == 'addItemListener': - control.addItemListener(EventsItemRoadmap(events, name)) - continue - - getattr(control, key)(listeners[key](events, name)) - - if is_grid: - controllers = EventsGrid(events, name) - control.addSelectionListener(controllers) - control.Model.GridDataModel.addGridDataListener(controllers) - return - - -class EventsSpin(EventsListenerBase, XSpinListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def up(self, event): - event_name = f'{self.name}_up' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def down(self, event): - event_name = f'{self.name}_up' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def first(self, event): - event_name = f'{self.name}_first' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def last(self, event): - event_name = f'{self.name}_last' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def mousePressed(self, event): - event_name = '{}_click'.format(self._name) - if event.ClickCount == 2: - event_name = '{}_double_click'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def mouseReleased(self, event): - event_name = '{}_after_click'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def mouseEntered(self, event): - pass - - def mouseExited(self, event): - pass - - # ~ XMouseMotionListener - def mouseMoved(self, event): - pass - - def mouseDragged(self, event): - pass - - -class EventsMouseLink(EventsMouse): - - def __init__(self, controller, name): - super().__init__(controller, name) - self._text_color = 0 - - def mouseEntered(self, event): - model = event.Source.Model - self._text_color = model.TextColor or 0 - model.TextColor = get_color('blue') - return - - def mouseExited(self, event): - model = event.Source.Model - model.TextColor = self._text_color - return - - -class EventsButton(EventsListenerBase, XActionListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def actionPerformed(self, event): - event_name = f'{self.name}_action' - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsFocus(EventsListenerBase, XFocusListener): - CONTROLS = ( - 'stardiv.Toolkit.UnoControlEditModel', - ) - - def __init__(self, controller, name): - super().__init__(controller, name) - - def focusGained(self, event): - service = event.Source.Model.ImplementationName - # ~ print('Focus enter', service) - if service in self.CONTROLS: - obj = event.Source.Model - obj.BackgroundColor = COLOR_ON_FOCUS - return - - def focusLost(self, event): - service = event.Source.Model.ImplementationName - if service in self.CONTROLS: - obj = event.Source.Model - obj.BackgroundColor = -1 - return - - -class EventsKey(EventsListenerBase, XKeyListener): - """ - event.KeyChar - event.KeyCode - event.KeyFunc - event.Modifiers - """ - - def __init__(self, controller, name): - super().__init__(controller, name) - - def keyPressed(self, event): - pass - - def keyReleased(self, event): - event_name = '{}_key_released'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - # ~ else: - # ~ if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): - # ~ self._cls.close() - return - - -class EventsItem(EventsListenerBase, XItemListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def disposing(self, event): - pass - - def itemStateChanged(self, event): - event_name = '{}_item_changed'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsItemRoadmap(EventsItem): - - def itemStateChanged(self, event): - dialog = event.Source.Context.Model - dialog.Step = event.ItemId + 1 - return - - -class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def dataChanged(self, event): - event_name = '{}_data_changed'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def rowHeadingChanged(self, event): - pass - - def rowsInserted(self, event): - pass - - def rowsRemoved(self, evemt): - pass - - def selectionChanged(self, event): - event_name = '{}_selection_changed'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsMouseGrid(EventsMouse): - selected = False - - def mousePressed(self, event): - super().mousePressed(event) - # ~ obj = event.Source - # ~ col = obj.getColumnAtPoint(event.X, event.Y) - # ~ row = obj.getRowAtPoint(event.X, event.Y) - # ~ print(col, row) - # ~ if col == -1 and row == -1: - # ~ if self.selected: - # ~ obj.deselectAllRows() - # ~ else: - # ~ obj.selectAllRows() - # ~ self.selected = not self.selected - return - - def mouseReleased(self, event): - # ~ obj = event.Source - # ~ col = obj.getColumnAtPoint(event.X, event.Y) - # ~ row = obj.getRowAtPoint(event.X, event.Y) - # ~ if row == -1 and col > -1: - # ~ gdm = obj.Model.GridDataModel - # ~ for i in range(gdm.RowCount): - # ~ gdm.updateRowHeading(i, i + 1) - return - - -class EventsTab(EventsListenerBase, XTabListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def activated(self, id): - event_name = '{}_activated'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(id) - return - - -class EventsMenu(EventsListenerBase, XMenuListener): - - def __init__(self, controller): - super().__init__(controller, '') - - def itemHighlighted(self, event): - pass - - def itemSelected(self, event): - name = event.Source.getCommand(event.MenuId) - if name.startswith('menu'): - event_name = '{}_selected'.format(name) - else: - event_name = 'menu_{}_selected'.format(name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def itemActivated(self, event): - return - - def itemDeactivated(self, event): - return - - -class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): - - def __init__(self, cls): - self._cls = cls - super().__init__(cls.events, cls.name, cls._window) - - def windowOpened(self, event): - event_name = '{}_opened'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def windowActivated(self, event): - control_name = '{}_activated'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - def windowDeactivated(self, event): - control_name = '{}_deactivated'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - def windowMinimized(self, event): - pass - - def windowNormalized(self, event): - pass - - def windowClosing(self, event): - if self._window: - control_name = 'window_closing' - else: - control_name = '{}_closing'.format(event.Source.Model.Name) - - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - # ~ else: - # ~ if not self._modal and not self._block: - # ~ event.Source.Visible = False - return - - def windowClosed(self, event): - control_name = '{}_closed'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - # ~ XWindowListener - def windowResized(self, event): - sb = self._cls._subcont - sb.setPosSize(0, 0, event.Width, event.Height, SIZE) - event_name = '{}_resized'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def windowMoved(self, event): - pass - - def windowShown(self, event): - pass - - def windowHidden(self, event): - pass - - -# ~ BorderColor = ? -# ~ FontStyleName = ? -# ~ HelpURL = ? -class UnoBaseObject(object): - - def __init__(self, obj, path=''): - self._obj = obj - self._model = obj.Model - - def __setattr__(self, name, value): - exists = hasattr(self, name) - if not exists and not name in ('_obj', '_model'): - setattr(self._model, name, value) - else: - super().__setattr__(name, value) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def model(self): - return self._model - @property - def m(self): - return self._model - - @property - def properties(self): - return {} - @properties.setter - def properties(self, values): - _set_properties(self.model, values) - - @property - def name(self): - return self.model.Name - - @property - def parent(self): - return self.obj.Context - - @property - def tag(self): - return self.model.Tag - @tag.setter - def tag(self, value): - self.model.Tag = value - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.setVisible(value) - - @property - def enabled(self): - return self.model.Enabled - @enabled.setter - def enabled(self, value): - self.model.Enabled = value - - @property - def step(self): - return self.model.Step - @step.setter - def step(self, value): - self.model.Step = value - - @property - def align(self): - return self.model.Align - @align.setter - def align(self, value): - self.model.Align = value - - @property - def valign(self): - return self.model.VerticalAlign - @valign.setter - def valign(self, value): - self.model.VerticalAlign = value - - @property - def font_weight(self): - return self.model.FontWeight - @font_weight.setter - def font_weight(self, value): - self.model.FontWeight = value - - @property - def font_height(self): - return self.model.FontHeight - @font_height.setter - def font_height(self, value): - self.model.FontHeight = value - - @property - def font_name(self): - return self.model.FontName - @font_name.setter - def font_name(self, value): - self.model.FontName = value - - @property - def font_underline(self): - return self.model.FontUnderline - @font_underline.setter - def font_underline(self, value): - self.model.FontUnderline = value - - @property - def text_color(self): - return self.model.TextColor - @text_color.setter - def text_color(self, value): - self.model.TextColor = value - - @property - def back_color(self): - return self.model.BackgroundColor - @back_color.setter - def back_color(self, value): - self.model.BackgroundColor = value - - @property - def multi_line(self): - return self.model.MultiLine - @multi_line.setter - def multi_line(self, value): - self.model.MultiLine = value - - @property - def help_text(self): - return self.model.HelpText - @help_text.setter - def help_text(self, value): - self.model.HelpText = value - - @property - def border(self): - return self.model.Border - @border.setter - def border(self, value): - # ~ Bug for report - self.model.Border = value - - @property - def width(self): - return self._model.Width - @width.setter - def width(self, value): - self.model.Width = value - - @property - def height(self): - return self.model.Height - @height.setter - def height(self, value): - self.model.Height = value - - def _get_possize(self, name): - ps = self.obj.getPosSize() - return getattr(ps, name) - - def _set_possize(self, name, value): - ps = self.obj.getPosSize() - setattr(ps, name, value) - self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) - return - - @property - def x(self): - if hasattr(self.model, 'PositionX'): - return self.model.PositionX - return self._get_possize('X') - @x.setter - def x(self, value): - if hasattr(self.model, 'PositionX'): - self.model.PositionX = value - else: - self._set_possize('X', value) - - @property - def y(self): - if hasattr(self.model, 'PositionY'): - return self.model.PositionY - return self._get_possize('Y') - @y.setter - def y(self, value): - if hasattr(self.model, 'PositionY'): - self.model.PositionY = value - else: - self._set_possize('Y', value) - - @property - def tab_index(self): - return self._model.TabIndex - @tab_index.setter - def tab_index(self, value): - self.model.TabIndex = value - - @property - def tab_stop(self): - return self._model.Tabstop - @tab_stop.setter - def tab_stop(self, value): - self.model.Tabstop = value - - @property - def ps(self): - ps = self.obj.getPosSize() - return ps - @ps.setter - def ps(self, ps): - self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) - - def set_focus(self): - self.obj.setFocus() - return - - def ps_from(self, source): - self.ps = source.ps - return - - def center(self, horizontal=True, vertical=False): - p = self.parent.Model - w = p.Width - h = p.Height - if horizontal: - x = w / 2 - self.width / 2 - self.x = x - if vertical: - y = h / 2 - self.height / 2 - self.y = y - return - - def move(self, origin, x=0, y=5, center=False): - if x: - self.x = origin.x + origin.width + x - else: - self.x = origin.x - if y: - h = origin.height - if y < 0: - h = 0 - self.y = origin.y + h + y - else: - self.y = origin.y - - if center: - self.center() - return - - -class UnoLabel(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'label' - - @property - def value(self): - return self.model.Label - @value.setter - def value(self, value): - self.model.Label = value - - -class UnoLabelLink(UnoLabel): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'link' - - -class UnoButton(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'button' - - @property - def value(self): - return self.model.Label - @value.setter - def value(self, value): - self.model.Label = value - - @property - def image(self): - return self.model.ImageURL - @image.setter - def image(self, value): - self.model.ImageURL = _P.to_url(value) - - -class UnoRadio(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'radio' - - @property - def value(self): - return self.model.Label - @value.setter - def value(self, value): - self.model.Label = value - - -class UnoCheckBox(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'checkbox' - - @property - def value(self): - return self.model.State - @value.setter - def value(self, value): - self.model.State = value - - @property - def label(self): - return self.model.Label - @label.setter - def label(self, value): - self.model.Label = value - - @property - def tri_state(self): - return self.model.TriState - @tri_state.setter - def tri_state(self, value): - self.model.TriState = value - - -# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html -class UnoText(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'text' - - @property - def value(self): - return self.model.Text - @value.setter - def value(self, value): - self.model.Text = value - - @property - def echochar(self): - return chr(self.model.EchoChar) - @echochar.setter - def echochar(self, value): - if value: - self.model.EchoChar = ord(value[0]) - else: - self.model.EchoChar = 0 - - def validate(self): - return - - -class UnoImage(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'image' - - @property - def value(self): - return self.url - @value.setter - def value(self, value): - self.url = value - - @property - def url(self): - return self.m.ImageURL - @url.setter - def url(self, value): - self.m.ImageURL = None - self.m.ImageURL = _P.to_url(value) - - -class UnoListBox(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._path = '' - - def __setattr__(self, name, value): - if name in ('_path',): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - @property - def type(self): - return 'listbox' - - @property - def value(self): - return self.obj.getSelectedItem() - - @property - def count(self): - return len(self.data) - - @property - def data(self): - return self.model.StringItemList - @data.setter - def data(self, values): - self.model.StringItemList = list(sorted(values)) - - @property - def path(self): - return self._path - @path.setter - def path(self, value): - self._path = value - - def unselect(self): - self.obj.selectItem(self.value, False) - return - - def select(self, pos=0): - if isinstance(pos, str): - self.obj.selectItem(pos, True) - else: - self.obj.selectItemPos(pos, True) - return - - def clear(self): - self.model.removeAllItems() - return - - def _set_image_url(self, image): - if _P.exists(image): - return _P.to_url(image) - - path = _P.join(self._path, DIR['images'], image) - return _P.to_url(path) - - def insert(self, value, path='', pos=-1, show=True): - if pos < 0: - pos = self.count - if path: - self.model.insertItem(pos, value, self._set_image_url(path)) - else: - self.model.insertItemText(pos, value) - if show: - self.select(pos) - return - - -class UnoRoadmap(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._options = () - - def __setattr__(self, name, value): - if name in ('_options',): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - @property - def options(self): - return self._options - @options.setter - def options(self, values): - self._options = values - for i, v in enumerate(values): - opt = self.model.createInstance() - opt.ID = i - opt.Label = v - self.model.insertByIndex(i, opt) - return - - @property - def enabled(self): - return True - @enabled.setter - def enabled(self, value): - for m in self.model: - m.Enabled = value - return - - def set_enabled(self, index, value): - self.model.getByIndex(index).Enabled = value - return - - -class UnoTree(UnoBaseObject): - - def __init__(self, obj, ): - super().__init__(obj) - self._tdm = None - self._data = [] - - def __setattr__(self, name, value): - if name in ('_tdm', '_data'): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - @property - def selection(self): - sel = self.obj.Selection - return sel.DataValue, sel.DisplayValue - - @property - def parent(self): - parent = self.obj.Selection.Parent - if parent is None: - return () - return parent.DataValue, parent.DisplayValue - - def _get_parents(self, node): - value = (node.DisplayValue,) - parent = node.Parent - if parent is None: - return value - return self._get_parents(parent) + value - - @property - def parents(self): - values = self._get_parents(self.obj.Selection) - return values - - @property - def root(self): - if self._tdm is None: - return '' - return self._tdm.Root.DisplayValue - @root.setter - def root(self, value): - self._add_data_model(value) - - def _add_data_model(self, name): - tdm = create_instance('com.sun.star.awt.tree.MutableTreeDataModel') - root = tdm.createNode(name, True) - root.DataValue = 0 - tdm.setRoot(root) - self.model.DataModel = tdm - self._tdm = self.model.DataModel - return - - @property - def path(self): - return self.root - @path.setter - def path(self, value): - self.data = _P.walk_dir(value, True) - - @property - def data(self): - return self._data - @data.setter - def data(self, values): - self._data = list(values) - self._add_data() - - def _add_data(self): - if not self.data: - return - - parents = {} - for node in self.data: - parent = parents.get(node[1], self._tdm.Root) - child = self._tdm.createNode(node[2], False) - child.DataValue = node[0] - parent.appendChild(child) - parents[node[0]] = child - self.obj.expandNode(self._tdm.Root) - return - - -# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html -class UnoGrid(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._gdm = self.model.GridDataModel - self._data = [] - self._formats = () - - def __setattr__(self, name, value): - if name in ('_gdm', '_data', '_formats'): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - def __getitem__(self, key): - value = self._gdm.getCellData(key[0], key[1]) - return value - - def __setitem__(self, key, value): - self._gdm.updateCellData(key[0], key[1], value) - return - - @property - def type(self): - return 'grid' - - @property - def columns(self): - return {} - @columns.setter - def columns(self, values): - # ~ self._columns = values - #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html - model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) - for properties in values: - column = create_instance('com.sun.star.awt.grid.GridColumn', True) - for k, v in properties.items(): - setattr(column, k, v) - model.addColumn(column) - self.model.ColumnModel = model - return - - @property - def data(self): - return self._data - @data.setter - def data(self, values): - self._data = values - self.clear() - headings = tuple(range(1, len(values) + 1)) - self._gdm.addRows(headings, values) - # ~ rows = range(grid_dm.RowCount) - # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows] - # ~ grid.Model.RowBackgroundColors = tuple(colors) - return - - @property - def value(self): - if self.column == -1 or self.row == -1: - return '' - return self[self.column, self.row] - @value.setter - def value(self, value): - if self.column > -1 and self.row > -1: - self[self.column, self.row] = value - - @property - def row(self): - return self.obj.CurrentRow - - @property - def row_count(self): - return self._gdm.RowCount - - @property - def column(self): - return self.obj.CurrentColumn - - @property - def is_valid(self): - return not (self.row == -1 or self.column == -1) - - @property - def selected_rows(self): - value = self.obj.SelectedRows - return value - - @property - def row_background_colors(self): - value = self.m.RowBackgroundColors - return value - @row_background_colors.setter - def row_background_colors(self, colors): - c = Color() - self.m.RowBackgroundColors = (c(colors[0]), c(colors[1])) - - @property - def formats(self): - return self._formats - @formats.setter - def formats(self, values): - self._formats = values - - def clear(self): - self._gdm.removeAllRows() - return - - def _format_columns(self, data): - row = data - if self.formats: - for i, f in enumerate(formats): - if f: - row[i] = f.format(data[i]) - return row - - def add_row(self, data): - self._data.append(data) - row = self._format_columns(data) - self._gdm.addRow(self.row_count + 1, row) - return - - def set_cell_tooltip(self, col, row, value): - self._gdm.updateCellToolTip(col, row, value) - return - - def get_cell_tooltip(self, col, row): - value = self._gdm.getCellToolTip(col, row) - return value - - def sort(self, column, asc=True): - self._gdm.sortByColumn(column, asc) - self.update_row_heading() - return - - def update_row_heading(self): - for i in range(self.row_count): - self._gdm.updateRowHeading(i, i + 1) - return - - def remove_row(self, row): - self._gdm.removeRow(row) - del self._data[row] - self.update_row_heading() - return - - -class UnoPage(object): - - def __init__(self, obj): - self._obj = obj - self._events = None - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass - - @property - def obj(self): - return self._obj - - @property - def model(self): - return self._obj.Model - - # ~ @property - # ~ def id(self): - # ~ return self.m.TabPageID - - @property - def parent(self): - return self.obj.Context - - def _set_image_url(self, image): - if _P.exists(image): - return _P.to_url(image) - - path = _P.join(self._path, DIR['images'], image) - return _P.to_url(path) - - def _special_properties(self, tipo, args): - if tipo == 'link' and not 'Label' in args: - args['Label'] = args['URL'] - return args - - if tipo == 'button': - if 'ImageURL' in args: - args['ImageURL'] = self._set_image_url(args['ImageURL']) - args['FocusOnClick'] = args.get('FocusOnClick', False) - return args - - if tipo == 'roadmap': - args['Height'] = args.get('Height', self.height) - if 'Title' in args: - args['Text'] = args.pop('Title') - return args - - if tipo == 'tree': - args['SelectionType'] = args.get('SelectionType', SINGLE) - return args - - if tipo == 'grid': - args['ShowRowHeader'] = args.get('ShowRowHeader', True) - return args - - if tipo == 'pages': - args['Width'] = args.get('Width', self.width) - args['Height'] = args.get('Height', self.height) - - return args - - def add_control(self, args): - tipo = args.pop('Type').lower() - root = args.pop('Root', '') - sheets = args.pop('Sheets', ()) - columns = args.pop('Columns', ()) - - args = self._special_properties(tipo, args) - model = self.model.createInstance(UNO_MODELS[tipo]) - _set_properties(model, args) - name = args['Name'] - self.model.insertByName(name, model) - control = self.obj.getControl(name) - _add_listeners(self._events, control, name) - control = UNO_CLASSES[tipo](control) - - if tipo in ('listbox',): - control.path = self.path - - if tipo == 'tree' and root: - control.root = root - elif tipo == 'grid' and columns: - control.columns = columns - elif tipo == 'pages' and sheets: - control.sheets = sheets - control.events = self.events - - setattr(self, name, control) - return control - - -class UnoPages(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._sheets = [] - self._events = None - - def __setattr__(self, name, value): - if name in ('_sheets', '_events'): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - - def __getitem__(self, index): - name = index - if isinstance(index, int): - name = f'sheet{index}' - sheet = self.obj.getControl(name) - page = UnoPage(sheet) - page._events = self._events - return page - - @property - def type(self): - return 'pages' - - @property - def current(self): - return self.obj.ActiveTabID - @property - def active(self): - return self.current - - @property - def sheets(self): - return self._sheets - @sheets.setter - def sheets(self, values): - self._sheets = values - for i, title in enumerate(values): - sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') - sheet.Title = title - self.m.insertByName(f'sheet{i + 1}', sheet) - return - - @property - def events(self): - return self._events - @events.setter - def events(self, controllers): - self._events = controllers - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.Visible = value - - def insert(self, title): - self._sheets.append(title) - id = len(self._sheets) - sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') - sheet.Title = title - self.m.insertByName(f'sheet{id}', sheet) - return self[id] - - def remove(self, id): - self.obj.removeTab(id) - return - - def activate(self, id): - self.obj.activateTab(id) - return - - -class UnoSpinButton(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'spinbutton' - - @property - def value(self): - return self.model.Label - @value.setter - def value(self, value): - self.model.Label = value - - -class UnoNumericField(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - - @property - def type(self): - return 'numeric' - - @property - def int(self): - return int(self.value) - - @property - def value(self): - return self.model.Value - @value.setter - def value(self, value): - self.model.Value = value - - -UNO_CLASSES = { - 'label': UnoLabel, - 'link': UnoLabelLink, - 'button': UnoButton, - 'radio': UnoRadio, - 'checkbox': UnoCheckBox, - 'text': UnoText, - 'image': UnoImage, - 'listbox': UnoListBox, - 'roadmap': UnoRoadmap, - 'tree': UnoTree, - 'grid': UnoGrid, - 'pages': UnoPages, - 'spinbutton': UnoSpinButton, - 'numeric': UnoNumericField, -} - -UNO_MODELS = { - 'label': 'com.sun.star.awt.UnoControlFixedTextModel', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', - 'button': 'com.sun.star.awt.UnoControlButtonModel', - 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', - 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', - 'text': 'com.sun.star.awt.UnoControlEditModel', - 'image': 'com.sun.star.awt.UnoControlImageControlModel', - 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', - 'tree': 'com.sun.star.awt.tree.TreeControlModel', - 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', - 'pages': 'com.sun.star.awt.UnoMultiPageModel', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', - 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', - 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', -} -# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', -# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', -# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', -# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', -# ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', -# ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', -# ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', -# ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', -# ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', -# ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', - - -class LODialog(object): - SEPARATION = 5 - MODELS = { - 'label': 'com.sun.star.awt.UnoControlFixedTextModel', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', - 'button': 'com.sun.star.awt.UnoControlButtonModel', - 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', - 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', - 'text': 'com.sun.star.awt.UnoControlEditModel', - 'image': 'com.sun.star.awt.UnoControlImageControlModel', - 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', - 'tree': 'com.sun.star.awt.tree.TreeControlModel', - 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', - 'pages': 'com.sun.star.awt.UnoMultiPageModel', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', - 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', - 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', - } - - def __init__(self, args={}): - self._obj = self._create(args) - self._model = self.obj.Model - self._events = None - self._modal = True - self._controls = {} - self._color_on_focus = COLOR_ON_FOCUS - self._id = '' - self._path = '' - self._init_controls() - - def _create(self, args): - service = 'com.sun.star.awt.DialogProvider' - path = args.pop('Path', '') - if path: - dp = create_instance(service, True) - dlg = dp.createDialog(_P.to_url(path)) - return dlg - - if 'Location' in args: - name = args['Name'] - library = args.get('Library', 'Standard') - location = args.get('Location', 'application').lower() - if location == 'user': - location = 'application' - url = f'vnd.sun.star.script:{library}.{name}?location={location}' - if location == 'document': - dp = create_instance(service, args=docs.active.obj) - else: - dp = create_instance(service, True) - # ~ uid = docs.active.uid - # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' - dlg = dp.createDialog(url) - return dlg - - dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) - model = create_instance('com.sun.star.awt.UnoControlDialogModel', True) - toolkit = create_instance('com.sun.star.awt.Toolkit', True) - - args['Title'] = args.get('Title', TITLE) - args['Width'] = args.get('Width', 200) - args['Height'] = args.get('Height', 150) - - _set_properties(model, args) - dlg.setModel(model) - dlg.setVisible(False) - dlg.createPeer(toolkit, None) - return dlg - - def _get_type_control(self, name): - name = name.split('.')[2] - types = { - 'UnoFixedTextControl': 'label', - 'UnoEditControl': 'text', - 'UnoButtonControl': 'button', - } - return types[name] - - def _init_controls(self): - for control in self.obj.getControls(): - tipo = self._get_type_control(control.ImplementationName) - name = control.Model.Name - control = UNO_CLASSES[tipo](control) - setattr(self, name, control) - return - - @property - def obj(self): - return self._obj - - @property - def model(self): - return self._model - - @property - def controls(self): - return self._controls - - @property - def path(self): - return self._path - @property - def path_images(self): - return _P.join(self.path, DIR['images']) - @property - def id(self): - return self._id - @id.setter - def id(self, value): - self._id = value - self._path = _P.from_id(value) - - @property - def height(self): - return self.model.Height - @height.setter - def height(self, value): - self.model.Height = value - - @property - def width(self): - return self.model.Width - @width.setter - def width(self, value): - self.model.Width = value - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.Visible = value - - @property - def step(self): - return self.model.Step - @step.setter - def step(self, value): - self.model.Step = value - - @property - def events(self): - return self._events - @events.setter - def events(self, controllers): - self._events = controllers(self) - self._connect_listeners() - - @property - def color_on_focus(self): - return self._color_on_focus - @color_on_focus.setter - def color_on_focus(self, value): - self._color_on_focus = get_color(value) - - def _connect_listeners(self): - for control in self.obj.Controls: - _add_listeners(self.events, control, control.Model.Name) - return - - def _set_image_url(self, image): - if _P.exists(image): - return _P.to_url(image) - - path = _P.join(self._path, DIR['images'], image) - return _P.to_url(path) - - def _special_properties(self, tipo, args): - if tipo == 'link' and not 'Label' in args: - args['Label'] = args['URL'] - return args - - if tipo == 'button': - if 'ImageURL' in args: - args['ImageURL'] = self._set_image_url(args['ImageURL']) - args['FocusOnClick'] = args.get('FocusOnClick', False) - return args - - if tipo == 'roadmap': - args['Height'] = args.get('Height', self.height) - if 'Title' in args: - args['Text'] = args.pop('Title') - return args - - if tipo == 'tree': - args['SelectionType'] = args.get('SelectionType', SINGLE) - return args - - if tipo == 'grid': - args['X'] = args.get('X', self.SEPARATION) - args['Y'] = args.get('Y', self.SEPARATION) - args['Width'] = args.get('Width', self.width - self.SEPARATION * 2) - args['Height'] = args.get('Height', self.height - self.SEPARATION * 2) - args['ShowRowHeader'] = args.get('ShowRowHeader', True) - return args - - if tipo == 'pages': - args['Width'] = args.get('Width', self.width) - args['Height'] = args.get('Height', self.height) - - return args - - def add_control(self, args): - tipo = args.pop('Type').lower() - root = args.pop('Root', '') - sheets = args.pop('Sheets', ()) - columns = args.pop('Columns', ()) - - args = self._special_properties(tipo, args) - model = self.model.createInstance(self.MODELS[tipo]) - - _set_properties(model, args) - name = args['Name'] - self.model.insertByName(name, model) - control = self.obj.getControl(name) - _add_listeners(self.events, control, name) - control = UNO_CLASSES[tipo](control) - - if tipo in ('listbox',): - control.path = self.path - - if tipo == 'tree' and root: - control.root = root - elif tipo == 'grid' and columns: - control.columns = columns - elif tipo == 'pages' and sheets: - control.sheets = sheets - control.events = self.events - - setattr(self, name, control) - self._controls[name] = control - return control - - def center(self, control, x=0, y=0): - w = self.width - h = self.height - - if isinstance(control, tuple): - wt = self.SEPARATION * -1 - for c in control: - wt += c.width + self.SEPARATION - x = w / 2 - wt / 2 - for c in control: - c.x = x - x = c.x + c.width + self.SEPARATION - return - - if x < 0: - x = w + x - control.width - elif x == 0: - x = w / 2 - control.width / 2 - if y < 0: - y = h + y - control.height - elif y == 0: - y = h / 2 - control.height / 2 - control.x = x - control.y = y - return - - def open(self, modal=True): - self._modal = modal - if modal: - return self.obj.execute() - else: - self.visible = True - return - - def close(self, value=0): - if self._modal: - value = self.obj.endDialog(value) - else: - self.visible = False - self.obj.dispose() - return value - - def set_values(self, data): - for k, v in data.items(): - self._controls[k].value = v - return - - -class LOCells(object): - - def __getitem__(self, index): - return LODocs().active.active[index] - - -class LOWindow(object): - EMPTY = """ - -""" - MODELS = { - 'label': 'com.sun.star.awt.UnoControlFixedTextModel', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', - 'button': 'com.sun.star.awt.UnoControlButtonModel', - 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', - 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', - 'text': 'com.sun.star.awt.UnoControlEditModel', - 'image': 'com.sun.star.awt.UnoControlImageControlModel', - 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', - 'tree': 'com.sun.star.awt.tree.TreeControlModel', - 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', - 'pages': 'com.sun.star.awt.UnoMultiPageModel', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', - } - - def __init__(self, args): - self._events = None - self._menu = None - self._container = None - self._model = None - self._id = '' - self._path = '' - self._obj = self._create(args) - - def _create(self, properties): - ps = ( - properties.get('X', 0), - properties.get('Y', 0), - properties.get('Width', 500), - properties.get('Height', 500), - ) - self._title = properties.get('Title', TITLE) - self._create_frame(ps) - self._create_container(ps) - self._create_subcontainer(ps) - # ~ self._create_splitter(ps) - return - - def _create_frame(self, ps): - service = 'com.sun.star.frame.TaskCreator' - tc = create_instance(service, True) - self._frame = tc.createInstanceWithArguments(( - NamedValue('FrameName', 'EasyMacroWin'), - NamedValue('PosSize', Rectangle(*ps)), - )) - self._window = self._frame.getContainerWindow() - self._toolkit = self._window.getToolkit() - desktop = get_desktop() - self._frame.setCreator(desktop) - desktop.getFrames().append(self._frame) - self._frame.Title = self._title - return - - def _create_container(self, ps): - service = 'com.sun.star.awt.UnoControlContainer' - self._container = create_instance(service, True) - service = 'com.sun.star.awt.UnoControlContainerModel' - model = create_instance(service, True) - model.BackgroundColor = get_color((225, 225, 225)) - self._container.setModel(model) - self._container.createPeer(self._toolkit, self._window) - self._container.setPosSize(*ps, POSSIZE) - self._frame.setComponent(self._container, None) - return - - def _create_subcontainer(self, ps): - service = 'com.sun.star.awt.ContainerWindowProvider' - cwp = create_instance(service, True) - - path_tmp = _P.save_tmp(self.EMPTY) - subcont = cwp.createContainerWindow( - _P.to_url(path_tmp), '', self._container.getPeer(), None) - _P.kill(path_tmp) - - subcont.setPosSize(0, 0, 500, 500, POSSIZE) - subcont.setVisible(True) - self._container.addControl('subcont', subcont) - self._subcont = subcont - self._model = subcont.Model - return - - def _create_popupmenu(self, menus): - menu = create_instance('com.sun.star.awt.PopupMenu', True) - for i, m in enumerate(menus): - label = m['label'] - cmd = m.get('event', '') - if not cmd: - cmd = label.lower().replace(' ', '_') - if label == '-': - menu.insertSeparator(i) - else: - menu.insertItem(i, label, m.get('style', 0), i) - menu.setCommand(i, cmd) - # ~ menu.setItemImage(i, path?, True) - menu.addMenuListener(EventsMenu(self.events)) - return menu - - def _create_menu(self, menus): - #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMenu.html - #~ nItemId specifies the ID of the menu item to be inserted. - #~ aText specifies the label of the menu item. - #~ nItemStyle 0 = Standard, CHECKABLE = 1, RADIOCHECK = 2, AUTOCHECK = 4 - #~ nItemPos specifies the position where the menu item will be inserted. - self._menu = create_instance('com.sun.star.awt.MenuBar', True) - for i, m in enumerate(menus): - self._menu.insertItem(i, m['label'], m.get('style', 0), i) - cmd = m['label'].lower().replace(' ', '_') - self._menu.setCommand(i, cmd) - submenu = self._create_popupmenu(m['submenu']) - self._menu.setPopupMenu(i, submenu) - - self._window.setMenuBar(self._menu) - return - - def _add_listeners(self, control=None): - if self.events is None: - return - controller = EventsWindow(self) - self._window.addTopWindowListener(controller) - self._window.addWindowListener(controller) - # ~ self._container.addKeyListener(EventsKeyWindow(self)) - return - - def _set_image_url(self, image): - if _P.exists(image): - return _P.to_url(image) - - path = _P.join(self._path, DIR['images'], image) - return _P.to_url(path) - - def _special_properties(self, tipo, args): - if tipo == 'link' and not 'Label' in args: - args['Label'] = args['URL'] - return args - - if tipo == 'button': - if 'ImageURL' in args: - args['ImageURL'] = self._set_image_url(args['ImageURL']) - args['FocusOnClick'] = args.get('FocusOnClick', False) - return args - - if tipo == 'roadmap': - args['Height'] = args.get('Height', self.height) - if 'Title' in args: - args['Text'] = args.pop('Title') - return args - - if tipo == 'tree': - args['SelectionType'] = args.get('SelectionType', SINGLE) - return args - - if tipo == 'grid': - args['ShowRowHeader'] = args.get('ShowRowHeader', True) - return args - - if tipo == 'pages': - args['Width'] = args.get('Width', self.width) - args['Height'] = args.get('Height', self.height) - - return args - - def add_control(self, args): - tipo = args.pop('Type').lower() - root = args.pop('Root', '') - sheets = args.pop('Sheets', ()) - columns = args.pop('Columns', ()) - - args = self._special_properties(tipo, args) - model = self.model.createInstance(self.MODELS[tipo]) - _set_properties(model, args) - name = args['Name'] - self.model.insertByName(name, model) - control = self._subcont.getControl(name) - _add_listeners(self.events, control, name) - control = UNO_CLASSES[tipo](control) - - # ~ if tipo in ('listbox',): - # ~ control.path = self.path - - if tipo == 'tree' and root: - control.root = root - elif tipo == 'grid' and columns: - control.columns = columns - elif tipo == 'pages' and sheets: - control.sheets = sheets - control.events = self.events - - setattr(self, name, control) - return control - - @property - def events(self): - return self._events - @events.setter - def events(self, controllers): - self._events = controllers(self) - self._add_listeners() - - @property - def model(self): - return self._model - - @property - def width(self): - return self._container.Size.Width - - @property - def height(self): - return self._container.Size.Height - - @property - def name(self): - return self._title.lower().replace(' ', '_') - - def add_menu(self, menus): - self._create_menu(menus) - return - - def open(self): - self._window.setVisible(True) - return - - def close(self): - self._window.setMenuBar(None) - self._window.dispose() - self._frame.close(True) - return - - -class LODBServer(object): - DRIVERS = { - 'mysql': 'mysqlc', - 'mariadb': 'mysqlc', - 'postgres': 'postgresql:postgresql', - } - PORTS = { - 'mysql': 3306, - 'mariadb': 3306, - 'postgres': 5432, - } - - def __init__(self): - self._conn = None - self._error = 'Not connected' - self._type = '' - self._drivers = [] - - def __str__(self): - return f'DB type {self._type}' - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.disconnet() - - @property - def is_connected(self): - return not self._conn is None - - @property - def error(self): - return self._error - - @property - def drivers(self): - return self._drivers - - def disconnet(self): - if not self._conn is None: - if not self._conn.isClosed(): - self._conn.close() - self._conn.dispose() - return - - def connect(self, options={}): - args = options.copy() - self._error = '' - self._type = args.get('type', 'postgres') - driver = self.DRIVERS[self._type] - server = args.get('server', 'localhost') - port = args.get('port', self.PORTS[self._type]) - dbname = args.get('dbname', '') - user = args['user'] - password = args['password'] - - data = {'user': user, 'password': password} - url = f'sdbc:{driver}:{server}:{port}/{dbname}' - - # ~ https://downloads.mariadb.com/Connectors/java/ - # ~ data['JavaDriverClass'] = 'org.mariadb.jdbc.Driver' - # ~ url = f'jdbc:mysql://{server}:{port}/{dbname}' - - args = dict_to_property(data) - manager = create_instance('com.sun.star.sdbc.DriverManager') - self._drivers = [d.ImplementationName for d in manager] - - try: - self._conn = manager.getConnectionWithInfo(url, args) - except Exception as e: - error(e) - self._error = str(e) - - return self - - def execute(self, sql): - query = self._conn.createStatement() - try: - query.execute(sql) - result = True - except Exception as e: - error(e) - self._error = str(e) - result = False - - return result - - -def create_window(args): - return LOWindow(args) - - -class Paths(object): - - @classmethod - def image(cls, path): - # ~ sfa = create_instance('com.sun.star.ucb.SimpleFileAccess') - # ~ stream = sfa.openFileRead(cls.to_url(path)) - gp = create_instance('com.sun.star.graphic.GraphicProvider') - if isinstance(path, str): - properties = (PropertyValue(Name='URL', Value=cls.to_url(path)),) - else: - properties = (PropertyValue(Name='InputStream', Value=path),) - image = gp.queryGraphic(properties) - return image - - -class IOStream(object): - - @classmethod - def qr(cls, data, **kwargs): - import segno - - kwargs['kind'] = kwargs.get('kind', 'svg') - kwargs['scale'] = kwargs.get('scale', 8) - kwargs['border'] = kwargs.get('border', 2) - buffer = cls.buffer() - segno.make(data).save(buffer, **kwargs) - stream = cls.input(buffer) - return stream - - -class SpellChecker(object): - - def __init__(self): - service = 'com.sun.star.linguistic2.SpellChecker' - self._spellchecker = create_instance(service, True) - self._locale = LOCALE - - @property - def locale(self): - slocal = f'{self._locale.Language}-{self._locale.Country}' - return slocale - @locale.setter - def locale(self, value): - lang = value.split('-') - self._locale = Locale(lang[0], lang[1], '') - - def is_valid(self, word): - result = self._spellchecker.isValid(word, self._locale, ()) - return result - - def spell(self, word): - result = self._spellchecker.spell(word, self._locale, ()) - if result: - result = result.getAlternatives() - if not isinstance(result, tuple): - result = () - return result - - -def spell(word, locale=''): - sc = SpellChecker() - if locale: - sc.locale = locale - return sc.spell(word) - - -def __getattr__(name): - if name == 'current_region': - return LODocs().active.selection.current_region - if name in ('rectangle', 'pos_size'): - return Rectangle() - if name == 'db': - return LODBServer() - if name == 'cells': - return LOCells() - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -def create_dialog(args={}): - return LODialog(args) - - -def inputbox(message, default='', title=TITLE, echochar=''): - - class ControllersInput(object): - - def __init__(self, dlg): - self.d = dlg - - def cmd_ok_action(self, event): - self.d.close(1) - return - - args = { - 'Title': title, - 'Width': 200, - 'Height': 80, - } - dlg = LODialog(args) - dlg.events = ControllersInput - - args = { - 'Type': 'Label', - 'Name': 'lbl_msg', - 'Label': message, - 'Width': 140, - 'Height': 50, - 'X': 5, - 'Y': 5, - 'MultiLine': True, - 'Border': 1, - } - dlg.add_control(args) - - args = { - 'Type': 'Text', - 'Name': 'txt_value', - 'Text': default, - 'Width': 190, - 'Height': 15, - } - if echochar: - args['EchoChar'] = ord(echochar[0]) - dlg.add_control(args) - dlg.txt_value.move(dlg.lbl_msg) - - args = { - 'Type': 'button', - 'Name': 'cmd_ok', - 'Label': _('OK'), - 'Width': 40, - 'Height': 15, - 'DefaultButton': True, - 'PushButtonType': 1, - } - dlg.add_control(args) - dlg.cmd_ok.move(dlg.lbl_msg, 10, 0) - - args = { - 'Type': 'button', - 'Name': 'cmd_cancel', - 'Label': _('Cancel'), - 'Width': 40, - 'Height': 15, - 'PushButtonType': 2, - } - dlg.add_control(args) - dlg.cmd_cancel.move(dlg.cmd_ok) - - if dlg.open(): - return dlg.txt_value.value - - return '' diff --git a/source/easymacro.bk1 b/source/easymacro.py similarity index 70% rename from source/easymacro.bk1 rename to source/easymacro.py index e73a7ba..37465d4 100644 --- a/source/easymacro.bk1 +++ b/source/easymacro.py @@ -18,31 +18,24 @@ # ~ along with easymacro. If not, see . -import csv + import getpass import hashlib import io -import json + import logging import os import platform import re import shlex -import shutil import socket import ssl -import subprocess + import tempfile import threading -import time - - - -from pathlib import Path from string import Template - from socket import timeout from urllib import parse from urllib.request import Request, urlopen @@ -57,7 +50,7 @@ from email.mime.text import MIMEText from email.utils import formatdate from email import encoders -import unohelper + from com.sun.star.awt import Rectangle, Size, Point from com.sun.star.awt import Key, KeyEvent, KeyModifier @@ -66,10 +59,10 @@ from com.sun.star.awt import Key, KeyEvent, KeyModifier from com.sun.star.datatransfer import XTransferable, DataFlavor from com.sun.star.io import IOException, XOutputStream -from com.sun.star.ui.dialogs import TemplateDescription + from com.sun.star.sheet import XRangeSelectionListener -from com.sun.star.lang import XEventListener + from com.sun.star.container import NoSuchElementException @@ -80,55 +73,12 @@ TIMEOUT = 10 SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc' _EVENTS = {} -PYTHON = 'python' -if IS_WIN: - PYTHON = 'python.exe' FILES = { 'CONFIG': 'zaz-{}.json', } DIRS = {} -MESSAGES = { - 'es': { - 'OK': 'Aceptar', - 'Cancel': 'Cancelar', - 'Select path': 'Seleccionar ruta', - 'Select directory': 'Seleccionar directorio', - 'Select file': 'Seleccionar archivo', - 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', - 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', - } -} - - - - - - - -def _(msg): - if LANG == 'en': - return msg - - if not LANG in MESSAGES: - return msg - - return MESSAGES[LANG][msg] - - - - - - - - -def sleep(seconds: int): - """Sleep - """ - time.sleep(seconds) - return - def run_in_thread(fn): """Run any function in thread @@ -173,160 +123,9 @@ def render(template, data): s = Template(template) return s.safe_substitute(**data) - -def _set_properties(model, properties): - # ~ if 'X' in properties: - # ~ properties['PositionX'] = properties.pop('X') - # ~ if 'Y' in properties: - # ~ properties['PositionY'] = properties.pop('Y') - keys = tuple(properties.keys()) - values = tuple(properties.values()) - model.setPropertyValues(keys, values) - return - - # Classes - -# ~ https://github.com/django/django/blob/main/django/utils/functional.py#L61 -class classproperty: - - def __init__(self, method=None): - self.fget = method - - def __get__(self, instance, cls=None): - return self.fget(cls) - - def getter(self, method): - self.fget = method - return self - - -class Dates(object): - """Class for datetimes - """ - _start = None - - @classproperty - def now(cls): - """Current local date and time - - :return: Return the current local date and time - :rtype: datetime - """ - return datetime.datetime.now().replace(microsecond=0) - - @classproperty - def today(cls): - """Current local date - - :return: Return the current local date - :rtype: date - """ - return datetime.date.today() - - @classproperty - def time(cls): - """Current local time - - :return: Return the current local time - :rtype: datetime.time - """ - t = cls.now.time().replace(microsecond=0) - return t - - @classproperty - def epoch(cls): - """Get unix time - - :return: Return unix time - :rtype: int - - `See Unix Time `_ - """ - n = cls.now - e = int(time.mktime(n.timetuple())) - return e - - @classmethod - def date(cls, year: int, month: int, day: int): - """Get date from year, month, day - - :param year: Year of date - :type year: int - :param month: Month of date - :type month: int - :param day: Day of day - :type day: int - :return: Return the date - :rtype: date - - `See Python date `_ - """ - d = datetime.date(year, month, day) - return d - - @classmethod - def str_to_date(cls, str_date: str, template: str, to_calc: bool=False): - """Get date from string - - :param str_date: Date in string - :type str_date: str - :param template: Formato of date string - :type template: str - :param to_calc: If date is for used in Calc cell - :type to_calc: bool - :return: Return date or int if used in Calc - :rtype: date or int - - `See Python strptime `_ - """ - d = datetime.datetime.strptime(str_date, template).date() - if to_calc: - d = d.toordinal() - DATE_OFFSET - return d - - @classmethod - def calc_to_date(cls, value: float): - """Get date from calc value - - :param value: Float value from cell - :type value: float - :return: Return the current local date - :rtype: date - - `See Python fromordinal `_ - """ - d = datetime.date.fromordinal(int(value) + DATE_OFFSET) - return d - - @classmethod - def start(cls): - """Start counter - """ - cls._start = cls.now - info('Start: ', cls._start) - return - - @classmethod - def end(cls, get_seconds: bool=True): - """End counter - - :param get_seconds: If return value in total seconds - :type get_seconds: bool - :return: Return the timedelta or total seconds - :rtype: timedelta or int - """ - e = cls.now - td = e - cls._start - result = str(td) - if get_seconds: - result = td.total_seconds() - info('End: ', e) - return result - - class Json(object): """Class for json data """ @@ -588,630 +387,6 @@ class Hash(object): return result -class Paths(object): - """Class for paths - """ - FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' - FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' - REMOTE_FILE_PICKER = 'com.sun.star.ui.dialogs.RemoteFilePicker' - OFFICE_FILE_PICKER = 'com.sun.star.ui.dialogs.OfficeFilePicker' - - def __init__(self, path=''): - if path.startswith('file://'): - path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) - self._path = Path(path) - - @property - def path(self): - """Get base path""" - return str(self._path.parent) - - @property - def file_name(self): - """Get file name""" - return self._path.name - - @property - def name(self): - """Get name""" - return self._path.stem - - @property - def ext(self): - """Get extension""" - return self._path.suffix[1:] - - @property - def size(self): - """Get size""" - return self._path.stat().st_size - - @property - def url(self): - """Get like URL""" - return self._path.as_uri() - - @property - def info(self): - """Get all info like tuple""" - i = (self.path, self.file_name, self.name, self.ext, self.size, self.url) - return i - - @property - def dict(self): - """Get all info like dict""" - data = { - 'path': self.path, - 'file_name': self.file_name, - 'name': self.name, - 'ext': self.ext, - 'size': self.size, - 'url': self.url, - } - return data - - @classproperty - def home(self): - """Get user home""" - return str(Path.home()) - - @classproperty - def documents(self): - """Get user save documents""" - return self.config() - - @classproperty - def user_profile(self): - """Get path user profile""" - path = self.config('UserConfig') - path = str(Path(path).parent) - return path - - @classproperty - def user_config(self): - """Get path config in user profile""" - path = self.config('UserConfig') - return path - - @classproperty - def python(self): - """Get path executable python""" - if IS_WIN: - path = self.join(self.config('Module'), PYTHON) - elif IS_MAC: - path = self.join(self.config('Module'), '..', 'Resources', PYTHON) - else: - path = sys.executable - return path - - @classmethod - def to_system(cls, path:str) -> str: - """Convert paths in URL to system - - :param path: Path to convert - :type path: str - :return: Path system format - :rtype: str - """ - if path.startswith('file://'): - path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) - return path - - @classmethod - def to_url(cls, path: str) -> str: - """Convert paths in format system to URL - - :param path: Path to convert - :type path: str - :return: Path in URL - :rtype: str - """ - if not path.startswith('file://'): - path = Path(path).as_uri() - return path - - @classmethod - def config(cls, name: str='Work') -> Union[str, list]: - """Return path from config - - :param name: Name in service PathSettings, default get path documents - :type name: str - :return: Path in config, if exists. - :rtype: str or list - - `See Api XPathSettings `_ - """ - path = create_instance('com.sun.star.util.PathSettings') - path = cls.to_system(getattr(path, name)).split(';') - if len(path) == 1: - path = path[0] - return path - - @classmethod - def join(cls, *paths: str) -> str: - """Join paths - - :param paths: Paths to join - :type paths: list - :return: New path with joins - :rtype: str - """ - path = str(Path(paths[0]).joinpath(*paths[1:])) - return path - - @classmethod - def exists(cls, path: str) -> bool: - """If exists path - - :param path: Path for validate - :type path: str - :return: True if path exists, False if not. - :rtype: bool - """ - path = cls.to_system(path) - result = Path(path).exists() - return result - - @classmethod - def exists_app(cls, name_app: str) -> bool: - """If exists app in system - - :param name_app: Name of application - :type name_app: str - :return: True if app exists, False if not. - :rtype: bool - """ - result = bool(shutil.which(name_app)) - return result - - @classmethod - def is_dir(cls, path: str): - """Validate if path is directory - - :param path: Path for validate - :type path: str - :return: True if path is directory, False if not. - :rtype: bool - """ - return Path(path).is_dir() - - @classmethod - def is_file(cls, path: str): - """Validate if path is a file - - :param path: Path for validate - :type path: str - :return: True if path is a file, False if not. - :rtype: bool - """ - return Path(path).is_file() - - @classmethod - def temp_file(self): - """Make temporary file""" - return tempfile.NamedTemporaryFile(mode='w') - - @classmethod - def temp_dir(self): - """Make temporary directory""" - return tempfile.TemporaryDirectory(ignore_cleanup_errors=True) - - @classmethod - def get(cls, init_dir: str='', filters: str='') -> str: - """Get path for save - - :param init_dir: Initial default path - :type init_dir: str - :param filters: Filter for show type files: 'xml' or 'txt,xml' - :type filters: str - :return: Selected path - :rtype: str - - `See API `_ - """ - if not init_dir: - init_dir = cls.documents - init_dir = cls.to_url(init_dir) - file_picker = create_instance(cls.FILE_PICKER) - file_picker.setTitle(_('Select path')) - file_picker.setDisplayDirectory(init_dir) - file_picker.initialize((TemplateDescription.FILEOPEN_SIMPLE,)) - if filters: - for f in filters.split(','): - file_picker.appendFilter(f.upper(), f'*.{f.lower()}') - - path = '' - if file_picker.execute(): - path = cls.to_system(file_picker.getSelectedFiles()[0]) - return path - - @classmethod - def get_dir(cls, init_dir: str='') -> str: - """Get path dir - - :param init_dir: Initial default path - :type init_dir: str - :return: Selected path - :rtype: str - """ - folder_picker = create_instance(cls.FOLDER_PICKER) - if not init_dir: - init_dir = cls.documents - init_dir = cls.to_url(init_dir) - folder_picker.setTitle(_('Select directory')) - folder_picker.setDisplayDirectory(init_dir) - - path = '' - if folder_picker.execute(): - path = cls.to_system(folder_picker.getDirectory()) - return path - - @classmethod - def get_file(cls, init_dir: str='', filters: str='', multiple: bool=False): - """Get path exists file - - :param init_dir: Initial default path - :type init_dir: str - :param filters: Filter for show type files: 'xml' or 'txt,xml' - :type filters: str - :param multiple: If user can selected multiple files - :type multiple: bool - :return: Selected path - :rtype: str - """ - if not init_dir: - init_dir = cls.documents - init_dir = cls.to_url(init_dir) - - file_picker = create_instance(cls.FILE_PICKER) - file_picker.setTitle(_('Select file')) - file_picker.setDisplayDirectory(init_dir) - file_picker.setMultiSelectionMode(multiple) - - if filters: - for f in filters.split(','): - file_picker.appendFilter(f.upper(), f'*.{f.lower()}') - - path = '' - if file_picker.execute(): - files = file_picker.getSelectedFiles() - path = [cls.to_system(f) for f in files] - if not multiple: - path = path[0] - return path - - @classmethod - def files(cls, path: str, pattern: str='*'): - """Get all files in path - - :param path: Path with files - :type path: str - :param pattern: For filter files, default get all. - :type pattern: str - :return: Files in path - :rtype: list - """ - files = [str(p) for p in Path(path).glob(pattern) if p.is_file()] - return files - - @classmethod - def walk(cls, path, filters=''): - """Get all files in path recursively - - :param path: Path with files - :type path: str - :param filters: For filter files, default get all. - :type filters: str - :return: Files in path - :rtype: list - """ - paths = [] - for folder, _, files in os.walk(path): - if filters: - pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) - paths += [cls.join(folder, f) for f in files if pattern.search(f)] - else: - paths += [cls.join(folder, f) for f in files] - return paths - - @classmethod - def dirs(cls, path): - """Get directories in path - - :param path: Path to scan - :type path: str - :return: Directories in path - :rtype: list - """ - dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()] - return dirs - - @classmethod - def walk_dirs(cls, path, tree=False): - """Get directories recursively - - :param path: Path to scan - :type path: str - :param tree: get info in a tuple (ID_FOLDER, ID_PARENT, NAME) - :type tree: bool - :return: Directories in path - :rtype: list - """ - folders = [] - if tree: - i = 0 - parents = {path: 0} - for root, dirs, _ in os.walk(path): - for name in dirs: - i += 1 - rn = cls.join(root, name) - if not rn in parents: - parents[rn] = i - folders.append((i, parents[root], name)) - else: - for root, dirs, _ in os.walk(path): - folders += [cls.join(root, name) for name in dirs] - return folders - - @classmethod - def extension(cls, id_ext: str): - """Get path extension install from id - - :param id_ext: ID extension - :type id_ext: str - :return: Path extension - :rtype: str - """ - pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') - path = Paths.to_system(pip.getPackageLocation(id_ext)) - return path - - @classmethod - def replace_ext(cls, path: str, new_ext: str): - """Replace extension in file path - - :param path: Path to file - :type path: str - :param new_ext: New extension - :type new_ext: str - :return: Path with new extension - :rtype: str - """ - p = Paths(path) - name = f'{p.name}.{new_ext}' - path = cls.join(p.path, name) - return path - - @classmethod - def open(cls, path: str): - """Open any file with default program in systema - - :param path: Path to file - :type path: str - :return: PID file, only Linux - :rtype: int - """ - pid = 0 - if IS_WIN: - os.startfile(path) - else: - pid = subprocess.Popen(['xdg-open', path]).pid - return pid - - # ~ Save/read data - - @classmethod - def save(cls, path: str, data: str, encoding: str='utf-8') -> bool: - """Save data in path with encoding - - :param path: Path to file save - :type path: str - :param data: Data to save - :type data: str - :param encoding: Encoding for save data, default utf-8 - :type encoding: str - :return: True, if save corrrectly - :rtype: bool - """ - result = bool(Path(path).write_text(data, encoding=encoding)) - return result - - @classmethod - def save_bin(cls, path: str, data: bytes) -> bool: - """Save binary data in path - - :param path: Path to file save - :type path: str - :param data: Data to save - :type data: bytes - :return: True, if save corrrectly - :rtype: bool - """ - result = bool(Path(path).write_bytes(data)) - return result - - @classmethod - def read(cls, path: str, get_lines: bool=False, encoding: str='utf-8') -> Union[str, list]: - """Read data in path - - :param path: Path to file read - :type path: str - :param get_lines: If read file line by line - :type get_lines: bool - :return: File content - :rtype: str or list - """ - if get_lines: - with Path(path).open(encoding=encoding) as f: - data = f.readlines() - else: - data = Path(path).read_text(encoding=encoding) - return data - - @classmethod - def read_bin(cls, path: str) -> bytes: - """Read binary data in path - - :param path: Path to file read - :type path: str - :return: File content - :rtype: bytes - """ - data = Path(path).read_bytes() - return data - - # ~ Import/export data - - @classmethod - def from_json(cls, path: str) -> Any: - """Read path file and load json data - - :param path: Path to file - :type path: str - :return: Any data - :rtype: Any - """ - data = json.loads(cls.read(path)) - return data - - @classmethod - def to_json(cls, path: str, data: str): - """Save data in path file like json - - :param path: Path to file - :type path: str - :return: True if save correctly - :rtype: bool - """ - data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) - return cls.save(path, data) - - @classmethod - def from_csv(cls, path: str, args: dict={}) -> tuple: - """Read CSV - - :param path: Path to file csv - :type path: str - :param args: Any argument support for Python library - :type args: dict - :return: Data csv like tuple - :rtype: tuple - - `See CSV Reader `_ - """ - with open(path) as f: - rows = tuple(csv.reader(f, **args)) - return rows - - @classmethod - def to_csv(cls, path: str, data: Any, args: dict={}): - """Write CSV - - :param path: Path to file write csv - :type path: str - :param data: Data to write - :type data: Iterable - :param args: Any argument support for Python library - :type args: dict - - `See CSV Writer `_ - """ - with open(path, 'w') as f: - writer = csv.writer(f, **args) - writer.writerows(data) - return - - @classmethod - def zip(cls, source: Union[str, tuple, list], target='') -> str: - path_zip = target - if not isinstance(source, (tuple, list)): - path, _, name, _ = _P(source).info - start = len(path) + 1 - if not target: - path_zip = f'{path}/{name}.zip' - - if isinstance(source, (tuple, list)): - files = [(f, f[len(_P(f).path)+1:]) for f in source] - elif _P.is_file(source): - files = ((source, source[start:]),) - else: - files = [(f, f[start:]) for f in _P.walk(source)] - - compression = zipfile.ZIP_DEFLATED - with zipfile.ZipFile(path_zip, 'w', compression=compression) as z: - for f in files: - z.write(f[0], f[1]) - return path_zip - - @classmethod - def unzip(cls, source, target='', members=None, pwd=None): - path = target - if not target: - path = _P(source).path - with zipfile.ZipFile(source) as z: - if not pwd is None: - pwd = pwd.encode() - if isinstance(members, str): - members = (members,) - z.extractall(path, members=members, pwd=pwd) - return - - @classmethod - def zip_content(cls, path: str): - with zipfile.ZipFile(path) as z: - names = z.namelist() - return names - - @classmethod - def merge_zip(cls, target, zips): - try: - with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: - for path in zips: - with zipfile.ZipFile(path, compression=zipfile.ZIP_DEFLATED) as s: - for name in s.namelist(): - t.writestr(name, s.open(name).read()) - except Exception as e: - error(e) - return False - - return True - - @classmethod - def kill(cls, path: str): - """Delete path - - :param path: Path to file or directory - :type path: str - :return: True if delete correctly - :rtype: bool - """ - p = Path(path) - try: - if p.is_file(): - p.unlink() - elif p.is_dir(): - shutil.rmtree(path) - result = True - except OSError as e: - log.error(e) - result = False - - return result - - @classmethod - def copy(cls, source: str, target: str='', name: str=''): - p, f, n, e, _, _ = Paths(source).info - if target: - p = target - e = f'.{e}' - if name: - e = '' - n = name - path_new = cls.join(p, f'{n}{e}') - shutil.copy(source, path_new) - return path_new - - class Config(object): """Class for set and get configurations """ @@ -1329,158 +504,6 @@ class Url(object): return cls._open(url, data, headers, verify, json, timeout, 'POST') -class Email(object): - """Class for send email - """ - class _SmtpServer(object): - - def __init__(self, config): - self._server = None - self._error = '' - self._sender = '' - self._is_connect = self._login(config) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @property - def is_connect(self): - return self._is_connect - - @property - def error(self): - return self._error - - def _login(self, config): - name = config['server'] - port = config['port'] - is_ssl = config['ssl'] - starttls = config.get('starttls', False) - self._sender = config['user'] - try: - if starttls: - self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) - self._server.ehlo() - self._server.starttls() - self._server.ehlo() - elif is_ssl: - self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) - self._server.ehlo() - else: - self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) - - self._server.login(self._sender, config['password']) - msg = 'Connect to: {}'.format(name) - debug(msg) - return True - except smtplib.SMTPAuthenticationError as e: - if '535' in str(e): - self._error = _('Incorrect user or password') - return False - if '534' in str(e) and 'gmail' in name: - self._error = _('Allow less secure apps in GMail') - return False - except smtplib.SMTPException as e: - self._error = str(e) - return False - except Exception as e: - self._error = str(e) - return False - return False - - def _body(self, msg): - body = msg.replace('\n', '
') - return body - - def send(self, message): - # ~ file_name = 'attachment; filename={}' - email = MIMEMultipart() - email['From'] = self._sender - email['To'] = message['to'] - email['Cc'] = message.get('cc', '') - email['Subject'] = message['subject'] - email['Date'] = formatdate(localtime=True) - if message.get('confirm', False): - email['Disposition-Notification-To'] = email['From'] - email.attach(MIMEText(self._body(message['body']), 'html')) - - paths = message.get('files', ()) - if isinstance(paths, str): - paths = (paths,) - for path in paths: - fn = _P(path).file_name - # ~ print('NAME', fn) - part = MIMEBase('application', 'octet-stream') - part.set_payload(_P.read_bin(path)) - encoders.encode_base64(part) - part.add_header('Content-Disposition', f'attachment; filename="{fn}"') - email.attach(part) - - receivers = ( - email['To'].split(',') + - email['CC'].split(',') + - message.get('bcc', '').split(',')) - try: - self._server.sendmail(self._sender, receivers, email.as_string()) - msg = 'Email sent...' - debug(msg) - if message.get('path', ''): - self.save_message(email, message['path']) - return True - except Exception as e: - self._error = str(e) - return False - return False - - def save_message(self, email, path): - mbox = mailbox.mbox(path, create=True) - mbox.lock() - try: - msg = mailbox.mboxMessage(email) - mbox.add(msg) - mbox.flush() - finally: - mbox.unlock() - return - - def close(self): - try: - self._server.quit() - msg = 'Close connection...' - debug(msg) - except: - pass - return - - @classmethod - def _send_email(cls, server, messages): - with cls._SmtpServer(server) as server: - if server.is_connect: - for msg in messages: - server.send(msg) - else: - error(server.error) - return server.error - - @classmethod - def send(cls, server: dict, messages: Union[dict, tuple, list]): - """Send email with config server, emails send in thread. - - :param server: Configuration for send emails - :type server: dict - :param server: Dictionary con message or list of messages - :type server: dict or iterator - """ - if isinstance(messages, dict): - messages = (messages,) - t = threading.Thread(target=cls._send_email, args=(server, messages)) - t.start() - return - - class Color(object): """Class for colors @@ -1737,23 +760,6 @@ class IOStream(object): return cls.OutputStream() -class EventsListenerBase(unohelper.Base, XEventListener): - - def __init__(self, controller, name, window=None): - self._controller = controller - self._name = name - self._window = window - - @property - def name(self): - return self._name - - def disposing(self, event): - self._controller = None - if not self._window is None: - self._window.setMenuBar(None) - - class EventsRangeSelectionListener(EventsListenerBase, XRangeSelectionListener): def __init__(self, controller): @@ -3384,16 +2390,13 @@ class LODocIDE(LODocument): def __getattr__(name): classes = { - 'dates': Dates, 'json': Json, 'macro': Macro, 'shell': Shell, 'timer': Timer, 'hash': Hash, - 'path': Paths, 'config': Config, 'url': Url, - 'email': Email, 'color': Color(), 'io': IOStream, 'clipboard': ClipBoard, @@ -3402,7 +2405,6 @@ def __getattr__(name): 'lo': LOMain, 'command': LOMain.cmd, 'docs': LODocuments(), - 'active': LODocuments().active, } if name in classes: return classes[name] diff --git a/source/easymacro/__init__.py b/source/easymacro/__init__.py index d6dbcfa..8bf878e 100644 --- a/source/easymacro/__init__.py +++ b/source/easymacro/__init__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from .easymain import * +from .easydialog import * from .easytools import * from .easydocs import LODocuments @@ -8,7 +9,11 @@ from .easydocs import LODocuments def __getattr__(name): classes = { 'active': LODocuments().active, - 'inspect': LOInspect + 'dates': Dates, + 'dialog': LODialog, + 'email': Email, + 'inspect': LOInspect, + 'paths': Paths, } if name in classes: diff --git a/source/easymacro/easydialog.py b/source/easymacro/easydialog.py new file mode 100644 index 0000000..9be3578 --- /dev/null +++ b/source/easymacro/easydialog.py @@ -0,0 +1,741 @@ +#!/usr/bin/env python3 + +from typing import Any + +from com.sun.star.view.SelectionType import SINGLE, MULTI, RANGE + +from .easyevents import * +from .easydocs import LODocuments +from .easymain import log, TITLE, create_instance, BaseObject +from .easytools import Paths, Services, LOInspect +from .easytools import _, set_properties + + +__all__ = [ + 'LODialog', + 'inputbox', +] + + +SEPARATION = 5 +MODELS = { + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', + 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', +} + +TYPE_CONTROL = { + 'stardiv.Toolkit.UnoFixedTextControl': 'label', + 'stardiv.Toolkit.UnoEditControl': 'text', + 'stardiv.Toolkit.UnoButtonControl': 'button', +} + +IMPLEMENTATIONS = { + 'grid': 'stardiv.Toolkit.GridControl', + 'link': 'stardiv.Toolkit.UnoFixedHyperlinkControl', + 'roadmap': 'stardiv.Toolkit.UnoRoadmapControl', + 'pages': 'stardiv.Toolkit.UnoMultiPageControl', +} + + +def add_listeners(events, control): + name = control.Model.Name + listeners = { + 'addActionListener': EventsButton, + # ~ 'addMouseListener': EventsMouse, + # ~ 'addFocusListener': EventsFocus, + # ~ 'addItemListener': EventsItem, + # ~ 'addKeyListener': EventsKey, + # ~ 'addTabListener': EventsTab, + # ~ 'addSpinListener': EventsSpin, + } + if hasattr(control, 'obj'): + control = control.obj + # ~ log.debug(control.ImplementationName) + is_grid = control.ImplementationName == IMPLEMENTATIONS['grid'] + is_link = control.ImplementationName == IMPLEMENTATIONS['link'] + is_roadmap = control.ImplementationName == IMPLEMENTATIONS['roadmap'] + is_pages = control.ImplementationName == IMPLEMENTATIONS['pages'] + + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue + + getattr(control, key)(listeners[key](events, name)) + + if is_grid: + controllers = EventsGrid(events, name) + control.addSelectionListener(controllers) + control.Model.GridDataModel.addGridDataListener(controllers) + return + + +class UnoBaseObject(object): + + def __init__(self, obj: Any): + self._obj = obj + self._model = obj.Model + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + @property + def m(self): + return self._model + + @property + def properties(self): + data = LOInspect(self.obj).properties + return data + @properties.setter + def properties(self, properties: dict): + set_properties(self.model, properties) + + @property + def name(self): + return self.model.Name + + @property + def parent(self): + return self.obj.Context + + @property + def tag(self): + return self.model.Tag + @tag.setter + def tag(self, value): + self.model.Tag = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.setVisible(value) + + @property + def enabled(self): + return self.model.Enabled + @enabled.setter + def enabled(self, value): + self.model.Enabled = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def align(self): + return self.model.Align + @align.setter + def align(self, value): + self.model.Align = value + + @property + def valign(self): + return self.model.VerticalAlign + @valign.setter + def valign(self, value): + self.model.VerticalAlign = value + + @property + def font_weight(self): + return self.model.FontWeight + @font_weight.setter + def font_weight(self, value): + self.model.FontWeight = value + + @property + def font_height(self): + return self.model.FontHeight + @font_height.setter + def font_height(self, value): + self.model.FontHeight = value + + @property + def font_name(self): + return self.model.FontName + @font_name.setter + def font_name(self, value): + self.model.FontName = value + + @property + def font_underline(self): + return self.model.FontUnderline + @font_underline.setter + def font_underline(self, value): + self.model.FontUnderline = value + + @property + def text_color(self): + return self.model.TextColor + @text_color.setter + def text_color(self, value): + self.model.TextColor = value + + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + + @property + def multi_line(self): + return self.model.MultiLine + @multi_line.setter + def multi_line(self, value): + self.model.MultiLine = value + + @property + def help_text(self): + return self.model.HelpText + @help_text.setter + def help_text(self, value): + self.model.HelpText = value + + @property + def border(self): + return self.model.Border + @border.setter + def border(self, value): + # ~ Bug for report + self.model.Border = value + + @property + def width(self): + return self._model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + def _get_possize(self, name): + ps = self.obj.getPosSize() + return getattr(ps, name) + + def _set_possize(self, name, value): + ps = self.obj.getPosSize() + setattr(ps, name, value) + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + return + + @property + def x(self): + if hasattr(self.model, 'PositionX'): + return self.model.PositionX + return self._get_possize('X') + @x.setter + def x(self, value): + if hasattr(self.model, 'PositionX'): + self.model.PositionX = value + else: + self._set_possize('X', value) + + @property + def y(self): + if hasattr(self.model, 'PositionY'): + return self.model.PositionY + return self._get_possize('Y') + @y.setter + def y(self, value): + if hasattr(self.model, 'PositionY'): + self.model.PositionY = value + else: + self._set_possize('Y', value) + + @property + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value + + @property + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value + + @property + def ps(self): + ps = self.obj.getPosSize() + return ps + @ps.setter + def ps(self, ps): + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + + def set_focus(self): + self.obj.setFocus() + return + + def ps_from(self, source): + self.ps = source.ps + return + + def center(self, horizontal=True, vertical=False): + p = self.parent.Model + w = p.Width + h = p.Height + if horizontal: + x = w / 2 - self.width / 2 + self.x = x + if vertical: + y = h / 2 - self.height / 2 + self.y = y + return + + def move(self, origin, x=0, y=5, center=False): + if x: + self.x = origin.x + origin.width + x + else: + self.x = origin.x + if y: + h = origin.height + if y < 0: + h = 0 + self.y = origin.y + h + y + else: + self.y = origin.y + + if center: + self.center() + return + + +class UnoLabel(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html +class UnoText(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self.model.Text + @value.setter + def value(self, value): + self.model.Text = value + + @property + def echochar(self): + return chr(self.model.EchoChar) + @echochar.setter + def echochar(self, value): + if value: + self.model.EchoChar = ord(value[0]) + else: + self.model.EchoChar = 0 + + def validate(self): + return + + +class UnoButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + @property + def image(self): + return self.model.ImageURL + @image.setter + def image(self, value): + self.model.ImageURL = Paths.to_url(value) + + +UNO_CLASSES = { + 'label': UnoLabel, + 'text': UnoText, + 'button': UnoButton, + # ~ 'link': UnoLabelLink, + # ~ 'radio': UnoRadio, + # ~ 'checkbox': UnoCheckBox, + # ~ 'image': UnoImage, + # ~ 'listbox': UnoListBox, + # ~ 'roadmap': UnoRoadmap, + # ~ 'tree': UnoTree, + # ~ 'grid': UnoGrid, + # ~ 'pages': UnoPages, + # ~ 'spinbutton': UnoSpinButton, + # ~ 'numeric': UnoNumericField, +} + + +class DialogBox(BaseObject): + SERVICE = 'com.sun.star.awt.DialogProvider' + SERVICE_DIALOG = 'com.sun.star.awt.UnoControlDialog' + + def __init__(self, properties: dict={}): + self._controls = {} + obj = self._create(properties) + super().__init__(obj) + self._init_controls() + + def _create_from_path(self, path: str): + dp = create_instance(self.SERVICE, True) + dialog = dp.createDialog(Paths.to_url(path)) + return dialog + + def _create_from_location(self, properties: dict): + # ~ uid = docs.active.uid + # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' + + name = properties['Name'] + library = properties.get('Library', 'Standard') + location = properties.get('Location', 'application').lower() + if location == 'user': + location = 'application' + + url = f'vnd.sun.star.script:{library}.{name}?location={location}' + + if location == 'document': + doc = LODocuments().active.obj + dp = create_instance(self.SERVICE, arguments=doc) + else: + dp = create_instance(self.SERVICE, True) + + dialog = dp.createDialog(url) + return dialog + + def _create_from_properties(self, properties: dict): + dialog = create_instance(self.SERVICE_DIALOG, True) + model = create_instance(f'{self.SERVICE_DIALOG}Model', True) + + properties['Width'] = properties.get('Width', 200) + properties['Height'] = properties.get('Height', 150) + + set_properties(model, properties) + dialog.setModel(model) + dialog.setVisible(False) + dialog.createPeer(Services.toolkit, None) + return dialog + + def _create(self, properties: dict): + path = properties.pop('Path', '') + if path: + dialog = self._create_from_path(path) + elif 'Location' in properties: + dialog = self._create_from_location(properties) + else: + dialog = self._create_from_properties(properties) + return dialog + + def _init_controls(self): + for control in self.obj.Controls: + tipo = control.ImplementationName + name = control.Model.Name + if not tipo in TYPE_CONTROL: + log.debug(f'Type control: {tipo}') + raise AttributeError(f"Has no class '{tipo}'") + control = UNO_CLASSES[TYPE_CONTROL[tipo]](control) + setattr(self, name, control) + self._controls[name] = control + return + + def execute(self): + return self.obj.execute() + + def open(self): + return self.execute() + + def close(self, value=0): + value = self.obj.endDialog(value) + return value + + # ~ def close(self, value=0): + # ~ if self._modal: + # ~ value = self.obj.endDialog(value) + # ~ else: + # ~ self.visible = False + # ~ self.obj.dispose() + # ~ return value + + # ~ def show(self, modal=True): + # ~ self._modal = modal + # ~ if modal: + # ~ return self.obj.execute() + # ~ else: + # ~ self.visible = True + # ~ return + + @property + def model(self): + return self.obj.Model + + @property + def name(self): + return self.model.Name + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + @property + def width(self): + return self.model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + self._color_on_focus = get_color(value) + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers(self) + self._connect_listeners() + + def _connect_listeners(self): + for control in self.obj.Controls: + add_listeners(self.events, control) + return + + def _set_image_url(self, image: str): + if Paths.exists(image): + return Paths.to_url(image) + + path = Paths.join(self._path, DIR['images'], image) + return Paths.to_url(path) + + def _special_properties(self, tipo, properties): + if tipo == 'link' and not 'Label' in properties: + properties['Label'] = properties['URL'] + return properties + + if tipo == 'button': + if 'ImageURL' in properties: + properties['ImageURL'] = self._set_image_url(properties['ImageURL']) + properties['FocusOnClick'] = properties.get('FocusOnClick', False) + return properties + + if tipo == 'roadmap': + properties['Height'] = properties.get('Height', self.height) + if 'Title' in properties: + properties['Text'] = properties.pop('Title') + return properties + + if tipo == 'tree': + properties['SelectionType'] = properties.get('SelectionType', SINGLE) + return properties + + if tipo == 'grid': + properties['X'] = properties.get('X', SEPARATION) + properties['Y'] = properties.get('Y', SEPARATION) + properties['Width'] = properties.get('Width', self.width - SEPARATION * 2) + properties['Height'] = properties.get('Height', self.height - SEPARATION * 2) + properties['ShowRowHeader'] = properties.get('ShowRowHeader', True) + return properties + + if tipo == 'pages': + properties['Width'] = properties.get('Width', self.width) + properties['Height'] = properties.get('Height', self.height) + + return properties + + def add_control(self, properties): + tipo = properties.pop('Type').lower() + root = properties.pop('Root', '') + sheets = properties.pop('Sheets', ()) + columns = properties.pop('Columns', ()) + + properties = self._special_properties(tipo, properties) + model = self.model.createInstance(MODELS[tipo]) + + set_properties(model, properties) + name = properties['Name'] + self.model.insertByName(name, model) + control = self.obj.getControl(name) + add_listeners(self.events, control) + control = UNO_CLASSES[tipo](control) + + if tipo in ('listbox',): + control.path = self.path + + if tipo == 'tree' and root: + control.root = root + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + self._controls[name] = control + return control + + +class LODialog(): + @classmethod + def create(cls, properties: dict={}): + return DialogBox(properties) + + +def inputbox(message, default='', title=TITLE, echochar=''): + + class ControllersInput(object): + + def __init__(self, dialog): + self.d = dialog + + def cmd_ok_action(self, event): + self.d.close(1) + return + + properties = { + 'Title': title, + 'Width': 200, + 'Height': 80, + } + dlg = DialogBox(properties) + dlg.events = ControllersInput + + properties = { + 'Type': 'Label', + 'Name': 'lbl_msg', + 'Label': message, + 'Width': 140, + 'Height': 50, + 'X': 5, + 'Y': 5, + 'MultiLine': True, + 'Border': 1, + } + dlg.add_control(properties) + + properties = { + 'Type': 'Text', + 'Name': 'txt_value', + 'Text': default, + 'Width': 190, + 'Height': 15, + } + if echochar: + properties['EchoChar'] = ord(echochar[0]) + dlg.add_control(properties) + dlg.txt_value.move(dlg.lbl_msg) + + properties = { + 'Type': 'button', + 'Name': 'cmd_ok', + 'Label': _('OK'), + 'Width': 40, + 'Height': 15, + 'DefaultButton': True, + 'PushButtonType': 1, + } + dlg.add_control(properties) + dlg.cmd_ok.move(dlg.lbl_msg, 10, 0) + + properties = { + 'Type': 'button', + 'Name': 'cmd_cancel', + 'Label': _('Cancel'), + 'Width': 40, + 'Height': 15, + 'PushButtonType': 2, + } + dlg.add_control(properties) + dlg.cmd_cancel.move(dlg.cmd_ok) + + value = '' + if dlg.open(): + value = dlg.txt_value.value + + return value diff --git a/source/easymacro/easyevents.py b/source/easymacro/easyevents.py new file mode 100644 index 0000000..e660d6f --- /dev/null +++ b/source/easymacro/easyevents.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +import unohelper + +from com.sun.star.awt import XActionListener +from com.sun.star.lang import XEventListener + + +__all__ = [ + 'EventsButton', +] + + +class EventsListenerBase(unohelper.Base, XEventListener): + + def __init__(self, controller, name, window=None): + self._controller = controller + self._name = name + self._window = window + + @property + def name(self): + return self._name + + def disposing(self, event): + self._controller = None + if not self._window is None: + self._window.setMenuBar(None) + + +class EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def actionPerformed(self, event): + event_name = f'{self.name}_action' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return diff --git a/source/easymacro/easymain.py b/source/easymacro/easymain.py index 590e8c8..b643805 100644 --- a/source/easymacro/easymain.py +++ b/source/easymacro/easymain.py @@ -57,7 +57,7 @@ logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) log = logging.getLogger(__name__) -def create_instance(name: str, with_context: bool=False, argument: Any=None) -> Any: +def create_instance(name: str, with_context: bool=False, arguments: Any=None) -> Any: """Create a service instance :param name: Name of service @@ -72,8 +72,8 @@ def create_instance(name: str, with_context: bool=False, argument: Any=None) -> if with_context: instance = SM.createInstanceWithContext(name, CTX) - elif argument: - instance = SM.createInstanceWithArguments(name, (argument,)) + elif arguments: + instance = SM.createInstanceWithArguments(name, (arguments,)) else: instance = SM.createInstance(name) @@ -155,3 +155,4 @@ class BaseObject(): def obj(self): """Return original pyUno object""" return self._obj + diff --git a/source/easymacro/easytools.py b/source/easymacro/easytools.py index d8a217c..59410cd 100644 --- a/source/easymacro/easytools.py +++ b/source/easymacro/easytools.py @@ -1,23 +1,51 @@ #!/usr/bin/env python3 +import csv import datetime +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import time from functools import wraps -from typing import Any +from pathlib import Path from pprint import pprint +from typing import Any, Union +import mailbox +import smtplib +from smtplib import SMTPException, SMTPAuthenticationError +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.utils import formatdate +from email import encoders + +import uno from com.sun.star.awt import MessageBoxButtons from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.beans import PropertyValue from com.sun.star.beans.PropertyConcept import ALL +from com.sun.star.ui.dialogs import TemplateDescription -from .easymain import IS_WIN, TITLE, log, create_instance +from .easymain import ( + CTX, DATE_OFFSET, IS_MAC, IS_WIN, LANG, TITLE, + log, create_instance + ) from .easyuno import MessageBoxType from .easydocs import LODocuments +from .messages import MESSAGES __all__ = [ + 'Dates', + 'Email', 'LOInspect', + 'Paths', 'catch_exception', 'debug', 'error', @@ -25,9 +53,25 @@ __all__ = [ 'mri', 'msgbox', 'save_log', + 'sleep', ] +PYTHON = 'python' +if IS_WIN: + PYTHON = 'python.exe' + + +def _(msg): + if LANG == 'en': + return msg + + if not LANG in MESSAGES: + return msg + + return MESSAGES[LANG][msg] + + def debug(*messages) -> None: """Show messages debug @@ -223,6 +267,46 @@ def errorbox(message: Any, title: str=TITLE) -> int: return msgbox(message, title, type_message_box=MessageBoxType.ERRORBOX) +def sleep(seconds: int): + """Sleep + """ + time.sleep(seconds) + return + + +def set_properties(model, properties): + if 'X' in properties: + properties['PositionX'] = properties.pop('X') + if 'Y' in properties: + properties['PositionY'] = properties.pop('Y') + keys = tuple(properties.keys()) + values = tuple(properties.values()) + model.setPropertyValues(keys, values) + return + + +# ~ https://github.com/django/django/blob/main/django/utils/functional.py#L61 +class classproperty: + + def __init__(self, method=None): + self.fget = method + + def __get__(self, instance, cls=None): + return self.fget(cls) + + def getter(self, method): + self.fget = method + return self + + +class Services(): + + @classproperty + def toolkit(cls): + instance = create_instance('com.sun.star.awt.Toolkit', True) + return instance + + class LOInspect(): """Class inspect @@ -386,11 +470,937 @@ class Dates(object): """ _start = None - @classmethod + @classproperty def now(cls): """Current local date and time - :return: Return the current local date and time, remove microsecond + :return: Return the current local date and time, remove microseconds. :rtype: datetime """ return datetime.datetime.now().replace(microsecond=0) + + @classproperty + def today(cls): + """Current local date + + :return: Return the current local date + :rtype: date + """ + return datetime.date.today() + + @classproperty + def epoch(cls): + """Get unix time + + :return: Return unix time + :rtype: int + + `See Unix Time `_ + """ + n = cls.now + e = int(time.mktime(n.timetuple())) + return e + + @classmethod + def date(cls, year: int, month: int, day: int): + """Get date from year, month, day + + :param year: Year of date + :type year: int + :param month: Month of date + :type month: int + :param day: Day of day + :type day: int + :return: Return the date + :rtype: date + + `See Python date `_ + """ + d = datetime.date(year, month, day) + return d + + @classmethod + def time(cls, hours: int, minutes: int, seconds: int): + """Get time from hour, minutes, seconds + + :param hours: Hours of time + :type hours: int + :param minutes: Minutes of time + :type minutes: int + :param seconds: Seconds of time + :type seconds: int + :return: Return the time + :rtype: datetime.time + """ + t = datetime.time(hours, minutes, seconds) + return t + + @classmethod + def datetime(cls, year: int, month: int, day: int, hours: int, minutes: int, seconds: int): + """Get datetime from year, month, day, hours, minutes and seconds + + :param year: Year of date + :type year: int + :param month: Month of date + :type month: int + :param day: Day of day + :type day: int + :param hours: Hours of time + :type hours: int + :param minutes: Minutes of time + :type minutes: int + :param seconds: Seconds of time + :type seconds: int + :return: Return datetime + :rtype: datetime + """ + dt = datetime.datetime(year, month, day, hours, minutes, seconds) + return dt + + @classmethod + def str_to_date(cls, str_date: str, template: str, to_calc: bool=False): + """Get date from string + + :param str_date: Date in string + :type str_date: str + :param template: Formato of date string + :type template: str + :param to_calc: If date is for used in Calc cell + :type to_calc: bool + :return: Return date or int if used in Calc + :rtype: date or int + + `See Python strptime `_ + """ + d = datetime.datetime.strptime(str_date, template).date() + if to_calc: + d = d.toordinal() - DATE_OFFSET + return d + + @classmethod + def calc_to_date(cls, value: float): + """Get date from calc value + + :param value: Float value from cell + :type value: float + :return: Return the current local date + :rtype: date + + `See Python fromordinal `_ + """ + d = datetime.date.fromordinal(int(value) + DATE_OFFSET) + return d + + @classmethod + def start(cls): + """Start counter + """ + cls._start = cls.now + info('Start: ', cls._start) + return + + @classmethod + def end(cls, get_seconds: bool=True): + """End counter + + :param get_seconds: If return value in total seconds + :type get_seconds: bool + :return: Return the timedelta or total seconds + :rtype: timedelta or int + """ + e = cls.now + td = e - cls._start + result = str(td) + if get_seconds: + result = td.total_seconds() + info('End: ', e) + return result + + +class Paths(object): + """Class for paths + """ + FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' + FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' + REMOTE_FILE_PICKER = 'com.sun.star.ui.dialogs.RemoteFilePicker' + OFFICE_FILE_PICKER = 'com.sun.star.ui.dialogs.OfficeFilePicker' + + def __init__(self, path=''): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + self._path = Path(path) + + @property + def path(self): + """Get base path""" + return str(self._path.parent) + + @property + def file_name(self): + """Get file name""" + return self._path.name + + @property + def name(self): + """Get name""" + return self._path.stem + + @property + def ext(self): + """Get extension""" + return self._path.suffix[1:] + + @property + def size(self): + """Get size""" + return self._path.stat().st_size + + @property + def url(self): + """Get like URL""" + return self._path.as_uri() + + @property + def info(self): + """Get all info like tuple""" + i = (self.path, self.file_name, self.name, self.ext, self.size, self.url) + return i + + @property + def dict(self): + """Get all info like dict""" + data = { + 'path': self.path, + 'file_name': self.file_name, + 'name': self.name, + 'ext': self.ext, + 'size': self.size, + 'url': self.url, + } + return data + + @classproperty + def home(self): + """Get user home""" + return str(Path.home()) + + @classproperty + def documents(self): + """Get user save documents""" + return self.config() + + @classproperty + def user_profile(self): + """Get path user profile""" + path = self.config('UserConfig') + path = str(Path(path).parent) + return path + + @classproperty + def user_config(self): + """Get path config in user profile""" + path = self.config('UserConfig') + return path + + @classproperty + def python(self): + """Get path executable python""" + if IS_WIN: + path = self.join(self.config('Module'), PYTHON) + elif IS_MAC: + path = self.join(self.config('Module'), '..', 'Resources', PYTHON) + else: + path = sys.executable + return path + + @classmethod + def to_url(cls, path: str) -> str: + """Convert paths in format system to URL + + :param path: Path to convert + :type path: str + :return: Path in URL + :rtype: str + """ + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def to_system(cls, path:str) -> str: + """Convert paths in URL to system + + :param path: Path to convert + :type path: str + :return: Path system format + :rtype: str + """ + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def config(cls, name: str='Work') -> Union[str, list]: + """Return path from config + + :param name: Name in service PathSettings, default get path documents + :type name: str + :return: Path in config, if exists. + :rtype: str or list + + `See Api XPathSettings `_ + """ + path = create_instance('com.sun.star.util.PathSettings') + path = cls.to_system(getattr(path, name)).split(';') + if len(path) == 1: + path = path[0] + return path + + @classmethod + def join(cls, *paths: str) -> str: + """Join paths + + :param paths: Paths to join + :type paths: list + :return: New path with joins + :rtype: str + """ + path = str(Path(paths[0]).joinpath(*paths[1:])) + return path + + @classmethod + def exists(cls, path: str) -> bool: + """If exists path + + :param path: Path for validate + :type path: str + :return: True if path exists, False if not. + :rtype: bool + """ + path = cls.to_system(path) + result = Path(path).exists() + return result + + @classmethod + def exists_app(cls, name_app: str) -> bool: + """If exists app in system + + :param name_app: Name of application + :type name_app: str + :return: True if app exists, False if not. + :rtype: bool + """ + result = bool(shutil.which(name_app)) + return result + + @classmethod + def is_dir(cls, path: str): + """Validate if path is directory + + :param path: Path for validate + :type path: str + :return: True if path is directory, False if not. + :rtype: bool + """ + return Path(path).is_dir() + + @classmethod + def is_file(cls, path: str): + """Validate if path is a file + + :param path: Path for validate + :type path: str + :return: True if path is a file, False if not. + :rtype: bool + """ + return Path(path).is_file() + + @classmethod + def temp_file(self): + """Make temporary file""" + return tempfile.NamedTemporaryFile(mode='w') + + @classmethod + def temp_dir(self): + """Make temporary directory""" + return tempfile.TemporaryDirectory(ignore_cleanup_errors=True) + + @classmethod + def get(cls, init_dir: str='', filters: str='', multiple: bool=False) -> Union[str, list]: + """Get path for open + + :param init_dir: Initial default path + :type init_dir: str + :param filters: Filter for show type files: 'xml' or 'txt,xml' + :type filters: str + :param multiple: If user can selected multiple files + :type multiple: bool + :return: Selected path or paths + :rtype: str or list + + `See API `_ + """ + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) + file_picker = create_instance(cls.FILE_PICKER) + file_picker.setTitle(_('Select path')) + file_picker.setDisplayDirectory(init_dir) + file_picker.initialize((TemplateDescription.FILEOPEN_SIMPLE,)) + file_picker.setMultiSelectionMode(multiple) + if filters: + for f in filters.split(','): + file_picker.appendFilter(f.upper(), f'*.{f.lower()}') + + if file_picker.execute(): + paths = [cls.to_system(p) for p in file_picker.getSelectedFiles()] + if not multiple: + paths = paths[0] + return paths + + @classmethod + def get_dir(cls, init_dir: str='') -> str: + """Get path dir + + :param init_dir: Initial default path + :type init_dir: str + :return: Selected path + :rtype: str + """ + folder_picker = create_instance(cls.FOLDER_PICKER) + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) + folder_picker.setTitle(_('Select directory')) + folder_picker.setDisplayDirectory(init_dir) + + path = '' + if folder_picker.execute(): + path = cls.to_system(folder_picker.getDirectory()) + return path + + @classmethod + def get_for_save(cls, init_dir: str='', filters: str=''): + """Get path for save + + :param init_dir: Initial default path + :type init_dir: str + :param filters: Filter for show type files: 'xml' or 'txt,xml' + :type filters: str + :return: Selected path + :rtype: str + """ + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) + + file_picker = create_instance(cls.FILE_PICKER) + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.initialize((TemplateDescription.FILESAVE_SIMPLE,)) + + if filters: + for f in filters.split(','): + file_picker.appendFilter(f.upper(), f'*.{f.lower()}') + + path = '' + if file_picker.execute(): + files = file_picker.getSelectedFiles() + path = [cls.to_system(f) for f in files][0] + return path + + @classmethod + def files(cls, path: str, pattern: str='*'): + """Get all files in path + + :param path: Path with files + :type path: str + :param pattern: For filter files, default get all. + :type pattern: str + :return: Files in path + :rtype: list + """ + files = [str(p) for p in Path(path).glob(pattern) if p.is_file()] + return files + + @classmethod + def walk(cls, path, filters=''): + """Get all files in path recursively + + :param path: Path with files + :type path: str + :param filters: For filter files, default get all. + :type filters: str + :return: Files in path + :rtype: list + """ + paths = [] + for folder, _, files in os.walk(path): + if filters: + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [cls.join(folder, f) for f in files if pattern.search(f)] + else: + paths += [cls.join(folder, f) for f in files] + return paths + + @classmethod + def dirs(cls, path): + """Get directories in path + + :param path: Path to scan + :type path: str + :return: Directories in path + :rtype: list + """ + dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()] + return dirs + + @classmethod + def walk_dirs(cls, path, tree=False): + """Get directories recursively + + :param path: Path to scan + :type path: str + :param tree: get info in a tuple (ID_FOLDER, ID_PARENT, NAME) + :type tree: bool + :return: Directories in path + :rtype: list + """ + folders = [] + if tree: + i = 0 + parents = {path: 0} + for root, dirs, _ in os.walk(path): + for name in dirs: + i += 1 + rn = cls.join(root, name) + if not rn in parents: + parents[rn] = i + folders.append((i, parents[root], name)) + else: + for root, dirs, _ in os.walk(path): + folders += [cls.join(root, name) for name in dirs] + return folders + + @classmethod + def extension(cls, id_ext: str): + """Get path extension install from id + + :param id_ext: ID extension + :type id_ext: str + :return: Path extension + :rtype: str + """ + pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') + path = Paths.to_system(pip.getPackageLocation(id_ext)) + return path + + @classmethod + def replace_ext(cls, path: str, new_ext: str): + """Replace extension in file path + + :param path: Path to file + :type path: str + :param new_ext: New extension + :type new_ext: str + :return: Path with new extension + :rtype: str + """ + p = Paths(path) + name = f'{p.name}.{new_ext}' + path = cls.join(p.path, name) + return path + + @classmethod + def open(cls, path: str): + """Open any file with default program in system + + :param path: Path to file + :type path: str + :return: PID file, only Linux + :rtype: int + """ + pid = 0 + if IS_WIN: + os.startfile(path) + else: + pid = subprocess.Popen(['xdg-open', path]).pid + return pid + + # ~ Save/read data + + @classmethod + def save(cls, path: str, data: str, encoding: str='utf-8') -> bool: + """Save data in path with encoding + + :param path: Path to file save + :type path: str + :param data: Data to save + :type data: str + :param encoding: Encoding for save data, default utf-8 + :type encoding: str + :return: True, if save corrrectly + :rtype: bool + """ + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path: str, data: bytes) -> bool: + """Save binary data in path + + :param path: Path to file save + :type path: str + :param data: Data to save + :type data: bytes + :return: True, if save corrrectly + :rtype: bool + """ + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def read(cls, path: str, get_lines: bool=False, encoding: str='utf-8') -> Union[str, list]: + """Read data in path + + :param path: Path to file read + :type path: str + :param get_lines: If read file line by line + :type get_lines: bool + :return: File content + :rtype: str or list + """ + if get_lines: + with Path(path).open(encoding=encoding) as f: + data = f.readlines() + else: + data = Path(path).read_text(encoding=encoding) + return data + + @classmethod + def read_bin(cls, path: str) -> bytes: + """Read binary data in path + + :param path: Path to file read + :type path: str + :return: File content + :rtype: bytes + """ + data = Path(path).read_bytes() + return data + + # ~ Import/export data + + @classmethod + def save_json(cls, path: str, data: str): + """Save data in path file like json + + :param path: Path to file + :type path: str + :return: True if save correctly + :rtype: bool + """ + data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) + return cls.save(path, data) + + @classmethod + def read_json(cls, path: str) -> Any: + """Read path file and load json data + + :param path: Path to file + :type path: str + :return: Any data + :rtype: Any + """ + data = json.loads(cls.read(path)) + return data + + @classmethod + def save_csv(cls, path: str, data: Any, args: dict={}): + """Write CSV + + :param path: Path to file write csv + :type path: str + :param data: Data to write + :type data: Iterable + :param args: Any argument support for Python library + :type args: dict + + `See CSV Writer `_ + """ + with open(path, 'w') as f: + writer = csv.writer(f, **args) + writer.writerows(data) + return + + @classmethod + def read_csv(cls, path: str, args: dict={}) -> list: + """Read CSV + + :param path: Path to file csv + :type path: str + :param args: Any argument support for Python library + :type args: dict + :return: Data csv like tuple + :rtype: tuple + + `See CSV Reader `_ + """ + with open(path) as f: + rows = list(csv.reader(f, **args)) + return rows + + @classmethod + def kill(cls, path: str) -> bool: + """Delete path + + :param path: Path to file or directory + :type path: str + :return: True if delete correctly + :rtype: bool + """ + result = False + + p = Path(path) + try: + if p.is_file(): + p.unlink() + result = True + elif p.is_dir(): + shutil.rmtree(path) + result = True + except OSError as e: + log.error(e) + + return result + + @classmethod + def copy(cls, source: str, target: str='', name: str='') -> str: + """Copy files + + :param source: Path source + :type source: str + :param target: Path target + :type target: str + :param name: New name in target + :type name: str + :return: Path target + :rtype: str + """ + p, f, n, e, _, _ = Paths(source).info + if target: + p = target + e = f'.{e}' + if name: + e = '' + n = name + path_new = cls.join(p, f'{n}{e}') + shutil.copy(source, path_new) + return path_new + + @classmethod + def zip(cls, source: Union[str, tuple, list], target: str='') -> str: + path_zip = target + if not isinstance(source, (tuple, list)): + path, _, name, _ = _P(source).info + start = len(path) + 1 + if not target: + path_zip = f'{path}/{name}.zip' + + if isinstance(source, (tuple, list)): + files = [(f, f[len(_P(f).path)+1:]) for f in source] + elif _P.is_file(source): + files = ((source, source[start:]),) + else: + files = [(f, f[start:]) for f in _P.walk(source)] + + compression = zipfile.ZIP_DEFLATED + with zipfile.ZipFile(path_zip, 'w', compression=compression) as z: + for f in files: + z.write(f[0], f[1]) + return path_zip + + @classmethod + def unzip(cls, source: str, target: str='', members=None, pwd=None): + path = target + if not target: + path = _P(source).path + with zipfile.ZipFile(source) as z: + if not pwd is None: + pwd = pwd.encode() + if isinstance(members, str): + members = (members,) + z.extractall(path, members=members, pwd=pwd) + return + + @classmethod + def zip_content(cls, path: str): + with zipfile.ZipFile(path) as z: + names = z.namelist() + return names + + @classmethod + def merge_zip(cls, target, zips): + try: + with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: + for path in zips: + with zipfile.ZipFile(path, compression=zipfile.ZIP_DEFLATED) as s: + for name in s.namelist(): + t.writestr(name, s.open(name).read()) + except Exception as e: + error(e) + return False + + return True + + +class Email(): + """Class for send email + """ + class SmtpServer(object): + + def __init__(self, config): + self._server = None + self._error = '' + self._sender = '' + self._is_connect = self._login(config) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @property + def is_connect(self): + return self._is_connect + + @property + def error(self): + return self._error + + def _login(self, config): + name = config['server'] + port = config['port'] + is_ssl = config['ssl'] + starttls = config.get('starttls', False) + self._sender = config['user'] + try: + if starttls: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + self._server.ehlo() + self._server.starttls() + self._server.ehlo() + elif is_ssl: + self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) + self._server.ehlo() + else: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + + self._server.login(self._sender, config['password']) + msg = 'Connect to: {}'.format(name) + debug(msg) + return True + except smtplib.SMTPAuthenticationError as e: + if '535' in str(e): + self._error = _('Incorrect user or password') + return False + if '534' in str(e) and 'gmail' in name: + self._error = _('Allow less secure apps in GMail') + return False + except smtplib.SMTPException as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return False + + def _body(self, msg): + body = msg.replace('\n', '
') + return body + + def send(self, message): + # ~ file_name = 'attachment; filename={}' + email = MIMEMultipart() + email['From'] = self._sender + email['To'] = message['to'] + email['Cc'] = message.get('cc', '') + email['Subject'] = message['subject'] + email['Date'] = formatdate(localtime=True) + if message.get('confirm', False): + email['Disposition-Notification-To'] = email['From'] + email.attach(MIMEText(self._body(message['body']), 'html')) + + paths = message.get('files', ()) + if isinstance(paths, str): + paths = (paths,) + for path in paths: + fn = _P(path).file_name + # ~ print('NAME', fn) + part = MIMEBase('application', 'octet-stream') + part.set_payload(_P.read_bin(path)) + encoders.encode_base64(part) + part.add_header('Content-Disposition', f'attachment; filename="{fn}"') + email.attach(part) + + receivers = ( + email['To'].split(',') + + email['CC'].split(',') + + message.get('bcc', '').split(',')) + try: + self._server.sendmail(self._sender, receivers, email.as_string()) + msg = 'Email sent...' + debug(msg) + if message.get('path', ''): + self.save_message(email, message['path']) + return True + except Exception as e: + self._error = str(e) + return False + return False + + def save_message(self, email, path): + mbox = mailbox.mbox(path, create=True) + mbox.lock() + try: + msg = mailbox.mboxMessage(email) + mbox.add(msg) + mbox.flush() + finally: + mbox.unlock() + return + + def close(self): + try: + self._server.quit() + msg = 'Close connection...' + debug(msg) + except: + pass + return + + @classmethod + def _send_email(cls, server, messages): + with cls.SmtpServer(server) as server: + if server.is_connect: + for msg in messages: + server.send(msg) + else: + log.error(server.error) + return server.error + + @classmethod + def send(cls, server: dict, messages: Union[dict, tuple, list]): + """Send email with config server, emails send in thread. + + :param server: Configuration for send emails + :type server: dict + :param server: Dictionary con message or list of messages + :type server: dict or iterator + """ + if isinstance(messages, dict): + messages = (messages,) + t = threading.Thread(target=cls._send_email, args=(server, messages)) + t.start() + return diff --git a/source/easymacro/messages.py b/source/easymacro/messages.py new file mode 100644 index 0000000..9302dc1 --- /dev/null +++ b/source/easymacro/messages.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +MESSAGES = { + 'es': { + 'OK': 'Aceptar', + 'Cancel': 'Cancelar', + 'Select path': 'Seleccionar ruta', + 'Select directory': 'Seleccionar directorio', + 'Select file': 'Seleccionar archivo', + 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', + 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', + } +}