From 2688ba6461683908c643b2b0b0bef7bfde5560db Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 7 Oct 2021 14:22:30 -0500 Subject: [PATCH 1/7] First functional version --- .gitignore | 12 + VERSION | 1 + conf.py | 439 ++ easymacro.py | 7571 ++++++++++++++++++++++++++++ files/ZAZPass_v0.1.0.oxt | Bin 0 -> 62375 bytes images/logo.png | Bin 0 -> 9189 bytes source/Addons.xcu | 130 + source/META-INF/manifest.xml | 6 + source/Office/Accelerators.xcu | 49 + source/ZAZPass.py | 22 + source/description.xml | 26 + source/description/desc_en.txt | 1 + source/description/desc_es.txt | 1 + source/images/close.svg | 4 + source/images/eye-close.svg | 6 + source/images/eye-open.svg | 4 + source/images/insert.svg | 4 + source/images/new.svg | 6 + source/images/zazpass.png | Bin 0 -> 9189 bytes source/pythonpath/easymacro.py | 7571 ++++++++++++++++++++++++++++ source/pythonpath/stats.py | 331 ++ source/pythonpath/zpass.py | 348 ++ source/registration/license_en.txt | 14 + source/registration/license_es.txt | 14 + zaz.py | 823 +++ 25 files changed, 17383 insertions(+) create mode 100644 .gitignore create mode 100644 VERSION create mode 100644 conf.py create mode 100644 easymacro.py create mode 100644 files/ZAZPass_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/ZAZPass.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 100755 source/images/close.svg create mode 100644 source/images/eye-close.svg create mode 100644 source/images/eye-open.svg create mode 100644 source/images/insert.svg create mode 100644 source/images/new.svg create mode 100644 source/images/zazpass.png create mode 100644 source/pythonpath/easymacro.py create mode 100644 source/pythonpath/stats.py create mode 100644 source/pythonpath/zpass.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/.gitignore b/.gitignore new file mode 100644 index 0000000..43d57fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*.po~ + +*.log + +# Virtualenv +.env/ +virtual/ + + + 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..1e03e95 --- /dev/null +++ b/conf.py @@ -0,0 +1,439 @@ +#!/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 = 'ZAZPass' + +# ~ https://semver.org/ +VERSION = '0.1.0' + + +# ~ Should be unique, used URL inverse +ID = 'net.elmau.zaz.pass' + +# ~ 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://git.cuates.net/elmau'}, + 'es': {'text': 'El Mau', 'link': 'https://git.cuates.net/elmau'}, +} + +# ~ 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': 'My first extension', + 'description': 'My great extension', + 'license': LICENSE_EN, + }, + 'es': { + 'display_name': 'Mi primer extensión', + 'description': 'Mi gran extensión', + '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': 'Zaz Passwords', + 'es': 'Zaz Contraseñas', +} +MENUS = ( + { + 'title': {'en': 'Insert password', 'es': 'Insertar contraseña'}, + 'argument': 'insert', + 'context': 'calc,writer', + 'icon': 'icon', + 'toolbar': True, + 'shortcut': 'P_SHIFT_MOD1_MOD2', + }, + { + 'title': {'en': 'Copy in clipboard', 'es': 'Copiar al portapapeles'}, + 'argument': 'copy', + 'context': 'calc,writer', + 'icon': 'icon', + 'toolbar': True, + 'shortcut': 'C_SHIFT_MOD1_MOD2', + }, + { + 'title': {'en': 'Generate password...', 'es': 'Generar contraseña...'}, + 'argument': 'generate', + 'context': 'calc,writer', + 'icon': 'icon', + 'toolbar': True, + 'shortcut': 'G_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 = '' + +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..bd060d4 --- /dev/null +++ b/easymacro.py @@ -0,0 +1,7571 @@ +#!/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.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 XSpinListener +from com.sun.star.awt import XWindowListener +from com.sun.star.awt import XTopWindowListener +from com.sun.star.awt.grid import XGridDataListener +from com.sun.star.awt.grid import XGridSelectionListener +from com.sun.star.script import ScriptEventDescriptor + +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_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' +OBJ_SHAPES = 'com.sun.star.drawing.SvxShapeCollection' +OBJ_GRAPHIC = 'SwXTextGraphicObject' + +OBJ_TEXTS = 'SwXTextRanges' +OBJ_TEXT = 'SwXTextRange' + +CLSID = { + 'FORMULA': '078B7ABA-54FC-457F-8551-6147e776a997', +} + +SERVICES = { + 'TEXT_EMBEDDED': 'com.sun.star.text.TextEmbeddedObject', + 'TEXT_TABLE': 'com.sun.star.text.TextTable', + 'GRAPHIC': 'com.sun.star.text.GraphicObject', +} + + +# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL +class FilterOperator(IntEnum): + EMPTY = 0 + NO_EMPTY = 1 + EQUAL = 2 + NOT_EQUAL = 3 + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 +class Border(IntEnum): + NO_BORDER = 0 + BORDER = 1 + SIMPLE = 2 + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a +class ValidationType(): + from com.sun.star.sheet.ValidationType \ + import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM +VT = ValidationType + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7 +class ValidationAlertStyle(): + from com.sun.star.sheet.ValidationAlertStyle \ + import STOP, WARNING, INFO, MACRO +VAS = ValidationAlertStyle + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html +class ConditionOperator(): + from com.sun.star.sheet.ConditionOperator2 \ + import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \ + LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE +CO = ConditionOperator + + +class DataPilotFieldOrientation(): + from com.sun.star.sheet.DataPilotFieldOrientation \ + import HIDDEN, COLUMN, ROW, PAGE, DATA +DPFO = DataPilotFieldOrientation + + +class CellInsertMode(): + from com.sun.star.sheet.CellInsertMode import DOWN, RIGHT, ROWS, COLUMNS +CIM = CellInsertMode + + +class CellDeleteMode(): + from com.sun.star.sheet.CellDeleteMode import UP, LEFT, ROWS, COLUMNS +CDM = CellDeleteMode + + +class FormButtonType(): + from com.sun.star.form.FormButtonType import PUSH, SUBMIT, RESET, URL +FBT = FormButtonType + + +class TextContentAnchorType(): + from com.sun.star.text.TextContentAnchorType \ + import AT_PARAGRAPH, AS_CHARACTER, AT_PAGE, AT_FRAME, AT_CHARACTER +TCAT = TextContentAnchorType + + +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 = {} + path = _P.join(_P.user_config, 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.user_config, name_file) + values = get_config(prefix=prefix, default={}) + 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_sheet(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): +class LOFormControl(): + EVENTS = { + 'action': 'actionPerformed', + 'click': 'mousePressed', + } + TYPES = { + 'actionPerformed': 'XActionListener', + 'mousePressed': 'XMouseListener', + } + + def __init__(self, obj, view, form): + self._obj = obj + self._view = view + self._form = form + self._m = view.Model + self._index = -1 + + # ~ def __setattr__(self, name, value): + # ~ if name in ('_form', '_view', '_m', '_index'): + # ~ self.__dict__[name] = value + # ~ else: + # ~ super().__setattr__(name, value) + + def __str__(self): + return f'{self.name} ({self.type}) {[self.index]}' + + @property + def obj(self): + return self._obj + + @property + def form(self): + return self._form + + @property + def doc(self): + return self.obj.Parent.Forms.Parent + + @property + def name(self): + return self._m.Name + @name.setter + def name(self, value): + self._m.Name = value + + @property + def tag(self): + return self._m.Tag + @tag.setter + def tag(self, value): + self._m.Tag = value + + @property + def index(self): + return self._index + @index.setter + def index(self, value): + self._index = value + + @property + def enabled(self): + return self._m.Enabled + @enabled.setter + def enabled(self, value): + self._m.Enabled = value + + @property + def anchor(self): + return self.obj.Anchor + @anchor.setter + def anchor(self, value): + size = None + if hasattr(value, 'obj'): + size = getattr(value, 'size', None) + value = value.obj + self.obj.Anchor = value + if not size is None: + self.size = size + try: + self.obj.ResizeWithCell = True + except: + pass + + @property + def size(self): + return self.obj.Size + @size.setter + def size(self, value): + self.obj.Size = value + + @property + def events(self): + return self.form.getScriptEvents(self.index) + def add_event(self, name, macro): + if not 'name' in macro: + macro['name'] = '{}_{}'.format(self.name, name) + + event = ScriptEventDescriptor() + event.AddListenerParam = '' + event.EventMethod = self.EVENTS[name] + event.ListenerType = self.TYPES[event.EventMethod] + event.ScriptCode = _get_url_script(macro) + event.ScriptType = 'Script' + + for ev in self.events: + if ev.EventMethod == event.EventMethod and \ + ev.ListenerType == event.ListenerType: + self.form.revokeScriptEvent(self.index, + event.ListenerType, event.EventMethod, event.AddListenerParam) + break + + self.form.registerScriptEvent(self.index, event) + return + + def set_focus(self): + self._view.setFocus() + return + + +class LOFormControlLabel(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Label = value + + +class LOFormControlText(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self._m.Text + @value.setter + def value(self, value): + self._m.Text = value + + +class LOFormControlButton(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Text = Label + + @property + def url(self): + return self._m.TargetURL + @url.setter + def url(self, value): + self._m.TargetURL = value + self._m.ButtonType = FormButtonType.URL + + +FORM_CONTROL_CLASS = { + 'label': LOFormControlLabel, + 'text': LOFormControlText, + 'button': LOFormControlButton, +} + + +class LOForm(object): + MODELS = { + 'label': 'com.sun.star.form.component.FixedText', + 'text': 'com.sun.star.form.component.TextField', + 'button': 'com.sun.star.form.component.CommandButton', + } + + def __init__(self, obj, draw_page): + self._obj = obj + self._dp = draw_page + self._controls = {} + self._init_controls() + + def __getitem__(self, index): + control = self.obj[index] + return self._controls[control.Name] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + def __str__(self): + return f'Form: {self.name}' + + def _init_controls(self): + types = { + 'com.sun.star.form.OFixedTextModel': 'label', + 'com.sun.star.form.OEditModel': 'text', + 'com.sun.star.form.OButtonModel': 'button', + } + for i, control in enumerate(self.obj): + name = control.Name + tipo = types[control.ImplementationName] + view = self.doc.CurrentController.getControl(control) + control = FORM_CONTROL_CLASS[tipo](control, view, self._obj) + control.index = i + setattr(self, name, control) + self._controls[name] = control + return + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self.obj.Name + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def source(self): + return self.obj.DataSourceName + @source.setter + def source(self, value): + self.obj.DataSourceName = value + + @property + def type(self): + return self.obj.CommandType + @type.setter + def type(self, value): + self.obj.CommandType = value + + @property + def command(self): + return self.obj.Command + @command.setter + def command(self, value): + self.obj.Command = value + + @property + def doc(self): + return self.obj.Parent.Parent + + def _special_properties(self, tipo, args): + if tipo == 'button': + # ~ if 'ImageURL' in args: + # ~ args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + return args + + def add(self, args): + name = args['Name'] + tipo = args.pop('Type').lower() + w = args.pop('Width', 1000) + h = args.pop('Height', 200) + x = args.pop('X', 0) + y = args.pop('Y', 0) + control = self.doc.createInstance('com.sun.star.drawing.ControlShape') + control.setSize(Size(w, h)) + control.setPosition(Point(x, y)) + model = self.doc.createInstance(self.MODELS[tipo]) + args = self._special_properties(tipo, args) + _set_properties(model, args) + control.Control = model + index = len(self) + self.obj.insertByIndex(index, model) + self._dp.add(control) + view = self.doc.CurrentController.getControl(self.obj.getByName(name)) + control = FORM_CONTROL_CLASS[tipo](control, view, self.obj) + control.index = index + setattr(self, name, control) + self._controls[name] = control + return control + + +class LOSheetForms(object): + + def __init__(self, draw_page): + self._dp = draw_page + self._obj = draw_page.Forms + + def __getitem__(self, index): + return LOForm(self.obj[index], self._dp) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + @property + def doc(self): + return self.obj.Parent + + @property + def count(self): + return len(self) + + @property + def names(self): + return self.obj.ElementNames + + def insert(self, name=''): + if not name: + name = f'form{self.count + 1}' + form = self.doc.createInstance('com.sun.star.form.component.Form') + self.obj.insertByName(name, form) + return LOForm(form, self._dp) + + def remove(self, index): + if isinstance(index, int): + self.obj.removeByIndex(index) + else: + self.obj.removeByName(index) + return + + +# ~ IsFiltered, +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetRows(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, int): + rows = LOSheetRows(self._sheet, self.obj[index]) + else: + rango = self._sheet[index.start:index.stop,0:] + rows = LOSheetRows(self._sheet, rango.obj.Rows) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def color(self): + return self.obj.CellBackColor + @color.setter + def color(self, value): + self.obj.CellBackColor = value + + @property + def is_transparent(self): + return self.obj.IsCellBackgroundTransparent + @is_transparent.setter + def is_transparent(self, value): + self.obj.IsCellBackgroundTransparent = value + + @property + def height(self): + return self.obj.Height + @height.setter + def height(self, value): + self.obj.Height = value + + def optimal(self): + self.obj.OptimalHeight = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetColumns(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, (int, str)): + rows = LOSheetColumns(self._sheet, self.obj[index]) + else: + rango = self._sheet[0,index.start:index.stop] + rows = LOSheetColumns(self._sheet, rango.obj.Columns) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def width(self): + return self.obj.Width + @width.setter + def width(self, value): + self.obj.Width = value + + def optimal(self): + self.obj.OptimalWidth = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +class LOCalcSheet(object): + + 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 str(self): + return self.obj.String + @str.setter + def str(self, value): + self.obj.setString(value) + + @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 = LOCalcRanges(rangos) + return rangos + + @property + def merged_area(self): + cursor = self.cursor + cursor.collapseToMergedArea() + rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + return rango + + @property + def empty(self): + cursor = self.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 + for r in obj: + sheet = r.Spreadsheet + rango = LOCalcRange(sheet[r.AbsoluteName]) + self._ranges[rango.name] = rango + + 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._is_section = not self.obj.TextSection is None + self._parts = [] + if self._is_paragraph: + self._parts = [LOWriterTextRange(p, doc) for p in obj] + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + obj = self._parts[self._index] + except IndexError: + raise StopIteration + + self._index += 1 + return obj + + @property + def 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 is_section(self): + return self._is_section + + @property + def text(self): + return self.obj.Text + + @property + def cursor(self): + return self.text.createTextCursorByRange(self.obj) + + @property + def text_cursor(self): + return self.text.createTextCursor() + + @property + def dp(self): + return self._doc.dp + + @property + def paragraph(self): + cursor = self.cursor + cursor.gotoStartOfParagraph(False) + cursor.gotoNextParagraph(True) + return LOWriterTextRange(cursor, self._doc) + + def goto_start(self): + if self.is_section: + rango = self.obj.TextSection.Anchor.Start + else: + rango = self.obj.Start + return LOWriterTextRange(rango, self._doc) + + def goto_end(self): + if self.is_section: + rango = self.obj.TextSection.Anchor.End + else: + rango = self.obj.End + return LOWriterTextRange(rango, self._doc) + + def goto_previous(self, expand=True): + cursor = self.cursor + cursor.gotoPreviousParagraph(expand) + return LOWriterTextRange(cursor, self._doc) + + def goto_next(self, expand=True): + cursor = self.cursor + cursor.gotoNextParagraph(expand) + return LOWriterTextRange(cursor, self._doc) + + def go_left(self, from_self=True, count=1, expand=False): + cursor = self.cursor + if not from_self: + cursor = self.text_cursor + cursor.gotoRange(self.obj, False) + cursor.goLeft(count, expand) + return LOWriterTextRange(cursor, self._doc) + + def go_right(self, from_self=True, count=1, expand=False): + cursor = self.cursor + if not from_self: + cursor = self.text_cursor + cursor.gotoRange(self.obj, False) + cursor.goRight(count, expand) + return LOWriterTextRange(cursor, self._doc) + + def delete(self): + self.value = '' + return + + def offset(self): + cursor = self.cursor.getEnd() + return LOWriterTextRange(cursor, self._doc) + + def insert_content(self, data, cursor=None, replace=False): + if cursor is None: + cursor = self.cursor + self.text.insertTextContent(cursor, data, replace) + return + + def insert_math(self, formula, + anchor_type=TextContentAnchorType.AS_CHARACTER, + cursor=None, replace=False): + + math = self._doc.create_instance(SERVICES['TEXT_EMBEDDED']) + math.CLSID = CLSID['FORMULA'] + math.AnchorType = anchor_type + self.insert_content(math, cursor, replace) + math.EmbeddedObject.Component.Formula = formula + return math + + def new_line(self, count=1): + cursor = self.cursor + for i in range(count): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + return LOWriterTextRange(cursor, self._doc) + + def insert_table(self, data): + table = self._doc.create_instance(SERVICES['TEXT_TABLE']) + rows = len(data) + cols = len(data[0]) + table.initialize(rows, cols) + self.insert_content(table) + table.DataArray = data + name = table.Name + table = LOWriterTextTable(self._doc.tables[name], self._doc) + return table + + def insert_shape(self, tipo, args={}): + # ~ args['Width'] = args.get('Width', 1000) + # ~ args['Height'] = args.get('Height', 1000) + # ~ args['X'] = args.get('X', 0) + # ~ args['Y'] = args.get('Y', 0) + shape = self._doc.dp.add(tipo, args) + # ~ shape.anchor = self.obj + return shape + + def insert_image(self, path, args={}): + w = args.get('Width', 1000) + h = args.get('Height', 1000) + + image = self._doc.create_instance(SERVICES['GRAPHIC']) + image.GraphicURL = _P.to_url(path) + image.AnchorType = TextContentAnchorType.AS_CHARACTER + image.Width = w + image.Height = h + self.insert_content(image) + return self._doc.dp.last + + +class LOWriterTextRanges(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + self._paragraphs = [LOWriterTextRange(p, doc) for p in obj] + + def __len__(self): + return len(self._paragraphs) + + def __getitem__(self, index): + return self._paragraphs[index] + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + obj = self._paragraphs[self._index] + except IndexError: + raise StopIteration + + self._index += 1 + return obj + + @property + def obj(self): + return self._obj + + +class LOWriterTextTable(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + + @property + def data(self): + return self.obj.DataArray + @data.setter + def data(self, values): + self.obj.DataArray = values + + @property + def style(self): + return self.obj.TableTemplateName + @style.setter + def style(self, value): + self.obj.autoFormat(value) + + +class LOWriterTextTables(object): + + def __init__(self, doc): + self._doc = doc + self._obj = doc.obj.TextTables + + def __getitem__(self, key): + return LOWriterTextTable(self._obj[key], self._doc) + + def __len__(self): + return self._obj.Count + + def insert(self, data, text_range=None): + if text_range is None: + text_range = self._doc.selection + text_range.insert_table(data) + return + + +class LOWriter(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = WRITER + + @property + def text(self): + return self.paragraphs + + @property + def paragraphs(self): + return LOWriterTextRanges(self.obj.Text, self) + + @property + def tables(self): + return LOWriterTextTables(self) + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName == OBJ_TEXTS: + if len(sel) == 1: + sel = LOWriterTextRanges(sel, self)[0] + else: + sel = LOWriterTextRanges(sel, self) + return sel + + if sel.ImplementationName == OBJ_SHAPES: + if len(sel) == 1: + sel = sel[0] + sel = LODrawPage(sel.Parent)[sel.Name] + return sel + + if sel.ImplementationName == OBJ_GRAPHIC: + sel = self.dp[sel.Name] + else: + debug(sel.ImplementationName) + + return sel + + @property + def dp(self): + return self.draw_page + @property + def shapes(self): + return self.draw_page + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def cursor(self): + return self.obj.Text.createTextCursor() + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def page_styles(self): + ps = self.obj.StyleFamilies['PageStyles'] + return LOWriterPageStyles(ps) + + @property + def styles(self): + return LOWriterStylesFamilies(self.obj.StyleFamilies) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + @property + def zoom(self): + return self._cc.ViewSettings.ZoomValue + @zoom.setter + def zoom(self, value): + self._cc.ViewSettings.ZoomValue = value + + def goto_start(self): + self.view_cursor.gotoStart(False) + return self.selection + + def goto_end(self): + self.view_cursor.gotoEnd(False) + return self.selection + + def search(self, options, find_all=True): + descriptor = self.search_descriptor + descriptor.setSearchString(options.get('Search', '')) + descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) + descriptor.SearchWords = options.get('Words', False) + if 'Attributes' in options: + attr = dict_to_property(options['Attributes']) + descriptor.setSearchAttributes(attr) + if hasattr(descriptor, 'SearchRegularExpression'): + descriptor.SearchRegularExpression = options.get('RegularExpression', False) + if hasattr(descriptor, 'SearchType') and 'Type' in options: + descriptor.SearchType = options['Type'] + + result = False + if find_all: + found = self.obj.findAll(descriptor) + if len(found): + result = [LOWriterTextRange(f, self) for f in found] + else: + found = self.obj.findFirst(descriptor) + if found: + result = LOWriterTextRange(found, self) + + return result + + def replace(self, options): + descriptor = self.replace_descriptor + descriptor.setSearchString(options['Search']) + descriptor.setReplaceString(options['Replace']) + descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) + descriptor.SearchWords = options.get('Words', False) + if 'Attributes' in options: + attr = dict_to_property(options['Attributes']) + descriptor.setSearchAttributes(attr) + if hasattr(descriptor, 'SearchRegularExpression'): + descriptor.SearchRegularExpression = options.get('RegularExpression', False) + if hasattr(descriptor, 'SearchType') and 'Type' in options: + descriptor.SearchType = options['Type'] + found = self.obj.replaceAll(descriptor) + return found + + def select(self, text): + if hasattr(text, 'obj'): + text = text.obj + self._cc.select(text) + return + + +class LOShape(LOBaseObject): + 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 position(self): + return self.obj.Position + @property + def x(self): + return self.position.X + @property + def y(self): + return self.position.Y + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def 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 + try: + self.obj.Anchor = value + except Exception as e: + self.obj.AnchorType = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def path(self): + return self.url + @property + def url(self): + url = '' + if self.is_image: + url = _P.to_system(self.obj.GraphicURL.OriginURL) + return url + + @property + def mimetype(self): + mt = '' + if self.is_image: + mt = self.obj.GraphicURL.MimeType + return mt + + @property + def linked(self): + l = False + if self.is_image: + l = self.obj.GraphicURL.Linked + return l + + def delete(self): + self.remove() + return + def remove(self): + self.obj.Parent.remove(self.obj) + return + + def save(self, path: str, mimetype=DEFAULT_MIME_TYPE): + if _P.is_dir(path): + name = self.name + ext = mimetype.lower() + else: + p = _P(path) + path = p.path + name = p.name + ext = p.ext.lower() + + path = _P.join(path, f'{name}.{ext}') + args = dict( + URL = _P.to_url(path), + MimeType = MIME_TYPE[ext], + ) + if not _export_image(self.obj, args): + path = '' + return path + + # ~ def save2(self, path: str): + # ~ size = len(self.obj.Bitmap.DIB) + # ~ data = self.obj.GraphicStream.readBytes((), size) + # ~ data = data[-1].value + # ~ path = _P.join(path, f'{self.name}.png') + # ~ _P.save_bin(path, b'') + # ~ return + + +class LODrawPage(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + shape = LOShape(self.obj[index], index) + else: + for i, o in enumerate(self.obj): + shape = self.obj[i] + name = shape.Name or f'shape{i}' + if name == index: + shape = LOShape(shape, i) + break + return shape + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + if self._index == self.count: + raise StopIteration + shape = self[self._index] + self._index += 1 + return shape + + + @property + def name(self): + return self.obj.Name + + @property + def doc(self): + return self.obj.Forms.Parent + + @property + def width(self): + return self.obj.Width + + @property + def height(self): + return self.obj.Height + + @property + def count(self): + return self.obj.Count + + @property + def last(self): + return self[self.count - 1] + + def create_instance(self, name): + return self.doc.createInstance(name) + + def add(self, type_shape, options={}): + args = options.copy() + """Insert a shape in page, type shapes: + Line + Rectangle + Ellipse + Text + Connector + """ + index = self.count + default_height = 3000 + if type_shape == 'Line': + default_height = 0 + w = args.pop('Width', 3000) + h = args.pop('Height', default_height) + x = args.pop('X', 1000) + y = args.pop('Y', 1000) + name = args.pop('Name', f'{type_shape.lower()}{index}') + + service = f'com.sun.star.drawing.{type_shape}Shape' + shape = self.create_instance(service) + shape.Size = Size(w, h) + shape.Position = Point(x, y) + shape.Name = name + self.obj.add(shape) + + if args: + _set_properties(shape, args) + + return LOShape(self.obj[index], index) + + def remove(self, shape): + if hasattr(shape, 'obj'): + shape = shape.obj + return self.obj.remove(shape) + + def remove_all(self): + while self.count: + self.obj.remove(self.obj[0]) + return + + def insert_image(self, path, options={}): + args = options.copy() + index = self.count + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + name = args.get('Name', f'image{index}') + + image = self.create_instance('com.sun.star.drawing.GraphicObjectShape') + if isinstance(path, str): + image.GraphicURL = _P.to_url(path) + else: + gp = create_instance('com.sun.star.graphic.GraphicProvider') + properties = dict_to_property({'InputStream': path}) + image.Graphic = gp.queryGraphic(properties) + + self.obj.add(image) + image.Size = Size(w, h) + image.Position = Point(x, y) + image.Name = name + return LOShape(self.obj[index], index) + + +class LODrawImpress(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + page = self.obj.DrawPages[index] + else: + page = self.obj.DrawPages.getByName(index) + return LODrawPage(page) + + @property + def selection(self): + sel = self.obj.CurrentSelection[0] + # ~ return _get_class_uno(sel) + return sel + + @property + def current_page(self): + return LODrawPage(self._cc.getCurrentPage()) + + def paste(self): + call_dispatch(self.frame, '.uno:Paste') + return self.current_page[-1] + + def add(self, type_shape, args={}): + return self.current_page.add(type_shape, args) + + def insert_image(self, path, args={}): + self.current_page.insert_image(path, args) + return + + # ~ def export(self, path, mimetype='png'): + # ~ args = dict( + # ~ URL = _P.to_url(path), + # ~ MimeType = MIME_TYPE[mimetype], + # ~ ) + # ~ result = _export_image(self.obj, args) + # ~ return result + + +class 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, + 'addSpinListener': EventsSpin, + } + if hasattr(control, 'obj'): + control = control.obj + # ~ debug(control.ImplementationName) + is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' + is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' + is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' + is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl' + + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue + + getattr(control, key)(listeners[key](events, name)) + + if is_grid: + controllers = EventsGrid(events, name) + control.addSelectionListener(controllers) + control.Model.GridDataModel.addGridDataListener(controllers) + return + + +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 EventsSpin(EventsListenerBase, XSpinListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def up(self, event): + event_name = f'{self.name}_up' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def down(self, event): + event_name = f'{self.name}_up' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def first(self, event): + event_name = f'{self.name}_first' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def last(self, event): + event_name = f'{self.name}_last' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def mousePressed(self, event): + event_name = '{}_click'.format(self._name) + if event.ClickCount == 2: + event_name = '{}_double_click'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def mouseReleased(self, event): + event_name = '{}_after_click'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def mouseEntered(self, event): + pass + + def mouseExited(self, event): + pass + + # ~ XMouseMotionListener + def mouseMoved(self, event): + pass + + def mouseDragged(self, event): + pass + + +class EventsMouseLink(EventsMouse): + + def __init__(self, controller, name): + super().__init__(controller, name) + self._text_color = 0 + + def mouseEntered(self, event): + model = event.Source.Model + self._text_color = model.TextColor or 0 + model.TextColor = get_color('blue') + return + + def mouseExited(self, event): + model = event.Source.Model + model.TextColor = self._text_color + return + + +class EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def actionPerformed(self, event): + event_name = f'{self.name}_action' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + CONTROLS = ( + 'stardiv.Toolkit.UnoControlEditModel', + ) + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + service = event.Source.Model.ImplementationName + # ~ print('Focus enter', service) + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + return + + def focusLost(self, event): + service = event.Source.Model.ImplementationName + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = -1 + return + + +class EventsKey(EventsListenerBase, XKeyListener): + """ + event.KeyChar + event.KeyCode + event.KeyFunc + event.Modifiers + """ + + def __init__(self, controller, name): + super().__init__(controller, name) + + def keyPressed(self, event): + pass + + def keyReleased(self, event): + event_name = '{}_key_released'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + # ~ else: + # ~ if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): + # ~ self._cls.close() + return + + +class EventsItem(EventsListenerBase, XItemListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def disposing(self, event): + pass + + def itemStateChanged(self, event): + event_name = '{}_item_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsItemRoadmap(EventsItem): + + def itemStateChanged(self, event): + dialog = event.Source.Context.Model + dialog.Step = event.ItemId + 1 + return + + +class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def dataChanged(self, event): + event_name = '{}_data_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def rowHeadingChanged(self, event): + pass + + def rowsInserted(self, event): + pass + + def rowsRemoved(self, evemt): + pass + + def selectionChanged(self, event): + event_name = '{}_selection_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsMouseGrid(EventsMouse): + selected = False + + def mousePressed(self, event): + super().mousePressed(event) + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ print(col, row) + # ~ if col == -1 and row == -1: + # ~ if self.selected: + # ~ obj.deselectAllRows() + # ~ else: + # ~ obj.selectAllRows() + # ~ self.selected = not self.selected + return + + def mouseReleased(self, event): + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ if row == -1 and col > -1: + # ~ gdm = obj.Model.GridDataModel + # ~ for i in range(gdm.RowCount): + # ~ gdm.updateRowHeading(i, i + 1) + return + + +class EventsTab(EventsListenerBase, XTabListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def activated(self, id): + event_name = '{}_activated'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(id) + return + + +class EventsMenu(EventsListenerBase, XMenuListener): + + def __init__(self, controller): + super().__init__(controller, '') + + def itemHighlighted(self, event): + pass + + def itemSelected(self, event): + name = event.Source.getCommand(event.MenuId) + if name.startswith('menu'): + event_name = '{}_selected'.format(name) + else: + event_name = 'menu_{}_selected'.format(name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def itemActivated(self, event): + return + + def itemDeactivated(self, event): + return + + +class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): + + def __init__(self, cls): + self._cls = cls + super().__init__(cls.events, cls.name, cls._window) + + def windowOpened(self, event): + event_name = '{}_opened'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowActivated(self, event): + control_name = '{}_activated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowDeactivated(self, event): + control_name = '{}_deactivated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowMinimized(self, event): + pass + + def windowNormalized(self, event): + pass + + def windowClosing(self, event): + if self._window: + control_name = 'window_closing' + else: + control_name = '{}_closing'.format(event.Source.Model.Name) + + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + # ~ else: + # ~ if not self._modal and not self._block: + # ~ event.Source.Visible = False + return + + def windowClosed(self, event): + control_name = '{}_closed'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + # ~ XWindowListener + def windowResized(self, event): + sb = self._cls._subcont + sb.setPosSize(0, 0, event.Width, event.Height, SIZE) + event_name = '{}_resized'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowMoved(self, event): + pass + + def windowShown(self, event): + pass + + def windowHidden(self, event): + pass + + +# ~ BorderColor = ? +# ~ FontStyleName = ? +# ~ HelpURL = ? +class UnoBaseObject(object): + + def __init__(self, obj, path=''): + self._obj = obj + self._model = obj.Model + + def __setattr__(self, name, value): + exists = hasattr(self, name) + if not exists and not name in ('_obj', '_model'): + setattr(self._model, name, value) + else: + super().__setattr__(name, value) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + @property + def m(self): + return self._model + + @property + def properties(self): + return {} + @properties.setter + def properties(self, values): + _set_properties(self.model, values) + + @property + def name(self): + return self.model.Name + + @property + def parent(self): + return self.obj.Context + + @property + def tag(self): + return self.model.Tag + @tag.setter + def tag(self, value): + self.model.Tag = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.setVisible(value) + + @property + def enabled(self): + return self.model.Enabled + @enabled.setter + def enabled(self, value): + self.model.Enabled = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def align(self): + return self.model.Align + @align.setter + def align(self, value): + self.model.Align = value + + @property + def valign(self): + return self.model.VerticalAlign + @valign.setter + def valign(self, value): + self.model.VerticalAlign = value + + @property + def font_weight(self): + return self.model.FontWeight + @font_weight.setter + def font_weight(self, value): + self.model.FontWeight = value + + @property + def font_height(self): + return self.model.FontHeight + @font_height.setter + def font_height(self, value): + self.model.FontHeight = value + + @property + def font_name(self): + return self.model.FontName + @font_name.setter + def font_name(self, value): + self.model.FontName = value + + @property + def font_underline(self): + return self.model.FontUnderline + @font_underline.setter + def font_underline(self, value): + self.model.FontUnderline = value + + @property + def text_color(self): + return self.model.TextColor + @text_color.setter + def text_color(self, value): + self.model.TextColor = value + + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + + @property + def multi_line(self): + return self.model.MultiLine + @multi_line.setter + def multi_line(self, value): + self.model.MultiLine = value + + @property + def help_text(self): + return self.model.HelpText + @help_text.setter + def help_text(self, value): + self.model.HelpText = value + + @property + def border(self): + return self.model.Border + @border.setter + def border(self, value): + # ~ Bug for report + self.model.Border = value + + @property + def width(self): + return self._model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + def _get_possize(self, name): + ps = self.obj.getPosSize() + return getattr(ps, name) + + def _set_possize(self, name, value): + ps = self.obj.getPosSize() + setattr(ps, name, value) + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + return + + @property + def x(self): + if hasattr(self.model, 'PositionX'): + return self.model.PositionX + return self._get_possize('X') + @x.setter + def x(self, value): + if hasattr(self.model, 'PositionX'): + self.model.PositionX = value + else: + self._set_possize('X', value) + + @property + def y(self): + if hasattr(self.model, 'PositionY'): + return self.model.PositionY + return self._get_possize('Y') + @y.setter + def y(self, value): + if hasattr(self.model, 'PositionY'): + self.model.PositionY = value + else: + self._set_possize('Y', value) + + @property + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value + + @property + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value + + @property + def ps(self): + ps = self.obj.getPosSize() + return ps + @ps.setter + def ps(self, ps): + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + + def set_focus(self): + self.obj.setFocus() + return + + def ps_from(self, source): + self.ps = source.ps + return + + def center(self, horizontal=True, vertical=False): + p = self.parent.Model + w = p.Width + h = p.Height + if horizontal: + x = w / 2 - self.width / 2 + self.x = x + if vertical: + y = h / 2 - self.height / 2 + self.y = y + return + + def move(self, origin, x=0, y=5, center=False): + if x: + self.x = origin.x + origin.width + x + else: + self.x = origin.x + if y: + h = origin.height + if y < 0: + h = 0 + self.y = origin.y + h + y + else: + self.y = origin.y + + if center: + self.center() + return + + +class UnoLabel(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoLabelLink(UnoLabel): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'link' + + +class UnoButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + @property + def image(self): + return self.model.ImageURL + @image.setter + def image(self, value): + self.model.ImageURL = _P.to_url(value) + + +class UnoRadio(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'radio' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoCheckBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'checkbox' + + @property + def value(self): + return self.model.State + @value.setter + def value(self, value): + self.model.State = value + + @property + def label(self): + return self.model.Label + @label.setter + def label(self, value): + self.model.Label = value + + @property + def tri_state(self): + return self.model.TriState + @tri_state.setter + def tri_state(self, value): + self.model.TriState = value + + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html +class UnoText(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self.model.Text + @value.setter + def value(self, value): + self.model.Text = value + + @property + def echochar(self): + return chr(self.model.EchoChar) + @echochar.setter + def echochar(self, value): + if value: + self.model.EchoChar = ord(value[0]) + else: + self.model.EchoChar = 0 + + def validate(self): + return + + +class UnoImage(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'image' + + @property + def value(self): + return self.url + @value.setter + def value(self, value): + self.url = value + + @property + def url(self): + return self.m.ImageURL + @url.setter + def url(self, value): + self.m.ImageURL = None + self.m.ImageURL = _P.to_url(value) + + +class UnoListBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._path = '' + + def __setattr__(self, name, value): + if name in ('_path',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def type(self): + return 'listbox' + + @property + def value(self): + return self.obj.getSelectedItem() + + @property + def count(self): + return len(self.data) + + @property + def data(self): + return self.model.StringItemList + @data.setter + def data(self, values): + self.model.StringItemList = list(sorted(values)) + + @property + def path(self): + return self._path + @path.setter + def path(self, value): + self._path = value + + def unselect(self): + self.obj.selectItem(self.value, False) + return + + def select(self, pos=0): + if isinstance(pos, str): + self.obj.selectItem(pos, True) + else: + self.obj.selectItemPos(pos, True) + return + + def clear(self): + self.model.removeAllItems() + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def insert(self, value, path='', pos=-1, show=True): + if pos < 0: + pos = self.count + if path: + self.model.insertItem(pos, value, self._set_image_url(path)) + else: + self.model.insertItemText(pos, value) + if show: + self.select(pos) + return + + +class UnoRoadmap(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._options = () + + def __setattr__(self, name, value): + if name in ('_options',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def options(self): + return self._options + @options.setter + def options(self, values): + self._options = values + for i, v in enumerate(values): + opt = self.model.createInstance() + opt.ID = i + opt.Label = v + self.model.insertByIndex(i, opt) + return + + @property + def enabled(self): + return True + @enabled.setter + def enabled(self, value): + for m in self.model: + m.Enabled = value + return + + def set_enabled(self, index, value): + self.model.getByIndex(index).Enabled = value + return + + +class UnoTree(UnoBaseObject): + + def __init__(self, obj, ): + super().__init__(obj) + self._tdm = None + self._data = [] + + def __setattr__(self, name, value): + if name in ('_tdm', '_data'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def selection(self): + sel = self.obj.Selection + return sel.DataValue, sel.DisplayValue + + @property + def parent(self): + parent = self.obj.Selection.Parent + if parent is None: + return () + return parent.DataValue, parent.DisplayValue + + def _get_parents(self, node): + value = (node.DisplayValue,) + parent = node.Parent + if parent is None: + return value + return self._get_parents(parent) + value + + @property + def parents(self): + values = self._get_parents(self.obj.Selection) + return values + + @property + def root(self): + if self._tdm is None: + return '' + return self._tdm.Root.DisplayValue + @root.setter + def root(self, value): + self._add_data_model(value) + + def _add_data_model(self, name): + tdm = create_instance('com.sun.star.awt.tree.MutableTreeDataModel') + root = tdm.createNode(name, True) + root.DataValue = 0 + tdm.setRoot(root) + self.model.DataModel = tdm + self._tdm = self.model.DataModel + return + + @property + def path(self): + return self.root + @path.setter + def path(self, value): + self.data = _P.walk_dir(value, True) + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = list(values) + self._add_data() + + def _add_data(self): + if not self.data: + return + + parents = {} + for node in self.data: + parent = parents.get(node[1], self._tdm.Root) + child = self._tdm.createNode(node[2], False) + child.DataValue = node[0] + parent.appendChild(child) + parents[node[0]] = child + self.obj.expandNode(self._tdm.Root) + return + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html +class UnoGrid(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._gdm = self.model.GridDataModel + self._data = [] + self._formats = () + + def __setattr__(self, name, value): + if name in ('_gdm', '_data', '_formats'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getitem__(self, key): + value = self._gdm.getCellData(key[0], key[1]) + return value + + def __setitem__(self, key, value): + self._gdm.updateCellData(key[0], key[1], value) + return + + @property + def type(self): + return 'grid' + + @property + def columns(self): + return {} + @columns.setter + def columns(self, values): + # ~ self._columns = values + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html + model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) + for properties in values: + column = create_instance('com.sun.star.awt.grid.GridColumn', True) + for k, v in properties.items(): + setattr(column, k, v) + model.addColumn(column) + self.model.ColumnModel = model + return + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = values + self.clear() + headings = tuple(range(1, len(values) + 1)) + self._gdm.addRows(headings, values) + # ~ rows = range(grid_dm.RowCount) + # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows] + # ~ grid.Model.RowBackgroundColors = tuple(colors) + return + + @property + def value(self): + if self.column == -1 or self.row == -1: + return '' + return self[self.column, self.row] + @value.setter + def value(self, value): + if self.column > -1 and self.row > -1: + self[self.column, self.row] = value + + @property + def row(self): + return self.obj.CurrentRow + + @property + def row_count(self): + return self._gdm.RowCount + + @property + def column(self): + return self.obj.CurrentColumn + + @property + def 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 + + +class UnoSpinButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'spinbutton' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoNumericField(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'numeric' + + @property + def int(self): + return int(self.value) + + @property + def value(self): + return self.model.Value + @value.setter + def value(self, value): + self.model.Value = value + + +UNO_CLASSES = { + 'label': UnoLabel, + 'link': UnoLabelLink, + 'button': UnoButton, + 'radio': UnoRadio, + 'checkbox': UnoCheckBox, + 'text': UnoText, + 'image': UnoImage, + 'listbox': UnoListBox, + 'roadmap': UnoRoadmap, + 'tree': UnoTree, + 'grid': UnoGrid, + 'pages': UnoPages, + 'spinbutton': UnoSpinButton, + 'numeric': UnoNumericField, +} + +UNO_MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', + 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', +} +# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', +# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', +# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', +# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', +# ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', +# ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', +# ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', +# ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', +# ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', +# ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', + + +class LODialog(object): + SEPARATION = 5 + MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', + 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', + } + + def __init__(self, args): + self._obj = self._create(args) + self._model = self.obj.Model + self._events = None + self._modal = True + self._controls = {} + self._color_on_focus = COLOR_ON_FOCUS + self._id = '' + self._path = '' + self._init_controls() + + def _create(self, args): + service = 'com.sun.star.awt.DialogProvider' + path = args.pop('Path', '') + if path: + dp = create_instance(service, True) + dlg = dp.createDialog(_P.to_url(path)) + return dlg + + if 'Location' in args: + name = args['Name'] + library = args.get('Library', 'Standard') + location = args.get('Location', 'application').lower() + if location == 'user': + location = 'application' + url = f'vnd.sun.star.script:{library}.{name}?location={location}' + if location == 'document': + dp = create_instance(service, args=docs.active.obj) + else: + dp = create_instance(service, True) + # ~ uid = docs.active.uid + # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' + dlg = dp.createDialog(url) + return dlg + + dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) + model = create_instance('com.sun.star.awt.UnoControlDialogModel', True) + toolkit = create_instance('com.sun.star.awt.Toolkit', True) + _set_properties(model, args) + dlg.setModel(model) + dlg.setVisible(False) + dlg.createPeer(toolkit, None) + return dlg + + def _get_type_control(self, name): + name = name.split('.')[2] + types = { + 'UnoFixedTextControl': 'label', + 'UnoEditControl': 'text', + 'UnoButtonControl': 'button', + } + return types[name] + + def _init_controls(self): + for control in self.obj.getControls(): + tipo = self._get_type_control(control.ImplementationName) + name = control.Model.Name + control = UNO_CLASSES[tipo](control) + setattr(self, name, control) + return + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + + @property + def controls(self): + return self._controls + + @property + def path(self): + return self._path + @property + def path_images(self): + return _P.join(self.path, DIR['images']) + @property + def id(self): + return self._id + @id.setter + def id(self, value): + self._id = value + self._path = _P.from_id(value) + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + @property + def width(self): + return self.model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers(self) + self._connect_listeners() + + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + self._color_on_focus = get_color(value) + + def _connect_listeners(self): + for control in self.obj.Controls: + _add_listeners(self.events, control, control.Model.Name) + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['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 + + @classproperty + def user_profile(self): + path = self.config('UserConfig') + path = str(Path(path).parent) + return path + + @classproperty + def user_config(self): + path = self.config('UserConfig') + 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, get_lines=False, encoding='utf-8'): + if get_lines: + with Path(path).open(encoding=encoding) as f: + data = f.readlines() + else: + data = Path(path).read_text(encoding=encoding) + return data + + @classmethod + def read_bin(cls, path): + 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/ZAZPass_v0.1.0.oxt b/files/ZAZPass_v0.1.0.oxt new file mode 100644 index 0000000000000000000000000000000000000000..a907536255e3820e6a2acb8831ca0f78219ce911 GIT binary patch literal 62375 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

