From b78a99a969faf58fb1f408bde06f329c9f05449e Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 6 Jul 2021 21:52:12 -0500 Subject: [PATCH] =?UTF-8?q?Primera=20versi=C3=B3n=20de=20pruebas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG | 2 + VERSION | 1 + conf.py | 431 ++ easymacro.py | 7289 ++++++++++++++++++++++++++++ files/ZAZPolizas_v0.1.0.oxt | Bin 0 -> 66679 bytes images/logo.png | Bin 0 -> 20104 bytes source/Addons.xcu | 54 + source/META-INF/manifest.xml | 6 + source/Office/Accelerators.xcu | 21 + source/ZAZPolizas.py | 21 + source/description.xml | 26 + source/description/desc_en.txt | 1 + source/description/desc_es.txt | 1 + source/images/zazpolizas.png | Bin 0 -> 20104 bytes source/pythonpath/easymacro.py | 7289 ++++++++++++++++++++++++++++ source/pythonpath/main.py | 217 + source/registration/license_en.txt | 14 + source/registration/license_es.txt | 14 + zaz.py | 822 ++++ 19 files changed, 16209 insertions(+) create mode 100644 CHANGELOG create mode 100644 VERSION create mode 100644 conf.py create mode 100644 easymacro.py create mode 100644 files/ZAZPolizas_v0.1.0.oxt create mode 100644 images/logo.png create mode 100644 source/Addons.xcu create mode 100644 source/META-INF/manifest.xml create mode 100644 source/Office/Accelerators.xcu create mode 100644 source/ZAZPolizas.py create mode 100644 source/description.xml create mode 100644 source/description/desc_en.txt create mode 100644 source/description/desc_es.txt create mode 100644 source/images/zazpolizas.png create mode 100644 source/pythonpath/easymacro.py create mode 100644 source/pythonpath/main.py create mode 100644 source/registration/license_en.txt create mode 100644 source/registration/license_es.txt create mode 100755 zaz.py diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..f1ec520 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,2 @@ +v 0.1.0 [07-jul-2021] + - Initial version diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/conf.py b/conf.py new file mode 100644 index 0000000..a5ccce5 --- /dev/null +++ b/conf.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 + +# ~ This file is part of ZAZ. + +# ~ ZAZ 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. + +# ~ ZAZ 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 ZAZ. If not, see . + + +import logging + + +# ~ Type extension: +# ~ 1 = normal extension +# ~ 2 = new component +# ~ 3 = Calc addin +TYPE_EXTENSION = 1 + +# ~ Your great extension name, not used spaces +NAME = 'ZAZPolizas' + +# ~ https://semver.org/ +VERSION = '0.1.0' + + +# ~ Should be unique, used URL inverse +ID = 'net.elmau.zaz.polizas' + +# ~ If you extension will be multilanguage set: True +# ~ This feature used gettext, set pythonpath and easymacro in True +# ~ You can used PoEdit for edit PO files and generate MO files. +# ~ https://poedit.net/ +USE_LOCALES = True +DOMAIN = 'base' +PATH_LOCALES = 'locales' +PATH_PYGETTEXT = '/usr/lib/python3.9/Tools/i18n/pygettext.py' +# ~ You can use PoEdit for update locales too +PATH_MSGMERGE = 'msgmerge' + + +# ~ Show in extension manager +PUBLISHER = { + 'en': {'text': 'El Mau', 'link': 'https://gitlab.com/mauriciobaeza'}, + 'es': {'text': 'El Mau', 'link': 'https://gitlab.com/mauriciobaeza'}, +} + +# ~ Name in this folder for copy +ICON = 'images/logo.png' +# ~ Name inside extensions +ICON_EXT = f'{NAME.lower()}.png' + + +# ~ Change for you favorite license +LICENSE_EN = f"""This file is part of {NAME}. + + {NAME} 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. + + {NAME} 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 {NAME}. If not, see . +""" +LICENSE_ES = LICENSE_EN + +INFO = { + 'en': { + 'display_name': 'ZAZ Polizas', + 'description': 'Process polizas from txt', + 'license': LICENSE_EN, + }, + 'es': { + 'display_name': 'ZAZ Polizas', + 'description': 'Procesa pólizas desde un archivo txt', + 'license': LICENSE_ES, + }, +} + + +# ~ Menus, only for TYPE_EXTENSION = 1 +# ~ Parent can be: AddonMenu or OfficeMenuBar +# ~ For icons con name: NAME_16.bmp, used only NAME +# ~ PARENT = '' +# ~ MENU_MAIN = {} +# ~ Shortcut: Key + "Modifier Keys" +# ~ Important: Not used any shortcuts used for LibreOffice +# ~ SHIFT is mapped to Shift on all platforms. +# ~ MOD1 is mapped to Ctrl on Windows/Linux, while it is mapped to Cmd on Mac. +# ~ MOD2 is mapped to Alt on all platforms. +# ~ For example: Shift+Ctrl+Alt+T -> T_SHIFT_MOD1_MOD2 +PARENT = 'OfficeMenuBar' +MENU_MAIN = { + 'en': 'Polizas', + 'es': 'Polizas', +} +MENUS = ( + { + 'title': {'en': 'Process...', 'es': 'Procesar...'}, + 'argument': 'procesar', + 'context': 'calc', + 'icon': 'icon', + 'toolbar': False, + 'shortcut': 'P_SHIFT_MOD1_MOD2', + }, + { + 'title': {'en': 'Generate TXT...', 'es': 'Generar TXT...'}, + 'argument': 'generar', + 'context': 'calc', + 'icon': 'icon', + 'toolbar': False, + 'shortcut': 'T_SHIFT_MOD1_MOD2', + }, +) + + +# ~ Functions, only for TYPE_EXTENSION = 3 +FUNCTIONS = { + 'test': { + 'displayname': {'en': 'test', 'es': 'prueba'}, + 'description': {'en': 'My test', 'es': 'Mi prueba'}, + 'parameters': { + 'value': { + 'displayname': {'en': 'value', 'es': 'valor'}, + 'description': {'en': 'The value', 'es': 'El valor'}, + }, + }, + }, +} +# ~ FUNCTIONS = {} + +EXTENSION = { + 'version': VERSION, + 'name': NAME, + 'id': ID, + 'icon': (ICON, ICON_EXT), + 'languages': tuple(INFO.keys()) +} + + +# ~ If used more libraries set python path in True and copy inside +# ~ If used easymacro pythonpath always is True, recommended +DIRS = { + 'meta': 'META-INF', + 'source': 'source', + 'description': 'description', + 'images': 'images', + 'registration': 'registration', + 'files': 'files', + 'office': 'Office', + 'locales': PATH_LOCALES, + 'pythonpath': True, +} + + +FILES = { + 'oxt': f'{NAME}_v{VERSION}.oxt', + 'py': f'{NAME}.py', + 'ext_desc': 'desc_{}.txt', + 'manifest': 'manifest.xml', + 'description': 'description.xml', + 'idl': f'X{NAME}.idl', + 'addons': 'Addons.xcu', + 'urd': f'X{NAME}.urd', + 'rdb': f'X{NAME}.rdb', + 'update': f'{NAME.lower()}.update.xml', + 'addin': 'CalcAddIn.xcu', + 'shortcut': 'Accelerators.xcu', + 'easymacro': True, +} + + +# ~ URLs for update for example +# ~ URL_XML_UPDATE = 'https://gitlab.com/USER/PROYECT/raw/BRANCH/FOLDERs/FILE_NAME' +URL_XML_UPDATE = '' +URL_OXT = '' + + +# ~ Default program for test: --calc, --writer, --draw +PROGRAM = '--calc' +# ~ Path to file for test +FILE_TEST = '/home/mau/Projects/clients/nacho/Layout_Inverso_Contpaqi_2021.ods' + +PATHS = { + 'idlc': '/usr/lib/libreoffice/sdk/bin/idlc', + 'include': '/usr/share/idl/libreoffice', + 'regmerge': '/usr/lib/libreoffice/program/regmerge', + 'soffice': ('soffice', PROGRAM, FILE_TEST), + 'install': ('unopkg', 'add', '-v', '-f', '-s'), + 'profile': '/home/mau/.config/libreoffice/4/user', + 'gettext': PATH_PYGETTEXT, + 'msgmerge': PATH_MSGMERGE, +} + + +SERVICES = { + 'job': "('com.sun.star.task.Job',)", + 'addin': "('com.sun.star.sheet.AddIn',)", +} + + +FORMAT = '%(asctime)s - %(levelname)s - %(message)s' +DATE = '%d/%m/%Y %H:%M:%S' +LEVEL_ERROR = logging.getLevelName(logging.ERROR) +LEVEL_INFO = logging.getLevelName(logging.INFO) +logging.addLevelName(logging.ERROR, f'\033[1;41m{LEVEL_ERROR}\033[1;0m') +logging.addLevelName(logging.INFO, f'\x1b[32m{LEVEL_INFO}\033[1;0m') +logging.basicConfig(level=logging.DEBUG, format=FORMAT, datefmt=DATE) +log = logging.getLogger(NAME) + + +def _methods(): + template = """ def {0}(self, {1}): + print({1}) + return 'ok'\n""" + functions = '' + for k, v in FUNCTIONS.items(): + args = ','.join(v['parameters'].keys()) + functions += template.format(k, args) + return functions + + +SRV = SERVICES['job'] +XSRV = 'XJobExecutor' +SRV_IMPORT = f'from com.sun.star.task import {XSRV}' +METHODS = """ def trigger(self, args='pyUNO'): + print('Hello World', args) + return\n""" + +if TYPE_EXTENSION > 1: + MENUS = () + XSRV = f'X{NAME}' + SRV_IMPORT = f'from {ID} import {XSRV}' +if TYPE_EXTENSION == 2: + SRV = f"('{ID}',)" + METHODS = """ def test(self, args='pyUNO'): + print('Hello World', args) + return\n""" +elif TYPE_EXTENSION == 3: + SRV = SERVICES['addin'] + METHODS = _methods() + + +DATA_PY = f"""import uno +import unohelper +{SRV_IMPORT} + + +ID_EXTENSION = '{ID}' +SERVICE = {SRV} + + +class {NAME}(unohelper.Base, {XSRV}): + + def __init__(self, ctx): + self.ctx = ctx + +{METHODS} + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation({NAME}, ID_EXTENSION, SERVICE) +""" + +def _functions(): + a = '[in] any {}' + t = ' any {}({});' + f = '' + for k, v in FUNCTIONS.items(): + args = ','.join([a.format(k) for k, v in v['parameters'].items()]) + f += t.format(k, args) + return f + + +FILE_IDL = '' +if TYPE_EXTENSION > 1: + id_ext = ID.replace('.', '_') + interface = f'X{NAME}' + module = '' + for i, P in enumerate(ID.split('.')): + module += f'module {P} {{ ' + close_module = '}; ' * (i + 1) + functions = ' void test([in] any argument);' + if TYPE_EXTENSION == 3: + functions = _functions() + + FILE_IDL = f"""#ifndef __{id_ext}_idl__ +#define __{id_ext}_idl__ + +#include + +{module} + + interface {interface} : com::sun::star::uno::XInterface + {{ +{functions} + }}; + + service {P} {{ + interface {interface}; + }}; + +{close_module} +#endif +""" + + +def _parameters(args): + NODE = """ + +{displayname} + + +{description} + + """ + line = '{}{}' + node = '' + for k, v in args.items(): + displayname = '\n'.join( + [line.format(' ' * 16, k, v) for k, v in v['displayname'].items()]) + description = '\n'.join( + [line.format(' ' * 16, k, v) for k, v in v['description'].items()]) + values = { + 'name': k, + 'displayname': displayname, + 'description': description, + } + node += NODE.format(**values) + return node + + +NODE_FUNCTIONS = '' +if TYPE_EXTENSION == 3: + tmp = '{}{}' + NODE_FUNCTION = """ + +{displayname} + + +{description} + + + Add-In + + + AutoAddIn.{name} + + +{parameters} + + """ + + for k, v in FUNCTIONS.items(): + displayname = '\n'.join( + [tmp.format(' ' * 12, k, v) for k, v in v['displayname'].items()]) + description = '\n'.join( + [tmp.format(' ' * 12, k, v) for k, v in v['description'].items()]) + parameters = _parameters(v['parameters']) + values = { + 'name': k, + 'displayname': displayname, + 'description': description, + 'parameters': parameters, + } + NODE_FUNCTIONS += NODE_FUNCTION.format(**values) + + +FILE_ADDIN = f""" + + + + +{NODE_FUNCTIONS} + + + +""" + + +DATA_MANIFEST = [FILES['py'], f"Office/{FILES['shortcut']}", 'Addons.xcu'] +if TYPE_EXTENSION > 1: + DATA_MANIFEST.append(FILES['rdb']) +if TYPE_EXTENSION == 3: + DATA_MANIFEST.append('CalcAddIn.xcu') + +DATA_DESCRIPTION = { + 'identifier': {'value': ID}, + 'version': {'value': VERSION}, + 'display-name': {k: v['display_name'] for k, v in INFO.items()}, + 'icon': ICON_EXT, + 'publisher': PUBLISHER, + 'update': URL_XML_UPDATE, +} + +DATA_ADDONS = { + 'parent': PARENT, + 'images': DIRS['images'], + 'main': MENU_MAIN, + 'menus': MENUS, +} + +DATA = { + 'py': DATA_PY, + 'manifest': DATA_MANIFEST, + 'description': DATA_DESCRIPTION, + 'addons': DATA_ADDONS, + 'update': URL_OXT, + 'idl': FILE_IDL, + 'addin': FILE_ADDIN, +} + + +with open('VERSION', 'w') as f: + f.write(VERSION) + + +# ~ LICENSE_ACCEPT_BY = 'user' # or admin +# ~ LICENSE_SUPPRESS_ON_UPDATE = True diff --git a/easymacro.py b/easymacro.py new file mode 100644 index 0000000..27ca1aa --- /dev/null +++ b/easymacro.py @@ -0,0 +1,7289 @@ +#!/usr/bin/env python3 + +# == Rapid Develop Macros in LibreOffice == + +# ~ This file is part of ZAZ. + +# ~ https://git.cuates.net/elmau/zaz + +# ~ ZAZ 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. + +# ~ ZAZ 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 ZAZ. If not, see . + +import base64 +import csv +import ctypes +import datetime +import getpass +import gettext +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 +import zipfile + +from collections import OrderedDict +from collections.abc import MutableMapping +from decimal import Decimal +from enum import IntEnum +from functools import wraps +from pathlib import Path +from pprint import pprint +from socket import timeout +from string import Template +from typing import Any, Union +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError + +import imaplib +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 mailbox + +import uno +import unohelper +from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS +from com.sun.star.awt.MessageBoxResults import YES +from com.sun.star.awt import Rectangle, Size, Point +from com.sun.star.awt.PosSize import POSSIZE, SIZE +from com.sun.star.awt import Key, KeyModifier, KeyEvent +from com.sun.star.container import NoSuchElementException +from com.sun.star.datatransfer import XTransferable, DataFlavor + +from com.sun.star.beans import PropertyValue, NamedValue +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.text.TextContentAnchorType import AS_CHARACTER + +from com.sun.star.lang import Locale +from com.sun.star.lang import XEventListener +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 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 + +from com.sun.star.io import IOException, XOutputStream + +# ~ 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' + +TIMEOUT = 10 +LOG_NAME = 'ZAZ' +FILE_NAME_CONFIG = 'zaz-{}.json' + +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', + BASE: 'com.sun.star.sdb.DocumentDataSource', + MATH: 'com.sun.star.formula.FormulaProperties', + BASIC: 'com.sun.star.script.BasicIDE', + MAIN: 'com.sun.star.frame.StartModule', +} + +OBJ_CELL = 'ScCellObj' +OBJ_RANGE = 'ScCellRangeObj' +OBJ_RANGES = 'ScCellRangesObj' +TYPE_RANGES = (OBJ_CELL, OBJ_RANGE) + +OBJ_SHAPES = 'com.sun.star.drawing.SvxShapeCollection' +OBJ_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' +OBJ_GRAPHIC = 'SwXTextGraphicObject' + +OBJ_TEXTS = 'SwXTextRanges' +OBJ_TEXT = 'SwXTextRange' + + +# ~ 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 + + +OS = platform.system() +IS_WIN = OS == 'Windows' +IS_MAC = OS == 'Darwin' +USER = getpass.getuser() +PC = platform.node() +DESKTOP = os.environ.get('DESKTOP_SESSION', '') +INFO_DEBUG = f"{sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) + +PYTHON = 'python' +if IS_WIN: + PYTHON = 'python.exe' + +_MACROS = {} +_start = 0 + +SECONDS_DAY = 60 * 60 * 24 +DIR = { + 'images': 'images', + 'locales': 'locales', +} + +KEY = { + 'enter': 1280, +} + +MODIFIERS = { + 'shift': KeyModifier.SHIFT, + 'ctrl': KeyModifier.MOD1, + 'alt': KeyModifier.MOD2, + 'ctrlmac': KeyModifier.MOD3, +} + +# ~ Menus +NODE_MENUBAR = 'private:resource/menubar/menubar' +MENUS = { + 'file': '.uno:PickList', + 'tools': '.uno:ToolsMenu', + 'help': '.uno:HelpMenu', + 'windows': '.uno:WindowList', + 'edit': '.uno:EditMenu', + 'view': '.uno:ViewMenu', + 'insert': '.uno:InsertMenu', + 'format': '.uno:FormatMenu', + 'styles': '.uno:FormatStylesMenu', + 'sheet': '.uno:SheetMenu', + 'data': '.uno:DataMenu', + 'table': '.uno:TableMenu', + 'form': '.uno:FormatFormMenu', + 'page': '.uno:PageMenu', + 'shape': '.uno:ShapeMenu', + 'slide': '.uno:SlideMenu', + 'show': '.uno:SlideShowMenu', +} + +DEFAULT_MIME_TYPE = 'png' +MIME_TYPE = { + 'png': 'image/png', + 'jpg': 'image/jpeg', +} + +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() + + +def create_instance(name: str, with_context: bool=False, args: Any=None) -> Any: + if with_context: + instance = SM.createInstanceWithContext(name, CTX) + elif args: + instance = SM.createInstanceWithArguments(name, (args,)) + else: + instance = SM.createInstance(name) + return instance + + +def get_app_config(node_name: str, key: str=''): + name = 'com.sun.star.configuration.ConfigurationProvider' + service = 'com.sun.star.configuration.ConfigurationAccess' + cp = create_instance(name, True) + node = PropertyValue(Name='nodepath', Value=node_name) + try: + ca = cp.createInstanceWithArguments(service, (node,)) + if ca and not key: + return ca + if ca and ca.hasByName(key): + return ca.getPropertyValue(key) + except Exception as e: + error(e) + return '' + + +LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') +LANG = LANGUAGE.split('-')[0] +try: + COUNTRY = LANGUAGE.split('-')[1] +except: + COUNTRY = '' +LOCALE = Locale(LANG, COUNTRY, '') +NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') +VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') + +INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{INFO_DEBUG}" + +node = '/org.openoffice.Office.Calc/Calculate/Other/Date' +y = get_app_config(node, 'YY') +m = get_app_config(node, 'MM') +d = get_app_config(node, 'DD') +DATE_OFFSET = datetime.date(y, m, d).toordinal() + + +def error(info): + log.error(info) + return + + +def debug(*args): + data = [str(a) for a in args] + log.debug('\t'.join(data)) + return + + +def info(*args): + data = [str(a) for a in args] + log.info('\t'.join(data)) + return + + +def save_log(path: str, data): + with open(path, 'a') as f: + f.write(f'{str(now())[:19]} -{LOG_NAME}- ') + pprint(data, stream=f) + return + + +def catch_exception(f): + @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 inspect(obj: Any) -> None: + zaz = create_instance('net.elmau.zaz.inspect') + if hasattr(obj, 'obj'): + obj = obj.obj + zaz.inspect(obj) + return + + +def mri(obj: Any) -> None: + 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 run_in_thread(fn): + def run(*k, **kw): + t = threading.Thread(target=fn, args=k, kwargs=kw) + t.start() + return t + return run + + +def now(only_time: bool=False): + now = datetime.datetime.now() + if only_time: + now = now.time() + return now + + +def today(): + return datetime.date.today() + + +def _(msg): + if LANG == 'en': + return msg + + if not LANG in MESSAGES: + return msg + + return MESSAGES[LANG][msg] + + +def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): + """ Create message box + type_msg: infobox, warningbox, errorbox, querybox, messbox + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMessageBoxFactory.html + """ + toolkit = create_instance('com.sun.star.awt.Toolkit') + parent = toolkit.getDesktopWindow() + box = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message)) + return box.execute() + + +def question(message, title=TITLE): + result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') + return result == YES + + +def warning(message, title=TITLE): + return msgbox(message, title, type_msg='warningbox') + + +def errorbox(message, title=TITLE): + return msgbox(message, title, type_msg='errorbox') + + +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 dict_to_property(values: dict, uno_any: bool=False): + 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 _array_to_dict(values): + d = {v[0]: v[1] for v in values} + return d + + +def _property_to_dict(values): + d = {v.Name: v.Value for v in values} + return d + + +def json_dumps(data): + return json.dumps(data, indent=4, sort_keys=True) + + +def json_loads(data): + return json.loads(data) + + +def data_to_dict(data): + if isinstance(data, (tuple, list)) and isinstance(data[0], (tuple, list)): + return _array_to_dict(data) + + if isinstance(data, (tuple, list)) and isinstance(data[0], (PropertyValue, NamedValue)): + return _property_to_dict(data) + return {} + + +def _get_dispatch() -> Any: + return create_instance('com.sun.star.frame.DispatchHelper') + + +# ~ https://wiki.documentfoundation.org/Development/DispatchCommands +# ~ Used only if not exists in API +def call_dispatch(frame: Any, url: str, args: dict={}) -> None: + dispatch = _get_dispatch() + if hasattr(frame, 'frame'): + frame = frame.frame + opt = dict_to_property(args) + dispatch.executeDispatch(frame, url, '', 0, opt) + return + + +def get_desktop(): + return create_instance('com.sun.star.frame.Desktop', True) + + +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 _get_url_script(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 + + +def _call_macro(args: dict): + #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification + + url = _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 + + +def call_macro(args, in_thread=False): + result = None + if in_thread: + t = threading.Thread(target=_call_macro, args=(args,)) + t.start() + else: + result = _call_macro(args) + return result + + +def run(command, capture=False, split=False): + 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 + + +def popen(command): + 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) + + +def sleep(seconds): + time.sleep(seconds) + return + + +class TimerThread(threading.Thread): + + def __init__(self, event, seconds, macro): + threading.Thread.__init__(self) + self.stopped = event + self.seconds = seconds + self.macro = macro + + def run(self): + info('Timer started... {}'.format(self.macro['name'])) + while not self.stopped.wait(self.seconds): + _call_macro(self.macro) + info('Timer stopped... {}'.format(self.macro['name'])) + return + + +def start_timer(name, seconds, macro): + global _MACROS + _MACROS[name] = threading.Event() + thread = TimerThread(_MACROS[name], seconds, macro) + thread.start() + return + + +def stop_timer(name): + global _MACROS + _MACROS[name].set() + del _MACROS[name] + return + + +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 sha256(data): + result = hashlib.sha256(data.encode()).hexdigest() + return result + + +def sha512(data): + result = hashlib.sha512(data.encode()).hexdigest() + return result + + +def get_config(key='', prefix='conf', default={}): + name_file = FILE_NAME_CONFIG.format(prefix) + values = None + path = _P.join(_P.config('UserConfig'), name_file) + if not _P.exists(path): + return default + + values = _P.from_json(path) + if key: + values = values.get(key, default) + + return values + + +def set_config(key, value, prefix='conf'): + name_file = FILE_NAME_CONFIG.format(prefix) + path = _P.join(_P.config('UserConfig'), name_file) + values = get_config(default={}, prefix=prefix) + values[key] = value + result = _P.to_json(path, values) + return result + + +def start(): + global _start + + _start = now() + info(_start) + return + + +def end(get_seconds: bool=False): + global _start + + e = now() + td = e - _start + result = str(td) + if get_seconds: + result = td.total_seconds() + return result + + +def get_epoch(): + n = now() + return int(time.mktime(n.timetuple())) + + +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +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 url_open(url, data=None, headers={}, verify=True, get_json=False, timeout=TIMEOUT): + err = '' + req = Request(url) + for k, v in headers.items(): + req.add_header(k, v) + try: + # ~ debug(url) + if verify: + if not data is None and isinstance(data, str): + data = data.encode() + response = urlopen(req, data=data, timeout=timeout) + else: + context = ssl._create_unverified_context() + response = urlopen(req, data=data, timeout=timeout, context=context) + except HTTPError as e: + error(e) + err = str(e) + except URLError as e: + error(e.reason) + err = str(e.reason) + except timeout: + err = 'timeout' + error(err) + else: + headers = dict(response.info()) + result = response.read().decode() + if get_json: + result = json.loads(result) + + return result, headers, err + + +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 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'] + self._sender = config['user'] + hosts = ('gmail' in name or 'outlook' in name) + try: + if is_ssl and hosts: + 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 + + +def _send_email(server, messages): + with SmtpServer(server) as server: + if server.is_connect: + for msg in messages: + server.send(msg) + else: + error(server.error) + return server.error + + +def send_email(server, message): + messages = message + if isinstance(message, dict): + messages = (message,) + t = threading.Thread(target=_send_email, args=(server, messages)) + t.start() + 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 LODocument(object): + + def __init__(self, obj): + self._obj = obj + self._cc = self.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 self._obj + + @property + def title(self): + return self.obj.getTitle() + @title.setter + def title(self, value): + self.obj.setTitle(value) + + @property + def type(self): + return self._type + + @property + def uid(self): + return self.obj.RuntimeUID + + @property + def frame(self): + return self._cc.getFrame() + + @property + def is_saved(self): + return self.obj.hasLocation() + + @property + def is_modified(self): + return self.obj.isModified() + + @property + def is_read_only(self): + return self.obj.isReadonly() + + @property + def path(self): + return _P.to_system(self.obj.URL) + + @property + def dir(self): + return _P(self.path).path + + @property + def file_name(self): + return _P(self.path).file_name + + @property + def name(self): + return _P(self.path).name + + @property + def visible(self): + w = self.frame.ContainerWindow + return w.isVisible() + @visible.setter + def visible(self, value): + w = self.frame.ContainerWindow + w.setVisible(value) + + @property + def zoom(self): + return self._cc.ZoomValue + @zoom.setter + def zoom(self, value): + self._cc.ZoomValue = value + + @property + def undo(self): + return self._undo + @undo.setter + def undo(self, value): + self._undo = value + um = self.obj.UndoManager + if value: + try: + um.leaveUndoContext() + except: + pass + else: + um.enterHiddenUndoContext() + + def clear_undo(self): + self.obj.getUndoManager().clear() + return + + @property + def selection(self): + sel = self.obj.CurrentSelection + return sel + + @property + def table_auto_formats(self): + taf = create_instance('com.sun.star.sheet.TableAutoFormats') + return taf.ElementNames + + @property + def status_bar(self): + bar = self._cc.getStatusIndicator() + return bar + + def create_instance(self, name): + obj = self.obj.createInstance(name) + return obj + + def set_focus(self): + w = self.frame.ComponentWindow + w.setFocus() + return + + def copy(self): + call_dispatch(self.frame, '.uno:Copy') + return + + def insert_contents(self, args={}): + call_dispatch(self.frame, '.uno:InsertContents', args) + return + + def paste(self): + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + transferable = sc.getContents() + self._cc.insertTransferable(transferable) + # ~ return self.obj.getCurrentSelection() + return + + # ~ def select(self, obj): + # ~ self._cc.select(obj) + # ~ return + + def to_pdf(self, path: str='', options: dict={}): + """ + https://wiki.documentfoundation.org/Macros/Python_Guide/PDF_export_filter_data + """ + args = options.copy() + stream = None + path_pdf = 'private:stream' + if path: + path_pdf = _P.to_url(path) + + filter_name = '{}_pdf_Export'.format(self.type) + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + if not path: + stream = IOStream.output() + args['OutputStream'] = stream + + opt = dict_to_property(args) + 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='', options: dict={}): + FILTERS = { + 'xlsx': 'Calc MS Excel 2007 XML', + 'xls': 'MS Excel 97', + 'docx': 'MS Word 2007 XML', + 'doc': 'MS Word 97', + 'rtf': 'Rich Text Format', + } + args = options.copy() + stream = None + path_target = 'private:stream' + if path: + path_target = _P.to_url(path) + + filter_name = FILTERS.get(filter_name, filter_name) + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + if not path: + stream = IOStream.output() + args['OutputStream'] = stream + + opt = dict_to_property(args) + try: + self.obj.storeToURL(path_target, opt) + except Exception as e: + error(e) + + if not stream is None: + stream = stream.buffer + + return stream + + def save(self, path: str='', options: dict={}): + if not path: + self.obj.store() + return + + args = options.copy() + path_target = _P.to_url(path) + + opt = dict_to_property(args) + try: + self.obj.storeAsURL(path_target, opt) + except Exception as e: + error(e) + + return + + def close(self): + self.obj.close(True) + return + + +class LOCellStyle(LOBaseObject): + + def __init__(self, obj): + super().__init__(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(object): + + 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') + if name: + self.obj[name] = obj + obj = LOCellStyle(obj) + return obj + + +class LOCalc(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = CALC + self._sheets = obj.Sheets + + def __getitem__(self, index): + return LOCalcSheet(self._sheets[index]) + + def __setitem__(self, key, value): + self._sheets[key] = value + + def __len__(self): + return self._sheets.Count + + def __contains__(self, item): + return item in self._sheets + + @property + def names(self): + names = self.obj.Sheets.ElementNames + return names + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName in TYPE_RANGES: + sel = LOCalcRange(sel) + elif sel.ImplementationName in OBJ_RANGES: + sel = LOCalcRanges(sel) + elif sel.ImplementationName == OBJ_SHAPES: + if len(sel) == 1: + sel = LOShape(sel[0]) + else: + debug(sel.ImplementationName) + return sel + + @property + def active(self): + return LOCalcSheet(self._cc.ActiveSheet) + + @property + def headers(self): + return self._cc.ColumnRowHeaders + @headers.setter + def headers(self, value): + self._cc.ColumnRowHeaders = value + + @property + def tabs(self): + return self._cc.SheetTabs + @tabs.setter + def tabs(self, value): + self._cc.SheetTabs = value + + @property + def cs(self): + return self.cell_styles + @property + def cell_styles(self): + obj = self.obj.StyleFamilies['CellStyles'] + return LOCellStyles(obj, self) + + @property + def db_ranges(self): + # ~ return LOCalcDataBaseRanges(self.obj.DataBaseRanges) + return self.obj.DatabaseRanges + + @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 + + def activate(self, sheet): + obj = sheet + if isinstance(sheet, LOCalcSheet): + obj = sheet.obj + elif isinstance(sheet, str): + obj = self._sheets[sheet] + self._cc.setActiveSheet(obj) + return + + def new_sheet(self): + s = self.create_instance('com.sun.star.sheet.Spreadsheet') + return s + + def insert(self, name): + names = name + if isinstance(name, str): + names = (name,) + for n in names: + self._sheets[n] = self.new_sheet() + return LOCalcSheet(self._sheets[n]) + + def move(self, name, pos=-1): + index = pos + if pos < 0: + index = len(self) + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.moveByName(name, index) + return + + def remove(self, name): + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.removeByName(name) + return + + def copy(self, name, new_name='', pos=-1): + if isinstance(name, LOCalcSheet): + name = name.name + index = pos + if pos < 0: + index = len(self) + self._sheets.copyByName(name, new_name, index) + return LOCalcSheet(self._sheets[new_name]) + + def copy_from(self, doc, source='', target='', pos=-1): + index = pos + if pos < 0: + index = len(self) + + names = source + if not source: + names = doc.names + elif isinstance(source, str): + names = (source,) + + 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): + names = sorted(self.names, reverse=reverse) + for i, n in enumerate(names): + self.move(n, i) + return + + def render(self, data, sheet=None, clean=True): + if sheet is None: + sheet = self.active + return sheet.render(data, clean=clean) + + def select(self, rango): + self._cc.select(rango.obj) + return + + +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): + EVENTS = { + 'action': 'actionPerformed', + 'click': 'mousePressed', + } + TYPES = { + 'actionPerformed': 'XActionListener', + 'mousePressed': 'XMouseListener', + } + + def __init__(self, obj, view, form): + super().__init__(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 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 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 + + +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) + 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') + h = args.pop('Height') + 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): + 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): + + 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'easymacro.LOCalcSheet: {self.name}' + + @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 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 is_protected(self): + return self._obj.isProtected() + + @property + def password(self): + return '' + @visible.setter + def password(self, value): + self.obj.protect(value) + + def unprotect(self, value): + try: + self.obj.unprotect(value) + return True + except: + pass + return False + + @property + def color(self): + return self._obj.TabColor + @color.setter + def color(self, value): + self._obj.TabColor = get_color(value) + + @property + def used_area(self): + cursor = self.get_cursor() + cursor.gotoEndOfUsedArea(True) + return LOCalcRange(self[cursor.AbsoluteName].obj) + + @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 doc(self): + return LOCalc(self.obj.DrawPage.Forms.Parent) + + @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 events(self): + names = ('OnFocus', 'OnUnfocus', 'OnSelect', 'OnDoubleClick', + 'OnRightClick', 'OnChange', 'OnCalculate') + evs = self.obj.Events + events = {n: _property_to_dict(evs.getByName(n)) for n in names + if evs.getByName(n)} + return events + @events.setter + def events(self, values): + pv = '[]com.sun.star.beans.PropertyValue' + ev = self.obj.Events + for name, v in values.items(): + url = _get_url_script(v) + args = dict_to_property(dict(EventType='Script', Script=url)) + # ~ e.replaceByName(k, args) + uno.invoke(ev, 'replaceByName', (name, uno.Any(pv, args))) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + def activate(self): + self.doc.activate(self.obj) + return + + # ~ ??? + def clean(self): + doc = self.doc + sheet = doc.create_instance('com.sun.star.sheet.Spreadsheet') + doc._sheets.replaceByName(self.name, sheet) + return + + def move(self, pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.moveByName(self.name, index) + return + + def remove(self): + self.doc._sheets.removeByName(self.name) + return + + def copy(self, new_name='', pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.copyByName(self.name, new_name, index) + return LOCalcSheet(self.doc._sheets[new_name]) + + def copy_to(self, doc, target='', pos=-1): + index = pos + if pos < 0: + index = len(doc) + + new_name = target or self.name + new_pos = doc._sheets.importSheet(self.doc.obj, self.name, index) + sheet = doc[new_pos] + sheet.name = new_name + return sheet + + def get_cursor(self, cell=None): + if cell is None: + cursor = self.obj.createCursor() + else: + cursor = self.obj.createCursorByRange(cell) + return cursor + + 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 __init__(self, obj): + self._obj = obj + self._sd = None + self._is_cell = obj.ImplementationName == OBJ_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.columns: + self._c = 0 + self._r +=1 + return rango + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item.in_range(self) + + def __str__(self): + if self.is_none: + s = 'Range: None' + else: + s = f'Range: {self.name}' + return s + + @property + def obj(self): + return self._obj + + @property + def is_none(self): + return self.obj is None + + @property + def is_cell(self): + return self._is_cell + + @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 dp(self): + return self.sheet.dp + + @property + def sheet(self): + return LOCalcSheet(self.obj.Spreadsheet) + + @property + def doc(self): + doc = self.obj.Spreadsheet.DrawPage.Forms.Parent + return LODocument(doc) + + @property + def name(self): + return self.obj.AbsoluteName + + @property + def code_name(self): + name = self.name.replace('$', '').replace('.', '_').replace(':', '') + return name + + @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() + + @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 data(self): + return self.obj.getDataArray() + @data.setter + def data(self, values): + if self._is_cell: + self.to_size(len(values), len(values[0])).data = values + else: + self.obj.setDataArray(values) + + @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 address(self): + return self.obj.CellAddress + + @property + def range_address(self): + return self.obj.RangeAddress + + @property + def cursor(self): + cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) + return cursor + + @property + def current_region(self): + cursor = self.cursor + cursor.collapseToCurrentRegion() + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + @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 = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(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.sheet.get_cursor(self.obj) + cursor = self.cursor + rangos = cursor.queryEmptyCells() + 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 style(self): + return self.obj.CellStyle + @style.setter + def style(self, value): + self.obj.CellStyle = 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 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) + + def to_size(self, rows, cols): + cursor = self.cursor + cursor.collapseToSize(cols, rows) + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + 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 LOCalcRanges(object): + + def __init__(self, obj): + self._obj = obj + self._ranges = {} + self._index = 0 + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __len__(self): + return self._obj.Count + + def __contains__(self, item): + return self._obj.hasByName(item.name) + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + r = self.obj[self._index] + rango = self._ranges[r.AbsoluteName] + except IndexError: + raise StopIteration + + self._index += 1 + return rango + + def __getitem__(self, index): + if isinstance(index, int): + r = self.obj[index] + rango = self._ranges[r.AbsoluteName] + else: + rango = self._ranges[index] + return rango + + @property + def obj(self): + return self._obj + + @property + def names(self): + return self.obj.ElementNames + + @property + def data(self): + return [r.data for r in self._ranges.values()] + + @property + def style(self): + return self.obj + @style.setter + def style(self, value): + self.obj.CellStyle = value + + def add(self, rangos): + if isinstance(rangos, LOCalcRange): + rangos = (rangos,) + for r in rangos: + self._ranges[r.name] = r + self.obj.addRangeAddress(r.range_address, False) + return + + def remove(self, rangos): + if isinstance(rangos, LOCalcRange): + rangos = (rangos,) + for r in rangos: + del self._ranges[r.name] + self.obj.removeRangeAddress(r.range_address) + return + + +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._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 obj(self): + return self._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 text(self): + return self.obj.Text + + @property + def cursor(self): + return self.text.createTextCursorByRange(self.obj) + + @property + def dp(self): + return self._doc.dp + + def delete(self): + cursor = self.cursor + cursor.gotoStartOfParagraph(False) + cursor.gotoNextParagraph(True) + cursor.String = '' + 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 new_line(self, count=1): + cursor = self.cursor + for i in range(count): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + return self._doc.selection + + def insert_table(self, data): + table = self._doc.create_instance('com.sun.star.text.TextTable') + 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_image(self, path, args={}): + w = args.get('Width', 1000) + h = args.get('Height', 1000) + image = self._doc.create_instance('com.sun.star.text.GraphicObject') + image.GraphicURL = _P.to_url(path) + image.AnchorType = 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 + + @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 draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def cursor(self): + return self.obj.Text.createTextCursor() + + @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() + + 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): + IMAGE = 'com.sun.star.drawing.GraphicObjectShape' + + def __init__(self, obj, index=-1): + self._index = index + super().__init__(obj) + + @property + def type(self): + t = self.shape_type[21:] + if self.is_image: + t = 'image' + return t + + @property + def shape_type(self): + return self.obj.ShapeType + + @property + def properties(self): + return {} + @properties.setter + def properties(self, values): + _set_properties(self.obj, values) + + @property + def is_image(self): + return self.shape_type == self.IMAGE + + @property + def name(self): + return self.obj.Name or f'{self.type}{self.index}' + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def index(self): + return self._index + + @property + def size(self): + s = self.obj.Size + a = dict(Width=s.Width, Height=s.Height) + return a + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def description(self): + return self.obj.Description + @description.setter + def description(self, value): + self.obj.Description = value + + @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 + self.obj.Anchor = 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 + w = args.pop('Width', 3000) + h = args.pop('Height', 3000) + 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 LODraw(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + self._type = DRAW + + +class LOImpress(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + self._type = IMPRESS + + +class BaseDateField(DateField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class BaseTimeField(TimeField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class BaseDateTimeField(DateTimeField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class FirebirdDatabase(Database): + field_types = {'BOOL': 'BOOLEAN', 'DATETIME': 'TIMESTAMP'} + + def __init__(self, database, **kwargs): + super().__init__(database, **kwargs) + self._db = database + + def _connect(self): + return self._db + + def create_tables(self, models, **options): + options['safe'] = False + tables = self._db.tables + models = [m for m in models if not m.__name__.lower() in tables] + super().create_tables(models, **options) + + def execute_sql(self, sql, params=None, commit=True): + with __exception_wrapper__: + cursor = self._db.execute(sql, params) + return cursor + + def last_insert_id(self, cursor, query_type=None): + # ~ debug('LAST_ID', cursor) + return 0 + + def rows_affected(self, cursor): + return self._db.rows_affected + + @property + def path(self): + return self._db.path + + +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) + + +class LOBase(object): + DB_TYPES = { + str: 'setString', + int: 'setInt', + float: 'setFloat', + bool: 'setBoolean', + Date: 'setDate', + Time: 'setTime', + DateTime: 'setTimestamp', + } + # ~ setArray + # ~ setBinaryStream + # ~ setBlob + # ~ setByte + # ~ setBytes + # ~ setCharacterStream + # ~ setClob + # ~ setNull + # ~ setObject + # ~ setObjectNull + # ~ setObjectWithInfo + # ~ setPropertyValue + # ~ setRef + + def __init__(self, obj, args={}): + self._obj = obj + self._type = BASE + self._dbc = create_instance('com.sun.star.sdb.DatabaseContext') + self._rows_affected = 0 + path = args.get('path', '') + self._path = _P(path) + self._name = self._path.name + if _P.exists(path): + if not self.is_registered: + self.register() + db = self._dbc.getByName(self.name) + else: + db = self._dbc.createInstance() + db.URL = 'sdbc:embedded:firebird' + db.DatabaseDocument.storeAsURL(self._path.url, ()) + self.register() + self._obj = db + self._con = db.getConnection('', '') + + def __contains__(self, item): + return item in self.tables + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def path(self): + return str(self._path) + + @property + def is_registered(self): + return self._dbc.hasRegisteredDatabase(self.name) + + @property + def tables(self): + tables = [t.Name.lower() for t in self._con.getTables()] + return tables + + @property + def rows_affected(self): + return self._rows_affected + + def register(self): + if not self.is_registered: + self._dbc.registerDatabaseLocation(self.name, self._path.url) + return + + def revoke(self, name): + self._dbc.revokeDatabaseLocation(name) + return True + + def save(self): + self.obj.DatabaseDocument.store() + self.refresh() + return + + def close(self): + self._con.close() + return + + def refresh(self): + self._con.getTables().refresh() + return + + def initialize(self, database_proxy, tables=[]): + db = FirebirdDatabase(self) + database_proxy.initialize(db) + if tables: + db.create_tables(tables) + return + + def _validate_sql(self, sql, params): + limit = ' LIMIT ' + for p in params: + sql = sql.replace('?', f"'{p}'", 1) + if limit in sql: + sql = sql.split(limit)[0] + sql = sql.replace('SELECT', f'SELECT FIRST {params[-1]}') + return sql + + def cursor(self, sql, params): + if sql.startswith('SELECT'): + sql = self._validate_sql(sql, params) + cursor = self._con.prepareStatement(sql) + return cursor + + if not params: + cursor = self._con.createStatement() + return cursor + + cursor = self._con.prepareStatement(sql) + for i, v in enumerate(params, 1): + t = type(v) + if not t in self.DB_TYPES: + error('Type not support') + debug((i, t, v, self.DB_TYPES[t])) + getattr(cursor, self.DB_TYPES[t])(i, v) + return cursor + + def execute(self, sql, params): + debug(sql, params) + cursor = self.cursor(sql, params) + + if sql.startswith('SELECT'): + result = cursor.executeQuery() + elif params: + result = cursor.executeUpdate() + self._rows_affected = result + self.save() + else: + result = cursor.execute(sql) + self.save() + + return result + + def select(self, sql): + debug('SELECT', sql) + if not sql.startswith('SELECT'): + return () + + cursor = self._con.prepareStatement(sql) + query = cursor.executeQuery() + return BaseQuery(query) + + def get_query(self, query): + sql, args = query.sql() + sql = self._validate_sql(sql, args) + return self.select(sql) + + +class LOMath(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = MATH + + +class LOBasic(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = BASIC + + +class LODocs(object): + _desktop = None + + def __init__(self): + self._desktop = get_desktop() + LODocs._desktop = self._desktop + + def __getitem__(self, index): + document = None + for i, doc in enumerate(self._desktop.Components): + if isinstance(index, int) and i == index: + document = _get_class_doc(doc) + break + elif isinstance(index, str) and doc.Title == index: + document = _get_class_doc(doc) + break + return document + + 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 __len__(self): + # ~ len(self._desktop.Components) + for i, _ in enumerate(self._desktop.Components): + pass + return i + 1 + + @property + def active(self): + return _get_class_doc(self._desktop.getCurrentComponent()) + + @classmethod + def new(cls, type_doc=CALC, args={}): + if type_doc == BASE: + return LOBase(None, args) + + path = f'private:factory/s{type_doc}' + opt = dict_to_property(args) + doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) + return _get_class_doc(doc) + + @classmethod + def open(cls, path, args={}): + """ Open document in path + 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 + + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1frame_1_1XComponentLoader.html + http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html + """ + path = _P.to_url(path) + opt = dict_to_property(args) + doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) + if doc is None: + return + + return _get_class_doc(doc) + + def connect(self, path): + db = LOBase(None, {'path': path}) + return db + + +def _add_listeners(events, control, name=''): + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + 'addFocusListener': EventsFocus, + 'addItemListener': EventsItem, + 'addKeyListener': EventsKey, + 'addTabListener': EventsTab, + } + 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 + + +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 + + +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 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): + pass + + 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: + self.y = origin.y + origin.height + 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 + + +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): + self.model.EchoChar = ord(value[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 column(self): + return self.obj.CurrentColumn + + @property + def is_valid(self): + return not (self.row == -1 or self.column == -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 + + +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, +} + +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', +} +# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', +# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', +# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', +# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', +# ~ 'NumericField': 'com.sun.star.awt.UnoControlNumericFieldModel', +# ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', +# ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', +# ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', +# ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', +# ~ 'SpinButton': 'com.sun.star.awt.UnoControlSpinButtonModel', +# ~ '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', + } + + 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) + _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 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['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 LOSheets(object): + + def __getitem__(self, index): + return LODocs().active[index] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +class LOCells(object): + + def __getitem__(self, index): + return LODocs().active.active[index] + + +class LOShortCut(object): +# ~ getKeyEventsByCommand + + def __init__(self, app): + self._app = app + self._scm = None + self._init_values() + + def _init_values(self): + name = 'com.sun.star.ui.GlobalAcceleratorConfiguration' + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + uicm = manager.getUIConfigurationManager(service) + self._scm = uicm.ShortCutManager + return + + def __contains__(self, item): + cmd = self._get_command(item) + return bool(cmd) + + def _get_key_event(self, command): + events = self._scm.AllKeyEvents + for event in events: + cmd = self._scm.getCommandByKeyEvent(event) + if cmd == command: + break + return event + + def _to_key_event(self, shortcut): + key_event = KeyEvent() + keys = shortcut.split('+') + for v in keys[:-1]: + key_event.Modifiers += MODIFIERS[v.lower()] + key_event.KeyCode = getattr(Key, keys[-1].upper()) + return key_event + + def _get_command(self, shortcut): + command = '' + key_event = self._to_key_event(shortcut) + try: + command = self._scm.getCommandByKeyEvent(key_event) + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return command + + def add(self, shortcut, command): + if isinstance(command, dict): + command = _get_url_script(command) + key_event = self._to_key_event(shortcut) + self._scm.setKeyEvent(key_event, command) + self._scm.store() + return + + def reset(self): + self._scm.reset() + self._scm.store() + return + + def remove(self, shortcut): + key_event = self._to_key_event(shortcut) + try: + self._scm.removeKeyEvent(key_event) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return + + def remove_by_command(self, command): + if isinstance(command, dict): + command = _get_url_script(command) + try: + self._scm.removeCommandFromAllKeyEvents(command) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {command}') + return + + +class LOShortCuts(object): + + def __getitem__(self, index): + return LOShortCut(index) + + +class LOMenu(object): + + def __init__(self, app): + self._app = app + self._ui = None + self._pymenus = None + self._menu = None + self._menus = self._get_menus() + + def __getitem__(self, index): + if isinstance(index, int): + self._menu = self._menus[index] + else: + for menu in self._menus: + cmd = menu.get('CommandURL', '') + if MENUS[index.lower()] == cmd: + self._menu = menu + break + # ~ line = self._menu.get('CommandURL', '') + # ~ line += self._get_submenus(self._menu['ItemDescriptorContainer']) + return self._menu + + def _get_menus(self): + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + self._ui = manager.getUIConfigurationManager(service) + self._pymenus = self._ui.getSettings(NODE_MENUBAR, True) + data = [] + for menu in self._pymenus: + data.append(data_to_dict(menu)) + return data + + def _get_info(self, menu): + line = menu.get('CommandURL', '') + line += self._get_submenus(menu['ItemDescriptorContainer']) + return line + + def _get_submenus(self, menu, level=1): + 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 += self._get_submenus(submenu, level + 1) + return line + + def __str__(self): + info = '\n'.join([self._get_info(m) for m in self._menus]) + return info + + def _get_index_menu(self, menu, command): + index = -1 + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '') + if cmd == command: + index = i + break + return index + + def insert(self, name, args): + idc = None + replace = False + command = args['CommandURL'] + label = args['Label'] + + self[name] + menu = self._menu['ItemDescriptorContainer'] + submenu = args.get('Submenu', False) + if submenu: + idc = self._ui.createSettings() + + index = self._get_index_menu(menu, command) + if index == -1: + if 'Index' in args: + index = args['Index'] + else: + index = self._get_index_menu(menu, args['After']) + 1 + else: + replace = True + + data = dict ( + CommandURL = command, + Label = label, + Style = 0, + Type = 0, + ItemDescriptorContainer = idc, + ) + self._save(menu, data, index, replace) + self._insert_submenu(idc, submenu) + return + + def _get_command(self, args): + shortcut = args.get('ShortCut', '') + cmd = args['CommandURL'] + if isinstance(cmd, dict): + cmd = _get_url_script(cmd) + if shortcut: + LOShortCut(self._app).add(shortcut, cmd) + return cmd + + def _insert_submenu(self, parent, menus): + for i, v in enumerate(menus): + submenu = v.pop('Submenu', False) + if submenu: + idc = self._ui.createSettings() + v['ItemDescriptorContainer'] = idc + v['Type'] = 0 + if v['Label'] == '-': + v['Type'] = 1 + else: + v['CommandURL'] = self._get_command(v) + self._save(parent, v, i) + if submenu: + self._insert_submenu(idc, submenu) + return + + def remove(self, name, command): + self[name] + menu = self._menu['ItemDescriptorContainer'] + index = self._get_index_menu(menu, command) + if index > -1: + uno.invoke(menu, 'removeByIndex', (index,)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + def _save(self, menu, properties, index, replace=False): + properties = dict_to_property(properties, True) + if replace: + uno.invoke(menu, 'replaceByIndex', (index, properties)) + else: + uno.invoke(menu, 'insertByIndex', (index, properties)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + +class LOMenus(object): + + def __getitem__(self, index): + return LOMenu(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 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 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 +_CB = ClipBoard + + +class Paths(object): + FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' + FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' + + def __init__(self, path=''): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + self._path = Path(path) + + @property + def path(self): + return str(self._path.parent) + + @property + def file_name(self): + return self._path.name + + @property + def name(self): + return self._path.stem + + @property + def ext(self): + return self._path.suffix[1:] + + @property + def info(self): + return self.path, self.file_name, self.name, self.ext + + @property + def url(self): + return self._path.as_uri() + + @property + def size(self): + return self._path.stat().st_size + + @classproperty + def home(self): + return str(Path.home()) + + @classproperty + def documents(self): + return self.config() + + @classproperty + def temp_dir(self): + return tempfile.gettempdir() + + @classproperty + def python(self): + 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 dir_tmp(self, only_name=False): + dt = tempfile.TemporaryDirectory() + if only_name: + dt = dt.name + return dt + + @classmethod + def tmp(cls, ext=''): + tmp = tempfile.NamedTemporaryFile(suffix=ext) + return tmp.name + + @classmethod + def save_tmp(cls, data): + path_tmp = cls.tmp() + cls.save(path_tmp, data) + return path_tmp + + @classmethod + def config(cls, name='Work'): + """ + Return path from config + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html + """ + path = create_instance('com.sun.star.util.PathSettings') + path = cls.to_system(getattr(path, name)) + return path + + @classmethod + def get(cls, init_dir='', filters: str=''): + """ + Get path for save + Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html + filters: 'xml' or 'txt,xml' + """ + 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((2,)) + if filters: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + path = '' + if file_picker.execute(): + path = cls.to_system(file_picker.getSelectedFiles()[0]) + return path + + @classmethod + def get_dir(cls, init_dir=''): + 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 file + + init_folder: folder default open + filters: 'xml' or 'xml,txt' + multiple: True for multiple selected + """ + 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: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + 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 replace_ext(cls, path, new_ext): + p = Paths(path) + name = f'{p.name}.{new_ext}' + path = cls.join(p.path, name) + return path + + @classmethod + def exists(cls, path): + result = False + if path: + path = cls.to_system(path) + result = Path(path).exists() + return result + + @classmethod + def exists_app(cls, name_app): + return bool(shutil.which(name_app)) + + @classmethod + def open(cls, path): + if IS_WIN: + os.startfile(path) + else: + pid = subprocess.Popen(['xdg-open', path]).pid + return + + @classmethod + def is_dir(cls, path): + return Path(path).is_dir() + + @classmethod + def is_file(cls, path): + return Path(path).is_file() + + @classmethod + def join(cls, *paths): + return str(Path(paths[0]).joinpath(*paths[1:])) + + @classmethod + def save(cls, path, data, encoding='utf-8'): + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path, data): + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def read(cls, path, encoding='utf-8'): + data = Path(path).read_text(encoding=encoding) + return data + + @classmethod + def read_bin(cls, path): + data = Path(path).read_bytes() + return data + + @classmethod + def to_url(cls, path): + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def to_system(cls, path): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def kill(cls, path): + 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 files(cls, path, pattern='*'): + files = [str(p) for p in Path(path).glob(pattern) if p.is_file()] + return files + + @classmethod + def dirs(cls, path): + dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()] + return dirs + + @classmethod + def walk(cls, path, filters=''): + paths = [] + if filters in ('*', '*.*'): + filters = '' + 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 walk_dirs(cls, path, tree=False): + """ + Get directories recursively + path: path source + tree: get info in a tuple (ID_FOLDER, ID_PARENT, NAME) + """ + folders = [] + if tree: + i = 0 + p = 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 from_id(cls, id_ext): + pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') + path = _P.to_system(pip.getPackageLocation(id_ext)) + return path + + @classmethod + def from_json(cls, path): + data = json.loads(cls.read(path)) + return data + + @classmethod + def to_json(cls, path, data): + data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) + return cls.save(path, data) + + @classmethod + def from_csv(cls, path, args={}): + # ~ See https://docs.python.org/3.7/library/csv.html#csv.reader + with open(path) as f: + rows = tuple(csv.reader(f, **args)) + return rows + + @classmethod + def to_csv(cls, path, data, args={}): + with open(path, 'w') as f: + writer = csv.writer(f, **args) + writer.writerows(data) + return + + @classmethod + def zip(cls, source, target=''): + 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 zip_content(cls, path): + with zipfile.ZipFile(path) as z: + names = z.namelist() + return names + + @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 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 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 + + @classmethod + def copy(cls, source, target='', name=''): + p, f, n, e = _P(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 +_P = Paths + + +class Dates(object): + + @classmethod + def date(cls, year, month, day): + d = datetime.date(year, month, day) + return d + + @classmethod + def str_to_date(cls, str_date, template, to_calc=False): + 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, frm=''): + d = datetime.date.fromordinal(int(value) + DATE_OFFSET) + if frm: + d = d.strftime(frm) + return d + + +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 + + +class IOStream(object): + + @classmethod + def buffer(cls): + return io.BytesIO() + + @classmethod + def input(cls, buffer): + instance = 'com.sun.star.io.SequenceInputStream' + stream = create_instance(instance, True) + stream.initialize((uno.ByteSequence(buffer.getvalue()),)) + return stream + + @classmethod + def output(cls): + return OutputStream() + + @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 == 'active': + return LODocs().active + if name == 'active_sheet': + return LODocs().active.active + if name == 'selection': + return LODocs().active.selection + if name == 'current_region': + return LODocs().active.selection.current_region + if name in ('rectangle', 'pos_size'): + return Rectangle() + if name == 'paths': + return Paths + if name == 'docs': + return LODocs() + if name == 'db': + return LODBServer() + if name == 'sheets': + return LOSheets() + if name == 'cells': + return LOCells() + if name == 'menus': + return LOMenus() + if name == 'shortcuts': + return LOShortCuts() + if name == 'clipboard': + return ClipBoard + if name == 'dates': + return Dates + if name == 'ios': + return IOStream() + 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 '' + + +def get_fonts(): + toolkit = create_instance('com.sun.star.awt.Toolkit') + device = toolkit.createScreenCompatibleDevice(0, 0) + return device.FontDescriptors + + +def get_filters(): + """ + Get all support filters + https://help.libreoffice.org/latest/en-US/text/shared/guide/convertfilters.html + """ + 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 + + +# ~ https://en.wikipedia.org/wiki/Web_colors +def get_color(value): + 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, + } + + if isinstance(value, tuple): + color = (value[0] << 16) + (value[1] << 8) + value[2] + else: + if value[0] == '#': + r, g, b = bytes.fromhex(value[1:]) + color = (r << 16) + (g << 8) + b + else: + color = COLORS.get(value.lower(), -1) + return color + + +COLOR_ON_FOCUS = get_color('LightYellow') + + +class LOServer(object): + 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): + 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/files/ZAZPolizas_v0.1.0.oxt b/files/ZAZPolizas_v0.1.0.oxt new file mode 100644 index 0000000000000000000000000000000000000000..5a7e806f1b1cfd657b67e9d1a307a94ff0e1ae4c GIT binary patch literal 66679 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 literal 0 HcmV?d00001 diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..37d9be266c66227ca06e23a89eaf7b2ff8e1ba4f GIT binary patch literal 20104 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 YKYM*>==oY<>;M1&07*qoM6N<$f)%i3;s5{u literal 0 HcmV?d00001 diff --git a/source/Addons.xcu b/source/Addons.xcu new file mode 100644 index 0000000..6adfc56 --- /dev/null +++ b/source/Addons.xcu @@ -0,0 +1,54 @@ + + + + + + + Polizas + Polizas + + + _self + + + + + Process... + Procesar... + + + com.sun.star.sheet.SpreadsheetDocument + + + service:net.elmau.zaz.polizas?procesar + + + _self + + + %origin%/images/icon + + + + + Generate TXT... + Generar TXT... + + + com.sun.star.sheet.SpreadsheetDocument + + + service:net.elmau.zaz.polizas?generar + + + _self + + + %origin%/images/icon + + + + + + + diff --git a/source/META-INF/manifest.xml b/source/META-INF/manifest.xml new file mode 100644 index 0000000..5db3ab0 --- /dev/null +++ b/source/META-INF/manifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/Office/Accelerators.xcu b/source/Office/Accelerators.xcu new file mode 100644 index 0000000..1c89221 --- /dev/null +++ b/source/Office/Accelerators.xcu @@ -0,0 +1,21 @@ + + + + + + + + service:net.elmau.zaz.polizas?procesar + + + + + + + service:net.elmau.zaz.polizas?generar + + + + + + diff --git a/source/ZAZPolizas.py b/source/ZAZPolizas.py new file mode 100644 index 0000000..9f3c254 --- /dev/null +++ b/source/ZAZPolizas.py @@ -0,0 +1,21 @@ +import uno +import unohelper +from com.sun.star.task import XJobExecutor +from main import main + +ID_EXTENSION = 'net.elmau.zaz.polizas' +SERVICE = ('com.sun.star.task.Job',) + + +class ZAZPolizas(unohelper.Base, XJobExecutor): + + def __init__(self, ctx): + self.ctx = ctx + + def trigger(self, args): + main(args) + return + + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation(ZAZPolizas, ID_EXTENSION, SERVICE) diff --git a/source/description.xml b/source/description.xml new file mode 100644 index 0000000..db1e795 --- /dev/null +++ b/source/description.xml @@ -0,0 +1,26 @@ + + + + + + ZAZ Polizas + ZAZ Polizas + + + + + + + + + + El Mau + El Mau + + + + + + + + diff --git a/source/description/desc_en.txt b/source/description/desc_en.txt new file mode 100644 index 0000000..86aecee --- /dev/null +++ b/source/description/desc_en.txt @@ -0,0 +1 @@ +Process polizas from txt \ No newline at end of file diff --git a/source/description/desc_es.txt b/source/description/desc_es.txt new file mode 100644 index 0000000..25a4035 --- /dev/null +++ b/source/description/desc_es.txt @@ -0,0 +1 @@ +Procesa pólizas desde un archivo txt \ No newline at end of file diff --git a/source/images/zazpolizas.png b/source/images/zazpolizas.png new file mode 100644 index 0000000000000000000000000000000000000000..37d9be266c66227ca06e23a89eaf7b2ff8e1ba4f GIT binary patch literal 20104 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 YKYM*>==oY<>;M1&07*qoM6N<$f)%i3;s5{u literal 0 HcmV?d00001 diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py new file mode 100644 index 0000000..27ca1aa --- /dev/null +++ b/source/pythonpath/easymacro.py @@ -0,0 +1,7289 @@ +#!/usr/bin/env python3 + +# == Rapid Develop Macros in LibreOffice == + +# ~ This file is part of ZAZ. + +# ~ https://git.cuates.net/elmau/zaz + +# ~ ZAZ 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. + +# ~ ZAZ 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 ZAZ. If not, see . + +import base64 +import csv +import ctypes +import datetime +import getpass +import gettext +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 +import zipfile + +from collections import OrderedDict +from collections.abc import MutableMapping +from decimal import Decimal +from enum import IntEnum +from functools import wraps +from pathlib import Path +from pprint import pprint +from socket import timeout +from string import Template +from typing import Any, Union +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError + +import imaplib +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 mailbox + +import uno +import unohelper +from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS +from com.sun.star.awt.MessageBoxResults import YES +from com.sun.star.awt import Rectangle, Size, Point +from com.sun.star.awt.PosSize import POSSIZE, SIZE +from com.sun.star.awt import Key, KeyModifier, KeyEvent +from com.sun.star.container import NoSuchElementException +from com.sun.star.datatransfer import XTransferable, DataFlavor + +from com.sun.star.beans import PropertyValue, NamedValue +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.text.TextContentAnchorType import AS_CHARACTER + +from com.sun.star.lang import Locale +from com.sun.star.lang import XEventListener +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 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 + +from com.sun.star.io import IOException, XOutputStream + +# ~ 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' + +TIMEOUT = 10 +LOG_NAME = 'ZAZ' +FILE_NAME_CONFIG = 'zaz-{}.json' + +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', + BASE: 'com.sun.star.sdb.DocumentDataSource', + MATH: 'com.sun.star.formula.FormulaProperties', + BASIC: 'com.sun.star.script.BasicIDE', + MAIN: 'com.sun.star.frame.StartModule', +} + +OBJ_CELL = 'ScCellObj' +OBJ_RANGE = 'ScCellRangeObj' +OBJ_RANGES = 'ScCellRangesObj' +TYPE_RANGES = (OBJ_CELL, OBJ_RANGE) + +OBJ_SHAPES = 'com.sun.star.drawing.SvxShapeCollection' +OBJ_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' +OBJ_GRAPHIC = 'SwXTextGraphicObject' + +OBJ_TEXTS = 'SwXTextRanges' +OBJ_TEXT = 'SwXTextRange' + + +# ~ 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 + + +OS = platform.system() +IS_WIN = OS == 'Windows' +IS_MAC = OS == 'Darwin' +USER = getpass.getuser() +PC = platform.node() +DESKTOP = os.environ.get('DESKTOP_SESSION', '') +INFO_DEBUG = f"{sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) + +PYTHON = 'python' +if IS_WIN: + PYTHON = 'python.exe' + +_MACROS = {} +_start = 0 + +SECONDS_DAY = 60 * 60 * 24 +DIR = { + 'images': 'images', + 'locales': 'locales', +} + +KEY = { + 'enter': 1280, +} + +MODIFIERS = { + 'shift': KeyModifier.SHIFT, + 'ctrl': KeyModifier.MOD1, + 'alt': KeyModifier.MOD2, + 'ctrlmac': KeyModifier.MOD3, +} + +# ~ Menus +NODE_MENUBAR = 'private:resource/menubar/menubar' +MENUS = { + 'file': '.uno:PickList', + 'tools': '.uno:ToolsMenu', + 'help': '.uno:HelpMenu', + 'windows': '.uno:WindowList', + 'edit': '.uno:EditMenu', + 'view': '.uno:ViewMenu', + 'insert': '.uno:InsertMenu', + 'format': '.uno:FormatMenu', + 'styles': '.uno:FormatStylesMenu', + 'sheet': '.uno:SheetMenu', + 'data': '.uno:DataMenu', + 'table': '.uno:TableMenu', + 'form': '.uno:FormatFormMenu', + 'page': '.uno:PageMenu', + 'shape': '.uno:ShapeMenu', + 'slide': '.uno:SlideMenu', + 'show': '.uno:SlideShowMenu', +} + +DEFAULT_MIME_TYPE = 'png' +MIME_TYPE = { + 'png': 'image/png', + 'jpg': 'image/jpeg', +} + +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() + + +def create_instance(name: str, with_context: bool=False, args: Any=None) -> Any: + if with_context: + instance = SM.createInstanceWithContext(name, CTX) + elif args: + instance = SM.createInstanceWithArguments(name, (args,)) + else: + instance = SM.createInstance(name) + return instance + + +def get_app_config(node_name: str, key: str=''): + name = 'com.sun.star.configuration.ConfigurationProvider' + service = 'com.sun.star.configuration.ConfigurationAccess' + cp = create_instance(name, True) + node = PropertyValue(Name='nodepath', Value=node_name) + try: + ca = cp.createInstanceWithArguments(service, (node,)) + if ca and not key: + return ca + if ca and ca.hasByName(key): + return ca.getPropertyValue(key) + except Exception as e: + error(e) + return '' + + +LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') +LANG = LANGUAGE.split('-')[0] +try: + COUNTRY = LANGUAGE.split('-')[1] +except: + COUNTRY = '' +LOCALE = Locale(LANG, COUNTRY, '') +NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') +VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') + +INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{INFO_DEBUG}" + +node = '/org.openoffice.Office.Calc/Calculate/Other/Date' +y = get_app_config(node, 'YY') +m = get_app_config(node, 'MM') +d = get_app_config(node, 'DD') +DATE_OFFSET = datetime.date(y, m, d).toordinal() + + +def error(info): + log.error(info) + return + + +def debug(*args): + data = [str(a) for a in args] + log.debug('\t'.join(data)) + return + + +def info(*args): + data = [str(a) for a in args] + log.info('\t'.join(data)) + return + + +def save_log(path: str, data): + with open(path, 'a') as f: + f.write(f'{str(now())[:19]} -{LOG_NAME}- ') + pprint(data, stream=f) + return + + +def catch_exception(f): + @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 inspect(obj: Any) -> None: + zaz = create_instance('net.elmau.zaz.inspect') + if hasattr(obj, 'obj'): + obj = obj.obj + zaz.inspect(obj) + return + + +def mri(obj: Any) -> None: + 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 run_in_thread(fn): + def run(*k, **kw): + t = threading.Thread(target=fn, args=k, kwargs=kw) + t.start() + return t + return run + + +def now(only_time: bool=False): + now = datetime.datetime.now() + if only_time: + now = now.time() + return now + + +def today(): + return datetime.date.today() + + +def _(msg): + if LANG == 'en': + return msg + + if not LANG in MESSAGES: + return msg + + return MESSAGES[LANG][msg] + + +def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): + """ Create message box + type_msg: infobox, warningbox, errorbox, querybox, messbox + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMessageBoxFactory.html + """ + toolkit = create_instance('com.sun.star.awt.Toolkit') + parent = toolkit.getDesktopWindow() + box = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message)) + return box.execute() + + +def question(message, title=TITLE): + result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') + return result == YES + + +def warning(message, title=TITLE): + return msgbox(message, title, type_msg='warningbox') + + +def errorbox(message, title=TITLE): + return msgbox(message, title, type_msg='errorbox') + + +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 dict_to_property(values: dict, uno_any: bool=False): + 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 _array_to_dict(values): + d = {v[0]: v[1] for v in values} + return d + + +def _property_to_dict(values): + d = {v.Name: v.Value for v in values} + return d + + +def json_dumps(data): + return json.dumps(data, indent=4, sort_keys=True) + + +def json_loads(data): + return json.loads(data) + + +def data_to_dict(data): + if isinstance(data, (tuple, list)) and isinstance(data[0], (tuple, list)): + return _array_to_dict(data) + + if isinstance(data, (tuple, list)) and isinstance(data[0], (PropertyValue, NamedValue)): + return _property_to_dict(data) + return {} + + +def _get_dispatch() -> Any: + return create_instance('com.sun.star.frame.DispatchHelper') + + +# ~ https://wiki.documentfoundation.org/Development/DispatchCommands +# ~ Used only if not exists in API +def call_dispatch(frame: Any, url: str, args: dict={}) -> None: + dispatch = _get_dispatch() + if hasattr(frame, 'frame'): + frame = frame.frame + opt = dict_to_property(args) + dispatch.executeDispatch(frame, url, '', 0, opt) + return + + +def get_desktop(): + return create_instance('com.sun.star.frame.Desktop', True) + + +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 _get_url_script(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 + + +def _call_macro(args: dict): + #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification + + url = _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 + + +def call_macro(args, in_thread=False): + result = None + if in_thread: + t = threading.Thread(target=_call_macro, args=(args,)) + t.start() + else: + result = _call_macro(args) + return result + + +def run(command, capture=False, split=False): + 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 + + +def popen(command): + 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) + + +def sleep(seconds): + time.sleep(seconds) + return + + +class TimerThread(threading.Thread): + + def __init__(self, event, seconds, macro): + threading.Thread.__init__(self) + self.stopped = event + self.seconds = seconds + self.macro = macro + + def run(self): + info('Timer started... {}'.format(self.macro['name'])) + while not self.stopped.wait(self.seconds): + _call_macro(self.macro) + info('Timer stopped... {}'.format(self.macro['name'])) + return + + +def start_timer(name, seconds, macro): + global _MACROS + _MACROS[name] = threading.Event() + thread = TimerThread(_MACROS[name], seconds, macro) + thread.start() + return + + +def stop_timer(name): + global _MACROS + _MACROS[name].set() + del _MACROS[name] + return + + +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 sha256(data): + result = hashlib.sha256(data.encode()).hexdigest() + return result + + +def sha512(data): + result = hashlib.sha512(data.encode()).hexdigest() + return result + + +def get_config(key='', prefix='conf', default={}): + name_file = FILE_NAME_CONFIG.format(prefix) + values = None + path = _P.join(_P.config('UserConfig'), name_file) + if not _P.exists(path): + return default + + values = _P.from_json(path) + if key: + values = values.get(key, default) + + return values + + +def set_config(key, value, prefix='conf'): + name_file = FILE_NAME_CONFIG.format(prefix) + path = _P.join(_P.config('UserConfig'), name_file) + values = get_config(default={}, prefix=prefix) + values[key] = value + result = _P.to_json(path, values) + return result + + +def start(): + global _start + + _start = now() + info(_start) + return + + +def end(get_seconds: bool=False): + global _start + + e = now() + td = e - _start + result = str(td) + if get_seconds: + result = td.total_seconds() + return result + + +def get_epoch(): + n = now() + return int(time.mktime(n.timetuple())) + + +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +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 url_open(url, data=None, headers={}, verify=True, get_json=False, timeout=TIMEOUT): + err = '' + req = Request(url) + for k, v in headers.items(): + req.add_header(k, v) + try: + # ~ debug(url) + if verify: + if not data is None and isinstance(data, str): + data = data.encode() + response = urlopen(req, data=data, timeout=timeout) + else: + context = ssl._create_unverified_context() + response = urlopen(req, data=data, timeout=timeout, context=context) + except HTTPError as e: + error(e) + err = str(e) + except URLError as e: + error(e.reason) + err = str(e.reason) + except timeout: + err = 'timeout' + error(err) + else: + headers = dict(response.info()) + result = response.read().decode() + if get_json: + result = json.loads(result) + + return result, headers, err + + +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 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'] + self._sender = config['user'] + hosts = ('gmail' in name or 'outlook' in name) + try: + if is_ssl and hosts: + 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 + + +def _send_email(server, messages): + with SmtpServer(server) as server: + if server.is_connect: + for msg in messages: + server.send(msg) + else: + error(server.error) + return server.error + + +def send_email(server, message): + messages = message + if isinstance(message, dict): + messages = (message,) + t = threading.Thread(target=_send_email, args=(server, messages)) + t.start() + 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 LODocument(object): + + def __init__(self, obj): + self._obj = obj + self._cc = self.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 self._obj + + @property + def title(self): + return self.obj.getTitle() + @title.setter + def title(self, value): + self.obj.setTitle(value) + + @property + def type(self): + return self._type + + @property + def uid(self): + return self.obj.RuntimeUID + + @property + def frame(self): + return self._cc.getFrame() + + @property + def is_saved(self): + return self.obj.hasLocation() + + @property + def is_modified(self): + return self.obj.isModified() + + @property + def is_read_only(self): + return self.obj.isReadonly() + + @property + def path(self): + return _P.to_system(self.obj.URL) + + @property + def dir(self): + return _P(self.path).path + + @property + def file_name(self): + return _P(self.path).file_name + + @property + def name(self): + return _P(self.path).name + + @property + def visible(self): + w = self.frame.ContainerWindow + return w.isVisible() + @visible.setter + def visible(self, value): + w = self.frame.ContainerWindow + w.setVisible(value) + + @property + def zoom(self): + return self._cc.ZoomValue + @zoom.setter + def zoom(self, value): + self._cc.ZoomValue = value + + @property + def undo(self): + return self._undo + @undo.setter + def undo(self, value): + self._undo = value + um = self.obj.UndoManager + if value: + try: + um.leaveUndoContext() + except: + pass + else: + um.enterHiddenUndoContext() + + def clear_undo(self): + self.obj.getUndoManager().clear() + return + + @property + def selection(self): + sel = self.obj.CurrentSelection + return sel + + @property + def table_auto_formats(self): + taf = create_instance('com.sun.star.sheet.TableAutoFormats') + return taf.ElementNames + + @property + def status_bar(self): + bar = self._cc.getStatusIndicator() + return bar + + def create_instance(self, name): + obj = self.obj.createInstance(name) + return obj + + def set_focus(self): + w = self.frame.ComponentWindow + w.setFocus() + return + + def copy(self): + call_dispatch(self.frame, '.uno:Copy') + return + + def insert_contents(self, args={}): + call_dispatch(self.frame, '.uno:InsertContents', args) + return + + def paste(self): + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + transferable = sc.getContents() + self._cc.insertTransferable(transferable) + # ~ return self.obj.getCurrentSelection() + return + + # ~ def select(self, obj): + # ~ self._cc.select(obj) + # ~ return + + def to_pdf(self, path: str='', options: dict={}): + """ + https://wiki.documentfoundation.org/Macros/Python_Guide/PDF_export_filter_data + """ + args = options.copy() + stream = None + path_pdf = 'private:stream' + if path: + path_pdf = _P.to_url(path) + + filter_name = '{}_pdf_Export'.format(self.type) + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + if not path: + stream = IOStream.output() + args['OutputStream'] = stream + + opt = dict_to_property(args) + 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='', options: dict={}): + FILTERS = { + 'xlsx': 'Calc MS Excel 2007 XML', + 'xls': 'MS Excel 97', + 'docx': 'MS Word 2007 XML', + 'doc': 'MS Word 97', + 'rtf': 'Rich Text Format', + } + args = options.copy() + stream = None + path_target = 'private:stream' + if path: + path_target = _P.to_url(path) + + filter_name = FILTERS.get(filter_name, filter_name) + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + if not path: + stream = IOStream.output() + args['OutputStream'] = stream + + opt = dict_to_property(args) + try: + self.obj.storeToURL(path_target, opt) + except Exception as e: + error(e) + + if not stream is None: + stream = stream.buffer + + return stream + + def save(self, path: str='', options: dict={}): + if not path: + self.obj.store() + return + + args = options.copy() + path_target = _P.to_url(path) + + opt = dict_to_property(args) + try: + self.obj.storeAsURL(path_target, opt) + except Exception as e: + error(e) + + return + + def close(self): + self.obj.close(True) + return + + +class LOCellStyle(LOBaseObject): + + def __init__(self, obj): + super().__init__(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(object): + + 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') + if name: + self.obj[name] = obj + obj = LOCellStyle(obj) + return obj + + +class LOCalc(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = CALC + self._sheets = obj.Sheets + + def __getitem__(self, index): + return LOCalcSheet(self._sheets[index]) + + def __setitem__(self, key, value): + self._sheets[key] = value + + def __len__(self): + return self._sheets.Count + + def __contains__(self, item): + return item in self._sheets + + @property + def names(self): + names = self.obj.Sheets.ElementNames + return names + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName in TYPE_RANGES: + sel = LOCalcRange(sel) + elif sel.ImplementationName in OBJ_RANGES: + sel = LOCalcRanges(sel) + elif sel.ImplementationName == OBJ_SHAPES: + if len(sel) == 1: + sel = LOShape(sel[0]) + else: + debug(sel.ImplementationName) + return sel + + @property + def active(self): + return LOCalcSheet(self._cc.ActiveSheet) + + @property + def headers(self): + return self._cc.ColumnRowHeaders + @headers.setter + def headers(self, value): + self._cc.ColumnRowHeaders = value + + @property + def tabs(self): + return self._cc.SheetTabs + @tabs.setter + def tabs(self, value): + self._cc.SheetTabs = value + + @property + def cs(self): + return self.cell_styles + @property + def cell_styles(self): + obj = self.obj.StyleFamilies['CellStyles'] + return LOCellStyles(obj, self) + + @property + def db_ranges(self): + # ~ return LOCalcDataBaseRanges(self.obj.DataBaseRanges) + return self.obj.DatabaseRanges + + @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 + + def activate(self, sheet): + obj = sheet + if isinstance(sheet, LOCalcSheet): + obj = sheet.obj + elif isinstance(sheet, str): + obj = self._sheets[sheet] + self._cc.setActiveSheet(obj) + return + + def new_sheet(self): + s = self.create_instance('com.sun.star.sheet.Spreadsheet') + return s + + def insert(self, name): + names = name + if isinstance(name, str): + names = (name,) + for n in names: + self._sheets[n] = self.new_sheet() + return LOCalcSheet(self._sheets[n]) + + def move(self, name, pos=-1): + index = pos + if pos < 0: + index = len(self) + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.moveByName(name, index) + return + + def remove(self, name): + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.removeByName(name) + return + + def copy(self, name, new_name='', pos=-1): + if isinstance(name, LOCalcSheet): + name = name.name + index = pos + if pos < 0: + index = len(self) + self._sheets.copyByName(name, new_name, index) + return LOCalcSheet(self._sheets[new_name]) + + def copy_from(self, doc, source='', target='', pos=-1): + index = pos + if pos < 0: + index = len(self) + + names = source + if not source: + names = doc.names + elif isinstance(source, str): + names = (source,) + + 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): + names = sorted(self.names, reverse=reverse) + for i, n in enumerate(names): + self.move(n, i) + return + + def render(self, data, sheet=None, clean=True): + if sheet is None: + sheet = self.active + return sheet.render(data, clean=clean) + + def select(self, rango): + self._cc.select(rango.obj) + return + + +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): + EVENTS = { + 'action': 'actionPerformed', + 'click': 'mousePressed', + } + TYPES = { + 'actionPerformed': 'XActionListener', + 'mousePressed': 'XMouseListener', + } + + def __init__(self, obj, view, form): + super().__init__(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 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 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 + + +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) + 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') + h = args.pop('Height') + 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): + 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): + + 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'easymacro.LOCalcSheet: {self.name}' + + @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 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 is_protected(self): + return self._obj.isProtected() + + @property + def password(self): + return '' + @visible.setter + def password(self, value): + self.obj.protect(value) + + def unprotect(self, value): + try: + self.obj.unprotect(value) + return True + except: + pass + return False + + @property + def color(self): + return self._obj.TabColor + @color.setter + def color(self, value): + self._obj.TabColor = get_color(value) + + @property + def used_area(self): + cursor = self.get_cursor() + cursor.gotoEndOfUsedArea(True) + return LOCalcRange(self[cursor.AbsoluteName].obj) + + @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 doc(self): + return LOCalc(self.obj.DrawPage.Forms.Parent) + + @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 events(self): + names = ('OnFocus', 'OnUnfocus', 'OnSelect', 'OnDoubleClick', + 'OnRightClick', 'OnChange', 'OnCalculate') + evs = self.obj.Events + events = {n: _property_to_dict(evs.getByName(n)) for n in names + if evs.getByName(n)} + return events + @events.setter + def events(self, values): + pv = '[]com.sun.star.beans.PropertyValue' + ev = self.obj.Events + for name, v in values.items(): + url = _get_url_script(v) + args = dict_to_property(dict(EventType='Script', Script=url)) + # ~ e.replaceByName(k, args) + uno.invoke(ev, 'replaceByName', (name, uno.Any(pv, args))) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + def activate(self): + self.doc.activate(self.obj) + return + + # ~ ??? + def clean(self): + doc = self.doc + sheet = doc.create_instance('com.sun.star.sheet.Spreadsheet') + doc._sheets.replaceByName(self.name, sheet) + return + + def move(self, pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.moveByName(self.name, index) + return + + def remove(self): + self.doc._sheets.removeByName(self.name) + return + + def copy(self, new_name='', pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.copyByName(self.name, new_name, index) + return LOCalcSheet(self.doc._sheets[new_name]) + + def copy_to(self, doc, target='', pos=-1): + index = pos + if pos < 0: + index = len(doc) + + new_name = target or self.name + new_pos = doc._sheets.importSheet(self.doc.obj, self.name, index) + sheet = doc[new_pos] + sheet.name = new_name + return sheet + + def get_cursor(self, cell=None): + if cell is None: + cursor = self.obj.createCursor() + else: + cursor = self.obj.createCursorByRange(cell) + return cursor + + 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 __init__(self, obj): + self._obj = obj + self._sd = None + self._is_cell = obj.ImplementationName == OBJ_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.columns: + self._c = 0 + self._r +=1 + return rango + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item.in_range(self) + + def __str__(self): + if self.is_none: + s = 'Range: None' + else: + s = f'Range: {self.name}' + return s + + @property + def obj(self): + return self._obj + + @property + def is_none(self): + return self.obj is None + + @property + def is_cell(self): + return self._is_cell + + @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 dp(self): + return self.sheet.dp + + @property + def sheet(self): + return LOCalcSheet(self.obj.Spreadsheet) + + @property + def doc(self): + doc = self.obj.Spreadsheet.DrawPage.Forms.Parent + return LODocument(doc) + + @property + def name(self): + return self.obj.AbsoluteName + + @property + def code_name(self): + name = self.name.replace('$', '').replace('.', '_').replace(':', '') + return name + + @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() + + @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 data(self): + return self.obj.getDataArray() + @data.setter + def data(self, values): + if self._is_cell: + self.to_size(len(values), len(values[0])).data = values + else: + self.obj.setDataArray(values) + + @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 address(self): + return self.obj.CellAddress + + @property + def range_address(self): + return self.obj.RangeAddress + + @property + def cursor(self): + cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) + return cursor + + @property + def current_region(self): + cursor = self.cursor + cursor.collapseToCurrentRegion() + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + @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 = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(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.sheet.get_cursor(self.obj) + cursor = self.cursor + rangos = cursor.queryEmptyCells() + 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 style(self): + return self.obj.CellStyle + @style.setter + def style(self, value): + self.obj.CellStyle = 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 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) + + def to_size(self, rows, cols): + cursor = self.cursor + cursor.collapseToSize(cols, rows) + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + 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 LOCalcRanges(object): + + def __init__(self, obj): + self._obj = obj + self._ranges = {} + self._index = 0 + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __len__(self): + return self._obj.Count + + def __contains__(self, item): + return self._obj.hasByName(item.name) + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + r = self.obj[self._index] + rango = self._ranges[r.AbsoluteName] + except IndexError: + raise StopIteration + + self._index += 1 + return rango + + def __getitem__(self, index): + if isinstance(index, int): + r = self.obj[index] + rango = self._ranges[r.AbsoluteName] + else: + rango = self._ranges[index] + return rango + + @property + def obj(self): + return self._obj + + @property + def names(self): + return self.obj.ElementNames + + @property + def data(self): + return [r.data for r in self._ranges.values()] + + @property + def style(self): + return self.obj + @style.setter + def style(self, value): + self.obj.CellStyle = value + + def add(self, rangos): + if isinstance(rangos, LOCalcRange): + rangos = (rangos,) + for r in rangos: + self._ranges[r.name] = r + self.obj.addRangeAddress(r.range_address, False) + return + + def remove(self, rangos): + if isinstance(rangos, LOCalcRange): + rangos = (rangos,) + for r in rangos: + del self._ranges[r.name] + self.obj.removeRangeAddress(r.range_address) + return + + +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._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 obj(self): + return self._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 text(self): + return self.obj.Text + + @property + def cursor(self): + return self.text.createTextCursorByRange(self.obj) + + @property + def dp(self): + return self._doc.dp + + def delete(self): + cursor = self.cursor + cursor.gotoStartOfParagraph(False) + cursor.gotoNextParagraph(True) + cursor.String = '' + 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 new_line(self, count=1): + cursor = self.cursor + for i in range(count): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + return self._doc.selection + + def insert_table(self, data): + table = self._doc.create_instance('com.sun.star.text.TextTable') + 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_image(self, path, args={}): + w = args.get('Width', 1000) + h = args.get('Height', 1000) + image = self._doc.create_instance('com.sun.star.text.GraphicObject') + image.GraphicURL = _P.to_url(path) + image.AnchorType = 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 + + @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 draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def cursor(self): + return self.obj.Text.createTextCursor() + + @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() + + 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): + IMAGE = 'com.sun.star.drawing.GraphicObjectShape' + + def __init__(self, obj, index=-1): + self._index = index + super().__init__(obj) + + @property + def type(self): + t = self.shape_type[21:] + if self.is_image: + t = 'image' + return t + + @property + def shape_type(self): + return self.obj.ShapeType + + @property + def properties(self): + return {} + @properties.setter + def properties(self, values): + _set_properties(self.obj, values) + + @property + def is_image(self): + return self.shape_type == self.IMAGE + + @property + def name(self): + return self.obj.Name or f'{self.type}{self.index}' + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def index(self): + return self._index + + @property + def size(self): + s = self.obj.Size + a = dict(Width=s.Width, Height=s.Height) + return a + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def description(self): + return self.obj.Description + @description.setter + def description(self, value): + self.obj.Description = value + + @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 + self.obj.Anchor = 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 + w = args.pop('Width', 3000) + h = args.pop('Height', 3000) + 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 LODraw(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + self._type = DRAW + + +class LOImpress(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + self._type = IMPRESS + + +class BaseDateField(DateField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class BaseTimeField(TimeField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class BaseDateTimeField(DateTimeField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class FirebirdDatabase(Database): + field_types = {'BOOL': 'BOOLEAN', 'DATETIME': 'TIMESTAMP'} + + def __init__(self, database, **kwargs): + super().__init__(database, **kwargs) + self._db = database + + def _connect(self): + return self._db + + def create_tables(self, models, **options): + options['safe'] = False + tables = self._db.tables + models = [m for m in models if not m.__name__.lower() in tables] + super().create_tables(models, **options) + + def execute_sql(self, sql, params=None, commit=True): + with __exception_wrapper__: + cursor = self._db.execute(sql, params) + return cursor + + def last_insert_id(self, cursor, query_type=None): + # ~ debug('LAST_ID', cursor) + return 0 + + def rows_affected(self, cursor): + return self._db.rows_affected + + @property + def path(self): + return self._db.path + + +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) + + +class LOBase(object): + DB_TYPES = { + str: 'setString', + int: 'setInt', + float: 'setFloat', + bool: 'setBoolean', + Date: 'setDate', + Time: 'setTime', + DateTime: 'setTimestamp', + } + # ~ setArray + # ~ setBinaryStream + # ~ setBlob + # ~ setByte + # ~ setBytes + # ~ setCharacterStream + # ~ setClob + # ~ setNull + # ~ setObject + # ~ setObjectNull + # ~ setObjectWithInfo + # ~ setPropertyValue + # ~ setRef + + def __init__(self, obj, args={}): + self._obj = obj + self._type = BASE + self._dbc = create_instance('com.sun.star.sdb.DatabaseContext') + self._rows_affected = 0 + path = args.get('path', '') + self._path = _P(path) + self._name = self._path.name + if _P.exists(path): + if not self.is_registered: + self.register() + db = self._dbc.getByName(self.name) + else: + db = self._dbc.createInstance() + db.URL = 'sdbc:embedded:firebird' + db.DatabaseDocument.storeAsURL(self._path.url, ()) + self.register() + self._obj = db + self._con = db.getConnection('', '') + + def __contains__(self, item): + return item in self.tables + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def path(self): + return str(self._path) + + @property + def is_registered(self): + return self._dbc.hasRegisteredDatabase(self.name) + + @property + def tables(self): + tables = [t.Name.lower() for t in self._con.getTables()] + return tables + + @property + def rows_affected(self): + return self._rows_affected + + def register(self): + if not self.is_registered: + self._dbc.registerDatabaseLocation(self.name, self._path.url) + return + + def revoke(self, name): + self._dbc.revokeDatabaseLocation(name) + return True + + def save(self): + self.obj.DatabaseDocument.store() + self.refresh() + return + + def close(self): + self._con.close() + return + + def refresh(self): + self._con.getTables().refresh() + return + + def initialize(self, database_proxy, tables=[]): + db = FirebirdDatabase(self) + database_proxy.initialize(db) + if tables: + db.create_tables(tables) + return + + def _validate_sql(self, sql, params): + limit = ' LIMIT ' + for p in params: + sql = sql.replace('?', f"'{p}'", 1) + if limit in sql: + sql = sql.split(limit)[0] + sql = sql.replace('SELECT', f'SELECT FIRST {params[-1]}') + return sql + + def cursor(self, sql, params): + if sql.startswith('SELECT'): + sql = self._validate_sql(sql, params) + cursor = self._con.prepareStatement(sql) + return cursor + + if not params: + cursor = self._con.createStatement() + return cursor + + cursor = self._con.prepareStatement(sql) + for i, v in enumerate(params, 1): + t = type(v) + if not t in self.DB_TYPES: + error('Type not support') + debug((i, t, v, self.DB_TYPES[t])) + getattr(cursor, self.DB_TYPES[t])(i, v) + return cursor + + def execute(self, sql, params): + debug(sql, params) + cursor = self.cursor(sql, params) + + if sql.startswith('SELECT'): + result = cursor.executeQuery() + elif params: + result = cursor.executeUpdate() + self._rows_affected = result + self.save() + else: + result = cursor.execute(sql) + self.save() + + return result + + def select(self, sql): + debug('SELECT', sql) + if not sql.startswith('SELECT'): + return () + + cursor = self._con.prepareStatement(sql) + query = cursor.executeQuery() + return BaseQuery(query) + + def get_query(self, query): + sql, args = query.sql() + sql = self._validate_sql(sql, args) + return self.select(sql) + + +class LOMath(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = MATH + + +class LOBasic(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = BASIC + + +class LODocs(object): + _desktop = None + + def __init__(self): + self._desktop = get_desktop() + LODocs._desktop = self._desktop + + def __getitem__(self, index): + document = None + for i, doc in enumerate(self._desktop.Components): + if isinstance(index, int) and i == index: + document = _get_class_doc(doc) + break + elif isinstance(index, str) and doc.Title == index: + document = _get_class_doc(doc) + break + return document + + 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 __len__(self): + # ~ len(self._desktop.Components) + for i, _ in enumerate(self._desktop.Components): + pass + return i + 1 + + @property + def active(self): + return _get_class_doc(self._desktop.getCurrentComponent()) + + @classmethod + def new(cls, type_doc=CALC, args={}): + if type_doc == BASE: + return LOBase(None, args) + + path = f'private:factory/s{type_doc}' + opt = dict_to_property(args) + doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) + return _get_class_doc(doc) + + @classmethod + def open(cls, path, args={}): + """ Open document in path + 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 + + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1frame_1_1XComponentLoader.html + http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html + """ + path = _P.to_url(path) + opt = dict_to_property(args) + doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) + if doc is None: + return + + return _get_class_doc(doc) + + def connect(self, path): + db = LOBase(None, {'path': path}) + return db + + +def _add_listeners(events, control, name=''): + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + 'addFocusListener': EventsFocus, + 'addItemListener': EventsItem, + 'addKeyListener': EventsKey, + 'addTabListener': EventsTab, + } + 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 + + +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 + + +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 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): + pass + + 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: + self.y = origin.y + origin.height + 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 + + +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): + self.model.EchoChar = ord(value[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 column(self): + return self.obj.CurrentColumn + + @property + def is_valid(self): + return not (self.row == -1 or self.column == -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 + + +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, +} + +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', +} +# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', +# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', +# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', +# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', +# ~ 'NumericField': 'com.sun.star.awt.UnoControlNumericFieldModel', +# ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', +# ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', +# ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', +# ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', +# ~ 'SpinButton': 'com.sun.star.awt.UnoControlSpinButtonModel', +# ~ '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', + } + + 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) + _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 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['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 LOSheets(object): + + def __getitem__(self, index): + return LODocs().active[index] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +class LOCells(object): + + def __getitem__(self, index): + return LODocs().active.active[index] + + +class LOShortCut(object): +# ~ getKeyEventsByCommand + + def __init__(self, app): + self._app = app + self._scm = None + self._init_values() + + def _init_values(self): + name = 'com.sun.star.ui.GlobalAcceleratorConfiguration' + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + uicm = manager.getUIConfigurationManager(service) + self._scm = uicm.ShortCutManager + return + + def __contains__(self, item): + cmd = self._get_command(item) + return bool(cmd) + + def _get_key_event(self, command): + events = self._scm.AllKeyEvents + for event in events: + cmd = self._scm.getCommandByKeyEvent(event) + if cmd == command: + break + return event + + def _to_key_event(self, shortcut): + key_event = KeyEvent() + keys = shortcut.split('+') + for v in keys[:-1]: + key_event.Modifiers += MODIFIERS[v.lower()] + key_event.KeyCode = getattr(Key, keys[-1].upper()) + return key_event + + def _get_command(self, shortcut): + command = '' + key_event = self._to_key_event(shortcut) + try: + command = self._scm.getCommandByKeyEvent(key_event) + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return command + + def add(self, shortcut, command): + if isinstance(command, dict): + command = _get_url_script(command) + key_event = self._to_key_event(shortcut) + self._scm.setKeyEvent(key_event, command) + self._scm.store() + return + + def reset(self): + self._scm.reset() + self._scm.store() + return + + def remove(self, shortcut): + key_event = self._to_key_event(shortcut) + try: + self._scm.removeKeyEvent(key_event) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return + + def remove_by_command(self, command): + if isinstance(command, dict): + command = _get_url_script(command) + try: + self._scm.removeCommandFromAllKeyEvents(command) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {command}') + return + + +class LOShortCuts(object): + + def __getitem__(self, index): + return LOShortCut(index) + + +class LOMenu(object): + + def __init__(self, app): + self._app = app + self._ui = None + self._pymenus = None + self._menu = None + self._menus = self._get_menus() + + def __getitem__(self, index): + if isinstance(index, int): + self._menu = self._menus[index] + else: + for menu in self._menus: + cmd = menu.get('CommandURL', '') + if MENUS[index.lower()] == cmd: + self._menu = menu + break + # ~ line = self._menu.get('CommandURL', '') + # ~ line += self._get_submenus(self._menu['ItemDescriptorContainer']) + return self._menu + + def _get_menus(self): + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + self._ui = manager.getUIConfigurationManager(service) + self._pymenus = self._ui.getSettings(NODE_MENUBAR, True) + data = [] + for menu in self._pymenus: + data.append(data_to_dict(menu)) + return data + + def _get_info(self, menu): + line = menu.get('CommandURL', '') + line += self._get_submenus(menu['ItemDescriptorContainer']) + return line + + def _get_submenus(self, menu, level=1): + 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 += self._get_submenus(submenu, level + 1) + return line + + def __str__(self): + info = '\n'.join([self._get_info(m) for m in self._menus]) + return info + + def _get_index_menu(self, menu, command): + index = -1 + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '') + if cmd == command: + index = i + break + return index + + def insert(self, name, args): + idc = None + replace = False + command = args['CommandURL'] + label = args['Label'] + + self[name] + menu = self._menu['ItemDescriptorContainer'] + submenu = args.get('Submenu', False) + if submenu: + idc = self._ui.createSettings() + + index = self._get_index_menu(menu, command) + if index == -1: + if 'Index' in args: + index = args['Index'] + else: + index = self._get_index_menu(menu, args['After']) + 1 + else: + replace = True + + data = dict ( + CommandURL = command, + Label = label, + Style = 0, + Type = 0, + ItemDescriptorContainer = idc, + ) + self._save(menu, data, index, replace) + self._insert_submenu(idc, submenu) + return + + def _get_command(self, args): + shortcut = args.get('ShortCut', '') + cmd = args['CommandURL'] + if isinstance(cmd, dict): + cmd = _get_url_script(cmd) + if shortcut: + LOShortCut(self._app).add(shortcut, cmd) + return cmd + + def _insert_submenu(self, parent, menus): + for i, v in enumerate(menus): + submenu = v.pop('Submenu', False) + if submenu: + idc = self._ui.createSettings() + v['ItemDescriptorContainer'] = idc + v['Type'] = 0 + if v['Label'] == '-': + v['Type'] = 1 + else: + v['CommandURL'] = self._get_command(v) + self._save(parent, v, i) + if submenu: + self._insert_submenu(idc, submenu) + return + + def remove(self, name, command): + self[name] + menu = self._menu['ItemDescriptorContainer'] + index = self._get_index_menu(menu, command) + if index > -1: + uno.invoke(menu, 'removeByIndex', (index,)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + def _save(self, menu, properties, index, replace=False): + properties = dict_to_property(properties, True) + if replace: + uno.invoke(menu, 'replaceByIndex', (index, properties)) + else: + uno.invoke(menu, 'insertByIndex', (index, properties)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + +class LOMenus(object): + + def __getitem__(self, index): + return LOMenu(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 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 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 +_CB = ClipBoard + + +class Paths(object): + FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' + FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' + + def __init__(self, path=''): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + self._path = Path(path) + + @property + def path(self): + return str(self._path.parent) + + @property + def file_name(self): + return self._path.name + + @property + def name(self): + return self._path.stem + + @property + def ext(self): + return self._path.suffix[1:] + + @property + def info(self): + return self.path, self.file_name, self.name, self.ext + + @property + def url(self): + return self._path.as_uri() + + @property + def size(self): + return self._path.stat().st_size + + @classproperty + def home(self): + return str(Path.home()) + + @classproperty + def documents(self): + return self.config() + + @classproperty + def temp_dir(self): + return tempfile.gettempdir() + + @classproperty + def python(self): + 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 dir_tmp(self, only_name=False): + dt = tempfile.TemporaryDirectory() + if only_name: + dt = dt.name + return dt + + @classmethod + def tmp(cls, ext=''): + tmp = tempfile.NamedTemporaryFile(suffix=ext) + return tmp.name + + @classmethod + def save_tmp(cls, data): + path_tmp = cls.tmp() + cls.save(path_tmp, data) + return path_tmp + + @classmethod + def config(cls, name='Work'): + """ + Return path from config + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html + """ + path = create_instance('com.sun.star.util.PathSettings') + path = cls.to_system(getattr(path, name)) + return path + + @classmethod + def get(cls, init_dir='', filters: str=''): + """ + Get path for save + Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html + filters: 'xml' or 'txt,xml' + """ + 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((2,)) + if filters: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + path = '' + if file_picker.execute(): + path = cls.to_system(file_picker.getSelectedFiles()[0]) + return path + + @classmethod + def get_dir(cls, init_dir=''): + 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 file + + init_folder: folder default open + filters: 'xml' or 'xml,txt' + multiple: True for multiple selected + """ + 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: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + 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 replace_ext(cls, path, new_ext): + p = Paths(path) + name = f'{p.name}.{new_ext}' + path = cls.join(p.path, name) + return path + + @classmethod + def exists(cls, path): + result = False + if path: + path = cls.to_system(path) + result = Path(path).exists() + return result + + @classmethod + def exists_app(cls, name_app): + return bool(shutil.which(name_app)) + + @classmethod + def open(cls, path): + if IS_WIN: + os.startfile(path) + else: + pid = subprocess.Popen(['xdg-open', path]).pid + return + + @classmethod + def is_dir(cls, path): + return Path(path).is_dir() + + @classmethod + def is_file(cls, path): + return Path(path).is_file() + + @classmethod + def join(cls, *paths): + return str(Path(paths[0]).joinpath(*paths[1:])) + + @classmethod + def save(cls, path, data, encoding='utf-8'): + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path, data): + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def read(cls, path, encoding='utf-8'): + data = Path(path).read_text(encoding=encoding) + return data + + @classmethod + def read_bin(cls, path): + data = Path(path).read_bytes() + return data + + @classmethod + def to_url(cls, path): + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def to_system(cls, path): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def kill(cls, path): + 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 files(cls, path, pattern='*'): + files = [str(p) for p in Path(path).glob(pattern) if p.is_file()] + return files + + @classmethod + def dirs(cls, path): + dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()] + return dirs + + @classmethod + def walk(cls, path, filters=''): + paths = [] + if filters in ('*', '*.*'): + filters = '' + 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 walk_dirs(cls, path, tree=False): + """ + Get directories recursively + path: path source + tree: get info in a tuple (ID_FOLDER, ID_PARENT, NAME) + """ + folders = [] + if tree: + i = 0 + p = 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 from_id(cls, id_ext): + pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') + path = _P.to_system(pip.getPackageLocation(id_ext)) + return path + + @classmethod + def from_json(cls, path): + data = json.loads(cls.read(path)) + return data + + @classmethod + def to_json(cls, path, data): + data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) + return cls.save(path, data) + + @classmethod + def from_csv(cls, path, args={}): + # ~ See https://docs.python.org/3.7/library/csv.html#csv.reader + with open(path) as f: + rows = tuple(csv.reader(f, **args)) + return rows + + @classmethod + def to_csv(cls, path, data, args={}): + with open(path, 'w') as f: + writer = csv.writer(f, **args) + writer.writerows(data) + return + + @classmethod + def zip(cls, source, target=''): + 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 zip_content(cls, path): + with zipfile.ZipFile(path) as z: + names = z.namelist() + return names + + @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 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 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 + + @classmethod + def copy(cls, source, target='', name=''): + p, f, n, e = _P(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 +_P = Paths + + +class Dates(object): + + @classmethod + def date(cls, year, month, day): + d = datetime.date(year, month, day) + return d + + @classmethod + def str_to_date(cls, str_date, template, to_calc=False): + 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, frm=''): + d = datetime.date.fromordinal(int(value) + DATE_OFFSET) + if frm: + d = d.strftime(frm) + return d + + +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 + + +class IOStream(object): + + @classmethod + def buffer(cls): + return io.BytesIO() + + @classmethod + def input(cls, buffer): + instance = 'com.sun.star.io.SequenceInputStream' + stream = create_instance(instance, True) + stream.initialize((uno.ByteSequence(buffer.getvalue()),)) + return stream + + @classmethod + def output(cls): + return OutputStream() + + @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 == 'active': + return LODocs().active + if name == 'active_sheet': + return LODocs().active.active + if name == 'selection': + return LODocs().active.selection + if name == 'current_region': + return LODocs().active.selection.current_region + if name in ('rectangle', 'pos_size'): + return Rectangle() + if name == 'paths': + return Paths + if name == 'docs': + return LODocs() + if name == 'db': + return LODBServer() + if name == 'sheets': + return LOSheets() + if name == 'cells': + return LOCells() + if name == 'menus': + return LOMenus() + if name == 'shortcuts': + return LOShortCuts() + if name == 'clipboard': + return ClipBoard + if name == 'dates': + return Dates + if name == 'ios': + return IOStream() + 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 '' + + +def get_fonts(): + toolkit = create_instance('com.sun.star.awt.Toolkit') + device = toolkit.createScreenCompatibleDevice(0, 0) + return device.FontDescriptors + + +def get_filters(): + """ + Get all support filters + https://help.libreoffice.org/latest/en-US/text/shared/guide/convertfilters.html + """ + 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 + + +# ~ https://en.wikipedia.org/wiki/Web_colors +def get_color(value): + 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, + } + + if isinstance(value, tuple): + color = (value[0] << 16) + (value[1] << 8) + value[2] + else: + if value[0] == '#': + r, g, b = bytes.fromhex(value[1:]) + color = (r << 16) + (g << 8) + b + else: + color = COLORS.get(value.lower(), -1) + return color + + +COLOR_ON_FOCUS = get_color('LightYellow') + + +class LOServer(object): + 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): + 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/pythonpath/main.py b/source/pythonpath/main.py new file mode 100644 index 0000000..534fc70 --- /dev/null +++ b/source/pythonpath/main.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 + +import easymacro as app + + +IS_HEADER = 'P' +IS_CARGO = '0' +CLASE = '1' +ORIGEN = '11' +PRINT = '0' +AJUSTE = '0' +FILE_TARGET = 'polizas.txt' + + +def main(args): + + if args == 'procesar': + _procesar() + + if args == 'generar': + _generar() + + return + + +def _get_header(row): + header = ('P |{poliza_date:%Y%m%d}|{poliza_type:>4}|{poliza_num:>9}|{clase}|' + '{poliza_id:<10}|{poliza_description:<100}|{origen}|{impresa}|{ajuste} ' + ) + poliza_num = row[12] + poliza_id = row[10].strip() or '0' + poliza_date = app.dates.calc_to_date(row[11]) + poliza_type = row[13] + poliza_description = row[14] + data = dict( + poliza_date = poliza_date, + poliza_type = poliza_type, + poliza_num = poliza_num, + clase = CLASE, + poliza_id = poliza_id, + poliza_description = poliza_description, + origen = ORIGEN, + impresa = PRINT, + ajuste = AJUSTE, + ) + line = header.format(**data).replace('|', ' ') + return line + + +def _get_mov(row): + template = ('M1|{num_cuenta:<30}|{reference:<10}|{type_mov}|{importe:<20}|' + '{diario:<10}|{importe_e:<20}|{description:<100}| ' + ) + num_cuenta = row[0] + reference = row[6] + type_mov = '0' + importe = row[2] + importe_e = row[4] + if row[3]: + type_mov = '1' + importe = row[3] + importe_e = row[5] + if not importe_e: + importe_e = 0.0 + diario = row[8].strip() or '0' + description = row[7] + uuid = row[15].strip() + data = dict( + num_cuenta = num_cuenta, + reference = reference, + type_mov = type_mov, + importe = importe, + diario = diario, + importe_e = importe_e, + description = description, + ) + line = template.format(**data).replace('|', ' ') + return line, uuid + + +def _generar(): + doc = app.active + + if not 'POLIZA' in doc: + message = 'No se encontró la hoja: POLIZA' + app.error(message) + app.errorbox(message) + return + + message = ( + '¿Estás seguro de generar el archivo TXT?\n\n' + 'Todos los datos serán tomados de la hoja POLIZA\n\n' + ) + if not app.question(message): + return + + path = app.paths.join(doc.dir, FILE_TARGET) + source = doc['POLIZA'] + data = source['A1'].current_region.data + + lines = [] + uuids = set() + num_current = '' + + for row in data[1:]: + poliza_num = row[12] + if num_current != poliza_num: + if uuids: + for u in uuids: + line = f'AD {u}' + lines.append(line) + uuids = set() + num_current = poliza_num + line = _get_header(row) + lines.append(line) + + line, uuid = _get_mov(row) + lines.append(line) + + if uuid: + uuids.add(uuid) + line = f'AM {uuid}' + lines.append(line) + + if uuids: + for u in uuids: + line = f'AD {u}' + lines.append(line) + + app.paths.save(path, '\n'.join(lines)) + + message = f'Archivo generado correctamente en:\n\n{path}' + app.msgbox(message) + + return + + +def _procesar(): + if not _validate_sheets(): + return + + message = ( + '¿Estás seguro de procesar este archivo?\n\n' + 'Todos los datos de la hoja POLIZA serán reemplazados\n\n' + 'ESTA ACCIÓN NO SE PUEDE DESHACER' + ) + if not app.question(message): + return + + doc = app.active + sheet_catalogo = doc['Catalogo'] + source = doc['TXT'] + target = doc['POLIZA'] + + data = sheet_catalogo['A1'].current_region.data + catalog = {str(int(r[1])): r[2] for r in data} + + rows = [] + data = source['A1'].current_region.data + for row in data: + r = row[0] + header = r[:2].strip() + if header == IS_HEADER: + date = app.dates.str_to_date(r[3:11].strip(), '%Y%m%d', True) + number = r[22:26].strip() + type_poliza = r[15:16] + description = r[40:141].strip() + poliza = (date, number, type_poliza, description) + # ~ print(poliza) + else: + account = r[3:13].strip() + name = catalog.get(account, 'NO EXISTE') + ca = r[55:56].strip() + importe = r[57:69].strip() + if ca == IS_CARGO: + cargo = importe + abono = '' + else: + cargo = '' + abono = importe + reference = r[34:55].strip() + description = r[110:211].strip() + movement = (account, name, cargo, abono, '', '', reference, + description, '', '', '') + poliza + rows.append(movement) + + target['A2'].data = rows + + message = 'Archivo procesado correctamente' + app.msgbox(message) + + return + + +def _validate_sheets(): + + doc = app.active + + if not 'Catalogo' in doc: + message = 'No se encontró la hoja: Catalogo' + app.error(message) + app.errorbox(message) + return False + + if not 'TXT' in doc: + message = 'No se encontró la hoja: TXT' + app.error(message) + app.errorbox(message) + return False + + if not 'POLIZA' in doc: + message = 'No se encontró la hoja: POLIZA' + app.error(message) + app.errorbox(message) + return False + + return True diff --git a/source/registration/license_en.txt b/source/registration/license_en.txt new file mode 100644 index 0000000..70374a1 --- /dev/null +++ b/source/registration/license_en.txt @@ -0,0 +1,14 @@ +This file is part of ZAZPolizas. + + ZAZPolizas 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. + + ZAZPolizas 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 ZAZPolizas. If not, see . diff --git a/source/registration/license_es.txt b/source/registration/license_es.txt new file mode 100644 index 0000000..70374a1 --- /dev/null +++ b/source/registration/license_es.txt @@ -0,0 +1,14 @@ +This file is part of ZAZPolizas. + + ZAZPolizas 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. + + ZAZPolizas 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 ZAZPolizas. If not, see . diff --git a/zaz.py b/zaz.py new file mode 100755 index 0000000..83e6058 --- /dev/null +++ b/zaz.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python3 + +# == Rapid Develop Macros in LibreOffice == + +# ~ This file is part of ZAZ. + +# ~ https://git.elmau.net/elmau/zaz + +# ~ ZAZ 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. + +# ~ ZAZ 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 ZAZ. If not, see . + +import argparse +import os +import py_compile +import re +import sys +import zipfile +from datetime import datetime +from pathlib import Path +from shutil import copyfile +from subprocess import call +from xml.etree import ElementTree as ET +from xml.dom.minidom import parseString + + +from conf import ( + DATA, + DIRS, + DOMAIN, + EXTENSION, + FILES, + INFO, + PATHS, + TYPE_EXTENSION, + USE_LOCALES, + log) + + +EASYMACRO = 'easymacro.py' + + +class LiboXML(object): + CONTEXT = { + 'calc': 'com.sun.star.sheet.SpreadsheetDocument', + 'writer': 'com.sun.star.text.TextDocument', + 'impress': 'com.sun.star.presentation.PresentationDocument', + 'draw': 'com.sun.star.drawing.DrawingDocument', + 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + 'math': 'com.sun.star.formula.FormulaProperties', + 'basic': 'com.sun.star.script.BasicIDE', + } + TYPES = { + 'py': 'application/vnd.sun.star.uno-component;type=Python', + 'pyc': 'application/binary', + 'zip': 'application/binary', + 'xcu': 'application/vnd.sun.star.configuration-data', + 'rdb': 'application/vnd.sun.star.uno-typelibrary;type=RDB', + 'xcs': 'application/vnd.sun.star.configuration-schema', + 'help': 'application/vnd.sun.star.help', + 'component': 'application/vnd.sun.star.uno-components', + } + NS_MANIFEST = { + 'manifest_version': '1.2', + 'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0', + 'xmlns:loext': 'urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0', + } + NS_DESCRIPTION = { + 'xmlns': 'http://openoffice.org/extensions/description/2006', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'xmlns:d': 'http://openoffice.org/extensions/description/2006', + } + NS_ADDONS = { + 'xmlns:xs': 'http://www.w3.org/2001/XMLSchema', + 'xmlns:oor': 'http://openoffice.org/2001/registry', + } + NS_UPDATE = { + 'xmlns': 'http://openoffice.org/extensions/update/2006', + 'xmlns:d': 'http://openoffice.org/extensions/description/2006', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + } + + def __init__(self): + self._manifest = None + self._paths = [] + self._path_images = '' + self._toolbars = [] + + def _save_path(self, attr): + self._paths.append(attr['{{{}}}full-path'.format(self.NS_MANIFEST['manifest'])]) + return + + def _clean(self, name, nodes): + has_words = re.compile('\\w') + + if not re.search(has_words, str(nodes.tail)): + nodes.tail = '' + if not re.search(has_words, str(nodes.text)): + nodes.text = '' + + for node in nodes: + if name == 'manifest': + self._save_path(node.attrib) + if not re.search(has_words, str(node.tail)): + node.tail = '' + if not re.search(has_words, str(node.text)): + node.text = '' + return + + def new_manifest(self, data): + attr = { + 'manifest:version': self.NS_MANIFEST['manifest_version'], + 'xmlns:manifest': self.NS_MANIFEST['manifest'], + 'xmlns:loext': self.NS_MANIFEST['xmlns:loext'], + } + self._manifest = ET.Element('manifest:manifest', attr) + return self.add_data_manifest(data) + + def parse_manifest(self, data): + ET.register_namespace('manifest', self.NS_MANIFEST['manifest']) + self._manifest = ET.fromstring(data) + attr = {'xmlns:loext': self.NS_MANIFEST['xmlns:loext']} + self._manifest.attrib.update(**attr) + self._clean('manifest', self._manifest) + return + + def add_data_manifest(self, data): + node_name = 'manifest:file-entry' + attr = { + 'manifest:full-path': '', + 'manifest:media-type': '', + } + for path in data: + if path in self._paths: + continue + ext = path.split('.')[-1] + attr['manifest:full-path'] = path + attr['manifest:media-type'] = self.TYPES.get(ext, '') + ET.SubElement(self._manifest, node_name, attr) + return self._get_xml(self._manifest) + + def new_description(self, data): + doc = ET.Element('description', self.NS_DESCRIPTION) + + key = 'identifier' + ET.SubElement(doc, key, data[key]) + + key = 'version' + ET.SubElement(doc, key, data[key]) + + key = 'display-name' + node = ET.SubElement(doc, key) + for k, v in data[key].items(): + sn = ET.SubElement(node, 'name', {'lang': k}) + sn.text = v + + node = ET.SubElement(doc, 'extension-description') + for k in data[key].keys(): + attr = { + 'lang': k, + 'xlink:href': f'description/desc_{k}.txt', + } + ET.SubElement(node, 'src', attr) + + key = 'icon' + node = ET.SubElement(doc, key) + attr = {'xlink:href': f"images/{data[key]}"} + ET.SubElement(node, 'default', attr) + + key = 'publisher' + node = ET.SubElement(doc, key) + for k, v in data[key].items(): + attr = { + 'xlink:href': v['link'], + 'lang': k, + } + sn = ET.SubElement(node, 'name', attr) + sn.text = v['text'] + + key = 'display-name' + node = ET.SubElement(doc, 'registration') + attr = { + 'accept-by': 'user', + 'suppress-on-update': 'true', + } + node = ET.SubElement(node, 'simple-license', attr) + for k in data[key].keys(): + attr = { + 'xlink:href': f"{DIRS['registration']}/license_{k}.txt", + 'lang': k + } + ET.SubElement(node, 'license-text', attr) + + if data['update']: + node = ET.SubElement(doc, 'update-information') + ET.SubElement(node, 'src', {'xlink:href': data['update']}) + + return self._get_xml(doc) + + def _get_context(self, args): + if not args: + return '' + context = ','.join([self.CONTEXT[v] for v in args.split(',')]) + return context + + def _add_node_value(self, node, name, value='_self'): + attr = {'oor:name': name, 'oor:type': 'xs:string'} + sn = ET.SubElement(node, 'prop', attr) + sn = ET.SubElement(sn, 'value') + sn.text = value + return + + def _add_menu(self, id_extension, node, index, menu, in_menu_bar=True): + if in_menu_bar: + attr = { + 'oor:name': index, + 'oor:op': 'replace', + } + subnode = ET.SubElement(node, 'node', attr) + else: + subnode = node + + attr = {'oor:name': 'Title', 'oor:type': 'xs:string'} + sn1 = ET.SubElement(subnode, 'prop', attr) + for k, v in menu['title'].items(): + sn2 = ET.SubElement(sn1, 'value', {'xml:lang': k}) + sn2.text = v + value = self._get_context(menu['context']) + self._add_node_value(subnode, 'Context', value) + + if 'submenu' in menu: + sn = ET.SubElement(subnode, 'node', {'oor:name': 'Submenu'}) + for i, m in enumerate(menu['submenu']): + self._add_menu(id_extension, sn, f'{index}.s{i}', m) + if m.get('toolbar', False): + self._toolbars.append(m) + return + + value = f"service:{id_extension}?{menu['argument']}" + self._add_node_value(subnode, 'URL', value) + self._add_node_value(subnode, 'Target') + value = f"%origin%/{self._path_images}/{menu['icon']}" + self._add_node_value(subnode, 'ImageIdentifier', value) + return + + def new_addons(self, id_extension, data): + in_menu_bar = data['parent'] == 'OfficeMenuBar' + self._path_images = data['images'] + attr = { + 'oor:name': 'Addons', + 'oor:package': 'org.openoffice.Office', + } + attr.update(self.NS_ADDONS) + doc = ET.Element('oor:component-data', attr) + parent = ET.SubElement(doc, 'node', {'oor:name': 'AddonUI'}) + node = ET.SubElement(parent, 'node', {'oor:name': data['parent']}) + + op = 'fuse' + if in_menu_bar: + op = 'replace' + + attr = {'oor:name': id_extension, 'oor:op': op} + node = ET.SubElement(node, 'node', attr) + + if in_menu_bar: + attr = {'oor:name': 'Title', 'oor:type': 'xs:string'} + subnode = ET.SubElement(node, 'prop', attr) + for k, v in data['main'].items(): + sn = ET.SubElement(subnode, 'value', {'xml:lang': k}) + sn.text = v + + self._add_node_value(node, 'Target') + node = ET.SubElement(node, 'node', {'oor:name': 'Submenu'}) + + for i, menu in enumerate(data['menus']): + self._add_menu(id_extension, node, f'm{i}', menu, in_menu_bar) + if menu.get('toolbar', False): + self._toolbars.append(menu) + + if self._toolbars: + attr = {'oor:name': 'OfficeToolBar'} + toolbar = ET.SubElement(parent, 'node', attr) + attr = {'oor:name': id_extension, 'oor:op': 'replace'} + toolbar = ET.SubElement(toolbar, 'node', attr) + for t, menu in enumerate(self._toolbars): + self._add_menu(id_extension, toolbar, f't{t}', menu) + + return self._get_xml(doc) + + def _add_shortcut(self, node, key, id_extension, arg): + attr = {'oor:name': key, 'oor:op': 'fuse'} + subnode = ET.SubElement(node, 'node', attr) + subnode = ET.SubElement(subnode, 'prop', {'oor:name': 'Command'}) + subnode = ET.SubElement(subnode, 'value', {'xml:lang': 'en-US'}) + subnode.text = f"service:{id_extension}?{arg}" + return + + def _get_acceleartors(self, menu): + if 'submenu' in menu: + for m in menu['submenu']: + return self._get_acceleartors(m) + + if not menu.get('shortcut', ''): + return '' + + return menu + + def new_accelerators(self, id_extension, menus): + attr = { + 'oor:name': 'Accelerators', + 'oor:package': 'org.openoffice.Office', + } + attr.update(self.NS_ADDONS) + doc = ET.Element('oor:component-data', attr) + parent = ET.SubElement(doc, 'node', {'oor:name': 'PrimaryKeys'}) + + data = [] + for m in menus: + info = self._get_acceleartors(m) + if info: + data.append(info) + + node_global = None + node_modules = None + for m in data: + if m['context']: + if node_modules is None: + node_modules = ET.SubElement( + parent, 'node', {'oor:name': 'Modules'}) + for app in m['context'].split(','): + node = ET.SubElement( + node_modules, 'node', {'oor:name': self.CONTEXT[app]}) + self._add_shortcut( + node, m['shortcut'], id_extension, m['argument']) + else: + if node_global is None: + node_global = ET.SubElement( + parent, 'node', {'oor:name': 'Global'}) + self._add_shortcut( + node_global, m['shortcut'], id_extension, m['argument']) + + return self._get_xml(doc) + + def new_update(self, extension, url_oxt): + doc = ET.Element('description', self.NS_UPDATE) + ET.SubElement(doc, 'identifier', {'value': extension['id']}) + ET.SubElement(doc, 'version', {'value': extension['version']}) + node = ET.SubElement(doc, 'update-download') + ET.SubElement(node, 'src', {'xlink:href': url_oxt}) + node = ET.SubElement(doc, 'release-notes') + return self._get_xml(doc) + + def _get_xml(self, doc): + xml = parseString(ET.tostring(doc, encoding='utf-8')) + return xml.toprettyxml(indent=' ', encoding='utf-8').decode('utf-8') + + +def _exists(path): + return os.path.exists(path) + + +def _join(*paths): + return os.path.join(*paths) + + +def _mkdir(path): + return Path(path).mkdir(parents=True, exist_ok=True) + + +def _save(path, data): + with open(path, 'w') as f: + f.write(data) + return + + +def _get_files(path, filters=''): + paths = [] + if filters in ('*', '*.*'): + filters = '' + for folder, _, files in os.walk(path): + if filters: + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [_join(folder, f) for f in files if pattern.search(f)] + else: + paths += files + return paths + + +def _compress_oxt(): + log.info('Compress OXT extension...') + + path_oxt = _join(DIRS['files'], FILES['oxt']) + + z = zipfile.ZipFile(path_oxt, 'w', compression=zipfile.ZIP_DEFLATED) + root_len = len(os.path.abspath(DIRS['source'])) + for root, dirs, files in os.walk(DIRS['source']): + relative = os.path.abspath(root)[root_len:] + for f in files: + fullpath = _join(root, f) + file_name = _join(relative, f) + if file_name == FILES['idl']: + continue + z.write(fullpath, file_name, zipfile.ZIP_DEFLATED) + z.close() + + log.info('Extension OXT created successfully...') + return + + +def _install_and_test(): + path_oxt = (_join(DIRS['files'], FILES['oxt']),) + call(PATHS['install'] + path_oxt) + log.info('Install extension successfully...') + log.info('Start LibreOffice...') + call(PATHS['soffice']) + return + + +def _validate_new(): + path_source = DIRS['source'] + if not _exists(path_source): + return True + + msg = f'Path: {path_source}, exists, delete first' + log.error(msg) + return False + + +def _create_new_directories(): + path_source = DIRS['source'] + _mkdir(path_source) + path = _join(path_source, DIRS['meta']) + _mkdir(path) + path = _join(path_source, DIRS['description']) + _mkdir(path) + path = _join(path_source, DIRS['images']) + _mkdir(path) + path = _join(path_source, DIRS['registration']) + _mkdir(path) + path = _join(path_source, DIRS['office']) + _mkdir(path) + + if FILES['easymacro'] or DIRS['pythonpath']: + path = _join(path_source, 'pythonpath') + _mkdir(path) + + path = DIRS['files'] + if not _exists(path): + _mkdir(path) + + msg = 'Created directories...' + log.info(msg) + return + + +def _create_new_files(): + path_source = DIRS['source'] + + for k, v in INFO.items(): + file_name = f'license_{k}.txt' + path = _join(path_source, DIRS['registration'], file_name) + _save(path, v['license']) + + if TYPE_EXTENSION > 1: + path = _join(path_source, FILES['idl']) + _save(path, DATA['idl']) + + path = _join(path_source, FILES['py']) + _save(path, DATA['py']) + + + msg = 'Created files...' + log.info(msg) + return + + +def _validate_update(): + if TYPE_EXTENSION == 1: + return True + + if not _exists(PATHS['idlc']): + msg = 'Binary: "idlc" not found' + log.error(msg) + return False + + if not _exists(PATHS['include']): + msg = 'Directory: "include" not found' + log.error(msg) + return False + + if not _exists(PATHS['regmerge']): + msg = 'Binary: "regmerge" not found' + log.error(msg) + return False + + path = _join(DIRS['source'], FILES['idl']) + if not _exists(path): + msg = f'File: "{FILES["idl"]}" not found' + log.error(msg) + return False + + return True + + +def _compile_idl(): + if TYPE_EXTENSION == 1: + return + + log.info('Compilate IDL...') + path_rdb = _join(DIRS['source'], FILES['rdb']) + path_urd = _join(DIRS['source'], FILES['urd']) + + path = _join(DIRS['source'], FILES['idl']) + call([PATHS['idlc'], '-I', PATHS['include'], path]) + call([PATHS['regmerge'], path_rdb, '/UCR', path_urd]) + os.remove(path_urd) + + log.info('Compilate IDL successfully...') + return + + +def _update_files(): + path_files = DIRS['files'] + if not _exists(path_files): + _mkdir(path_files) + + path_source = DIRS['source'] + + for k, v in INFO.items(): + file_name = FILES['ext_desc'].format(k) + path = _join(path_source, DIRS['description'], file_name) + _save(path, v['description']) + + path_logo = EXTENSION['icon'][0] + if _exists(path_logo): + file_name = EXTENSION['icon'][1] + path = _join(path_source, DIRS['images'], file_name) + copyfile(path_logo, path) + + files = os.listdir(DIRS['images']) + for f in files: + if f[-3:].lower() == 'bmp': + source = _join(DIRS['images'], f) + target = _join(path_source, DIRS['images'], f) + copyfile(source, target) + + if FILES['easymacro']: + source = EASYMACRO + target = _join(path_source, 'pythonpath', source) + copyfile(source, target) + + xml = LiboXML() + + path = _join(path_source, DIRS['meta'], FILES['manifest']) + data = xml.new_manifest(DATA['manifest']) + _save(path, data) + + path = _join(path_source, FILES['description']) + data = xml.new_description(DATA['description']) + _save(path, data) + + if TYPE_EXTENSION == 1: + path = _join(path_source, FILES['addons']) + data = xml.new_addons(EXTENSION['id'], DATA['addons']) + _save(path, data) + + path = _join(path_source, DIRS['office']) + _mkdir(path) + path = _join(path_source, DIRS['office'], FILES['shortcut']) + data = xml.new_accelerators(EXTENSION['id'], DATA['addons']['menus']) + _save(path, data) + + + if TYPE_EXTENSION == 3: + path = _join(path_source, FILES['addin']) + _save(path, DATA['addin']) + + if USE_LOCALES: + msg = "Don't forget generate DOMAIN.pot for locales" + for lang in EXTENSION['languages']: + path = _join(path_source, DIRS['locales'], lang, 'LC_MESSAGES') + Path(path).mkdir(parents=True, exist_ok=True) + log.info(msg) + + if DATA['update']: + path_xml = _join(path_files, FILES['update']) + data = xml.new_update(EXTENSION, DATA['update']) + _save(path_xml, data) + + _compile_idl() + return + + +def _create(): + if not _validate_new(): + return + + _create_new_directories() + _create_new_files() + _update_files() + + msg = f"New extension: {EXTENSION['name']} make sucesfully...\n" + msg += '\tNow, you can install and test: zaz.py -i' + log.info(msg) + return + + +def _get_info_path(path): + path, filename = os.path.split(path) + name, extension = os.path.splitext(filename) + return (path, filename, name, extension) + + +def _zip_embed(source, files): + PATH = 'Scripts/python/' + FILE_PYC = 'easymacro.pyc' + + p, f, name, e = _get_info_path(source) + now = datetime.now().strftime('_%Y%m%d_%H%M%S') + path_source = _join(p, name + now + e) + copyfile(source, path_source) + target = source + + py_compile.compile(EASYMACRO, FILE_PYC) + xml = LiboXML() + + path_easymacro = PATH + FILE_PYC + names = [f[1] for f in files] + [path_easymacro] + nodes = [] + with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as zt: + with zipfile.ZipFile(path_source, compression=zipfile.ZIP_DEFLATED) as zs: + for name in zs.namelist(): + if FILES['manifest'] in name: + path_manifest = name + xml_manifest = zs.open(name).read() + elif name in names: + continue + else: + zt.writestr(name, zs.open(name).read()) + + data = [] + for path, name in files: + data.append(name) + zt.write(path, name) + + zt.write(FILE_PYC, path_easymacro) + data.append(path_easymacro) + + xml.parse_manifest(xml_manifest) + xml_manifest = xml.add_data_manifest(data) + zt.writestr(path_manifest, xml_manifest) + + os.unlink(FILE_PYC) + return + + +def _embed(args): + PATH = 'Scripts/python' + PYTHONPATH = 'pythonpath' + + doc = args.document + if not doc: + msg = '-d/--document Path file to embed is mandatory' + log.error(msg) + return + if not _exists(doc): + msg = 'Path file not exists' + log.error(msg) + return + + files = [] + if args.files: + files = args.files.split(',') + source = _join(PATHS['profile'], PATH) + content = os.listdir(source) + if PYTHONPATH in content: + content.remove(PYTHONPATH) + + if files: + files = [(_join(source, f), _join(PATH, f)) for f in files if f in content] + else: + files = [(_join(source, f), _join(PATH, f)) for f in content] + + _zip_embed(doc, files) + + log.info('Embedded macros successfully...') + return + + +def _locales(args): + if args.files: + files = args.files.split(',') + else: + files = _get_files(DIRS['source'], 'py') + paths = ' '.join([f for f in files if not EASYMACRO in f]) + path_pot = _join(DIRS['source'], DIRS['locales'], '{}.pot'.format(DOMAIN)) + call([PATHS['gettext'], '-o', path_pot, paths]) + log.info('POT generate successfully...') + return + + +def _update(): + path_locales = _join(DIRS['source'], DIRS['locales']) + path_pot = _join(DIRS['source'], DIRS['locales'], '{}.pot'.format(DOMAIN)) + if not _exists(path_pot): + log.error('Not exists file POT...') + return + + files = _get_files(path_locales, 'po') + if not files: + log.error('First, generate files PO...') + return + + for f in files: + call([PATHS['msgmerge'], '-U', f, path_pot]) + log.info('\tUpdate: {}'.format(f)) + + log.info('Locales update successfully...') + return + + +def _new(args): + if not args.target: + msg = 'Add argument target: -t PATH_TARGET' + log.error(msg) + return + + if not args.name: + msg = 'Add argument name: -n name-new-extension' + log.error(msg) + return + + path = _join(args.target, args.name) + _mkdir(path) + _mkdir(_join(path, 'files')) + _mkdir(_join(path, 'images')) + path_logo = 'images/pymacros.png' + copyfile(path_logo, _join(path, 'images/logo.png')) + copyfile('zaz.py', _join(path, 'zaz.py')) + copyfile(EASYMACRO, _join(path, 'easymacro.py')) + copyfile('conf.py.example', _join(path, 'conf.py')) + + msg = 'Folders and files copy successfully for new extension.' + log.info(msg) + msg = f'Change to folder: {path}' + log.info(msg) + return + + +def main(args): + if args.new: + _new(args) + return + + if args.update: + _update() + return + + if args.locales: + _locales(args) + return + + if args.embed: + _embed(args) + return + + if args.create: + _create() + return + + if not _validate_update(): + return + + if not args.only_compress: + _update_files() + + _compress_oxt() + + if args.install: + _install_and_test() + + log.info('Extension make successfully...') + return + + +def _process_command_line_arguments(): + parser = argparse.ArgumentParser( + description='Make LibreOffice extensions') + parser.add_argument('-new', '--new', dest='new', action='store_true', + default=False, required=False) + parser.add_argument('-t', '--target', dest='target', default='') + parser.add_argument('-n', '--name', dest='name', default='', required=False) + parser.add_argument('-c', '--create', dest='create', action='store_true', + default=False, required=False) + parser.add_argument('-i', '--install', dest='install', action='store_true', + default=False, required=False) + parser.add_argument('-e', '--embed', dest='embed', action='store_true', + default=False, required=False) + parser.add_argument('-d', '--document', dest='document', default='') + parser.add_argument('-f', '--files', dest='files', default='') + parser.add_argument('-l', '--locales', dest='locales', action='store_true', + default=False, required=False) + parser.add_argument('-u', '--update', dest='update', action='store_true', + default=False, required=False) + parser.add_argument('-oc', '--only_compress', dest='only_compress', + action='store_true', default=False, required=False) + return parser.parse_args() + + +if __name__ == '__main__': + args = _process_command_line_arguments() + main(args) +