g~_P%u(ny>kg%g{ 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 literal 0 HcmV?d00001 diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4e0d53fa22bdba5af0aab3dc7896af161d54932f GIT binary patch literal 9189 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 + + + + + + Zaz Passwords + Zaz Contraseñas + + + _self + + + + + Insert password + Insertar contraseña + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument + + + service:net.elmau.zaz.pass?insert + + + _self + + + %origin%/images/icon + + + + + Copy in clipboard + Copiar al portapapeles + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument + + + service:net.elmau.zaz.pass?copy + + + _self + + + %origin%/images/icon + + + + + Generate password... + Generar contraseña... + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument + + + service:net.elmau.zaz.pass?generate + + + _self + + + %origin%/images/icon + + + + + + + + + + Insert password + Insertar contraseña + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument + + + service:net.elmau.zaz.pass?insert + + + _self + + + %origin%/images/icon + + + + + Copy in clipboard + Copiar al portapapeles + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument + + + service:net.elmau.zaz.pass?copy + + + _self + + + %origin%/images/icon + + + + + Generate password... + Generar contraseña... + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument + + + service:net.elmau.zaz.pass?generate + + + _self + + + %origin%/images/icon + + + + + + diff --git a/source/META-INF/manifest.xml b/source/META-INF/manifest.xml new file mode 100644 index 0000000..ddea409 --- /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..9917d4f --- /dev/null +++ b/source/Office/Accelerators.xcu @@ -0,0 +1,49 @@ + + + + + + + + service:net.elmau.zaz.pass?insert + + + + + + + service:net.elmau.zaz.pass?insert + + + + + + + service:net.elmau.zaz.pass?copy + + + + + + + service:net.elmau.zaz.pass?copy + + + + + + + service:net.elmau.zaz.pass?generate + + + + + + + service:net.elmau.zaz.pass?generate + + + + + + diff --git a/source/ZAZPass.py b/source/ZAZPass.py new file mode 100644 index 0000000..688c94b --- /dev/null +++ b/source/ZAZPass.py @@ -0,0 +1,22 @@ +import uno +import unohelper +from com.sun.star.task import XJobExecutor +from zpass import main + + +ID_EXTENSION = 'net.elmau.zaz.pass' +SERVICE = ('com.sun.star.task.Job',) + + +class ZAZPass(unohelper.Base, XJobExecutor): + + def __init__(self, ctx): + self.ctx = ctx + + def trigger(self, args): + main(ID_EXTENSION, args, __file__) + return + + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation(ZAZPass, ID_EXTENSION, SERVICE) diff --git a/source/description.xml b/source/description.xml new file mode 100644 index 0000000..a79f4d3 --- /dev/null +++ b/source/description.xml @@ -0,0 +1,26 @@ + + + + + + My first extension + Mi primer extensión + + + + + + + + + + El Mau + El Mau + + + + + + + + diff --git a/source/description/desc_en.txt b/source/description/desc_en.txt new file mode 100644 index 0000000..b667a4b --- /dev/null +++ b/source/description/desc_en.txt @@ -0,0 +1 @@ +My great extension \ 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..d8d8fdc --- /dev/null +++ b/source/description/desc_es.txt @@ -0,0 +1 @@ +Mi gran extensión \ No newline at end of file diff --git a/source/images/close.svg b/source/images/close.svg new file mode 100755 index 0000000..534460e --- /dev/null +++ b/source/images/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/source/images/eye-close.svg b/source/images/eye-close.svg new file mode 100644 index 0000000..c6455d7 --- /dev/null +++ b/source/images/eye-close.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/images/eye-open.svg b/source/images/eye-open.svg new file mode 100644 index 0000000..0eef411 --- /dev/null +++ b/source/images/eye-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/source/images/insert.svg b/source/images/insert.svg new file mode 100644 index 0000000..6c98b94 --- /dev/null +++ b/source/images/insert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/source/images/new.svg b/source/images/new.svg new file mode 100644 index 0000000..6d70aae --- /dev/null +++ b/source/images/new.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/images/zazpass.png b/source/images/zazpass.png new file mode 100644 index 0000000000000000000000000000000000000000..4e0d53fa22bdba5af0aab3dc7896af161d54932f GIT binary patch literal 9189 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. + +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.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 XSpinListener +from com.sun.star.awt import XWindowListener +from com.sun.star.awt import XTopWindowListener +from com.sun.star.awt.grid import XGridDataListener +from com.sun.star.awt.grid import XGridSelectionListener +from com.sun.star.script import ScriptEventDescriptor + +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_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' +OBJ_SHAPES = 'com.sun.star.drawing.SvxShapeCollection' +OBJ_GRAPHIC = 'SwXTextGraphicObject' + +OBJ_TEXTS = 'SwXTextRanges' +OBJ_TEXT = 'SwXTextRange' + +CLSID = { + 'FORMULA': '078B7ABA-54FC-457F-8551-6147e776a997', +} + +SERVICES = { + 'TEXT_EMBEDDED': 'com.sun.star.text.TextEmbeddedObject', + 'TEXT_TABLE': 'com.sun.star.text.TextTable', + 'GRAPHIC': 'com.sun.star.text.GraphicObject', +} + + +# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL +class FilterOperator(IntEnum): + EMPTY = 0 + NO_EMPTY = 1 + EQUAL = 2 + NOT_EQUAL = 3 + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 +class Border(IntEnum): + NO_BORDER = 0 + BORDER = 1 + SIMPLE = 2 + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a +class ValidationType(): + from com.sun.star.sheet.ValidationType \ + import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM +VT = ValidationType + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7 +class ValidationAlertStyle(): + from com.sun.star.sheet.ValidationAlertStyle \ + import STOP, WARNING, INFO, MACRO +VAS = ValidationAlertStyle + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html +class ConditionOperator(): + from com.sun.star.sheet.ConditionOperator2 \ + import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \ + LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE +CO = ConditionOperator + + +class DataPilotFieldOrientation(): + from com.sun.star.sheet.DataPilotFieldOrientation \ + import HIDDEN, COLUMN, ROW, PAGE, DATA +DPFO = DataPilotFieldOrientation + + +class CellInsertMode(): + from com.sun.star.sheet.CellInsertMode import DOWN, RIGHT, ROWS, COLUMNS +CIM = CellInsertMode + + +class CellDeleteMode(): + from com.sun.star.sheet.CellDeleteMode import UP, LEFT, ROWS, COLUMNS +CDM = CellDeleteMode + + +class FormButtonType(): + from com.sun.star.form.FormButtonType import PUSH, SUBMIT, RESET, URL +FBT = FormButtonType + + +class TextContentAnchorType(): + from com.sun.star.text.TextContentAnchorType \ + import AT_PARAGRAPH, AS_CHARACTER, AT_PAGE, AT_FRAME, AT_CHARACTER +TCAT = TextContentAnchorType + + +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 = {} + path = _P.join(_P.user_config, 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.user_config, name_file) + values = get_config(prefix=prefix, default={}) + 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_sheet(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): +class LOFormControl(): + EVENTS = { + 'action': 'actionPerformed', + 'click': 'mousePressed', + } + TYPES = { + 'actionPerformed': 'XActionListener', + 'mousePressed': 'XMouseListener', + } + + def __init__(self, obj, view, form): + self._obj = obj + self._view = view + self._form = form + self._m = view.Model + self._index = -1 + + # ~ def __setattr__(self, name, value): + # ~ if name in ('_form', '_view', '_m', '_index'): + # ~ self.__dict__[name] = value + # ~ else: + # ~ super().__setattr__(name, value) + + def __str__(self): + return f'{self.name} ({self.type}) {[self.index]}' + + @property + def obj(self): + return self._obj + + @property + def form(self): + return self._form + + @property + def doc(self): + return self.obj.Parent.Forms.Parent + + @property + def name(self): + return self._m.Name + @name.setter + def name(self, value): + self._m.Name = value + + @property + def tag(self): + return self._m.Tag + @tag.setter + def tag(self, value): + self._m.Tag = value + + @property + def index(self): + return self._index + @index.setter + def index(self, value): + self._index = value + + @property + def enabled(self): + return self._m.Enabled + @enabled.setter + def enabled(self, value): + self._m.Enabled = value + + @property + def anchor(self): + return self.obj.Anchor + @anchor.setter + def anchor(self, value): + size = None + if hasattr(value, 'obj'): + size = getattr(value, 'size', None) + value = value.obj + self.obj.Anchor = value + if not size is None: + self.size = size + try: + self.obj.ResizeWithCell = True + except: + pass + + @property + def size(self): + return self.obj.Size + @size.setter + def size(self, value): + self.obj.Size = value + + @property + def events(self): + return self.form.getScriptEvents(self.index) + def add_event(self, name, macro): + if not 'name' in macro: + macro['name'] = '{}_{}'.format(self.name, name) + + event = ScriptEventDescriptor() + event.AddListenerParam = '' + event.EventMethod = self.EVENTS[name] + event.ListenerType = self.TYPES[event.EventMethod] + event.ScriptCode = _get_url_script(macro) + event.ScriptType = 'Script' + + for ev in self.events: + if ev.EventMethod == event.EventMethod and \ + ev.ListenerType == event.ListenerType: + self.form.revokeScriptEvent(self.index, + event.ListenerType, event.EventMethod, event.AddListenerParam) + break + + self.form.registerScriptEvent(self.index, event) + return + + def set_focus(self): + self._view.setFocus() + return + + +class LOFormControlLabel(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Label = value + + +class LOFormControlText(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self._m.Text + @value.setter + def value(self, value): + self._m.Text = value + + +class LOFormControlButton(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Text = Label + + @property + def url(self): + return self._m.TargetURL + @url.setter + def url(self, value): + self._m.TargetURL = value + self._m.ButtonType = FormButtonType.URL + + +FORM_CONTROL_CLASS = { + 'label': LOFormControlLabel, + 'text': LOFormControlText, + 'button': LOFormControlButton, +} + + +class LOForm(object): + MODELS = { + 'label': 'com.sun.star.form.component.FixedText', + 'text': 'com.sun.star.form.component.TextField', + 'button': 'com.sun.star.form.component.CommandButton', + } + + def __init__(self, obj, draw_page): + self._obj = obj + self._dp = draw_page + self._controls = {} + self._init_controls() + + def __getitem__(self, index): + control = self.obj[index] + return self._controls[control.Name] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + def __str__(self): + return f'Form: {self.name}' + + def _init_controls(self): + types = { + 'com.sun.star.form.OFixedTextModel': 'label', + 'com.sun.star.form.OEditModel': 'text', + 'com.sun.star.form.OButtonModel': 'button', + } + for i, control in enumerate(self.obj): + name = control.Name + tipo = types[control.ImplementationName] + view = self.doc.CurrentController.getControl(control) + control = FORM_CONTROL_CLASS[tipo](control, view, self._obj) + control.index = i + setattr(self, name, control) + self._controls[name] = control + return + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self.obj.Name + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def source(self): + return self.obj.DataSourceName + @source.setter + def source(self, value): + self.obj.DataSourceName = value + + @property + def type(self): + return self.obj.CommandType + @type.setter + def type(self, value): + self.obj.CommandType = value + + @property + def command(self): + return self.obj.Command + @command.setter + def command(self, value): + self.obj.Command = value + + @property + def doc(self): + return self.obj.Parent.Parent + + def _special_properties(self, tipo, args): + if tipo == 'button': + # ~ if 'ImageURL' in args: + # ~ args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + return args + + def add(self, args): + name = args['Name'] + tipo = args.pop('Type').lower() + w = args.pop('Width', 1000) + h = args.pop('Height', 200) + x = args.pop('X', 0) + y = args.pop('Y', 0) + control = self.doc.createInstance('com.sun.star.drawing.ControlShape') + control.setSize(Size(w, h)) + control.setPosition(Point(x, y)) + model = self.doc.createInstance(self.MODELS[tipo]) + args = self._special_properties(tipo, args) + _set_properties(model, args) + control.Control = model + index = len(self) + self.obj.insertByIndex(index, model) + self._dp.add(control) + view = self.doc.CurrentController.getControl(self.obj.getByName(name)) + control = FORM_CONTROL_CLASS[tipo](control, view, self.obj) + control.index = index + setattr(self, name, control) + self._controls[name] = control + return control + + +class LOSheetForms(object): + + def __init__(self, draw_page): + self._dp = draw_page + self._obj = draw_page.Forms + + def __getitem__(self, index): + return LOForm(self.obj[index], self._dp) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + @property + def doc(self): + return self.obj.Parent + + @property + def count(self): + return len(self) + + @property + def names(self): + return self.obj.ElementNames + + def insert(self, name=''): + if not name: + name = f'form{self.count + 1}' + form = self.doc.createInstance('com.sun.star.form.component.Form') + self.obj.insertByName(name, form) + return LOForm(form, self._dp) + + def remove(self, index): + if isinstance(index, int): + self.obj.removeByIndex(index) + else: + self.obj.removeByName(index) + return + + +# ~ IsFiltered, +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetRows(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, int): + rows = LOSheetRows(self._sheet, self.obj[index]) + else: + rango = self._sheet[index.start:index.stop,0:] + rows = LOSheetRows(self._sheet, rango.obj.Rows) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def color(self): + return self.obj.CellBackColor + @color.setter + def color(self, value): + self.obj.CellBackColor = value + + @property + def is_transparent(self): + return self.obj.IsCellBackgroundTransparent + @is_transparent.setter + def is_transparent(self, value): + self.obj.IsCellBackgroundTransparent = value + + @property + def height(self): + return self.obj.Height + @height.setter + def height(self, value): + self.obj.Height = value + + def optimal(self): + self.obj.OptimalHeight = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetColumns(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, (int, str)): + rows = LOSheetColumns(self._sheet, self.obj[index]) + else: + rango = self._sheet[0,index.start:index.stop] + rows = LOSheetColumns(self._sheet, rango.obj.Columns) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def width(self): + return self.obj.Width + @width.setter + def width(self, value): + self.obj.Width = value + + def optimal(self): + self.obj.OptimalWidth = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +class LOCalcSheet(object): + + 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 str(self): + return self.obj.String + @str.setter + def str(self, value): + self.obj.setString(value) + + @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 = LOCalcRanges(rangos) + return rangos + + @property + def merged_area(self): + cursor = self.cursor + cursor.collapseToMergedArea() + rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + return rango + + @property + def empty(self): + cursor = self.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 + for r in obj: + sheet = r.Spreadsheet + rango = LOCalcRange(sheet[r.AbsoluteName]) + self._ranges[rango.name] = rango + + 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._is_section = not self.obj.TextSection is None + self._parts = [] + if self._is_paragraph: + self._parts = [LOWriterTextRange(p, doc) for p in obj] + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + obj = self._parts[self._index] + except IndexError: + raise StopIteration + + self._index += 1 + return obj + + @property + def 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 is_section(self): + return self._is_section + + @property + def text(self): + return self.obj.Text + + @property + def cursor(self): + return self.text.createTextCursorByRange(self.obj) + + @property + def text_cursor(self): + return self.text.createTextCursor() + + @property + def dp(self): + return self._doc.dp + + @property + def paragraph(self): + cursor = self.cursor + cursor.gotoStartOfParagraph(False) + cursor.gotoNextParagraph(True) + return LOWriterTextRange(cursor, self._doc) + + def goto_start(self): + if self.is_section: + rango = self.obj.TextSection.Anchor.Start + else: + rango = self.obj.Start + return LOWriterTextRange(rango, self._doc) + + def goto_end(self): + if self.is_section: + rango = self.obj.TextSection.Anchor.End + else: + rango = self.obj.End + return LOWriterTextRange(rango, self._doc) + + def goto_previous(self, expand=True): + cursor = self.cursor + cursor.gotoPreviousParagraph(expand) + return LOWriterTextRange(cursor, self._doc) + + def goto_next(self, expand=True): + cursor = self.cursor + cursor.gotoNextParagraph(expand) + return LOWriterTextRange(cursor, self._doc) + + def go_left(self, from_self=True, count=1, expand=False): + cursor = self.cursor + if not from_self: + cursor = self.text_cursor + cursor.gotoRange(self.obj, False) + cursor.goLeft(count, expand) + return LOWriterTextRange(cursor, self._doc) + + def go_right(self, from_self=True, count=1, expand=False): + cursor = self.cursor + if not from_self: + cursor = self.text_cursor + cursor.gotoRange(self.obj, False) + cursor.goRight(count, expand) + return LOWriterTextRange(cursor, self._doc) + + def delete(self): + self.value = '' + return + + def offset(self): + cursor = self.cursor.getEnd() + return LOWriterTextRange(cursor, self._doc) + + def insert_content(self, data, cursor=None, replace=False): + if cursor is None: + cursor = self.cursor + self.text.insertTextContent(cursor, data, replace) + return + + def insert_math(self, formula, + anchor_type=TextContentAnchorType.AS_CHARACTER, + cursor=None, replace=False): + + math = self._doc.create_instance(SERVICES['TEXT_EMBEDDED']) + math.CLSID = CLSID['FORMULA'] + math.AnchorType = anchor_type + self.insert_content(math, cursor, replace) + math.EmbeddedObject.Component.Formula = formula + return math + + def new_line(self, count=1): + cursor = self.cursor + for i in range(count): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + return LOWriterTextRange(cursor, self._doc) + + def insert_table(self, data): + table = self._doc.create_instance(SERVICES['TEXT_TABLE']) + rows = len(data) + cols = len(data[0]) + table.initialize(rows, cols) + self.insert_content(table) + table.DataArray = data + name = table.Name + table = LOWriterTextTable(self._doc.tables[name], self._doc) + return table + + def insert_shape(self, tipo, args={}): + # ~ args['Width'] = args.get('Width', 1000) + # ~ args['Height'] = args.get('Height', 1000) + # ~ args['X'] = args.get('X', 0) + # ~ args['Y'] = args.get('Y', 0) + shape = self._doc.dp.add(tipo, args) + # ~ shape.anchor = self.obj + return shape + + def insert_image(self, path, args={}): + w = args.get('Width', 1000) + h = args.get('Height', 1000) + + image = self._doc.create_instance(SERVICES['GRAPHIC']) + image.GraphicURL = _P.to_url(path) + image.AnchorType = TextContentAnchorType.AS_CHARACTER + image.Width = w + image.Height = h + self.insert_content(image) + return self._doc.dp.last + + +class LOWriterTextRanges(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + self._paragraphs = [LOWriterTextRange(p, doc) for p in obj] + + def __len__(self): + return len(self._paragraphs) + + def __getitem__(self, index): + return self._paragraphs[index] + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + obj = self._paragraphs[self._index] + except IndexError: + raise StopIteration + + self._index += 1 + return obj + + @property + def obj(self): + return self._obj + + +class LOWriterTextTable(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + + @property + def data(self): + return self.obj.DataArray + @data.setter + def data(self, values): + self.obj.DataArray = values + + @property + def style(self): + return self.obj.TableTemplateName + @style.setter + def style(self, value): + self.obj.autoFormat(value) + + +class LOWriterTextTables(object): + + def __init__(self, doc): + self._doc = doc + self._obj = doc.obj.TextTables + + def __getitem__(self, key): + return LOWriterTextTable(self._obj[key], self._doc) + + def __len__(self): + return self._obj.Count + + def insert(self, data, text_range=None): + if text_range is None: + text_range = self._doc.selection + text_range.insert_table(data) + return + + +class LOWriter(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = WRITER + + @property + def text(self): + return self.paragraphs + + @property + def paragraphs(self): + return LOWriterTextRanges(self.obj.Text, self) + + @property + def tables(self): + return LOWriterTextTables(self) + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName == OBJ_TEXTS: + if len(sel) == 1: + sel = LOWriterTextRanges(sel, self)[0] + else: + sel = LOWriterTextRanges(sel, self) + return sel + + if sel.ImplementationName == OBJ_SHAPES: + if len(sel) == 1: + sel = sel[0] + sel = LODrawPage(sel.Parent)[sel.Name] + return sel + + if sel.ImplementationName == OBJ_GRAPHIC: + sel = self.dp[sel.Name] + else: + debug(sel.ImplementationName) + + return sel + + @property + def dp(self): + return self.draw_page + @property + def shapes(self): + return self.draw_page + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def cursor(self): + return self.obj.Text.createTextCursor() + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def page_styles(self): + ps = self.obj.StyleFamilies['PageStyles'] + return LOWriterPageStyles(ps) + + @property + def styles(self): + return LOWriterStylesFamilies(self.obj.StyleFamilies) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + @property + def zoom(self): + return self._cc.ViewSettings.ZoomValue + @zoom.setter + def zoom(self, value): + self._cc.ViewSettings.ZoomValue = value + + def goto_start(self): + self.view_cursor.gotoStart(False) + return self.selection + + def goto_end(self): + self.view_cursor.gotoEnd(False) + return self.selection + + def search(self, options, find_all=True): + descriptor = self.search_descriptor + descriptor.setSearchString(options.get('Search', '')) + descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) + descriptor.SearchWords = options.get('Words', False) + if 'Attributes' in options: + attr = dict_to_property(options['Attributes']) + descriptor.setSearchAttributes(attr) + if hasattr(descriptor, 'SearchRegularExpression'): + descriptor.SearchRegularExpression = options.get('RegularExpression', False) + if hasattr(descriptor, 'SearchType') and 'Type' in options: + descriptor.SearchType = options['Type'] + + result = False + if find_all: + found = self.obj.findAll(descriptor) + if len(found): + result = [LOWriterTextRange(f, self) for f in found] + else: + found = self.obj.findFirst(descriptor) + if found: + result = LOWriterTextRange(found, self) + + return result + + def replace(self, options): + descriptor = self.replace_descriptor + descriptor.setSearchString(options['Search']) + descriptor.setReplaceString(options['Replace']) + descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) + descriptor.SearchWords = options.get('Words', False) + if 'Attributes' in options: + attr = dict_to_property(options['Attributes']) + descriptor.setSearchAttributes(attr) + if hasattr(descriptor, 'SearchRegularExpression'): + descriptor.SearchRegularExpression = options.get('RegularExpression', False) + if hasattr(descriptor, 'SearchType') and 'Type' in options: + descriptor.SearchType = options['Type'] + found = self.obj.replaceAll(descriptor) + return found + + def select(self, text): + if hasattr(text, 'obj'): + text = text.obj + self._cc.select(text) + return + + +class LOShape(LOBaseObject): + 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 position(self): + return self.obj.Position + @property + def x(self): + return self.position.X + @property + def y(self): + return self.position.Y + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def 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 + try: + self.obj.Anchor = value + except Exception as e: + self.obj.AnchorType = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def path(self): + return self.url + @property + def url(self): + url = '' + if self.is_image: + url = _P.to_system(self.obj.GraphicURL.OriginURL) + return url + + @property + def mimetype(self): + mt = '' + if self.is_image: + mt = self.obj.GraphicURL.MimeType + return mt + + @property + def linked(self): + l = False + if self.is_image: + l = self.obj.GraphicURL.Linked + return l + + def delete(self): + self.remove() + return + def remove(self): + self.obj.Parent.remove(self.obj) + return + + def save(self, path: str, mimetype=DEFAULT_MIME_TYPE): + if _P.is_dir(path): + name = self.name + ext = mimetype.lower() + else: + p = _P(path) + path = p.path + name = p.name + ext = p.ext.lower() + + path = _P.join(path, f'{name}.{ext}') + args = dict( + URL = _P.to_url(path), + MimeType = MIME_TYPE[ext], + ) + if not _export_image(self.obj, args): + path = '' + return path + + # ~ def save2(self, path: str): + # ~ size = len(self.obj.Bitmap.DIB) + # ~ data = self.obj.GraphicStream.readBytes((), size) + # ~ data = data[-1].value + # ~ path = _P.join(path, f'{self.name}.png') + # ~ _P.save_bin(path, b'') + # ~ return + + +class LODrawPage(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + shape = LOShape(self.obj[index], index) + else: + for i, o in enumerate(self.obj): + shape = self.obj[i] + name = shape.Name or f'shape{i}' + if name == index: + shape = LOShape(shape, i) + break + return shape + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + if self._index == self.count: + raise StopIteration + shape = self[self._index] + self._index += 1 + return shape + + + @property + def name(self): + return self.obj.Name + + @property + def doc(self): + return self.obj.Forms.Parent + + @property + def width(self): + return self.obj.Width + + @property + def height(self): + return self.obj.Height + + @property + def count(self): + return self.obj.Count + + @property + def last(self): + return self[self.count - 1] + + def create_instance(self, name): + return self.doc.createInstance(name) + + def add(self, type_shape, options={}): + args = options.copy() + """Insert a shape in page, type shapes: + Line + Rectangle + Ellipse + Text + Connector + """ + index = self.count + default_height = 3000 + if type_shape == 'Line': + default_height = 0 + w = args.pop('Width', 3000) + h = args.pop('Height', default_height) + x = args.pop('X', 1000) + y = args.pop('Y', 1000) + name = args.pop('Name', f'{type_shape.lower()}{index}') + + service = f'com.sun.star.drawing.{type_shape}Shape' + shape = self.create_instance(service) + shape.Size = Size(w, h) + shape.Position = Point(x, y) + shape.Name = name + self.obj.add(shape) + + if args: + _set_properties(shape, args) + + return LOShape(self.obj[index], index) + + def remove(self, shape): + if hasattr(shape, 'obj'): + shape = shape.obj + return self.obj.remove(shape) + + def remove_all(self): + while self.count: + self.obj.remove(self.obj[0]) + return + + def insert_image(self, path, options={}): + args = options.copy() + index = self.count + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + name = args.get('Name', f'image{index}') + + image = self.create_instance('com.sun.star.drawing.GraphicObjectShape') + if isinstance(path, str): + image.GraphicURL = _P.to_url(path) + else: + gp = create_instance('com.sun.star.graphic.GraphicProvider') + properties = dict_to_property({'InputStream': path}) + image.Graphic = gp.queryGraphic(properties) + + self.obj.add(image) + image.Size = Size(w, h) + image.Position = Point(x, y) + image.Name = name + return LOShape(self.obj[index], index) + + +class LODrawImpress(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + page = self.obj.DrawPages[index] + else: + page = self.obj.DrawPages.getByName(index) + return LODrawPage(page) + + @property + def selection(self): + sel = self.obj.CurrentSelection[0] + # ~ return _get_class_uno(sel) + return sel + + @property + def current_page(self): + return LODrawPage(self._cc.getCurrentPage()) + + def paste(self): + call_dispatch(self.frame, '.uno:Paste') + return self.current_page[-1] + + def add(self, type_shape, args={}): + return self.current_page.add(type_shape, args) + + def insert_image(self, path, args={}): + self.current_page.insert_image(path, args) + return + + # ~ def export(self, path, mimetype='png'): + # ~ args = dict( + # ~ URL = _P.to_url(path), + # ~ MimeType = MIME_TYPE[mimetype], + # ~ ) + # ~ result = _export_image(self.obj, args) + # ~ return result + + +class 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, + 'addSpinListener': EventsSpin, + } + if hasattr(control, 'obj'): + control = control.obj + # ~ debug(control.ImplementationName) + is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' + is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' + is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' + is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl' + + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue + + getattr(control, key)(listeners[key](events, name)) + + if is_grid: + controllers = EventsGrid(events, name) + control.addSelectionListener(controllers) + control.Model.GridDataModel.addGridDataListener(controllers) + return + + +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 EventsSpin(EventsListenerBase, XSpinListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def up(self, event): + event_name = f'{self.name}_up' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def down(self, event): + event_name = f'{self.name}_up' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def first(self, event): + event_name = f'{self.name}_first' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def last(self, event): + event_name = f'{self.name}_last' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def mousePressed(self, event): + event_name = '{}_click'.format(self._name) + if event.ClickCount == 2: + event_name = '{}_double_click'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def mouseReleased(self, event): + event_name = '{}_after_click'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def mouseEntered(self, event): + pass + + def mouseExited(self, event): + pass + + # ~ XMouseMotionListener + def mouseMoved(self, event): + pass + + def mouseDragged(self, event): + pass + + +class EventsMouseLink(EventsMouse): + + def __init__(self, controller, name): + super().__init__(controller, name) + self._text_color = 0 + + def mouseEntered(self, event): + model = event.Source.Model + self._text_color = model.TextColor or 0 + model.TextColor = get_color('blue') + return + + def mouseExited(self, event): + model = event.Source.Model + model.TextColor = self._text_color + return + + +class EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def actionPerformed(self, event): + event_name = f'{self.name}_action' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + CONTROLS = ( + 'stardiv.Toolkit.UnoControlEditModel', + ) + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + service = event.Source.Model.ImplementationName + # ~ print('Focus enter', service) + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + return + + def focusLost(self, event): + service = event.Source.Model.ImplementationName + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = -1 + return + + +class EventsKey(EventsListenerBase, XKeyListener): + """ + event.KeyChar + event.KeyCode + event.KeyFunc + event.Modifiers + """ + + def __init__(self, controller, name): + super().__init__(controller, name) + + def keyPressed(self, event): + pass + + def keyReleased(self, event): + event_name = '{}_key_released'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + # ~ else: + # ~ if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): + # ~ self._cls.close() + return + + +class EventsItem(EventsListenerBase, XItemListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def disposing(self, event): + pass + + def itemStateChanged(self, event): + event_name = '{}_item_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsItemRoadmap(EventsItem): + + def itemStateChanged(self, event): + dialog = event.Source.Context.Model + dialog.Step = event.ItemId + 1 + return + + +class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def dataChanged(self, event): + event_name = '{}_data_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def rowHeadingChanged(self, event): + pass + + def rowsInserted(self, event): + pass + + def rowsRemoved(self, evemt): + pass + + def selectionChanged(self, event): + event_name = '{}_selection_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsMouseGrid(EventsMouse): + selected = False + + def mousePressed(self, event): + super().mousePressed(event) + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ print(col, row) + # ~ if col == -1 and row == -1: + # ~ if self.selected: + # ~ obj.deselectAllRows() + # ~ else: + # ~ obj.selectAllRows() + # ~ self.selected = not self.selected + return + + def mouseReleased(self, event): + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ if row == -1 and col > -1: + # ~ gdm = obj.Model.GridDataModel + # ~ for i in range(gdm.RowCount): + # ~ gdm.updateRowHeading(i, i + 1) + return + + +class EventsTab(EventsListenerBase, XTabListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def activated(self, id): + event_name = '{}_activated'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(id) + return + + +class EventsMenu(EventsListenerBase, XMenuListener): + + def __init__(self, controller): + super().__init__(controller, '') + + def itemHighlighted(self, event): + pass + + def itemSelected(self, event): + name = event.Source.getCommand(event.MenuId) + if name.startswith('menu'): + event_name = '{}_selected'.format(name) + else: + event_name = 'menu_{}_selected'.format(name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def itemActivated(self, event): + return + + def itemDeactivated(self, event): + return + + +class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): + + def __init__(self, cls): + self._cls = cls + super().__init__(cls.events, cls.name, cls._window) + + def windowOpened(self, event): + event_name = '{}_opened'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowActivated(self, event): + control_name = '{}_activated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowDeactivated(self, event): + control_name = '{}_deactivated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowMinimized(self, event): + pass + + def windowNormalized(self, event): + pass + + def windowClosing(self, event): + if self._window: + control_name = 'window_closing' + else: + control_name = '{}_closing'.format(event.Source.Model.Name) + + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + # ~ else: + # ~ if not self._modal and not self._block: + # ~ event.Source.Visible = False + return + + def windowClosed(self, event): + control_name = '{}_closed'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + # ~ XWindowListener + def windowResized(self, event): + sb = self._cls._subcont + sb.setPosSize(0, 0, event.Width, event.Height, SIZE) + event_name = '{}_resized'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowMoved(self, event): + pass + + def windowShown(self, event): + pass + + def windowHidden(self, event): + pass + + +# ~ BorderColor = ? +# ~ FontStyleName = ? +# ~ HelpURL = ? +class UnoBaseObject(object): + + def __init__(self, obj, path=''): + self._obj = obj + self._model = obj.Model + + def __setattr__(self, name, value): + exists = hasattr(self, name) + if not exists and not name in ('_obj', '_model'): + setattr(self._model, name, value) + else: + super().__setattr__(name, value) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + @property + def m(self): + return self._model + + @property + def properties(self): + return {} + @properties.setter + def properties(self, values): + _set_properties(self.model, values) + + @property + def name(self): + return self.model.Name + + @property + def parent(self): + return self.obj.Context + + @property + def tag(self): + return self.model.Tag + @tag.setter + def tag(self, value): + self.model.Tag = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.setVisible(value) + + @property + def enabled(self): + return self.model.Enabled + @enabled.setter + def enabled(self, value): + self.model.Enabled = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def align(self): + return self.model.Align + @align.setter + def align(self, value): + self.model.Align = value + + @property + def valign(self): + return self.model.VerticalAlign + @valign.setter + def valign(self, value): + self.model.VerticalAlign = value + + @property + def font_weight(self): + return self.model.FontWeight + @font_weight.setter + def font_weight(self, value): + self.model.FontWeight = value + + @property + def font_height(self): + return self.model.FontHeight + @font_height.setter + def font_height(self, value): + self.model.FontHeight = value + + @property + def font_name(self): + return self.model.FontName + @font_name.setter + def font_name(self, value): + self.model.FontName = value + + @property + def font_underline(self): + return self.model.FontUnderline + @font_underline.setter + def font_underline(self, value): + self.model.FontUnderline = value + + @property + def text_color(self): + return self.model.TextColor + @text_color.setter + def text_color(self, value): + self.model.TextColor = value + + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + + @property + def multi_line(self): + return self.model.MultiLine + @multi_line.setter + def multi_line(self, value): + self.model.MultiLine = value + + @property + def help_text(self): + return self.model.HelpText + @help_text.setter + def help_text(self, value): + self.model.HelpText = value + + @property + def border(self): + return self.model.Border + @border.setter + def border(self, value): + # ~ Bug for report + self.model.Border = value + + @property + def width(self): + return self._model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + def _get_possize(self, name): + ps = self.obj.getPosSize() + return getattr(ps, name) + + def _set_possize(self, name, value): + ps = self.obj.getPosSize() + setattr(ps, name, value) + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + return + + @property + def x(self): + if hasattr(self.model, 'PositionX'): + return self.model.PositionX + return self._get_possize('X') + @x.setter + def x(self, value): + if hasattr(self.model, 'PositionX'): + self.model.PositionX = value + else: + self._set_possize('X', value) + + @property + def y(self): + if hasattr(self.model, 'PositionY'): + return self.model.PositionY + return self._get_possize('Y') + @y.setter + def y(self, value): + if hasattr(self.model, 'PositionY'): + self.model.PositionY = value + else: + self._set_possize('Y', value) + + @property + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value + + @property + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value + + @property + def ps(self): + ps = self.obj.getPosSize() + return ps + @ps.setter + def ps(self, ps): + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + + def set_focus(self): + self.obj.setFocus() + return + + def ps_from(self, source): + self.ps = source.ps + return + + def center(self, horizontal=True, vertical=False): + p = self.parent.Model + w = p.Width + h = p.Height + if horizontal: + x = w / 2 - self.width / 2 + self.x = x + if vertical: + y = h / 2 - self.height / 2 + self.y = y + return + + def move(self, origin, x=0, y=5, center=False): + if x: + self.x = origin.x + origin.width + x + else: + self.x = origin.x + if y: + h = origin.height + if y < 0: + h = 0 + self.y = origin.y + h + y + else: + self.y = origin.y + + if center: + self.center() + return + + +class UnoLabel(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoLabelLink(UnoLabel): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'link' + + +class UnoButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + @property + def image(self): + return self.model.ImageURL + @image.setter + def image(self, value): + self.model.ImageURL = _P.to_url(value) + + +class UnoRadio(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'radio' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoCheckBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'checkbox' + + @property + def value(self): + return self.model.State + @value.setter + def value(self, value): + self.model.State = value + + @property + def label(self): + return self.model.Label + @label.setter + def label(self, value): + self.model.Label = value + + @property + def tri_state(self): + return self.model.TriState + @tri_state.setter + def tri_state(self, value): + self.model.TriState = value + + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html +class UnoText(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self.model.Text + @value.setter + def value(self, value): + self.model.Text = value + + @property + def echochar(self): + return chr(self.model.EchoChar) + @echochar.setter + def echochar(self, value): + if value: + self.model.EchoChar = ord(value[0]) + else: + self.model.EchoChar = 0 + + def validate(self): + return + + +class UnoImage(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'image' + + @property + def value(self): + return self.url + @value.setter + def value(self, value): + self.url = value + + @property + def url(self): + return self.m.ImageURL + @url.setter + def url(self, value): + self.m.ImageURL = None + self.m.ImageURL = _P.to_url(value) + + +class UnoListBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._path = '' + + def __setattr__(self, name, value): + if name in ('_path',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def type(self): + return 'listbox' + + @property + def value(self): + return self.obj.getSelectedItem() + + @property + def count(self): + return len(self.data) + + @property + def data(self): + return self.model.StringItemList + @data.setter + def data(self, values): + self.model.StringItemList = list(sorted(values)) + + @property + def path(self): + return self._path + @path.setter + def path(self, value): + self._path = value + + def unselect(self): + self.obj.selectItem(self.value, False) + return + + def select(self, pos=0): + if isinstance(pos, str): + self.obj.selectItem(pos, True) + else: + self.obj.selectItemPos(pos, True) + return + + def clear(self): + self.model.removeAllItems() + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def insert(self, value, path='', pos=-1, show=True): + if pos < 0: + pos = self.count + if path: + self.model.insertItem(pos, value, self._set_image_url(path)) + else: + self.model.insertItemText(pos, value) + if show: + self.select(pos) + return + + +class UnoRoadmap(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._options = () + + def __setattr__(self, name, value): + if name in ('_options',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def options(self): + return self._options + @options.setter + def options(self, values): + self._options = values + for i, v in enumerate(values): + opt = self.model.createInstance() + opt.ID = i + opt.Label = v + self.model.insertByIndex(i, opt) + return + + @property + def enabled(self): + return True + @enabled.setter + def enabled(self, value): + for m in self.model: + m.Enabled = value + return + + def set_enabled(self, index, value): + self.model.getByIndex(index).Enabled = value + return + + +class UnoTree(UnoBaseObject): + + def __init__(self, obj, ): + super().__init__(obj) + self._tdm = None + self._data = [] + + def __setattr__(self, name, value): + if name in ('_tdm', '_data'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def selection(self): + sel = self.obj.Selection + return sel.DataValue, sel.DisplayValue + + @property + def parent(self): + parent = self.obj.Selection.Parent + if parent is None: + return () + return parent.DataValue, parent.DisplayValue + + def _get_parents(self, node): + value = (node.DisplayValue,) + parent = node.Parent + if parent is None: + return value + return self._get_parents(parent) + value + + @property + def parents(self): + values = self._get_parents(self.obj.Selection) + return values + + @property + def root(self): + if self._tdm is None: + return '' + return self._tdm.Root.DisplayValue + @root.setter + def root(self, value): + self._add_data_model(value) + + def _add_data_model(self, name): + tdm = create_instance('com.sun.star.awt.tree.MutableTreeDataModel') + root = tdm.createNode(name, True) + root.DataValue = 0 + tdm.setRoot(root) + self.model.DataModel = tdm + self._tdm = self.model.DataModel + return + + @property + def path(self): + return self.root + @path.setter + def path(self, value): + self.data = _P.walk_dir(value, True) + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = list(values) + self._add_data() + + def _add_data(self): + if not self.data: + return + + parents = {} + for node in self.data: + parent = parents.get(node[1], self._tdm.Root) + child = self._tdm.createNode(node[2], False) + child.DataValue = node[0] + parent.appendChild(child) + parents[node[0]] = child + self.obj.expandNode(self._tdm.Root) + return + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html +class UnoGrid(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._gdm = self.model.GridDataModel + self._data = [] + self._formats = () + + def __setattr__(self, name, value): + if name in ('_gdm', '_data', '_formats'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getitem__(self, key): + value = self._gdm.getCellData(key[0], key[1]) + return value + + def __setitem__(self, key, value): + self._gdm.updateCellData(key[0], key[1], value) + return + + @property + def type(self): + return 'grid' + + @property + def columns(self): + return {} + @columns.setter + def columns(self, values): + # ~ self._columns = values + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html + model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) + for properties in values: + column = create_instance('com.sun.star.awt.grid.GridColumn', True) + for k, v in properties.items(): + setattr(column, k, v) + model.addColumn(column) + self.model.ColumnModel = model + return + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = values + self.clear() + headings = tuple(range(1, len(values) + 1)) + self._gdm.addRows(headings, values) + # ~ rows = range(grid_dm.RowCount) + # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows] + # ~ grid.Model.RowBackgroundColors = tuple(colors) + return + + @property + def value(self): + if self.column == -1 or self.row == -1: + return '' + return self[self.column, self.row] + @value.setter + def value(self, value): + if self.column > -1 and self.row > -1: + self[self.column, self.row] = value + + @property + def row(self): + return self.obj.CurrentRow + + @property + def row_count(self): + return self._gdm.RowCount + + @property + def column(self): + return self.obj.CurrentColumn + + @property + def 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 + + +class UnoSpinButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'spinbutton' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoNumericField(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'numeric' + + @property + def int(self): + return int(self.value) + + @property + def value(self): + return self.model.Value + @value.setter + def value(self, value): + self.model.Value = value + + +UNO_CLASSES = { + 'label': UnoLabel, + 'link': UnoLabelLink, + 'button': UnoButton, + 'radio': UnoRadio, + 'checkbox': UnoCheckBox, + 'text': UnoText, + 'image': UnoImage, + 'listbox': UnoListBox, + 'roadmap': UnoRoadmap, + 'tree': UnoTree, + 'grid': UnoGrid, + 'pages': UnoPages, + 'spinbutton': UnoSpinButton, + 'numeric': UnoNumericField, +} + +UNO_MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', + 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', +} +# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', +# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', +# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', +# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', +# ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', +# ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', +# ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', +# ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', +# ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', +# ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', + + +class LODialog(object): + SEPARATION = 5 + MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + 'spinbutton': 'com.sun.star.awt.UnoControlSpinButtonModel', + 'numeric': 'com.sun.star.awt.UnoControlNumericFieldModel', + } + + def __init__(self, args): + self._obj = self._create(args) + self._model = self.obj.Model + self._events = None + self._modal = True + self._controls = {} + self._color_on_focus = COLOR_ON_FOCUS + self._id = '' + self._path = '' + self._init_controls() + + def _create(self, args): + service = 'com.sun.star.awt.DialogProvider' + path = args.pop('Path', '') + if path: + dp = create_instance(service, True) + dlg = dp.createDialog(_P.to_url(path)) + return dlg + + if 'Location' in args: + name = args['Name'] + library = args.get('Library', 'Standard') + location = args.get('Location', 'application').lower() + if location == 'user': + location = 'application' + url = f'vnd.sun.star.script:{library}.{name}?location={location}' + if location == 'document': + dp = create_instance(service, args=docs.active.obj) + else: + dp = create_instance(service, True) + # ~ uid = docs.active.uid + # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' + dlg = dp.createDialog(url) + return dlg + + dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) + model = create_instance('com.sun.star.awt.UnoControlDialogModel', True) + toolkit = create_instance('com.sun.star.awt.Toolkit', True) + _set_properties(model, args) + dlg.setModel(model) + dlg.setVisible(False) + dlg.createPeer(toolkit, None) + return dlg + + def _get_type_control(self, name): + name = name.split('.')[2] + types = { + 'UnoFixedTextControl': 'label', + 'UnoEditControl': 'text', + 'UnoButtonControl': 'button', + } + return types[name] + + def _init_controls(self): + for control in self.obj.getControls(): + tipo = self._get_type_control(control.ImplementationName) + name = control.Model.Name + control = UNO_CLASSES[tipo](control) + setattr(self, name, control) + return + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + + @property + def controls(self): + return self._controls + + @property + def path(self): + return self._path + @property + def path_images(self): + return _P.join(self.path, DIR['images']) + @property + def id(self): + return self._id + @id.setter + def id(self, value): + self._id = value + self._path = _P.from_id(value) + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + @property + def width(self): + return self.model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers(self) + self._connect_listeners() + + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + self._color_on_focus = get_color(value) + + def _connect_listeners(self): + for control in self.obj.Controls: + _add_listeners(self.events, control, control.Model.Name) + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['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 + + @classproperty + def user_profile(self): + path = self.config('UserConfig') + path = str(Path(path).parent) + return path + + @classproperty + def user_config(self): + path = self.config('UserConfig') + 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, get_lines=False, encoding='utf-8'): + if get_lines: + with Path(path).open(encoding=encoding) as f: + data = f.readlines() + else: + data = Path(path).read_text(encoding=encoding) + return data + + @classmethod + def read_bin(cls, path): + 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/stats.py b/source/pythonpath/stats.py new file mode 100644 index 0000000..a7e4fe6 --- /dev/null +++ b/source/pythonpath/stats.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 + +# ~ Thanks: +# ~ https://github.com/kolypto/py-password-strength/blob/master/password_strength/stats.py + + +import re +import unicodedata +from collections import Counter +from functools import wraps +from math import log + + +def cached_property(f): + """ Property that will replace itself with a calculated value """ + name = '__' + f.__name__ + + @wraps(f) + def wrapper(self): + if not hasattr(self, name): + setattr(self, name, f(self)) + return getattr(self, name) + return property(wrapper) + + +class PasswordStats(object): + """ PasswordStats allows to calculate statistics on a password. + + It considers a password as a unicode string, and all statistics are unicode-based. + """ + + def __init__(self, password): + self.password = password + + #region Statistics + + @cached_property + def alphabet(self): + """ Get alphabet: set of used characters + + :rtype: set + """ + return set(self.password) + + @cached_property + def alphabet_cardinality(self): + """ Get alphabet cardinality: alphabet length + + :rtype: int + """ + return len(self.alphabet) + + @cached_property + def char_categories_detailed(self): + """ Character count per unicode category, detailed format. + + See: http://www.unicode.org/reports/tr44/#GC_Values_Table + + :returns: Counter( unicode-character-category: count ) + :rtype: collections.Counter + """ + return Counter(map(unicodedata.category, self.password)) + + @cached_property + def char_categories(self): + """ Character count per top-level category + + The following top-level categories are defined: + + - L: letter + - M: Mark + - N: Number + - P: Punctuation + - S: Symbol + - Z: Separator + - C: Other + + :return: Counter(unicode-character-category: count } + :rtype: collections.Counter + """ + c = Counter() + for cat, n in self.char_categories_detailed.items(): + c[cat[0]] += n + return c + + #endregion + + #region Counters + + @cached_property + def length(self): + """ Get password length + + :rtype: int + """ + return len(self.password) + + @cached_property + def letters(self): + """ Count all letters + + :rtype: int + """ + return self.char_categories['L'] + + @cached_property + def letters_uppercase(self): + """ Count uppercase letters + + :rtype: int + """ + return self.char_categories_detailed['Lu'] + + @cached_property + def letters_lowercase(self): + """ Count lowercase letters + + :rtype: int + """ + return self.char_categories_detailed['Ll'] + + @cached_property + def numbers(self): + """ Count numbers + + :rtype: int + """ + return self.char_categories['N'] + + def count(self, *categories): + """ Count characters of the specified classes only + + :param categories: Character categories to count + :type categories: Iterable + :rtype: int + """ + return sum([int(cat_n[0] in categories) * cat_n[1] for cat_n in list(self.char_categories.items())]) + + def count_except(self, *categories): + """ Count characters of all classes except the specified ones + + :param categories: Character categories to exclude from count + :type categories: Iterable + :rtype: int + """ + return sum([int(cat_n1[0] not in categories) * cat_n1[1] for cat_n1 in list(self.char_categories.items())]) + + @cached_property + def special_characters(self): + """ Count special characters + + Special characters is everything that's not a letter or a number + + :rtype: int + """ + return self.count_except('L', 'N') + + #region Security + + @cached_property + def combinations(self): + """ The number of possible combinations with the current alphabet + + :rtype: long + """ + return self.alphabet_cardinality ** self.length + + @cached_property + def entropy_bits(self): + """ Get information entropy bits: log2 of the number of possible passwords + + https://en.wikipedia.org/wiki/Password_strength + + :rtype: float + """ + return self.length * log(self.alphabet_cardinality, 2) + + @cached_property + def entropy_density(self): + """ Get information entropy density factor, ranged {0 .. 1}. + + This is ratio of entropy_bits() to max bits a password of this length could have. + E.g. if all characters are unique -- then it's 1.0. + If half of the characters are reused once -- then it's 0.5. + + :rtype: float + """ + # Simplifying: + # entropy_bits / (length * log(length, 2)) = + # = log(alphabet_cardinality, 2) / log(length, 2) = + # = log(alphabet_cardinality, length) + return log(self.alphabet_cardinality, self.length) + + def strength(self, weak_bits=30): + """ Get password strength as a number normalized to range {0 .. 1}. + + Normalization is done in the following fashion: + + 1. If entropy_bits <= weak_bits -- linear in range{0.0 .. 0.33} (weak) + 2. If entropy_bits <= weak_bits*2 -- almost linear in range{0.33 .. 0.66} (medium) + 3. If entropy_bits > weak_bits*3 -- asymptotic towards 1.0 (strong) + + :param weak_bits: Minimum entropy bits a medium password should have. + :type weak_bits: int + :return: Normalized password strength: + * <0.33 is WEAK + * <0.66 is MEDIUM + * >0.66 is STRONG + :rtype: float + """ + WEAK_MAX = 0.333333333 + + if self.entropy_bits <= weak_bits: + return WEAK_MAX * self.entropy_bits / weak_bits + + HARD_BITS = weak_bits*3 + HARD_VAL = 0.950 + + # Here, we want a function that: + # 1. f(x)=0.333 at x=weak_bits + # 2. f(x)=0.950 at x=weak_bits*3 (great estimation for a perfect password) + # 3. f(x) is almost linear in range{weak_bits .. weak_bits*2}: doubling the bits should double the strength + # 4. f(x) has an asymptote of 1.0 (normalization) + + # First, the function: + # f(x) = 1 - (1-WEAK_MAX)*2^( -k*x) + + # Now, the equation: + # f(HARD_BITS) = HARD_VAL + # 1 - (1-WEAK_MAX)*2^( -k*HARD_BITS) = HARD_VAL + # 2^( -k*HARD_BITS) = (1 - HARD_VAL) / (1-WEAK_MAX) + # k = -log2((1 - HARD_VAL) / (1-WEAK_MAX)) / HARD_BITS + k = -log((1 - HARD_VAL) / (1-WEAK_MAX), 2) / HARD_BITS + f = lambda x: 1 - (1-WEAK_MAX)*pow(2, -k*x) + + return f(self.entropy_bits - weak_bits) # with offset + + #endregion + + #region Detectors + + _repeated_patterns_rex = re.compile(r'((.+?)\2+)', re.UNICODE | re.DOTALL | re.IGNORECASE) + + @cached_property + def repeated_patterns_length(self): + """ Detect and return the length of repeated patterns. + + You will probably be comparing it with the length of the password itself and ban if it's longer than 10% + + :rtype: int + """ + length = 0 + for substring, pattern in self._repeated_patterns_rex.findall(self.password): + length += len(substring) + return length + + _sequences = ( + 'abcdefghijklmnopqrstuvwxyz' # Alphabet + 'qwertyuiopasdfghjklzxcvbnm' # Keyboard + '~!@#$%^&*()_+-=' # Keyboard special, top row + '01234567890' # Numbers + ) + _sequences = _sequences + _sequences[::-1] # reversed + + @cached_property + def sequences_length(self): + """ Detect and return the length of used sequences: + + - Alphabet letters: abcd... + - Keyboard letters: qwerty, etc + - Keyboard special characters in the top row: ~!@#$%^&*()_+ + - Numbers: 0123456 + + :return: Total length of character sequences that are subsets of the common sequences + :rtype: int + """ + # FIXME: Optimize this. I'm sure there is a better way!... + sequences_length = 0 + + # Iterate through the string, with manual variable (to allow skips) + i = 0 + while i < len(self.password): + # Slice (since we use it often) + password = self.password[i:] + + # Iterate over sequences to find longest common prefix + j = -1 + common_length = 1 + while True: + # Detect the first match with the current character + # A character may appear multiple times + j = self._sequences.find(password[0], j+1) + if j == -1: + break + + # Find the longest common prefix + common_here = '' + for a, b in zip(password, self._sequences[j:]): + if a != b: break + else: common_here += a + + # It it's longer than previous discoveries -- store it + common_length = max(common_length, len(common_here)) + + # Repeated sequence? + if common_length > 2: + sequences_length += common_length + + # Next: skip to the end of the detected sequence + i += common_length + + return sequences_length + + @cached_property + def weakness_factor(self): + """ Get weakness factor as a float in range {0 .. 1} + + This detects the portion of the string that contains: + * repeated patterns + * sequences + + E.g. a value of 1.0 means the whole string is weak, and 0.5 means half of the string is weak. + + Typical usage: + + password_strength = (1 - weakness_factor) * strength + + :return: Weakness factor + :rtype: float + """ + return min(1.0, (self.repeated_patterns_length + self.sequences_length) / self.length) diff --git a/source/pythonpath/zpass.py b/source/pythonpath/zpass.py new file mode 100644 index 0000000..49fc150 --- /dev/null +++ b/source/pythonpath/zpass.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 + +import string +import random +import easymacro as app +from stats import PasswordStats + + +_ = None +PREFIX = 'zazpass' +DEFAULT_LENGTH = 25 +DEFAULT_CHARACTERS = ( + string.ascii_uppercase + + string.ascii_lowercase + + string.digits + + string.punctuation) + + +def main(id_extension, args, path_locales): + global _ + + if args == 'insert': + _password_insert() + return + + if args == 'copy': + _password_copy() + return + + _ = app.install_locales(path_locales) + _password_generate(id_extension) + + return + + +def _from_config(): + characters = '' + config = app.get_config('setting', PREFIX) + length = config.get('length', DEFAULT_LENGTH) + if config: + if config['letters']: + characters += string.ascii_uppercase + if config['letters2']: + characters += string.ascii_lowercase + if config['digits']: + characters += string.digits + if config['punctuation']: + characters += string.punctuation + else: + characters = DEFAULT_CHARACTERS + + return list(characters), length + + +def _from_controls(dialog): + characters = '' + + length = int(dialog.txt_length.value) + if dialog.chk_letters.value: + characters += string.ascii_uppercase + if dialog.chk_letters2.value: + characters += string.ascii_lowercase + if dialog.chk_digits.value: + characters += string.digits + if dialog.chk_punctuation.value: + characters += string.punctuation + + if not characters: + dialog.chk_letters.value = True + characters = string.ascii_uppercase + + return list(characters), length + + +def _get_data(dialog): + if dialog is None: + characters, lenght = _from_config() + else: + characters, lenght = _from_controls(dialog) + return characters, lenght + + +def _generate(dialog=None): + characters, length = _get_data(dialog) + random.shuffle(characters) + password = [random.choice(characters) for i in range(length)] + random.shuffle(password) + password = ''.join(password) + return password + + +def _password_insert(): + for cell in app.selection: + cell.str = _generate() + return + + +def _password_copy(): + app.clipboard.set(_generate()) + return + + +def _password_generate(id_extension): + config = app.get_config('setting', prefix=PREFIX) + dialog = _create_dialog(id_extension) + + length = config.get('length', DEFAULT_LENGTH) + letters = True + letters2 = True + digits = True + punctuation = True + if config: + letters = config['letters'] + letters2 = config['letters2'] + digits = config['digits'] + punctuation = config['punctuation'] + + dialog.chk_letters.value = letters + dialog.chk_letters2.value = letters2 + dialog.chk_digits.value = digits + dialog.chk_punctuation.value = punctuation + + dialog.txt_length.value = length + dialog.txt_password.value = _generate(dialog) + dialog.open() + return + + +class Controllers(object): + + def __init__(self, dialog): + self.d = dialog + + def cmd_close_action(self, event): + self.d.close() + return + + def cmd_switch_action(self, event): + char = self.d.txt_password.echochar + name_image = 'eye-close.svg' + if char == '*': + self.d.txt_password.echochar = '' + name_image = 'eye-open.svg' + else: + self.d.txt_password.echochar = '*' + + path_image = app.paths.join(self.d.path_images, name_image) + self.d.cmd_switch.image = path_image + self.d.txt_password.set_focus() + + return + + def cmd_insert_action(self, event): + for cell in app.selection: + cell.str = self.d.txt_password.value + self.d.close() + return + + def _save_config(self): + data = dict( + length = self.d.txt_length.int, + letters = self.d.chk_letters.value, + letters2 = self.d.chk_letters2.value, + digits = self.d.chk_digits.value, + punctuation = self.d.chk_punctuation.value, + ) + app.set_config('setting', data, PREFIX) + return + + + def _new_password(self, save=True): + stats = PasswordStats(_generate(self.d)) + self.d.txt_password.value = stats.password + if save: + self._save_config() + return + + def cmd_new_action(self, event): + self._new_password(False) + return + + def txt_length_after_click(self, event): + self._new_password() + return + + def chk_letters_after_click(self, event): + self._new_password() + return + + def chk_letters2_after_click(self, event): + self._new_password() + return + + def chk_digits_after_click(self, event): + self._new_password() + return + + def chk_punctuation_after_click(self, event): + self._new_password() + return + + +@app.catch_exception +def _create_dialog(id_extension): + BUTTON_WH = 16 + CHK_WIDTH = 25 + + attr = dict( + Name = 'Dialog', + Title = _('Generate Password'), + Width = 200, + Height = 100, + ) + dialog = app.create_dialog(attr) + dialog.id = id_extension + dialog.events = Controllers + + attr = dict( + Type = 'Text', + Name = 'txt_password', + Width = 150, + Height = 12, + X = 5, + Y = 10, + EchoChar = ord('*'), + ) + dialog.add_control(attr) + + attr = dict( + Type = 'Button', + Name = 'cmd_switch', + Width = BUTTON_WH, + Height = BUTTON_WH, + ImageURL = 'eye-close.svg', + FocusOnClick = True, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'Button', + Name = 'cmd_new', + Width = BUTTON_WH, + Height = BUTTON_WH, + ImageURL = 'new.svg', + ) + dialog.add_control(attr) + + attr = dict( + Type = 'Label', + Name = 'lbl_title_length', + Label = _('Length: '), + Width = 50, + Height = BUTTON_WH, + Border = 1, + Align = 2, + VerticalAlign = 1, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'Numeric', + Name = 'txt_length', + Width = 50, + Height = BUTTON_WH, + DecimalAccuracy = 0, + Spin = True, + Value = 25, + ValueStep = 1, + ValueMin = 10, + ValueMax = 100, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'CheckBox', + Name = 'chk_letters', + Label = 'A-Z', + Width = CHK_WIDTH, + Height = BUTTON_WH, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'CheckBox', + Name = 'chk_letters2', + Label = 'a-z', + Width = CHK_WIDTH, + Height = BUTTON_WH, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'CheckBox', + Name = 'chk_digits', + Label = '0-9', + Width = CHK_WIDTH, + Height = BUTTON_WH, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'CheckBox', + Name = 'chk_punctuation', + Label = string.punctuation, + Width = CHK_WIDTH * 4, + Height = BUTTON_WH, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'Button', + Name = 'cmd_insert', + Label = _('~Insert'), + Width = 70, + Height = BUTTON_WH, + ImageURL = 'insert.svg', + ImagePosition = 1, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'Button', + Name = 'cmd_close', + Label = _('~Close'), + Width = 70, + Height = BUTTON_WH, + ImageURL = 'close.svg', + ImagePosition = 1, + ) + dialog.add_control(attr) + + dialog.cmd_switch.move(dialog.txt_password, x=3, y=-2) + dialog.cmd_new.move(dialog.cmd_switch, x=3, y=0) + dialog.lbl_title_length.move(dialog.txt_password) + dialog.txt_length.move(dialog.lbl_title_length, x=3, y=0) + dialog.chk_letters.move(dialog.lbl_title_length) + dialog.chk_letters2.move(dialog.chk_letters, x=3, y=0) + dialog.chk_digits.move(dialog.chk_letters2, x=3, y=0) + dialog.chk_punctuation.move(dialog.chk_digits, x=3, y=0) + + dialog.center(dialog.cmd_insert, y=-5) + dialog.center(dialog.cmd_close, y=-5) + dialog.center((dialog.cmd_close, dialog.cmd_insert)) + + return dialog + + + diff --git a/source/registration/license_en.txt b/source/registration/license_en.txt new file mode 100644 index 0000000..5f68220 --- /dev/null +++ b/source/registration/license_en.txt @@ -0,0 +1,14 @@ +This file is part of ZAZPass. + + ZAZPass 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. + + ZAZPass 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 ZAZPass. If not, see . diff --git a/source/registration/license_es.txt b/source/registration/license_es.txt new file mode 100644 index 0000000..5f68220 --- /dev/null +++ b/source/registration/license_es.txt @@ -0,0 +1,14 @@ +This file is part of ZAZPass. + + ZAZPass 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. + + ZAZPass 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 ZAZPass. If not, see . diff --git a/zaz.py b/zaz.py new file mode 100755 index 0000000..4846605 --- /dev/null +++ b/zaz.py @@ -0,0 +1,823 @@ +#!/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) + From 3ba66f772801b14354c4322bea1e52f02d05e944 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 7 Oct 2021 14:29:30 -0500 Subject: [PATCH 2/7] Add demo in gif --- README.md | 3 +++ files/ZAZPass_v0.1.0.oxt | Bin 62375 -> 62375 bytes images/zazpass.gif | Bin 0 -> 124236 bytes 3 files changed, 3 insertions(+) create mode 100644 images/zazpass.gif diff --git a/README.md b/README.md index dad1801..ef65091 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ https://git.cuates.net/elmau/zaz ### Software libre, no gratis +![ZAZ-Pass](images/zazpass.gif) + + This extension have a cost of maintenance of 1 euro every year. BCH: `qztd3l00xle5tffdqvh2snvadkuau2ml0uqm4n875d` diff --git a/files/ZAZPass_v0.1.0.oxt b/files/ZAZPass_v0.1.0.oxt index a907536255e3820e6a2acb8831ca0f78219ce911..6d886c4d1e9e7f98f6095729de0b18565b99bb0e 100644 GIT binary patch delta 159 zcmZ4foO$_kX5IjAW)=|!1_llWn_~BkynC6Lf%N9HOc5+#h6ej&YcOM<)Abc##*Eh{ zj9|w3H_O0`$rbP1KmwBwzViUnJnv1xw9ET-V1-}b?}sSd{_zS}qVJOfn7;VQi36xH WILwaMY;wY9vB`#?)xi?cpM3zgDm=dc delta 159 zcmZ4foO$_kX5IjAW)=|!1_lm>Cp+9W^6q6~2GX0)GDWa}85-=9t-*|aPS;m}88cp+ zFoGH9-z)<&CRe<30|`t%_|5}N^Sn0&(=PAVffaszzaOG-`^PI_iM~$`VEW=GCk~*- X;4nL0v&jjc#U>kmRtHN&fA#?YD2+>h diff --git a/images/zazpass.gif b/images/zazpass.gif new file mode 100644 index 0000000000000000000000000000000000000000..5ace145ca2fe2ac020f3c1de2c83602a4dfb4456 GIT binary patch literal 124236 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

vk08m5Hbgk==~G!IKzLM{YQN5Y@d7J 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 Ryw3-g@2-+tQ5jV9KLA*?yGH;3 literal 0 HcmV?d00001 From 7ddce709df7309304bb8a5300bab29c0aa90e5a5 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 7 Oct 2021 14:42:43 -0500 Subject: [PATCH 3/7] Add local spanish --- CHANGELOG | 4 +++ files/ZAZPass_v0.1.0.oxt | Bin 62375 -> 64993 bytes source/locales/base.pot | 33 +++++++++++++++++++++++++ source/locales/en/LC_MESSAGES/base.mo | Bin 0 -> 461 bytes source/locales/en/LC_MESSAGES/base.po | 34 ++++++++++++++++++++++++++ source/locales/es/LC_MESSAGES/base.mo | Bin 0 -> 526 bytes source/locales/es/LC_MESSAGES/base.po | 34 ++++++++++++++++++++++++++ 7 files changed, 105 insertions(+) create mode 100644 CHANGELOG create mode 100644 source/locales/base.pot create mode 100644 source/locales/en/LC_MESSAGES/base.mo create mode 100644 source/locales/en/LC_MESSAGES/base.po create mode 100644 source/locales/es/LC_MESSAGES/base.mo create mode 100644 source/locales/es/LC_MESSAGES/base.po diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..464d7db --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,4 @@ +v 0.1.0 [07-oct-2021] + - Initial version + - Generate password + - Add spanish diff --git a/files/ZAZPass_v0.1.0.oxt b/files/ZAZPass_v0.1.0.oxt index 6d886c4d1e9e7f98f6095729de0b18565b99bb0e..615ce34aa60b6ec1a8b4ccf0d4b0421ac94bf198 100644 GIT binary patch delta 2839 zcmai02{e>@8=q$4B0|PKlzrdIl8mfl&0I3dzRnCX_GHbAq_Gvogh;X^WOvI|hzQr# zSGK`5>1)9l#$IIZH*>#pk8Zc~eeZkT^ZWnb^M8KlInVF>{?AkS2{OM2;k31e9EO5G zAU2RtL?dSh7%UXgJOp++%tX1La`l+lZtPqEJiKgfzr8%vRB{LengWABJd7Y*u#XoG z6Rzg%6^>C23BF9s_6UBX4(%oHDb!61^0eAaI}{EXo=Tl@djB)Am6#u;k!hK9Mz2Z# z1e`_+Z6j*9znZIiPuAUmk=b*jtt1{`Pz_wZdK^V-$w+fP{N>5Q$3d`Xc=v8|D}bXv zU29;9YhHO2-sp`ONW^`KaDOgi!){$?0TXOdCbyqed^Wl;F%ih}?=^Kt-xU968oM>K zdOeCQgqM$<2-y3`W0fsQ+_{0u9BBwEQEgIhqV)?{VJm0I!aodOn zDl?7`pi1eP8v5V#22biDNi~J=GQeD$cS0g4x#_3*mac|rS$1>}@X`Pw9^ybwb(U1^ zRY^%kQ>a;6CoSuaQc;Dw#nw(%$3?Hd+4Ic4fK=?PUyYNO16rLTd1_-IduBpPzt5<6 z+)uAu&Z4Gk;;Y;1oE>~xKhL&FHQ%-p@n|q!jpcOnu-3^(KAo#~J=*W533PdG-6ah= zO+GcNLAGMIoB1YUI7eT+y_`o7#(kZM46N?t-ZCh}L^WBKSG!%C7hXcNo~{BkFcqPr zyrZLZeA%_f>#5s12YSyk1eJXKwbYOy=rH3G|3*+uklHyD4;zHNy^%S>o~bW@Q4I`^ z$m&GCQ0I9mCJ*Fh7j)rHyw7Ptp;VpZRjS9**qnP3lFx;g8&!$n1bI`eGT@+F-M_sI zpeegrn#v(}2hLuNB;Va{cZik~;zx_>noVSho;OXwN#5!|vz~-IQQzNT((h6QoW4$9 zgzgF!BNrRf9_600IF82273BcYdJe4p&*ZrLtc`2MJ#kKI-WN-3N~J4v3fHw6L;6h(clqTiNvw2W#I=>ZK&i%7%$_6aiO zG)uHz+vPv@UmX8?`LV|r>?#yFATN zAGl*7G_+kce3;WUdXh$pG-2zG-ATL}>LUP0_2gW)<6P0E3nkMgJf7mV=G>Hx0fV%; z-;3f=#dAiL(ct}fF~72Uau%4=6&NA9CIOZBC;LRPDLDe=n0-wn{v$hA^imPh0hgm>S`N7wP@j;xfV%>( zU6ZgXx+o)Q6C}5)B;}j|wBbeW8)>dCEZ|L)?$wSRN#JK#Z%K(w(mGO+1hKg<{7JX( zmbr7!quR~Pd6X=nw(4SlCO1Vir*5+9iPPSmSf%GkETl5O`T@!37j@d2EaxJJSoMIb zkx$qe$Bg(x2Zm&7Mf4=QJY#4j{kfq#m{G-OxMv>}h-YFsZ=H8q!ICgGS$MRmYA z?}5YUBKcZH2N(D+ZUt@uSN#7izWeij3f(bKy+Ri?i}juBC?HY0Vt%E0eXMVnckJyd z;|}Gy5#RX+@-vbDgFp6d_tWH^4$-cYZ4s;2xPxHIz-oZExG6 zSn6NC-&E~#bEls#Kmq}~z(JUO-T=T{pU~gg8a#4ewt(X5{AzUj!+W6D z=dC(6A6(k=1h3LV#$OftJ+L3S*>S4^XBUFKhHc~ zGx_06#OR`bKIMsA3;&#uY^@K0p^*O$Tt8~S2hlT&2hsm*$IPq37Wj_r`*w0bc9HVG z$wGp^P3%Xv)&bXP>R(*wa5cv8_uZiIXJ+3d_9I&zkj=0BMfPK^kYECJO%Y6@5ro&x z!TIZig+(y}u+GU8By$}GzPL%KS(jq&UNLqzw+JiiGHgN&CtoHwn7jV~lxxA` delta 258 zcmaF(n|b+jW}X0VW)=|!1_llWo8pZ;`g-djLG)gysHK}9f2d;y zagx87GJ@z?UzdaE$~WUre6! zM`CjJAAP1XFM&)=1)E~`;7I@I#Vm{r49!do45C1%z`&Bm?$?u_{!s=Q$n{r->Cl_W z8h=$mJbxfB``zT+zp7vZj00=$fczi?#NsgBKzd2zmUolqe3ujp@MdKLi3, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2021-10-07 14:37-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" + + +#: source/pythonpath/zpass.py:210 +msgid "Generate Password" +msgstr "" + +#: source/pythonpath/zpass.py:251 +msgid "Length: " +msgstr "" + +#: source/pythonpath/zpass.py:313 +msgid "~Insert" +msgstr "" + +#: source/pythonpath/zpass.py:324 +msgid "~Close" +msgstr "" + diff --git a/source/locales/en/LC_MESSAGES/base.mo b/source/locales/en/LC_MESSAGES/base.mo new file mode 100644 index 0000000000000000000000000000000000000000..871e2217cde886678874c10dd14fd2337da9250a GIT binary patch literal 461 zcmZ{g%}xR_5XV==kL=O2hw-K{#a)m{ihB{kkU-oh7!M|KW z<74VFb|Nfts05Xeg#il=Aw1yKItyw+;CF>grXoSsM=_yuy-u&SXcLwLzp11}X<5I> zsf)!YyPszwl?@nfYN1NwyTZ_H*E18=X?~ndwa8do>51VW^Lc8bN&|kvXh*_! T1mSMu?{yqd-V84MfB*0sZ0vY= literal 0 HcmV?d00001 diff --git a/source/locales/en/LC_MESSAGES/base.po b/source/locales/en/LC_MESSAGES/base.po new file mode 100644 index 0000000..9787530 --- /dev/null +++ b/source/locales/en/LC_MESSAGES/base.po @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2021-10-07 14:37-0500\n" +"PO-Revision-Date: 2021-10-07 14:38-0500\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" +"X-Generator: Poedit 3.0\n" +"Last-Translator: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: en_US\n" + +#: source/pythonpath/zpass.py:210 +msgid "Generate Password" +msgstr "" + +#: source/pythonpath/zpass.py:251 +msgid "Length: " +msgstr "Length: " + +#: source/pythonpath/zpass.py:313 +msgid "~Insert" +msgstr "~Insert" + +#: source/pythonpath/zpass.py:324 +msgid "~Close" +msgstr "~Close" diff --git a/source/locales/es/LC_MESSAGES/base.mo b/source/locales/es/LC_MESSAGES/base.mo new file mode 100644 index 0000000000000000000000000000000000000000..aa18a2571bc73b15fde14b9467b233e088f8d3cc GIT binary patch literal 526 zcmYL_y>1gh5XU!QzOD;ONilSwXq`I~i)gKD@CUMFixYApchTaTTn=4!PqS+hH?GM8 zq@tyxLU{zBRk2VYC^4&r~y(rKJvu3@3q}h(9a@}A_L1r;8J-p%ZBkg#0 z@T{~C-=eIVldj~z(uGrC>Q(8|RtNsc^p$~^DI~8u|F=_s?sPnj)>kD6OObZ?>!)^N Qq^Yp0yCRXM&0$&n1)Lj-cmMzZ literal 0 HcmV?d00001 diff --git a/source/locales/es/LC_MESSAGES/base.po b/source/locales/es/LC_MESSAGES/base.po new file mode 100644 index 0000000..391db05 --- /dev/null +++ b/source/locales/es/LC_MESSAGES/base.po @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2021-10-07 14:37-0500\n" +"PO-Revision-Date: 2021-10-07 14:40-0500\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" +"X-Generator: Poedit 3.0\n" +"Last-Translator: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: es_MX\n" + +#: source/pythonpath/zpass.py:210 +msgid "Generate Password" +msgstr "Generar Contraseñas" + +#: source/pythonpath/zpass.py:251 +msgid "Length: " +msgstr "Largo: " + +#: source/pythonpath/zpass.py:313 +msgid "~Insert" +msgstr "~Insertar" + +#: source/pythonpath/zpass.py:324 +msgid "~Close" +msgstr "~Cerrar" From fa751a207ecef660821b2b8999802748acc6be1a Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 7 Oct 2021 22:27:55 -0500 Subject: [PATCH 4/7] Add password quality --- conf.py | 8 ++--- files/ZAZPass_v0.1.0.oxt | Bin 64993 -> 65175 bytes source/description.xml | 4 +-- source/description/desc_en.txt | 2 +- source/description/desc_es.txt | 2 +- source/pythonpath/zpass.py | 53 +++++++++++++++++++++++++++++---- 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/conf.py b/conf.py index 1e03e95..34f1758 100644 --- a/conf.py +++ b/conf.py @@ -79,13 +79,13 @@ LICENSE_ES = LICENSE_EN INFO = { 'en': { - 'display_name': 'My first extension', - 'description': 'My great extension', + 'display_name': 'ZAZ Pass', + 'description': 'Generate passwords', 'license': LICENSE_EN, }, 'es': { - 'display_name': 'Mi primer extensión', - 'description': 'Mi gran extensión', + 'display_name': 'ZAZ Pass', + 'description': 'Generar contraseñas', 'license': LICENSE_ES, }, } diff --git a/files/ZAZPass_v0.1.0.oxt b/files/ZAZPass_v0.1.0.oxt index 615ce34aa60b6ec1a8b4ccf0d4b0421ac94bf198..120611bf6c33d3168caec43424307be2dfef42ff 100644 GIT binary patch delta 3012 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 XiNb;yH0xmoVyXy%Acs}^7PI>gy3I3` diff --git a/source/description.xml b/source/description.xml index a79f4d3..bd624cf 100644 --- a/source/description.xml +++ b/source/description.xml @@ -3,8 +3,8 @@ - My first extension - Mi primer extensión + ZAZ Pass + ZAZ Pass diff --git a/source/description/desc_en.txt b/source/description/desc_en.txt index b667a4b..8df5af8 100644 --- a/source/description/desc_en.txt +++ b/source/description/desc_en.txt @@ -1 +1 @@ -My great extension \ No newline at end of file +Generate passwords \ No newline at end of file diff --git a/source/description/desc_es.txt b/source/description/desc_es.txt index d8d8fdc..362f8b2 100644 --- a/source/description/desc_es.txt +++ b/source/description/desc_es.txt @@ -1 +1 @@ -Mi gran extensión \ No newline at end of file +Generar contraseñas \ No newline at end of file diff --git a/source/pythonpath/zpass.py b/source/pythonpath/zpass.py index 49fc150..a0e0659 100644 --- a/source/pythonpath/zpass.py +++ b/source/pythonpath/zpass.py @@ -100,6 +100,16 @@ def _password_copy(): return +def _get_quality(strength): + if strength <= 0.33: + quality = _('Week') + elif strength <= 0.66: + quality = _('Medium') + else: + quality = _('Excellent') + return quality + + def _password_generate(id_extension): config = app.get_config('setting', prefix=PREFIX) dialog = _create_dialog(id_extension) @@ -121,7 +131,9 @@ def _password_generate(id_extension): dialog.chk_punctuation.value = punctuation dialog.txt_length.value = length - dialog.txt_password.value = _generate(dialog) + stats = PasswordStats(_generate(dialog)) + dialog.txt_password.value = stats.password + dialog.lbl_quality.value = _get_quality(stats.strength()) dialog.open() return @@ -171,6 +183,7 @@ class Controllers(object): def _new_password(self, save=True): stats = PasswordStats(_generate(self.d)) self.d.txt_password.value = stats.password + self.d.lbl_quality.value = _get_quality(stats.strength()) if save: self._save_config() return @@ -203,13 +216,14 @@ class Controllers(object): @app.catch_exception def _create_dialog(id_extension): BUTTON_WH = 16 + CHK_HEIGHT = 10 CHK_WIDTH = 25 attr = dict( Name = 'Dialog', Title = _('Generate Password'), Width = 200, - Height = 100, + Height = 120, ) dialog = app.create_dialog(attr) dialog.id = id_extension @@ -266,7 +280,7 @@ def _create_dialog(id_extension): Spin = True, Value = 25, ValueStep = 1, - ValueMin = 10, + ValueMin = 5, ValueMax = 100, ) dialog.add_control(attr) @@ -276,7 +290,7 @@ def _create_dialog(id_extension): Name = 'chk_letters', Label = 'A-Z', Width = CHK_WIDTH, - Height = BUTTON_WH, + Height = CHK_HEIGHT, ) dialog.add_control(attr) @@ -285,7 +299,7 @@ def _create_dialog(id_extension): Name = 'chk_letters2', Label = 'a-z', Width = CHK_WIDTH, - Height = BUTTON_WH, + Height = CHK_HEIGHT, ) dialog.add_control(attr) @@ -294,7 +308,7 @@ def _create_dialog(id_extension): Name = 'chk_digits', Label = '0-9', Width = CHK_WIDTH, - Height = BUTTON_WH, + Height = CHK_HEIGHT, ) dialog.add_control(attr) @@ -303,7 +317,31 @@ def _create_dialog(id_extension): Name = 'chk_punctuation', Label = string.punctuation, Width = CHK_WIDTH * 4, + Height = CHK_HEIGHT, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'Label', + Name = 'lbl_title_quality', + Label = _('Password Quality:'), + Width = 75, Height = BUTTON_WH, + Border = app.Border.BORDER, + Align = app.RIGHT, + VerticalAlign = app.MIDDLE, + ) + dialog.add_control(attr) + + attr = dict( + Type = 'Label', + Name = 'lbl_quality', + Label = _('Poor'), + Width = 35, + Height = BUTTON_WH, + Border = app.Border.BORDER, + Align = app.CENTER, + VerticalAlign = app.MIDDLE, ) dialog.add_control(attr) @@ -338,6 +376,9 @@ def _create_dialog(id_extension): dialog.chk_digits.move(dialog.chk_letters2, x=3, y=0) dialog.chk_punctuation.move(dialog.chk_digits, x=3, y=0) + dialog.lbl_title_quality.move(dialog.chk_letters) + dialog.lbl_quality.move(dialog.lbl_title_quality, x=3, y=0) + dialog.center(dialog.cmd_insert, y=-5) dialog.center(dialog.cmd_close, y=-5) dialog.center((dialog.cmd_close, dialog.cmd_insert)) From 0035f2b8048794271963fcbab1943e00cf6133b5 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 8 Oct 2021 11:39:05 -0500 Subject: [PATCH 5/7] Update locales --- conf.py | 2 +- easymacro.py | 3 +- {files => extension}/ZAZPass_v0.1.0.oxt | Bin 65175 -> 66568 bytes source/locales/base.pot | 30 +++++++++++++++---- source/locales/en/LC_MESSAGES/base.mo | Bin 461 -> 357 bytes source/locales/en/LC_MESSAGES/base.po | 38 ++++++++++++++++++------ source/locales/es/LC_MESSAGES/base.mo | Bin 526 -> 737 bytes source/locales/es/LC_MESSAGES/base.po | 36 +++++++++++++++++----- source/pythonpath/easymacro.py | 3 +- 9 files changed, 85 insertions(+), 27 deletions(-) rename {files => extension}/ZAZPass_v0.1.0.oxt (70%) diff --git a/conf.py b/conf.py index 34f1758..903dc59 100644 --- a/conf.py +++ b/conf.py @@ -167,7 +167,7 @@ DIRS = { 'description': 'description', 'images': 'images', 'registration': 'registration', - 'files': 'files', + 'files': 'extension', 'office': 'Office', 'locales': PATH_LOCALES, 'pythonpath': True, diff --git a/easymacro.py b/easymacro.py index bd060d4..7e8fb94 100644 --- a/easymacro.py +++ b/easymacro.py @@ -6644,7 +6644,7 @@ class ClipBoard(object): def __init__(self, text): df = DataFlavor() df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT - df.HumanPresentableName = "encoded text utf-16" + df.HumanPresentableName = 'encoded text utf-16' self.flavors = (df,) self._data = text @@ -6654,7 +6654,6 @@ class ClipBoard(object): def getTransferDataFlavors(self): return self.flavors - @classmethod def set(cls, value): ts = cls.TextTransferable(value) diff --git a/files/ZAZPass_v0.1.0.oxt b/extension/ZAZPass_v0.1.0.oxt similarity index 70% rename from files/ZAZPass_v0.1.0.oxt rename to extension/ZAZPass_v0.1.0.oxt index 120611bf6c33d3168caec43424307be2dfef42ff..be4b2855d8a6228e1c07fbffaf57bdee25bbabc4 100644 GIT binary patch delta 16712 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 delta 15408 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\n" "Language-Team: LANGUAGE \n" @@ -15,19 +15,39 @@ msgstr "" "Generated-By: pygettext.py 1.5\n" -#: source/pythonpath/zpass.py:210 +#: source/pythonpath/zpass.py:105 +msgid "Week" +msgstr "" + +#: source/pythonpath/zpass.py:107 +msgid "Medium" +msgstr "" + +#: source/pythonpath/zpass.py:109 +msgid "Excellent" +msgstr "" + +#: source/pythonpath/zpass.py:224 msgid "Generate Password" msgstr "" -#: source/pythonpath/zpass.py:251 +#: source/pythonpath/zpass.py:265 msgid "Length: " msgstr "" -#: source/pythonpath/zpass.py:313 +#: source/pythonpath/zpass.py:327 +msgid "Password Quality:" +msgstr "" + +#: source/pythonpath/zpass.py:339 +msgid "Poor" +msgstr "" + +#: source/pythonpath/zpass.py:351 msgid "~Insert" msgstr "" -#: source/pythonpath/zpass.py:324 +#: source/pythonpath/zpass.py:362 msgid "~Close" msgstr "" diff --git a/source/locales/en/LC_MESSAGES/base.mo b/source/locales/en/LC_MESSAGES/base.mo index 871e2217cde886678874c10dd14fd2337da9250a..6c06b126df4b941f6d7cd5dcc96bef07a342d5db 100644 GIT binary patch delta 75 zcmX@h{FKS!o)F7a1|VPrVi_P-0b*t#)&XJ=umEChprj>`2C0F8i5-UQ77B(2RtCls H4}1jxLc0lc delta 181 zcmaFLbe6gPo)F7a1|VPoVi_Q|0b*7ljsap2C;(y(AT9)AHXyD7Vs;>I1Y&JQ28K=` z4U*pqWP{}Q0cnu@Q6LROV89F{nSdB%2ZK*)UV2G}l>$SZb54FSXDUOTXI^n?QOQIL XLw0ioLlY}wi-`qaB~X>YRWSen&psYJ diff --git a/source/locales/en/LC_MESSAGES/base.po b/source/locales/en/LC_MESSAGES/base.po index 9787530..71424ff 100644 --- a/source/locales/en/LC_MESSAGES/base.po +++ b/source/locales/en/LC_MESSAGES/base.po @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2021-10-07 14:37-0500\n" -"PO-Revision-Date: 2021-10-07 14:38-0500\n" +"POT-Creation-Date: 2021-10-08 10:00-0500\n" +"PO-Revision-Date: 2021-10-08 10:03-0500\n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -17,18 +17,38 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Language: en_US\n" -#: source/pythonpath/zpass.py:210 +#: source/pythonpath/zpass.py:105 +msgid "Week" +msgstr "" + +#: source/pythonpath/zpass.py:107 +msgid "Medium" +msgstr "" + +#: source/pythonpath/zpass.py:109 +msgid "Excellent" +msgstr "" + +#: source/pythonpath/zpass.py:224 msgid "Generate Password" msgstr "" -#: source/pythonpath/zpass.py:251 +#: source/pythonpath/zpass.py:265 msgid "Length: " -msgstr "Length: " +msgstr "" -#: source/pythonpath/zpass.py:313 +#: source/pythonpath/zpass.py:327 +msgid "Password Quality:" +msgstr "" + +#: source/pythonpath/zpass.py:339 +msgid "Poor" +msgstr "" + +#: source/pythonpath/zpass.py:351 msgid "~Insert" -msgstr "~Insert" +msgstr "" -#: source/pythonpath/zpass.py:324 +#: source/pythonpath/zpass.py:362 msgid "~Close" -msgstr "~Close" +msgstr "" diff --git a/source/locales/es/LC_MESSAGES/base.mo b/source/locales/es/LC_MESSAGES/base.mo index aa18a2571bc73b15fde14b9467b233e088f8d3cc..fb7335ff1b66f36bcac692e03eae72e4b8f22201 100644 GIT binary patch delta 422 zcmYL@F-yci5JpGMc`96?f`}+AwicdZp;w(mQ9ybgNEcL o(}7{~5gF-7MXZcg;s0~l5?H3K{zDKODtxvGq+&10*gBm40J51-mJ7tASXX zk%3_|kQM>r`#?5G{0o%+1Ee{Dd=@4K1|Sm*fJ_EvAO_kAk^)Ks@x)&;lPwwT+07LU zO{`1|Cf755)pt+LOD#$)QgF`CD=A7WPCdLav6#Upu_!&?N`axyIkgBV$x!E+SDac@ H0%QRIK{6v8 diff --git a/source/locales/es/LC_MESSAGES/base.po b/source/locales/es/LC_MESSAGES/base.po index 391db05..cb7098a 100644 --- a/source/locales/es/LC_MESSAGES/base.po +++ b/source/locales/es/LC_MESSAGES/base.po @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2021-10-07 14:37-0500\n" -"PO-Revision-Date: 2021-10-07 14:40-0500\n" +"POT-Creation-Date: 2021-10-08 10:00-0500\n" +"PO-Revision-Date: 2021-10-08 10:02-0500\n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -17,18 +17,38 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Language: es_MX\n" -#: source/pythonpath/zpass.py:210 +#: source/pythonpath/zpass.py:105 +msgid "Week" +msgstr "Débil" + +#: source/pythonpath/zpass.py:107 +msgid "Medium" +msgstr "Aceptable" + +#: source/pythonpath/zpass.py:109 +msgid "Excellent" +msgstr "Excelente" + +#: source/pythonpath/zpass.py:224 msgid "Generate Password" -msgstr "Generar Contraseñas" +msgstr "Generar Contraseña" -#: source/pythonpath/zpass.py:251 +#: source/pythonpath/zpass.py:265 msgid "Length: " -msgstr "Largo: " +msgstr "Longitud: " -#: source/pythonpath/zpass.py:313 +#: source/pythonpath/zpass.py:327 +msgid "Password Quality:" +msgstr "Calidad de la contraseña:" + +#: source/pythonpath/zpass.py:339 +msgid "Poor" +msgstr "Pobre" + +#: source/pythonpath/zpass.py:351 msgid "~Insert" msgstr "~Insertar" -#: source/pythonpath/zpass.py:324 +#: source/pythonpath/zpass.py:362 msgid "~Close" msgstr "~Cerrar" diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py index bd060d4..7e8fb94 100644 --- a/source/pythonpath/easymacro.py +++ b/source/pythonpath/easymacro.py @@ -6644,7 +6644,7 @@ class ClipBoard(object): def __init__(self, text): df = DataFlavor() df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT - df.HumanPresentableName = "encoded text utf-16" + df.HumanPresentableName = 'encoded text utf-16' self.flavors = (df,) self._data = text @@ -6654,7 +6654,6 @@ class ClipBoard(object): def getTransferDataFlavors(self): return self.flavors - @classmethod def set(cls, value): ts = cls.TextTransferable(value) From 1e833470a8b3862621e725641ed1e3f9088845f3 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 8 Oct 2021 12:01:06 -0500 Subject: [PATCH 6/7] Update README --- CHANGELOG | 4 ++-- README.md | 23 ++++++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 464d7db..870fed2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -v 0.1.0 [07-oct-2021] +v 0.1.0 [08-oct-2021] - Initial version - Generate password - - Add spanish + - Translate to spanish diff --git a/README.md b/README.md index ef65091..6d8d079 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,33 @@ ZAZ Pass -Extension for generate passwords LibreOffice. +Extension for generate passwords in LibreOffice. Thanks! https://git.cuates.net/elmau/zaz +https://github.com/kolypto/py-password-strength/blob/master/password_strength/stats.py ### Software libre, no gratis - -![ZAZ-Pass](images/zazpass.gif) - - This extension have a cost of maintenance of 1 euro every year. -BCH: `qztd3l00xle5tffdqvh2snvadkuau2ml0uqm4n875d` - -BTC: `3FhiXcXmAesmQzrNEngjHFnvaJRhU1AGWV` +* BCH: `qztd3l00xle5tffdqvh2snvadkuau2ml0uqm4n875d` +* ETH: `0x61a4f614a30ff686445751ed8328b82b77ecfc69` +* XRP: `rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK` Tag `6643162` +* BTC: `3FhiXcXmAesmQzrNEngjHFnvaJRhU1AGWV` You have others cryptos, welcome too! +Or [invite me a coffe](https://ko-fi.com/elmau) + +You don't have money, not is problem, send me a postal card, I love know other places and people, but, remember; **Software libre, no gratis** Thanks for translations: -* Help Us +* English +* Spanish + + +![ZAZ-Pass](images/zazpass.gif) From d7a7b05cfea33b77784c41b226b872eccb23b338 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 8 Oct 2021 12:03:19 -0500 Subject: [PATCH 7/7] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d8d079..ec110ea 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Extension for generate passwords in LibreOffice. Thanks! -https://git.cuates.net/elmau/zaz -https://github.com/kolypto/py-password-strength/blob/master/password_strength/stats.py +* https://git.cuates.net/elmau/zaz +* https://github.com/kolypto/py-password-strength/blob/master/password_strength/stats.py ### Software libre, no gratis