From 530b26dd8271deffade179bf104caee321f3af22 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 12 Apr 2021 12:15:05 -0500 Subject: [PATCH] Initial version --- .gitignore | 3 + VERSION | 1 + easymacro.py | 6817 ++++++++++++++++++++++++++++ files/ZazDoc_v0.1.0.oxt | Bin 0 -> 62486 bytes images/logo.png | Bin 0 -> 20104 bytes source/Addons.xcu | 36 + source/META-INF/manifest.xml | 6 + source/Office/Accelerators.xcu | 4 + source/ZazDoc.py | 21 + source/description.xml | 26 + source/description/desc_en.txt | 1 + source/description/desc_es.txt | 1 + source/images/zazdoc.png | Bin 0 -> 20104 bytes source/pythonpath/easymacro.py | 6817 ++++++++++++++++++++++++++++ source/registration/license_en.txt | 14 + source/registration/license_es.txt | 14 + zaz.py | 822 ++++ 17 files changed, 14583 insertions(+) create mode 100644 VERSION create mode 100644 easymacro.py create mode 100644 files/ZazDoc_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/ZazDoc.py create mode 100644 source/description.xml create mode 100644 source/description/desc_en.txt create mode 100644 source/description/desc_es.txt create mode 100644 source/images/zazdoc.png create mode 100644 source/pythonpath/easymacro.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 index f8b73e7..87f0ff9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ # ---> Python + +conf.py + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] 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/easymacro.py b/easymacro.py new file mode 100644 index 0000000..74a3aa1 --- /dev/null +++ b/easymacro.py @@ -0,0 +1,6817 @@ +#!/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 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 string import Template +from typing import Any, Union +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError + +import imaplib +import smtplib +from smtplib import SMTPException, SMTPAuthenticationError +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.utils import formatdate +from email import encoders +import mailbox + +import uno +import unohelper +from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS +from com.sun.star.awt.MessageBoxResults import YES +from com.sun.star.awt import Rectangle, Size, Point +from com.sun.star.awt.PosSize import POSSIZE, SIZE +from com.sun.star.awt import Key, KeyModifier, KeyEvent +from com.sun.star.container import NoSuchElementException +from com.sun.star.datatransfer import XTransferable, DataFlavor + +from com.sun.star.beans import PropertyValue, NamedValue +from com.sun.star.sheet import TableFilterField +from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA +from com.sun.star.util import Time, Date, DateTime + +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER + +from com.sun.star.lang import Locale +from com.sun.star.lang import XEventListener +from com.sun.star.awt import XActionListener +from com.sun.star.awt import XMenuListener +from com.sun.star.awt import XMouseListener +from com.sun.star.awt import XMouseMotionListener +from com.sun.star.awt import XFocusListener +from com.sun.star.awt import XKeyListener +from com.sun.star.awt import XItemListener +from com.sun.star.awt import XTabListener +from com.sun.star.awt import XWindowListener +from com.sun.star.awt import XTopWindowListener +from com.sun.star.awt.grid import XGridDataListener +from com.sun.star.awt.grid import XGridSelectionListener +from com.sun.star.script import ScriptEventDescriptor + +# ~ 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_RANGES) + +OBJ_SHAPES = 'com.sun.star.drawing.SvxShapeCollection' +OBJ_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' +OBJ_GRAPHIC = 'SwXTextGraphicObject' + +OBJ_TEXTS = 'SwXTextRanges' +OBJ_TEXT = 'SwXTextRange' + + +# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL +class FilterOperator(IntEnum): + EMPTY = 0 + NO_EMPTY = 1 + EQUAL = 2 + NOT_EQUAL = 3 + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 +class Border(IntEnum): + NO_BORDER = 0 + BORDER = 1 + SIMPLE = 2 + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a +class ValidationType(): + from com.sun.star.sheet.ValidationType \ + import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM +VT = ValidationType + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7 +class ValidationAlertStyle(): + from com.sun.star.sheet.ValidationAlertStyle \ + import STOP, WARNING, INFO, MACRO +VAS = ValidationAlertStyle + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html +class ConditionOperator(): + from com.sun.star.sheet.ConditionOperator2 \ + import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \ + LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE +CO = ConditionOperator + + +class DataPilotFieldOrientation(): + from com.sun.star.sheet.DataPilotFieldOrientation \ + import HIDDEN, COLUMN, ROW, PAGE, DATA +DPFO = DataPilotFieldOrientation + + +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] +COUNTRY = LANGUAGE.split('-')[1] +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) and isinstance(data[0], tuple): + return _array_to_dict(data) + + if isinstance(data, tuple) 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() + 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'] + module = '.' + name = args['name'] + language = args.get('language', 'Python') + location = args.get('location', 'user') + + 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=True): + if not split: + return subprocess.check_output(command, shell=True).decode() + + 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 + 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='', default={}, prefix='conf'): + name_file = FILE_NAME_CONFIG.format(prefix) + values = None + path = _P.join(_P.config('UserConfig'), name_file) + if not _P.exists(path): + return default + + values = _P.from_json(path) + if key: + values = values.get(key, default) + + return values + + +def set_config(key, value, prefix='conf'): + name_file = FILE_NAME_CONFIG.format(prefix) + path = _P.join(_P.config('UserConfig'), name_file) + values = get_config(default={}, prefix=prefix) + values[key] = value + result = _P.to_json(path, values) + return result + + +def start(): + global _start + _start = now() + info(_start) + return + + +def end(get_seconds: bool=False): + global _start + e = now() + td = e - _start + result = str(td) + if get_seconds: + result = td.total_seconds() + return result + + +def get_epoch(): + n = now() + return int(time.mktime(n.timetuple())) + + +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +def get_size_screen(): + if IS_WIN: + user32 = ctypes.windll.user32 + res = f'{user32.GetSystemMetrics(0)}x{user32.GetSystemMetrics(1)}' + else: + args = 'xrandr | grep "*" | cut -d " " -f4' + res = run(args, split=False) + return res.strip() + + +def url_open(url, data=None, headers={}, verify=True, get_json=False): + 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) + else: + context = ssl._create_unverified_context() + response = urlopen(req, context=context) + except HTTPError as e: + error(e) + err = str(e) + except URLError as e: + error(e.reason) + err = str(e.reason) + else: + headers = dict(response.info()) + result = response.read() + 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')) + + for path in message.get('files', ()): + fn = _P(path).file_name + 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): + FILTERS = { + 'doc': 'MS Word 97', + 'docx': 'MS Word 2007 XML', + } + + 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 status_bar(self): + return self._cc.getStatusIndicator() + + @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 _get_class_uno(sel) + return sel + + @property + def table_auto_formats(self): + taf = create_instance('com.sun.star.sheet.TableAutoFormats') + return taf.ElementNames + + 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='', args: dict={}): + """ + https://wiki.documentfoundation.org/Macros/Python_Guide/PDF_export_filter_data + """ + path_pdf = path + filter_name = '{}_pdf_Export'.format(self.type) + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + opt = dict_to_property(args) + try: + self.obj.storeToURL(_P.to_url(path), opt) + except Exception as e: + error(e) + path_pdf = '' + + return _P.exists(path_pdf) + + def export(self, path: str, ext: str='', args: dict={}): + if not ext: + ext = _P(path).ext + filter_name = self.FILTERS[ext] + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + opt = dict_to_property(args) + try: + self.obj.storeToURL(_P.to_url(path), opt) + except Exception as e: + error(e) + path = '' + return _P.exists(path) + + def save(self, path: str='', args: dict={}) -> bool: + result = True + opt = dict_to_property(args) + if path: + try: + self.obj.storeAsURL(_P.to_url(path), opt) + except Exception as e: + error(e) + result = False + else: + self.obj.store() + return result + + 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 == OBJ_SHAPES: + if len(sel) == 1: + sel = sel[0] + sel = LODrawPage(sel.Parent)[sel.Name] + 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 + + def activate(self, sheet): + obj = sheet + if isinstance(sheet, LOCalcSheet): + obj = sheet.obj + elif isinstance(sheet, str): + obj = self._sheets[sheet] + self._cc.setActiveSheet(obj) + return + + def new_sheet(self): + s = self.create_instance('com.sun.star.sheet.Spreadsheet') + return s + + def insert(self, name): + names = name + if isinstance(name, str): + names = (name,) + for n in names: + self._sheets[n] = self.new_sheet() + return LOCalcSheet(self._sheets[n]) + + def move(self, name, pos=-1): + index = pos + if pos < 0: + index = len(self) + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.moveByName(name, index) + return + + def remove(self, name): + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.removeByName(name) + return + + def copy(self, name, new_name='', pos=-1): + if isinstance(name, LOCalcSheet): + name = name.name + index = pos + if pos < 0: + index = len(self) + self._sheets.copyByName(name, new_name, index) + return LOCalcSheet(self._sheets[new_name]) + + def copy_from(self, doc, source='', target='', pos=-1): + index = pos + if pos < 0: + index = len(self) + + names = source + if not source: + names = doc.names + elif isinstance(source, str): + names = (source,) + + new_names = target + if not target: + new_names = names + elif isinstance(target, str): + new_names = (target,) + + for i, name in enumerate(names): + self._sheets.importSheet(doc.obj, name, index + i) + self[index + i].name = new_names[i] + + return LOCalcSheet(self._sheets[index]) + + def sort(self, reverse=False): + names = sorted(self.names, reverse=reverse) + for i, n in enumerate(names): + self.move(n, i) + return + + def render(self, data, sheet=None, clean=True): + if sheet is None: + sheet = self.active + return sheet.render(data, clean=clean) + + +class LOChart(object): + + def __init__(self, name, obj, draw_page): + self._name = name + self._obj = obj + self._eobj = self._obj.EmbeddedObject + self._type = 'Column' + self._cell = None + self._shape = self._get_shape(draw_page) + self._pos = self._shape.Position + + def __getitem__(self, index): + return LOBaseObject(self.diagram.getDataRowProperties(index)) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def diagram(self): + return self._eobj.Diagram + + @property + def type(self): + return self._type + @type.setter + def type(self, value): + self._type = value + if value == 'Bar': + self.diagram.Vertical = True + return + type_chart = f'com.sun.star.chart.{value}Diagram' + self._eobj.setDiagram(self._eobj.createInstance(type_chart)) + + @property + def cell(self): + return self._cell + @cell.setter + def cell(self, value): + self._cell = value + self._shape.Anchor = value.obj + + @property + def position(self): + return self._pos + @position.setter + def position(self, value): + self._pos = value + self._shape.Position = value + + def _get_shape(self, draw_page): + for shape in draw_page: + if shape.PersistName == self.name: + break + return shape + + +class LOSheetCharts(object): + + def __init__(self, obj, sheet): + self._obj = obj + self._sheet = sheet + + def __getitem__(self, index): + return LOChart(index, self.obj[index], self._sheet.draw_page) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + def new(self, name, pos_size, data): + self.obj.addNewByName(name, pos_size, data, True, True) + return LOChart(name, self.obj[name], self._sheet.draw_page) + + +class LOSheetTableField(object): + + def __init__(self, obj): + self._obj = obj + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self.obj.Name + + @property + def orientation(self): + return self.obj.Orientation + @orientation.setter + def orientation(self, value): + self.obj.Orientation = value + + +# ~ com.sun.star.sheet.DataPilotFieldOrientation.ROW +class LOSheetTable(object): + + def __init__(self, obj): + self._obj = obj + self._source = None + + def __getitem__(self, index): + field = self.obj.DataPilotFields[index] + return LOSheetTableField(field) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def filter(self): + return self.obj.ShowFilterButton + @filter.setter + def filter(self, value): + self.obj.ShowFilterButton = value + + @property + def source(self): + return self._source + @source.setter + def source(self, value): + self._source = value + self.obj.SourceRange = value.range_address + + @property + def rows(self): + return self.obj.RowFields + @rows.setter + def rows(self, values): + if not isinstance(values, tuple): + values = (values,) + for v in values: + with self[v] as f: + f.orientation = DPFO.ROW + @property + def columns(self): + return self.obj.ColumnFields + @columns.setter + def columns(self, values): + if not isinstance(values, tuple): + values = (values,) + for v in values: + with self[v] as f: + f.orientation = DPFO.COLUMN + + @property + def data(self): + return self.obj.DataFields + @data.setter + def data(self, values): + if not isinstance(values, tuple): + values = (values,) + for v in values: + with self[v] as f: + f.orientation = DPFO.DATA + + +class LOSheetTables(object): + + def __init__(self, obj, sheet): + self._obj = obj + self._sheet = sheet + + def __getitem__(self, index): + return LOSheetTable(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + @property + def obj(self): + return self._obj + + @property + def count(self): + return self.obj.Count + + @property + def names(self): + return self.obj.ElementNames + + def new(self, name, target): + table = self.obj.createDataPilotDescriptor() + self.obj.insertNewByName(name, target.address, table) + return LOSheetTable(self.obj[name]) + + def remove(self, name): + self.obj.removeByName(name) + return + + +class LOFormControl(LOBaseObject): + EVENTS = { + 'action': 'actionPerformed', + 'click': 'mousePressed', + } + TYPES = { + 'actionPerformed': 'XActionListener', + 'mousePressed': 'XMouseListener', + } + + def __init__(self, obj, view, form): + super().__init__(obj) + self._view = view + self._form = form + self._m = view.Model + self._index = -1 + + def __setattr__(self, name, value): + if name in ('_form', '_view', '_m', '_index'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __str__(self): + return f'{self.name} ({self.type}) {[self.index]}' + + @property + def form(self): + return self._form + + @property + def doc(self): + return self.obj.Parent.Forms.Parent + + @property + def name(self): + return self._m.Name + @name.setter + def name(self, value): + self._m.Name = value + + @property + def tag(self): + return self._m.Tag + @tag.setter + def tag(self, value): + self._m.Tag = value + + @property + def index(self): + return self._index + @index.setter + def index(self, value): + self._index = value + + @property + def enabled(self): + return self._m.Enabled + @enabled.setter + def enabled(self, value): + self._m.Enabled = value + + @property + def events(self): + return self.form.getScriptEvents(self.index) + def add_event(self, name, macro): + if not 'name' in macro: + macro['name'] = '{}_{}'.format(self.name, name) + + event = ScriptEventDescriptor() + event.AddListenerParam = '' + event.EventMethod = self.EVENTS[name] + event.ListenerType = self.TYPES[event.EventMethod] + event.ScriptCode = _get_url_script(macro) + event.ScriptType = 'Script' + + for ev in self.events: + if ev.EventMethod == event.EventMethod and \ + ev.ListenerType == event.ListenerType: + self.form.revokeScriptEvent(self.index, + event.ListenerType, event.EventMethod, event.AddListenerParam) + break + + self.form.registerScriptEvent(self.index, event) + return + + def set_focus(self): + self._view.setFocus() + return + + +class LOFormControlLabel(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Label = value + + +class LOFormControlText(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self._m.Text + @value.setter + def value(self, value): + self._m.Text = value + + +class LOFormControlButton(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Text = Label + + +FORM_CONTROL_CLASS = { + 'label': LOFormControlLabel, + 'text': LOFormControlText, + 'button': LOFormControlButton, +} + + +class LOForm(object): + MODELS = { + 'label': 'com.sun.star.form.component.FixedText', + 'text': 'com.sun.star.form.component.TextField', + 'button': 'com.sun.star.form.component.CommandButton', + } + + def __init__(self, obj, draw_page): + self._obj = obj + self._dp = draw_page + self._controls = {} + self._init_controls() + + def __getitem__(self, index): + control = self.obj[index] + return self._controls[control.Name] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + def __str__(self): + return f'Form: {self.name}' + + def _init_controls(self): + types = { + 'com.sun.star.form.OFixedTextModel': 'label', + 'com.sun.star.form.OEditModel': 'text', + 'com.sun.star.form.OButtonModel': 'button', + } + for i, control in enumerate(self.obj): + name = control.Name + tipo = types[control.ImplementationName] + view = self.doc.CurrentController.getControl(control) + control = FORM_CONTROL_CLASS[tipo](control, view) + control.index = i + setattr(self, name, control) + self._controls[name] = control + return + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self.obj.Name + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def source(self): + return self.obj.DataSourceName + @source.setter + def source(self, value): + self.obj.DataSourceName = value + + @property + def type(self): + return self.obj.CommandType + @type.setter + def type(self, value): + self.obj.CommandType = value + + @property + def command(self): + return self.obj.Command + @command.setter + def command(self, value): + self.obj.Command = value + + @property + def doc(self): + return self.obj.Parent.Parent + + def _special_properties(self, tipo, args): + if tipo == 'button': + # ~ if 'ImageURL' in args: + # ~ args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + return args + + def add(self, args): + name = args['Name'] + tipo = args.pop('Type').lower() + w = args.pop('Width') + h = args.pop('Height') + x = args.pop('X', 0) + y = args.pop('Y', 0) + control = self.doc.createInstance('com.sun.star.drawing.ControlShape') + control.setSize(Size(w, h)) + control.setPosition(Point(x, y)) + model = self.doc.createInstance(self.MODELS[tipo]) + args = self._special_properties(tipo, args) + _set_properties(model, args) + control.Control = model + index = len(self) + self.obj.insertByIndex(index, model) + self._dp.add(control) + view = self.doc.CurrentController.getControl(self.obj.getByName(name)) + control = FORM_CONTROL_CLASS[tipo](control, view, self.obj) + control.index = index + setattr(self, name, control) + self._controls[name] = control + return control + + +class LOSheetForms(object): + + def __init__(self, draw_page): + self._dp = draw_page + self._obj = draw_page.Forms + + def __getitem__(self, index): + return LOForm(self.obj[index], self._dp) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + @property + def doc(self): + return self.obj.Parent + + @property + def count(self): + return len(self) + + @property + def names(self): + return self.obj.ElementNames + + def insert(self, name): + form = self.doc.createInstance('com.sun.star.form.component.Form') + self.obj.insertByName(name, form) + return LOForm(form, self._dp) + + def remove(self, index): + if isinstance(index, int): + self.obj.removeByIndex(index) + else: + self.obj.removeByName(index) + return + + +# ~ IsFiltered, +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetRows(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, int): + rows = LOSheetRows(self._sheet, self.obj[index]) + else: + rango = self._sheet[index.start:index.stop,0:] + rows = LOSheetRows(self._sheet, rango.obj.Rows) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def color(self): + return self.obj.CellBackColor + @color.setter + def color(self, value): + self.obj.CellBackColor = value + + @property + def is_transparent(self): + return self.obj.IsCellBackgroundTransparent + @is_transparent.setter + def is_transparent(self, value): + self.obj.IsCellBackgroundTransparent = value + + @property + def height(self): + return self.obj.Height + @height.setter + def height(self, value): + self.obj.Height = value + + def optimal(self): + self.obj.OptimalHeight = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetColumns(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, (int, str)): + rows = LOSheetColumns(self._sheet, self.obj[index]) + else: + rango = self._sheet[0,index.start:index.stop] + rows = LOSheetColumns(self._sheet, rango.obj.Columns) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def width(self): + return self.obj.Width + @width.setter + def width(self, value): + self.obj.Width = value + + def optimal(self): + self.obj.OptimalWidth = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +class LOCalcSheet(object): + + def __init__(self, obj): + self._obj = obj + + def __getitem__(self, index): + return LOCalcRange(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __str__(self): + return f'easymacro.LOCalcSheet: {self.name}' + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + @name.setter + def name(self, value): + self._obj.Name = value + + @property + def code_name(self): + return self._obj.CodeName + @code_name.setter + def code_name(self, value): + self._obj.CodeName = value + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def is_protected(self): + return self._obj.isProtected() + + @property + def password(self): + return '' + @visible.setter + def password(self, value): + self.obj.protect(value) + + def unprotect(self, value): + try: + self.obj.unprotect(value) + return True + except: + pass + return False + + @property + def color(self): + return self._obj.TabColor + @color.setter + def color(self, value): + self._obj.TabColor = get_color(value) + + @property + def used_area(self): + cursor = self.get_cursor() + cursor.gotoEndOfUsedArea(True) + return LOCalcRange(self[cursor.AbsoluteName].obj) + + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def dp(self): + return self.draw_page + + @property + def shapes(self): + return self.draw_page + + @property + def doc(self): + return LOCalc(self.obj.DrawPage.Forms.Parent) + + @property + def charts(self): + return LOSheetCharts(self.obj.Charts, self) + + @property + def tables(self): + return LOSheetTables(self.obj.DataPilotTables, self) + + @property + def rows(self): + return LOSheetRows(self, self.obj.Rows) + + @property + def columns(self): + return LOSheetColumns(self, self.obj.Columns) + + @property + def forms(self): + return LOSheetForms(self.obj.DrawPage) + + @property + def events(self): + names = ('OnFocus', 'OnUnfocus', 'OnSelect', 'OnDoubleClick', + 'OnRightClick', 'OnChange', 'OnCalculate') + evs = self.obj.Events + events = {n: _property_to_dict(evs.getByName(n)) for n in names + if evs.getByName(n)} + return events + @events.setter + def events(self, values): + pv = '[]com.sun.star.beans.PropertyValue' + ev = self.obj.Events + for name, v in values.items(): + url = _get_url_script(v) + args = dict_to_property(dict(EventType='Script', Script=url)) + # ~ e.replaceByName(k, args) + uno.invoke(ev, 'replaceByName', (name, uno.Any(pv, args))) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + def activate(self): + self.doc.activate(self.obj) + return + + def clean(self): + doc = self.doc + sheet = doc.create_instance('com.sun.star.sheet.Spreadsheet') + doc._sheets.replaceByName(self.name, sheet) + return + + def move(self, pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.moveByName(self.name, index) + return + + def remove(self): + self.doc._sheets.removeByName(self.name) + return + + def copy(self, new_name='', pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.copyByName(self.name, new_name, index) + return LOCalcSheet(self.doc._sheets[new_name]) + + def copy_to(self, doc, target='', pos=-1): + index = pos + if pos < 0: + index = len(doc) + name = self.name + if not target: + new_name = name + + doc._sheets.importSheet(self.doc.obj, name, index) + sheet = doc[name] + 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 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): + # ~ print(isinstance(data, str), data[0]) + if data[0] in '=': + self.obj.setFormula(data) + # ~ print('Set Formula') + else: + self.obj.setString(data) + elif isinstance(data, Decimal): + self.obj.setValue(float(data)) + elif isinstance(data, (int, float, bool)): + self.obj.setValue(data) + elif isinstance(data, datetime.datetime): + d = data.toordinal() + t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY + self.obj.setValue(d - DATE_OFFSET + t) + elif isinstance(data, datetime.date): + d = data.toordinal() + self.obj.setValue(d - DATE_OFFSET) + elif isinstance(data, datetime.time): + d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY + self.obj.setValue(d) + + @property + def date(self): + value = int(self.obj.Value) + date = datetime.date.fromordinal(value + DATE_OFFSET) + return date + + @property + def time(self): + seconds = self.obj.Value * SECONDS_DAY + time_delta = datetime.timedelta(seconds=seconds) + time = (datetime.datetime.min + time_delta).time() + return time + + @property + def datetime(self): + return datetime.datetime.combine(self.date, self.time) + + @property + def data(self): + return self.obj.getDataArray() + @data.setter + def data(self, values): + if self._is_cell: + self.to_size(len(values), len(values[0])).data = values + else: + self.obj.setDataArray(values) + + @property + def dict(self): + rows = self.data + k = rows[0] + data = [dict(zip(k, r)) for r in rows[1:]] + return data + @dict.setter + def dict(self, values): + data = [tuple(values[0].keys())] + data += [tuple(d.values()) for d in values] + self.data = data + + @property + def formula(self): + return self.obj.getFormulaArray() + @formula.setter + def formula(self, values): + self.obj.setFormulaArray(values) + + @property + def array_formula(self): + return self.obj.ArrayFormula + @array_formula.setter + def array_formula(self, value): + self.obj.ArrayFormula = value + + @property + def address(self): + return self.obj.CellAddress + + @property + def range_address(self): + return self.obj.RangeAddress + + @property + def cursor(self): + cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) + return cursor + + @property + def current_region(self): + cursor = self.cursor + cursor.collapseToCurrentRegion() + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + @property + def next_cell(self): + a = self.current_region.range_address + col = a.StartColumn + row = a.EndRow + 1 + return LOCalcRange(self.sheet[row, col].obj) + + @property + def position(self): + return self.obj.Position + + @property + def size(self): + return self.obj.Size + + @property + def possize(self): + data = { + 'Width': self.size.Width, + 'Height': self.size.Height, + 'X': self.position.X, + 'Y': self.position.Y, + } + return data + + @property + def visible(self): + cursor = self.cursor + rangos = cursor.queryVisibleCells() + rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(rangos) + + @property + def merged_area(self): + cursor = self.cursor + cursor.collapseToMergedArea() + rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + return rango + + @property + def empty(self): + cursor = self.sheet.get_cursor(self.obj) + cursor = self.cursor + rangos = cursor.queryEmptyCells() + rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(rangos) + + @property + def merge(self): + return self.obj.IsMerged + @merge.setter + def merge(self, value): + self.obj.merge(value) + + @property + def style(self): + return self.obj.CellStyle + @style.setter + def style(self, value): + self.obj.CellStyle = value + + @property + def auto_format(self): + return '' + @auto_format.setter + def auto_format(self, value): + self.obj.autoFormat(value) + + @property + def validation(self): + return self.obj.Validation + @validation.setter + def validation(self, values): + current = self.validation + if not values: + current.Type = ValidationType.ANY + current.ShowInputMessage = False + else: + is_list = False + for k, v in values.items(): + if k == 'Type' and v == VT.LIST: + is_list = True + if k == 'Formula1' and is_list: + if isinstance(v, (tuple, list)): + v = ';'.join(['"{}"'.format(i) for i in v]) + setattr(current, k, v) + self.obj.Validation = current + + def select(self): + self.doc.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 copy(self, source): + self.sheet.obj.copyRange(self.address, source.range_address) + return + + def copy_to(self, cell, formula=False): + rango = cell.to_size(self.rows, self.columns) + if formula: + rango.formula = self.data + 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, 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']) + # ~ 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 + + +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' + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + for i, p in enumerate(self.obj): + if i == self._index: + obj = LOWriterTextRange(p, self._doc) + self._index += 1 + return obj + raise StopIteration + + @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 is_table(self): + return self._is_table + + @property + def text(self): + return self.obj.Text + + @property + def cursor(self): + return self.text.createTextCursorByRange(self.obj) + + @property + def dp(self): + return self._doc.dp + + def delete(self): + cursor = self.cursor + cursor.gotoStartOfParagraph(False) + cursor.gotoNextParagraph(True) + cursor.String = '' + return + + def offset(self): + cursor = self.cursor.getEnd() + return LOWriterTextRange(cursor, self._doc) + + def insert_content(self, data, cursor=None, replace=False): + if cursor is None: + cursor = self.cursor + self.text.insertTextContent(cursor, data, replace) + return + + def new_line(self, count=1): + cursor = self.cursor + for i in range(count): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + return self._doc.selection + + def insert_table(self, data): + table = self._doc.create_instance('com.sun.star.text.TextTable') + rows = len(data) + cols = len(data[0]) + table.initialize(rows, cols) + self.insert_content(table) + table.DataArray = data + name = table.Name + table = LOWriterTextTable(self._doc.tables[name], self._doc) + return table + + def insert_image(self, path, args={}): + w = args.get('Width', 1000) + h = args.get('Height', 1000) + image = self._doc.create_instance('com.sun.star.text.GraphicObject') + image.GraphicURL = _P.to_url(path) + image.AnchorType = AS_CHARACTER + image.Width = w + image.Height = h + self.insert_content(image) + return self._doc.dp.last + + +class LOWriterTextRanges(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + self._paragraphs = [LOWriterTextRange(p, doc) for p in obj] + + def __len__(self): + return len(self._paragraphs) + + def __getitem__(self, index): + return self._paragraphs[index] + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + obj = self._paragraphs[self._index] + except IndexError: + raise StopIteration + + self._index += 1 + return obj + + @property + def obj(self): + return self._obj + + +class LOWriterTextTable(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + + @property + def data(self): + return self._obj.DataArray + @data.setter + def data(self, values): + self._obj.DataArray = values + + +class LOWriterTextTables(object): + + def __init__(self, doc): + self._doc = doc + self._obj = doc.obj.TextTables + + def __getitem__(self, key): + return LOWriterTextTable(self._obj[key], self._doc) + + def __len__(self): + return self._obj.Count + + def insert(self, data, text_range=None): + if text_range is None: + text_range = self._doc.selection + text_range.insert_table(data) + return + + +class LOWriter(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = WRITER + + @property + def text(self): + return self.paragraphs + + @property + def paragraphs(self): + return LOWriterTextRanges(self.obj.Text, self) + + @property + def tables(self): + return LOWriterTextTables(self) + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName == OBJ_TEXTS: + if len(sel) == 1: + sel = LOWriterTextRanges(sel, self)[0] + else: + sel = LOWriterTextRanges(sel, self) + return sel + + if sel.ImplementationName == OBJ_SHAPES: + if len(sel) == 1: + sel = sel[0] + sel = LODrawPage(sel.Parent)[sel.Name] + return sel + + if sel.ImplementationName == OBJ_GRAPHIC: + sel = self.dp[sel.Name] + else: + debug(sel.ImplementationName) + + return sel + + @property + def dp(self): + return self.draw_page + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def cursor(self): + return self.obj.Text.createTextCursor() + + @property + def page_styles(self): + ps = self.obj.StyleFamilies['PageStyles'] + return LOWriterPageStyles(ps) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + def goto_start(self): + self.view_cursor.gotoStart(False) + return self.selection + + def goto_end(self): + self.view_cursor.gotoEnd(False) + return self.selection + + def search(self, options, find_all=True): + descriptor = self.search_descriptor + descriptor.setSearchString(options.get('Search', '')) + descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) + descriptor.SearchWords = options.get('Words', False) + if 'Attributes' in options: + attr = dict_to_property(options['Attributes']) + descriptor.setSearchAttributes(attr) + if hasattr(descriptor, 'SearchRegularExpression'): + descriptor.SearchRegularExpression = options.get('RegularExpression', False) + if hasattr(descriptor, 'SearchType') and 'Type' in options: + descriptor.SearchType = options['Type'] + + result = False + if find_all: + found = self.obj.findAll(descriptor) + if len(found): + result = [LOWriterTextRange(f, self) for f in found] + else: + found = self.obj.findFirst(descriptor) + if found: + result = LOWriterTextRange(found, self) + + return result + + def replace(self, options): + descriptor = self.replace_descriptor + descriptor.setSearchString(options['Search']) + descriptor.setReplaceString(options['Replace']) + descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) + descriptor.SearchWords = options.get('Words', False) + if 'Attributes' in options: + attr = dict_to_property(options['Attributes']) + descriptor.setSearchAttributes(attr) + if hasattr(descriptor, 'SearchRegularExpression'): + descriptor.SearchRegularExpression = options.get('RegularExpression', False) + if hasattr(descriptor, 'SearchType') and 'Type' in options: + descriptor.SearchType = options['Type'] + found = self.obj.replaceAll(descriptor) + return found + + def select(self, text): + if hasattr(text, 'obj'): + text = text.obj + self._cc.select(text) + return + + +class LOShape(LOBaseObject): + IMAGE = 'com.sun.star.drawing.GraphicObjectShape' + + def __init__(self, obj, index): + 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 is_image(self): + return self.shape_type == self.IMAGE + + @property + def name(self): + return self.obj.Name or f'{self.type}{self.index}' + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def index(self): + return self._index + + @property + def size(self): + s = self.obj.Size + a = dict(Width=s.Width, Height=s.Height) + return a + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def description(self): + return self.obj.Description + @description.setter + def description(self, value): + self.obj.Description = value + + @property + def cell(self): + return self.anchor + + @property + def anchor(self): + obj = self.obj.Anchor + if obj.ImplementationName == OBJ_CELL: + obj = LOCalcRange(obj) + elif obj.ImplementationName == OBJ_TEXT: + obj = LOWriterTextRange(obj, LODocs().active) + else: + debug('Anchor', obj.ImplementationName) + return obj + @anchor.setter + def anchor(self, value): + if hasattr(value, 'obj'): + value = value.obj + self.obj.Anchor = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def path(self): + return self.url + @property + def url(self): + url = '' + if self.is_image: + url = _P.to_system(self.obj.GraphicURL.OriginURL) + return url + + @property + def mimetype(self): + mt = '' + if self.is_image: + mt = self.obj.GraphicURL.MimeType + return mt + + @property + def linked(self): + l = False + if self.is_image: + l = self.obj.GraphicURL.Linked + return l + + def delete(self): + self.remove() + return + def remove(self): + self.obj.Parent.remove(self.obj) + return + + def save(self, path: str, mimetype=DEFAULT_MIME_TYPE): + if _P.is_dir(path): + name = self.name + ext = mimetype.lower() + else: + p = _P(path) + path = p.path + name = p.name + ext = p.ext.lower() + + path = _P.join(path, f'{name}.{ext}') + args = dict( + URL = _P.to_url(path), + MimeType = MIME_TYPE[ext], + ) + if not _export_image(self.obj, args): + path = '' + return path + + # ~ def save2(self, path: str): + # ~ size = len(self.obj.Bitmap.DIB) + # ~ data = self.obj.GraphicStream.readBytes((), size) + # ~ data = data[-1].value + # ~ path = _P.join(path, f'{self.name}.png') + # ~ _P.save_bin(path, b'') + # ~ return + + +class LODrawPage(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + shape = LOShape(self.obj[index], index) + else: + for i, o in enumerate(self.obj): + shape = self.obj[i] + name = shape.Name or f'shape{i}' + if name == index: + shape = LOShape(shape, i) + break + return shape + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + if self._index == self.count: + raise StopIteration + shape = self[self._index] + self._index += 1 + return shape + + + @property + def name(self): + return self.obj.Name + + @property + def doc(self): + return self.obj.Forms.Parent + + @property + def width(self): + return self.obj.Width + + @property + def height(self): + return self.obj.Height + + @property + def count(self): + return self.obj.Count + + @property + def last(self): + return self[self.count - 1] + + def create_instance(self, name): + return self.doc.createInstance(name) + + def add(self, type_shape, args={}): + """Insert a shape in page, type shapes: + Line + Rectangle + Ellipse + Text + """ + 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'{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) + 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, args={}): + 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') + image.GraphicURL = _P.to_url(path) + image.Size = Size(w, h) + image.Position = Point(x, y) + image.Name = name + self.obj.add(image) + 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 = { + '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') + + 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) + 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) + 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): + 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): + return LOBase(None, {'path': path}) + + +def _add_listeners(events, control, name=''): + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + 'addFocusListener': EventsFocus, + 'addItemListener': EventsItem, + 'addKeyListener': EventsKey, + 'addTabListener': EventsTab, + } + if hasattr(control, 'obj'): + control = control.obj + # ~ debug(control.ImplementationName) + is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' + is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' + is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' + is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl' + + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue + + getattr(control, key)(listeners[key](events, name)) + + if is_grid: + controllers = EventsGrid(events, name) + control.addSelectionListener(controllers) + control.Model.GridDataModel.addGridDataListener(controllers) + return + + +def _set_properties(model, properties): + if 'X' in properties: + properties['PositionX'] = properties.pop('X') + if 'Y' in properties: + properties['PositionY'] = properties.pop('Y') + keys = tuple(properties.keys()) + values = tuple(properties.values()) + model.setPropertyValues(keys, values) + return + + +class EventsListenerBase(unohelper.Base, XEventListener): + + def __init__(self, controller, name, window=None): + self._controller = controller + self._name = name + self._window = window + + @property + def name(self): + return self._name + + def disposing(self, event): + self._controller = None + if not self._window is None: + self._window.setMenuBar(None) + + +class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def mousePressed(self, event): + event_name = '{}_click'.format(self._name) + if event.ClickCount == 2: + event_name = '{}_double_click'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def mouseReleased(self, event): + pass + + def mouseEntered(self, event): + pass + + def mouseExited(self, event): + pass + + # ~ XMouseMotionListener + def mouseMoved(self, event): + pass + + def mouseDragged(self, event): + pass + + +class EventsMouseLink(EventsMouse): + + def __init__(self, controller, name): + super().__init__(controller, name) + self._text_color = 0 + + def mouseEntered(self, event): + model = event.Source.Model + self._text_color = model.TextColor or 0 + model.TextColor = get_color('blue') + return + + def mouseExited(self, event): + model = event.Source.Model + model.TextColor = self._text_color + return + + +class EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def actionPerformed(self, event): + event_name = f'{self.name}_action' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + CONTROLS = ( + 'stardiv.Toolkit.UnoControlEditModel', + ) + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + service = event.Source.Model.ImplementationName + # ~ print('Focus enter', service) + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + return + + def focusLost(self, event): + service = event.Source.Model.ImplementationName + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = -1 + return + + +class EventsKey(EventsListenerBase, XKeyListener): + """ + event.KeyChar + event.KeyCode + event.KeyFunc + event.Modifiers + """ + + def __init__(self, controller, name): + super().__init__(controller, name) + + def keyPressed(self, event): + pass + + def keyReleased(self, event): + event_name = '{}_key_released'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + # ~ else: + # ~ if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): + # ~ self._cls.close() + return + + +class EventsItem(EventsListenerBase, XItemListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def disposing(self, event): + pass + + def itemStateChanged(self, event): + event_name = '{}_item_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsItemRoadmap(EventsItem): + + def itemStateChanged(self, event): + dialog = event.Source.Context.Model + dialog.Step = event.ItemId + 1 + return + + +class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def dataChanged(self, event): + event_name = '{}_data_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def rowHeadingChanged(self, event): + pass + + def rowsInserted(self, event): + pass + + def rowsRemoved(self, evemt): + pass + + def selectionChanged(self, event): + event_name = '{}_selection_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsMouseGrid(EventsMouse): + selected = False + + def mousePressed(self, event): + super().mousePressed(event) + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ print(col, row) + # ~ if col == -1 and row == -1: + # ~ if self.selected: + # ~ obj.deselectAllRows() + # ~ else: + # ~ obj.selectAllRows() + # ~ self.selected = not self.selected + return + + def mouseReleased(self, event): + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ if row == -1 and col > -1: + # ~ gdm = obj.Model.GridDataModel + # ~ for i in range(gdm.RowCount): + # ~ gdm.updateRowHeading(i, i + 1) + return + + +class EventsTab(EventsListenerBase, XTabListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def activated(self, id): + event_name = '{}_activated'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(id) + return + + +class EventsMenu(EventsListenerBase, XMenuListener): + + def __init__(self, controller): + super().__init__(controller, '') + + def itemHighlighted(self, event): + pass + + def itemSelected(self, event): + name = event.Source.getCommand(event.MenuId) + if name.startswith('menu'): + event_name = '{}_selected'.format(name) + else: + event_name = 'menu_{}_selected'.format(name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def itemActivated(self, event): + return + + def itemDeactivated(self, event): + return + + +class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): + + def __init__(self, cls): + self._cls = cls + super().__init__(cls.events, cls.name, cls._window) + + def windowOpened(self, event): + event_name = '{}_opened'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowActivated(self, event): + control_name = '{}_activated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowDeactivated(self, event): + control_name = '{}_deactivated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowMinimized(self, event): + pass + + def windowNormalized(self, event): + pass + + def windowClosing(self, event): + if self._window: + control_name = 'window_closing' + else: + control_name = '{}_closing'.format(event.Source.Model.Name) + + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + # ~ else: + # ~ if not self._modal and not self._block: + # ~ event.Source.Visible = False + return + + def windowClosed(self, event): + control_name = '{}_closed'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + # ~ XWindowListener + def windowResized(self, event): + sb = self._cls._subcont + sb.setPosSize(0, 0, event.Width, event.Height, SIZE) + event_name = '{}_resized'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowMoved(self, event): + pass + + def windowShown(self, event): + pass + + def windowHidden(self, event): + pass + + +# ~ BorderColor = ? +# ~ FontStyleName = ? +# ~ HelpURL = ? +class UnoBaseObject(object): + + def __init__(self, obj, path=''): + self._obj = obj + self._model = obj.Model + + def __setattr__(self, name, value): + exists = hasattr(self, name) + if not exists and not name in ('_obj', '_model'): + setattr(self._model, name, value) + else: + super().__setattr__(name, value) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + @property + def m(self): + return self._model + + @property + def properties(self): + return {} + @properties.setter + def properties(self, values): + _set_properties(self.model, values) + + @property + def name(self): + return self.model.Name + + @property + def parent(self): + return self.obj.Context + + @property + def tag(self): + return self.model.Tag + @tag.setter + def tag(self, value): + self.model.Tag = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.setVisible(value) + + @property + def enabled(self): + return self.model.Enabled + @enabled.setter + def enabled(self, value): + self.model.Enabled = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def align(self): + return self.model.Align + @align.setter + def align(self, value): + self.model.Align = value + + @property + def valign(self): + return self.model.VerticalAlign + @valign.setter + def valign(self, value): + self.model.VerticalAlign = value + + @property + def font_weight(self): + return self.model.FontWeight + @font_weight.setter + def font_weight(self, value): + self.model.FontWeight = value + + @property + def font_height(self): + return self.model.FontHeight + @font_height.setter + def font_height(self, value): + self.model.FontHeight = value + + @property + def font_name(self): + return self.model.FontName + @font_name.setter + def font_name(self, value): + self.model.FontName = value + + @property + def font_underline(self): + return self.model.FontUnderline + @font_underline.setter + def font_underline(self, value): + self.model.FontUnderline = value + + @property + def text_color(self): + return self.model.TextColor + @text_color.setter + def text_color(self, value): + self.model.TextColor = value + + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + + @property + def multi_line(self): + return self.model.MultiLine + @multi_line.setter + def multi_line(self, value): + self.model.MultiLine = value + + @property + def help_text(self): + return self.model.HelpText + @help_text.setter + def help_text(self, value): + self.model.HelpText = value + + @property + def border(self): + return self.model.Border + @border.setter + def border(self, value): + # ~ Bug for report + self.model.Border = value + + @property + def width(self): + return self._model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + def _get_possize(self, name): + ps = self.obj.getPosSize() + return getattr(ps, name) + + def _set_possize(self, name, value): + ps = self.obj.getPosSize() + setattr(ps, name, value) + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + return + + @property + def x(self): + if hasattr(self.model, 'PositionX'): + return self.model.PositionX + return self._get_possize('X') + @x.setter + def x(self, value): + if hasattr(self.model, 'PositionX'): + self.model.PositionX = value + else: + self._set_possize('X', value) + + @property + def y(self): + if hasattr(self.model, 'PositionY'): + return self.model.PositionY + return self._get_possize('Y') + @y.setter + def y(self, value): + if hasattr(self.model, 'PositionY'): + self.model.PositionY = value + else: + self._set_possize('Y', value) + + @property + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value + + @property + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value + + @property + def ps(self): + ps = self.obj.getPosSize() + return ps + @ps.setter + def ps(self, ps): + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + + def set_focus(self): + self.obj.setFocus() + return + + def ps_from(self, source): + self.ps = source.ps + return + + def center(self, horizontal=True, vertical=False): + p = self.parent.Model + w = p.Width + h = p.Height + if horizontal: + x = w / 2 - self.width / 2 + self.x = x + if vertical: + y = h / 2 - self.height / 2 + self.y = y + return + + def move(self, origin, x=0, y=5, center=False): + if x: + self.x = origin.x + origin.width + x + else: + self.x = origin.x + if y: + self.y = origin.y + origin.height + y + else: + self.y = origin.y + + if center: + self.center() + return + + +class UnoLabel(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoLabelLink(UnoLabel): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'link' + + +class UnoButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoRadio(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'radio' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoCheckBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'checkbox' + + @property + def value(self): + return self.model.State + @value.setter + def value(self, value): + self.model.State = value + + @property + def label(self): + return self.model.Label + @label.setter + def label(self, value): + self.model.Label = value + + @property + def tri_state(self): + return self.model.TriState + @tri_state.setter + def tri_state(self, value): + self.model.TriState = value + + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html +class UnoText(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self.model.Text + @value.setter + def value(self, value): + self.model.Text = value + + def validate(self): + return + + +class UnoImage(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'image' + + @property + def value(self): + return self.url + @value.setter + def value(self, value): + self.url = value + + @property + def url(self): + return self.m.ImageURL + @url.setter + def url(self, value): + self.m.ImageURL = None + self.m.ImageURL = _P.to_url(value) + + +class UnoListBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._path = '' + + def __setattr__(self, name, value): + if name in ('_path',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def type(self): + return 'listbox' + + @property + def value(self): + return self.obj.getSelectedItem() + + @property + def count(self): + return len(self.data) + + @property + def data(self): + return self.model.StringItemList + @data.setter + def data(self, values): + self.model.StringItemList = list(sorted(values)) + + @property + def path(self): + return self._path + @path.setter + def path(self, value): + self._path = value + + def unselect(self): + self.obj.selectItem(self.value, False) + return + + def select(self, pos=0): + if isinstance(pos, str): + self.obj.selectItem(pos, True) + else: + self.obj.selectItemPos(pos, True) + return + + def clear(self): + self.model.removeAllItems() + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def insert(self, value, path='', pos=-1, show=True): + if pos < 0: + pos = self.count + if path: + self.model.insertItem(pos, value, self._set_image_url(path)) + else: + self.model.insertItemText(pos, value) + if show: + self.select(pos) + return + + +class UnoRoadmap(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._options = () + + def __setattr__(self, name, value): + if name in ('_options',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def options(self): + return self._options + @options.setter + def options(self, values): + self._options = values + for i, v in enumerate(values): + opt = self.model.createInstance() + opt.ID = i + opt.Label = v + self.model.insertByIndex(i, opt) + return + + @property + def enabled(self): + return True + @enabled.setter + def enabled(self, value): + for m in self.model: + m.Enabled = value + return + + def set_enabled(self, index, value): + self.model.getByIndex(index).Enabled = value + return + + +class UnoTree(UnoBaseObject): + + def __init__(self, obj, ): + super().__init__(obj) + self._tdm = None + self._data = [] + + def __setattr__(self, name, value): + if name in ('_tdm', '_data'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def selection(self): + sel = self.obj.Selection + return sel.DataValue, sel.DisplayValue + + @property + def parent(self): + parent = self.obj.Selection.Parent + if parent is None: + return () + return parent.DataValue, parent.DisplayValue + + def _get_parents(self, node): + value = (node.DisplayValue,) + parent = node.Parent + if parent is None: + return value + return self._get_parents(parent) + value + + @property + def parents(self): + values = self._get_parents(self.obj.Selection) + return values + + @property + def root(self): + if self._tdm is None: + return '' + return self._tdm.Root.DisplayValue + @root.setter + def root(self, value): + self._add_data_model(value) + + def _add_data_model(self, name): + tdm = create_instance('com.sun.star.awt.tree.MutableTreeDataModel') + root = tdm.createNode(name, True) + root.DataValue = 0 + tdm.setRoot(root) + self.model.DataModel = tdm + self._tdm = self.model.DataModel + return + + @property + def path(self): + return self.root + @path.setter + def path(self, value): + self.data = _P.walk_dir(value, True) + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = list(values) + self._add_data() + + def _add_data(self): + if not self.data: + return + + parents = {} + for node in self.data: + parent = parents.get(node[1], self._tdm.Root) + child = self._tdm.createNode(node[2], False) + child.DataValue = node[0] + parent.appendChild(child) + parents[node[0]] = child + self.obj.expandNode(self._tdm.Root) + return + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html +class UnoGrid(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._gdm = self.model.GridDataModel + self._data = [] + self._formats = () + + def __setattr__(self, name, value): + if name in ('_gdm', '_data', '_formats'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getitem__(self, key): + value = self._gdm.getCellData(key[0], key[1]) + return value + + def __setitem__(self, key, value): + self._gdm.updateCellData(key[0], key[1], value) + return + + @property + def type(self): + return 'grid' + + @property + def columns(self): + return {} + @columns.setter + def columns(self, values): + # ~ self._columns = values + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html + model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) + for properties in values: + column = create_instance('com.sun.star.awt.grid.GridColumn', True) + for k, v in properties.items(): + setattr(column, k, v) + model.addColumn(column) + self.model.ColumnModel = model + return + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = values + self.clear() + headings = tuple(range(1, len(values) + 1)) + self._gdm.addRows(headings, values) + # ~ rows = range(grid_dm.RowCount) + # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows] + # ~ grid.Model.RowBackgroundColors = tuple(colors) + return + + @property + def value(self): + if self.column == -1 or self.row == -1: + return '' + return self[self.column, self.row] + @value.setter + def value(self, value): + if self.column > -1 and self.row > -1: + self[self.column, self.row] = value + + @property + def row(self): + return self.obj.CurrentRow + + @property + def row_count(self): + return self._gdm.RowCount + + @property + def column(self): + return self.obj.CurrentColumn + + @property + def column(self): + return self.obj.CurrentColumn + + @property + def is_valid(self): + return not (self.row == -1 or self.column == -1) + + @property + def formats(self): + return self._formats + @formats.setter + def formats(self, values): + self._formats = values + + def clear(self): + self._gdm.removeAllRows() + return + + def _format_columns(self, data): + row = data + if self.formats: + for i, f in enumerate(formats): + if f: + row[i] = f.format(data[i]) + return row + + def add_row(self, data): + self._data.append(data) + row = self._format_columns(data) + self._gdm.addRow(self.row_count + 1, row) + return + + def set_cell_tooltip(self, col, row, value): + self._gdm.updateCellToolTip(col, row, value) + return + + def get_cell_tooltip(self, col, row): + value = self._gdm.getCellToolTip(col, row) + return value + + def sort(self, column, asc=True): + self._gdm.sortByColumn(column, asc) + self.update_row_heading() + return + + def update_row_heading(self): + for i in range(self.row_count): + self._gdm.updateRowHeading(i, i + 1) + return + + def remove_row(self, row): + self._gdm.removeRow(row) + del self._data[row] + self.update_row_heading() + return + + +class UnoPage(object): + + def __init__(self, obj): + self._obj = obj + self._events = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._obj.Model + + # ~ @property + # ~ def id(self): + # ~ return self.m.TabPageID + + @property + def parent(self): + return self.obj.Context + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args + + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + args = self._special_properties(tipo, args) + model = self.model.createInstance(UNO_MODELS[tipo]) + _set_properties(model, args) + name = args['Name'] + self.model.insertByName(name, model) + control = self.obj.getControl(name) + _add_listeners(self._events, control, name) + control = UNO_CLASSES[tipo](control) + + if tipo in ('listbox',): + control.path = self.path + + if tipo == 'tree' and root: + control.root = root + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + return control + + +class UnoPages(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._sheets = [] + self._events = None + + def __setattr__(self, name, value): + if name in ('_sheets', '_events'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getitem__(self, index): + name = index + if isinstance(index, int): + name = f'sheet{index}' + sheet = self.obj.getControl(name) + page = UnoPage(sheet) + page._events = self._events + return page + + @property + def type(self): + return 'pages' + + @property + def current(self): + return self.obj.ActiveTabID + @property + def active(self): + return self.current + + @property + def sheets(self): + return self._sheets + @sheets.setter + def sheets(self, values): + self._sheets = values + for i, title in enumerate(values): + sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') + sheet.Title = title + self.m.insertByName(f'sheet{i + 1}', sheet) + return + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + def insert(self, title): + self._sheets.append(title) + id = len(self._sheets) + sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') + sheet.Title = title + self.m.insertByName(f'sheet{id}', sheet) + return self[id] + + def remove(self, id): + self.obj.removeTab(id) + return + + def activate(self, id): + self.obj.activateTab(id) + return + + +UNO_CLASSES = { + 'label': UnoLabel, + 'link': UnoLabelLink, + 'button': UnoButton, + 'radio': UnoRadio, + 'checkbox': UnoCheckBox, + 'text': UnoText, + 'image': UnoImage, + 'listbox': UnoListBox, + 'roadmap': UnoRoadmap, + 'tree': UnoTree, + 'grid': UnoGrid, + 'pages': UnoPages, +} + +UNO_MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', +} +# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', +# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', +# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', +# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', +# ~ 'NumericField': 'com.sun.star.awt.UnoControlNumericFieldModel', +# ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', +# ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', +# ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', +# ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', +# ~ 'SpinButton': 'com.sun.star.awt.UnoControlSpinButtonModel', +# ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', +# ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', + + +class LODialog(object): + SEPARATION = 5 + MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + } + + def __init__(self, args): + self._obj = self._create(args) + self._model = self.obj.Model + self._events = None + self._modal = True + self._controls = {} + self._color_on_focus = COLOR_ON_FOCUS + self._id = '' + self._path = '' + self._init_controls() + + def _create(self, args): + service = 'com.sun.star.awt.DialogProvider' + path = args.pop('Path', '') + if path: + dp = create_instance(service, True) + dlg = dp.createDialog(_P.to_url(path)) + return dlg + + if 'Location' in args: + name = args['Name'] + library = args.get('Library', 'Standard') + location = args.get('Location', 'application').lower() + if location == 'user': + location = 'application' + url = f'vnd.sun.star.script:{library}.{name}?location={location}' + if location == 'document': + dp = create_instance(service, args=docs.active.obj) + else: + dp = create_instance(service, True) + # ~ uid = docs.active.uid + # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' + dlg = dp.createDialog(url) + return dlg + + dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) + model = create_instance('com.sun.star.awt.UnoControlDialogModel', True) + toolkit = create_instance('com.sun.star.awt.Toolkit', True) + _set_properties(model, args) + dlg.setModel(model) + dlg.setVisible(False) + dlg.createPeer(toolkit, None) + return dlg + + def _get_type_control(self, name): + name = name.split('.')[2] + types = { + 'UnoFixedTextControl': 'label', + 'UnoEditControl': 'text', + 'UnoButtonControl': 'button', + } + return types[name] + + def _init_controls(self): + for control in self.obj.getControls(): + tipo = self._get_type_control(control.ImplementationName) + name = control.Model.Name + control = UNO_CLASSES[tipo](control) + setattr(self, name, control) + return + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + + @property + def controls(self): + return self._controls + + @property + def path(self): + return self._path + @property + def id(self): + return self._id + @id.setter + def id(self, value): + self._id = value + self._path = _P.from_id(value) + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + @property + def width(self): + return self.model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers(self) + self._connect_listeners() + + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + self._color_on_focus = get_color(value) + + def _connect_listeners(self): + for control in self.obj.Controls: + _add_listeners(self.events, control, control.Model.Name) + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args + + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + args = self._special_properties(tipo, args) + model = self.model.createInstance(self.MODELS[tipo]) + _set_properties(model, args) + name = args['Name'] + self.model.insertByName(name, model) + control = self.obj.getControl(name) + _add_listeners(self.events, control, name) + control = UNO_CLASSES[tipo](control) + + if tipo in ('listbox',): + control.path = self.path + + if tipo == 'tree' and root: + control.root = root + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + self._controls[name] = control + return control + + def center(self, control, x=0, y=0): + w = self.width + h = self.height + + if isinstance(control, tuple): + wt = self.SEPARATION * -1 + for c in control: + wt += c.width + self.SEPARATION + x = w / 2 - wt / 2 + for c in control: + c.x = x + x = c.x + c.width + self.SEPARATION + return + + if x < 0: + x = w + x - control.width + elif x == 0: + x = w / 2 - control.width / 2 + if y < 0: + y = h + y - control.height + elif y == 0: + y = h / 2 - control.height / 2 + control.x = x + control.y = y + return + + def open(self, modal=True): + self._modal = modal + if modal: + return self.obj.execute() + else: + self.visible = True + return + + def close(self, value=0): + if self._modal: + value = self.obj.endDialog(value) + else: + self.visible = False + self.obj.dispose() + return value + + +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 + + +def create_window(args): + return LOWindow(args) + + +class classproperty: + def __init__(self, method=None): + self.fget = method + + def __get__(self, instance, cls=None): + return self.fget(cls) + + def getter(self, method): + self.fget = method + return self + + +class ClipBoard(object): + SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard' + CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' + + class TextTransferable(unohelper.Base, XTransferable): + + def __init__(self, text): + df = DataFlavor() + df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT + df.HumanPresentableName = "encoded text utf-16" + self.flavors = (df,) + self._data = text + + def getTransferData(self, flavor): + return self._data + + def getTransferDataFlavors(self): + return self.flavors + + + @classmethod + def set(cls, value): + ts = cls.TextTransferable(value) + sc = create_instance(cls.SERVICE) + sc.setContents(ts, None) + return + + @classproperty + def contents(cls): + df = None + text = '' + sc = create_instance(cls.SERVICE) + transferable = sc.getContents() + data = transferable.getTransferDataFlavors() + for df in data: + if df.MimeType == cls.CLIPBOARD_FORMAT_TEXT: + break + if df: + text = transferable.getTransferData(df) + return text +_CB = ClipBoard + + +class Paths(object): + FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' + FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' + + def __init__(self, path=''): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + self._path = Path(path) + + @property + def path(self): + return str(self._path.parent) + + @property + def file_name(self): + return self._path.name + + @property + def name(self): + return self._path.stem + + @property + def ext(self): + return self._path.suffix[1:] + + @property + def info(self): + return self.path, self.file_name, self.name, self.ext + + @property + def url(self): + return self._path.as_uri() + + @property + def size(self): + return self._path.stat().st_size + + @classproperty + def home(self): + return str(Path.home()) + + @classproperty + def documents(self): + return self.config() + + @classproperty + def temp_dir(self): + return tempfile.gettempdir() + + @classproperty + def python(self): + if IS_WIN: + path = self.join(self.config('Module'), PYTHON) + elif IS_MAC: + path = self.join(self.config('Module'), '..', 'Resources', PYTHON) + else: + path = sys.executable + return path + + @classmethod + def dir_tmp(self, only_name=False): + dt = tempfile.TemporaryDirectory() + if only_name: + dt = dt.name + return dt + + @classmethod + def tmp(cls, ext=''): + tmp = tempfile.NamedTemporaryFile(suffix=ext) + return tmp.name + + @classmethod + def save_tmp(cls, data): + path_tmp = cls.tmp() + cls.save(path_tmp, data) + return path_tmp + + @classmethod + def config(cls, name='Work'): + """ + Return de path name in 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') + return cls.to_system(getattr(path, name)) + + @classmethod + def get(cls, init_dir='', filters: str=''): + """ + 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): + """ + init_folder: folder default open + multiple: True for multiple selected + filters: 'xml' or 'xml,txt' + """ + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) + + file_picker = create_instance(cls.FILE_PICKER) + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.setMultiSelectionMode(multiple) + + if filters: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + path = '' + if file_picker.execute(): + files = file_picker.getSelectedFiles() + path = [cls.to_system(f) for f in files] + if not multiple: + path = path[0] + return path + + @classmethod + def replace_ext(cls, path, new_ext): + p = Paths(path) + name = f'{p.name}.{new_ext}' + path = cls.join(p.path, name) + return path + + @classmethod + def exists(cls, path): + result = False + if path: + path = cls.to_system(path) + result = Path(path).exists() + return result + + @classmethod + def exists_app(cls, name_app): + return bool(shutil.which(name_app)) + + @classmethod + def open(cls, path): + if IS_WIN: + os.startfile(path) + else: + pid = subprocess.Popen(['xdg-open', path]).pid + return + + @classmethod + def is_dir(cls, path): + return Path(path).is_dir() + + @classmethod + def is_file(cls, path): + return Path(path).is_file() + + @classmethod + def join(cls, *paths): + return str(Path(paths[0]).joinpath(*paths[1:])) + + @classmethod + def save(cls, path, data, encoding='utf-8'): + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path, data): + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def read(cls, path, encoding='utf-8'): + data = Path(path).read_text(encoding=encoding) + return data + + @classmethod + def read_bin(cls, path): + data = Path(path).read_bytes() + return data + + @classmethod + def to_url(cls, path): + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def to_system(cls, path): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def kill(cls, path): + result = True + p = Path(path) + + try: + if p.is_file(): + p.unlink() + elif p.is_dir(): + shutil.rmtree(path) + 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_dir(cls, path, tree=False): + 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='', pwd=''): + 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 + + @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 True + + @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): + gp = create_instance('com.sun.star.graphic.GraphicProvider') + image = gp.queryGraphic(( + PropertyValue(Name='URL', Value=cls.to_url(path)), + )) + return image + + @classmethod + def copy(cls, source, target='', name=''): + p, f, n, e = _P(source).info + if target: + p = target + if name: + e = '' + n = name + path_new = cls.join(p, f'{n}{e}') + shutil.copy(source, path_new) + return path_new +_P = Paths + + +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 == 'sheets': + return LOSheets() + if name == 'cells': + return LOCells() + if name == 'menus': + return LOMenus() + if name == 'shortcuts': + return LOShortCuts() + if name == 'clipboard': + return ClipBoard + 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 + + +# ~ From request +# ~ https://github.com/psf/requests/blob/master/requests/structures.py#L15 +class CaseInsensitiveDict(MutableMapping): + + def __init__(self, data=None, **kwargs): + self._store = OrderedDict() + if data is None: + data = {} + self.update(data, **kwargs) + + def __setitem__(self, key, value): + # Use the lowercased key for lookups, but store the actual + # key alongside the value. + self._store[key.lower()] = (key, value) + + def __getitem__(self, key): + return self._store[key.lower()][1] + + def __delitem__(self, key): + del self._store[key.lower()] + + def __iter__(self): + return (casedkey for casedkey, mappedvalue in self._store.values()) + + def __len__(self): + return len(self._store) + + def lower_items(self): + """Like iteritems(), but with all lowercase keys.""" + values = ( + (lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items() + ) + return values + + # Copy is required + def copy(self): + return CaseInsensitiveDict(self._store.values()) + + def __repr__(self): + return str(dict(self.items())) + + +# ~ 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/ZazDoc_v0.1.0.oxt b/files/ZazDoc_v0.1.0.oxt new file mode 100644 index 0000000000000000000000000000000000000000..0a1a2998aedd0d614a7789880826121f64c6727d GIT binary patch literal 62486 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
kK5JN90SB3_S}8nl)`*}8U?~G9qRUSV3$(6d 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 rzs(57f1}7hLH;@Q{{_-I&Qk_zpnllHI))B literal 0 HcmV?d00001 diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..37d9be266c66227ca06e23a89eaf7b2ff8e1ba4f GIT binary patch literal 20104 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 YKYM*>==oY<>;M1&07*qoM6N<$f)%i3;s5{u literal 0 HcmV?d00001 diff --git a/source/Addons.xcu b/source/Addons.xcu new file mode 100644 index 0000000..5e4621e --- /dev/null +++ b/source/Addons.xcu @@ -0,0 +1,36 @@ + + + + + + + Zaz Doc + Zaz Doc + + + _self + + + + + Replace styles... + Reemplazar estilos... + + + com.sun.star.text.TextDocument + + + service:net.elmau.zaz.doc?replacestyles + + + _self + + + %origin%/images/replace + + + + + + + diff --git a/source/META-INF/manifest.xml b/source/META-INF/manifest.xml new file mode 100644 index 0000000..90e4e3b --- /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..ad7ae40 --- /dev/null +++ b/source/Office/Accelerators.xcu @@ -0,0 +1,4 @@ + + + + diff --git a/source/ZazDoc.py b/source/ZazDoc.py new file mode 100644 index 0000000..39f0646 --- /dev/null +++ b/source/ZazDoc.py @@ -0,0 +1,21 @@ +import uno +import unohelper +from com.sun.star.task import XJobExecutor + + +ID_EXTENSION = 'net.elmau.zaz.doc' +SERVICE = ('com.sun.star.task.Job',) + + +class ZazDoc(unohelper.Base, XJobExecutor): + + def __init__(self, ctx): + self.ctx = ctx + + def trigger(self, args='pyUNO'): + print('Hello World', args) + return + + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation(ZazDoc, ID_EXTENSION, SERVICE) diff --git a/source/description.xml b/source/description.xml new file mode 100644 index 0000000..76d6897 --- /dev/null +++ b/source/description.xml @@ -0,0 +1,26 @@ + + + + + + Zaz-Doc + Zaz-Doc + + + + + + + + + + El Mau + El Mau + + + + + + + + diff --git a/source/description/desc_en.txt b/source/description/desc_en.txt new file mode 100644 index 0000000..87aa25a --- /dev/null +++ b/source/description/desc_en.txt @@ -0,0 +1 @@ +Help for LibreOffice Documentation Teams \ 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..73e6931 --- /dev/null +++ b/source/description/desc_es.txt @@ -0,0 +1 @@ +Ayuda para los equipos de documentación de LibreOffice \ No newline at end of file diff --git a/source/images/zazdoc.png b/source/images/zazdoc.png new file mode 100644 index 0000000000000000000000000000000000000000..37d9be266c66227ca06e23a89eaf7b2ff8e1ba4f GIT binary patch literal 20104 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 YKYM*>==oY<>;M1&07*qoM6N<$f)%i3;s5{u literal 0 HcmV?d00001 diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py new file mode 100644 index 0000000..74a3aa1 --- /dev/null +++ b/source/pythonpath/easymacro.py @@ -0,0 +1,6817 @@ +#!/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 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 string import Template +from typing import Any, Union +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError + +import imaplib +import smtplib +from smtplib import SMTPException, SMTPAuthenticationError +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.utils import formatdate +from email import encoders +import mailbox + +import uno +import unohelper +from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS +from com.sun.star.awt.MessageBoxResults import YES +from com.sun.star.awt import Rectangle, Size, Point +from com.sun.star.awt.PosSize import POSSIZE, SIZE +from com.sun.star.awt import Key, KeyModifier, KeyEvent +from com.sun.star.container import NoSuchElementException +from com.sun.star.datatransfer import XTransferable, DataFlavor + +from com.sun.star.beans import PropertyValue, NamedValue +from com.sun.star.sheet import TableFilterField +from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA +from com.sun.star.util import Time, Date, DateTime + +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER + +from com.sun.star.lang import Locale +from com.sun.star.lang import XEventListener +from com.sun.star.awt import XActionListener +from com.sun.star.awt import XMenuListener +from com.sun.star.awt import XMouseListener +from com.sun.star.awt import XMouseMotionListener +from com.sun.star.awt import XFocusListener +from com.sun.star.awt import XKeyListener +from com.sun.star.awt import XItemListener +from com.sun.star.awt import XTabListener +from com.sun.star.awt import XWindowListener +from com.sun.star.awt import XTopWindowListener +from com.sun.star.awt.grid import XGridDataListener +from com.sun.star.awt.grid import XGridSelectionListener +from com.sun.star.script import ScriptEventDescriptor + +# ~ 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_RANGES) + +OBJ_SHAPES = 'com.sun.star.drawing.SvxShapeCollection' +OBJ_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' +OBJ_GRAPHIC = 'SwXTextGraphicObject' + +OBJ_TEXTS = 'SwXTextRanges' +OBJ_TEXT = 'SwXTextRange' + + +# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL +class FilterOperator(IntEnum): + EMPTY = 0 + NO_EMPTY = 1 + EQUAL = 2 + NOT_EQUAL = 3 + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 +class Border(IntEnum): + NO_BORDER = 0 + BORDER = 1 + SIMPLE = 2 + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a +class ValidationType(): + from com.sun.star.sheet.ValidationType \ + import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM +VT = ValidationType + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7 +class ValidationAlertStyle(): + from com.sun.star.sheet.ValidationAlertStyle \ + import STOP, WARNING, INFO, MACRO +VAS = ValidationAlertStyle + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html +class ConditionOperator(): + from com.sun.star.sheet.ConditionOperator2 \ + import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \ + LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE +CO = ConditionOperator + + +class DataPilotFieldOrientation(): + from com.sun.star.sheet.DataPilotFieldOrientation \ + import HIDDEN, COLUMN, ROW, PAGE, DATA +DPFO = DataPilotFieldOrientation + + +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] +COUNTRY = LANGUAGE.split('-')[1] +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) and isinstance(data[0], tuple): + return _array_to_dict(data) + + if isinstance(data, tuple) 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() + 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'] + module = '.' + name = args['name'] + language = args.get('language', 'Python') + location = args.get('location', 'user') + + 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=True): + if not split: + return subprocess.check_output(command, shell=True).decode() + + 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 + 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='', default={}, prefix='conf'): + name_file = FILE_NAME_CONFIG.format(prefix) + values = None + path = _P.join(_P.config('UserConfig'), name_file) + if not _P.exists(path): + return default + + values = _P.from_json(path) + if key: + values = values.get(key, default) + + return values + + +def set_config(key, value, prefix='conf'): + name_file = FILE_NAME_CONFIG.format(prefix) + path = _P.join(_P.config('UserConfig'), name_file) + values = get_config(default={}, prefix=prefix) + values[key] = value + result = _P.to_json(path, values) + return result + + +def start(): + global _start + _start = now() + info(_start) + return + + +def end(get_seconds: bool=False): + global _start + e = now() + td = e - _start + result = str(td) + if get_seconds: + result = td.total_seconds() + return result + + +def get_epoch(): + n = now() + return int(time.mktime(n.timetuple())) + + +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +def get_size_screen(): + if IS_WIN: + user32 = ctypes.windll.user32 + res = f'{user32.GetSystemMetrics(0)}x{user32.GetSystemMetrics(1)}' + else: + args = 'xrandr | grep "*" | cut -d " " -f4' + res = run(args, split=False) + return res.strip() + + +def url_open(url, data=None, headers={}, verify=True, get_json=False): + 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) + else: + context = ssl._create_unverified_context() + response = urlopen(req, context=context) + except HTTPError as e: + error(e) + err = str(e) + except URLError as e: + error(e.reason) + err = str(e.reason) + else: + headers = dict(response.info()) + result = response.read() + 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')) + + for path in message.get('files', ()): + fn = _P(path).file_name + 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): + FILTERS = { + 'doc': 'MS Word 97', + 'docx': 'MS Word 2007 XML', + } + + 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 status_bar(self): + return self._cc.getStatusIndicator() + + @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 _get_class_uno(sel) + return sel + + @property + def table_auto_formats(self): + taf = create_instance('com.sun.star.sheet.TableAutoFormats') + return taf.ElementNames + + 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='', args: dict={}): + """ + https://wiki.documentfoundation.org/Macros/Python_Guide/PDF_export_filter_data + """ + path_pdf = path + filter_name = '{}_pdf_Export'.format(self.type) + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + opt = dict_to_property(args) + try: + self.obj.storeToURL(_P.to_url(path), opt) + except Exception as e: + error(e) + path_pdf = '' + + return _P.exists(path_pdf) + + def export(self, path: str, ext: str='', args: dict={}): + if not ext: + ext = _P(path).ext + filter_name = self.FILTERS[ext] + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + opt = dict_to_property(args) + try: + self.obj.storeToURL(_P.to_url(path), opt) + except Exception as e: + error(e) + path = '' + return _P.exists(path) + + def save(self, path: str='', args: dict={}) -> bool: + result = True + opt = dict_to_property(args) + if path: + try: + self.obj.storeAsURL(_P.to_url(path), opt) + except Exception as e: + error(e) + result = False + else: + self.obj.store() + return result + + 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 == OBJ_SHAPES: + if len(sel) == 1: + sel = sel[0] + sel = LODrawPage(sel.Parent)[sel.Name] + 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 + + def activate(self, sheet): + obj = sheet + if isinstance(sheet, LOCalcSheet): + obj = sheet.obj + elif isinstance(sheet, str): + obj = self._sheets[sheet] + self._cc.setActiveSheet(obj) + return + + def new_sheet(self): + s = self.create_instance('com.sun.star.sheet.Spreadsheet') + return s + + def insert(self, name): + names = name + if isinstance(name, str): + names = (name,) + for n in names: + self._sheets[n] = self.new_sheet() + return LOCalcSheet(self._sheets[n]) + + def move(self, name, pos=-1): + index = pos + if pos < 0: + index = len(self) + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.moveByName(name, index) + return + + def remove(self, name): + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.removeByName(name) + return + + def copy(self, name, new_name='', pos=-1): + if isinstance(name, LOCalcSheet): + name = name.name + index = pos + if pos < 0: + index = len(self) + self._sheets.copyByName(name, new_name, index) + return LOCalcSheet(self._sheets[new_name]) + + def copy_from(self, doc, source='', target='', pos=-1): + index = pos + if pos < 0: + index = len(self) + + names = source + if not source: + names = doc.names + elif isinstance(source, str): + names = (source,) + + new_names = target + if not target: + new_names = names + elif isinstance(target, str): + new_names = (target,) + + for i, name in enumerate(names): + self._sheets.importSheet(doc.obj, name, index + i) + self[index + i].name = new_names[i] + + return LOCalcSheet(self._sheets[index]) + + def sort(self, reverse=False): + names = sorted(self.names, reverse=reverse) + for i, n in enumerate(names): + self.move(n, i) + return + + def render(self, data, sheet=None, clean=True): + if sheet is None: + sheet = self.active + return sheet.render(data, clean=clean) + + +class LOChart(object): + + def __init__(self, name, obj, draw_page): + self._name = name + self._obj = obj + self._eobj = self._obj.EmbeddedObject + self._type = 'Column' + self._cell = None + self._shape = self._get_shape(draw_page) + self._pos = self._shape.Position + + def __getitem__(self, index): + return LOBaseObject(self.diagram.getDataRowProperties(index)) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def diagram(self): + return self._eobj.Diagram + + @property + def type(self): + return self._type + @type.setter + def type(self, value): + self._type = value + if value == 'Bar': + self.diagram.Vertical = True + return + type_chart = f'com.sun.star.chart.{value}Diagram' + self._eobj.setDiagram(self._eobj.createInstance(type_chart)) + + @property + def cell(self): + return self._cell + @cell.setter + def cell(self, value): + self._cell = value + self._shape.Anchor = value.obj + + @property + def position(self): + return self._pos + @position.setter + def position(self, value): + self._pos = value + self._shape.Position = value + + def _get_shape(self, draw_page): + for shape in draw_page: + if shape.PersistName == self.name: + break + return shape + + +class LOSheetCharts(object): + + def __init__(self, obj, sheet): + self._obj = obj + self._sheet = sheet + + def __getitem__(self, index): + return LOChart(index, self.obj[index], self._sheet.draw_page) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + def new(self, name, pos_size, data): + self.obj.addNewByName(name, pos_size, data, True, True) + return LOChart(name, self.obj[name], self._sheet.draw_page) + + +class LOSheetTableField(object): + + def __init__(self, obj): + self._obj = obj + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self.obj.Name + + @property + def orientation(self): + return self.obj.Orientation + @orientation.setter + def orientation(self, value): + self.obj.Orientation = value + + +# ~ com.sun.star.sheet.DataPilotFieldOrientation.ROW +class LOSheetTable(object): + + def __init__(self, obj): + self._obj = obj + self._source = None + + def __getitem__(self, index): + field = self.obj.DataPilotFields[index] + return LOSheetTableField(field) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def filter(self): + return self.obj.ShowFilterButton + @filter.setter + def filter(self, value): + self.obj.ShowFilterButton = value + + @property + def source(self): + return self._source + @source.setter + def source(self, value): + self._source = value + self.obj.SourceRange = value.range_address + + @property + def rows(self): + return self.obj.RowFields + @rows.setter + def rows(self, values): + if not isinstance(values, tuple): + values = (values,) + for v in values: + with self[v] as f: + f.orientation = DPFO.ROW + @property + def columns(self): + return self.obj.ColumnFields + @columns.setter + def columns(self, values): + if not isinstance(values, tuple): + values = (values,) + for v in values: + with self[v] as f: + f.orientation = DPFO.COLUMN + + @property + def data(self): + return self.obj.DataFields + @data.setter + def data(self, values): + if not isinstance(values, tuple): + values = (values,) + for v in values: + with self[v] as f: + f.orientation = DPFO.DATA + + +class LOSheetTables(object): + + def __init__(self, obj, sheet): + self._obj = obj + self._sheet = sheet + + def __getitem__(self, index): + return LOSheetTable(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + @property + def obj(self): + return self._obj + + @property + def count(self): + return self.obj.Count + + @property + def names(self): + return self.obj.ElementNames + + def new(self, name, target): + table = self.obj.createDataPilotDescriptor() + self.obj.insertNewByName(name, target.address, table) + return LOSheetTable(self.obj[name]) + + def remove(self, name): + self.obj.removeByName(name) + return + + +class LOFormControl(LOBaseObject): + EVENTS = { + 'action': 'actionPerformed', + 'click': 'mousePressed', + } + TYPES = { + 'actionPerformed': 'XActionListener', + 'mousePressed': 'XMouseListener', + } + + def __init__(self, obj, view, form): + super().__init__(obj) + self._view = view + self._form = form + self._m = view.Model + self._index = -1 + + def __setattr__(self, name, value): + if name in ('_form', '_view', '_m', '_index'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __str__(self): + return f'{self.name} ({self.type}) {[self.index]}' + + @property + def form(self): + return self._form + + @property + def doc(self): + return self.obj.Parent.Forms.Parent + + @property + def name(self): + return self._m.Name + @name.setter + def name(self, value): + self._m.Name = value + + @property + def tag(self): + return self._m.Tag + @tag.setter + def tag(self, value): + self._m.Tag = value + + @property + def index(self): + return self._index + @index.setter + def index(self, value): + self._index = value + + @property + def enabled(self): + return self._m.Enabled + @enabled.setter + def enabled(self, value): + self._m.Enabled = value + + @property + def events(self): + return self.form.getScriptEvents(self.index) + def add_event(self, name, macro): + if not 'name' in macro: + macro['name'] = '{}_{}'.format(self.name, name) + + event = ScriptEventDescriptor() + event.AddListenerParam = '' + event.EventMethod = self.EVENTS[name] + event.ListenerType = self.TYPES[event.EventMethod] + event.ScriptCode = _get_url_script(macro) + event.ScriptType = 'Script' + + for ev in self.events: + if ev.EventMethod == event.EventMethod and \ + ev.ListenerType == event.ListenerType: + self.form.revokeScriptEvent(self.index, + event.ListenerType, event.EventMethod, event.AddListenerParam) + break + + self.form.registerScriptEvent(self.index, event) + return + + def set_focus(self): + self._view.setFocus() + return + + +class LOFormControlLabel(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Label = value + + +class LOFormControlText(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self._m.Text + @value.setter + def value(self, value): + self._m.Text = value + + +class LOFormControlButton(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Text = Label + + +FORM_CONTROL_CLASS = { + 'label': LOFormControlLabel, + 'text': LOFormControlText, + 'button': LOFormControlButton, +} + + +class LOForm(object): + MODELS = { + 'label': 'com.sun.star.form.component.FixedText', + 'text': 'com.sun.star.form.component.TextField', + 'button': 'com.sun.star.form.component.CommandButton', + } + + def __init__(self, obj, draw_page): + self._obj = obj + self._dp = draw_page + self._controls = {} + self._init_controls() + + def __getitem__(self, index): + control = self.obj[index] + return self._controls[control.Name] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + def __str__(self): + return f'Form: {self.name}' + + def _init_controls(self): + types = { + 'com.sun.star.form.OFixedTextModel': 'label', + 'com.sun.star.form.OEditModel': 'text', + 'com.sun.star.form.OButtonModel': 'button', + } + for i, control in enumerate(self.obj): + name = control.Name + tipo = types[control.ImplementationName] + view = self.doc.CurrentController.getControl(control) + control = FORM_CONTROL_CLASS[tipo](control, view) + control.index = i + setattr(self, name, control) + self._controls[name] = control + return + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self.obj.Name + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def source(self): + return self.obj.DataSourceName + @source.setter + def source(self, value): + self.obj.DataSourceName = value + + @property + def type(self): + return self.obj.CommandType + @type.setter + def type(self, value): + self.obj.CommandType = value + + @property + def command(self): + return self.obj.Command + @command.setter + def command(self, value): + self.obj.Command = value + + @property + def doc(self): + return self.obj.Parent.Parent + + def _special_properties(self, tipo, args): + if tipo == 'button': + # ~ if 'ImageURL' in args: + # ~ args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + return args + + def add(self, args): + name = args['Name'] + tipo = args.pop('Type').lower() + w = args.pop('Width') + h = args.pop('Height') + x = args.pop('X', 0) + y = args.pop('Y', 0) + control = self.doc.createInstance('com.sun.star.drawing.ControlShape') + control.setSize(Size(w, h)) + control.setPosition(Point(x, y)) + model = self.doc.createInstance(self.MODELS[tipo]) + args = self._special_properties(tipo, args) + _set_properties(model, args) + control.Control = model + index = len(self) + self.obj.insertByIndex(index, model) + self._dp.add(control) + view = self.doc.CurrentController.getControl(self.obj.getByName(name)) + control = FORM_CONTROL_CLASS[tipo](control, view, self.obj) + control.index = index + setattr(self, name, control) + self._controls[name] = control + return control + + +class LOSheetForms(object): + + def __init__(self, draw_page): + self._dp = draw_page + self._obj = draw_page.Forms + + def __getitem__(self, index): + return LOForm(self.obj[index], self._dp) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + @property + def doc(self): + return self.obj.Parent + + @property + def count(self): + return len(self) + + @property + def names(self): + return self.obj.ElementNames + + def insert(self, name): + form = self.doc.createInstance('com.sun.star.form.component.Form') + self.obj.insertByName(name, form) + return LOForm(form, self._dp) + + def remove(self, index): + if isinstance(index, int): + self.obj.removeByIndex(index) + else: + self.obj.removeByName(index) + return + + +# ~ IsFiltered, +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetRows(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, int): + rows = LOSheetRows(self._sheet, self.obj[index]) + else: + rango = self._sheet[index.start:index.stop,0:] + rows = LOSheetRows(self._sheet, rango.obj.Rows) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def color(self): + return self.obj.CellBackColor + @color.setter + def color(self, value): + self.obj.CellBackColor = value + + @property + def is_transparent(self): + return self.obj.IsCellBackgroundTransparent + @is_transparent.setter + def is_transparent(self, value): + self.obj.IsCellBackgroundTransparent = value + + @property + def height(self): + return self.obj.Height + @height.setter + def height(self, value): + self.obj.Height = value + + def optimal(self): + self.obj.OptimalHeight = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetColumns(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, (int, str)): + rows = LOSheetColumns(self._sheet, self.obj[index]) + else: + rango = self._sheet[0,index.start:index.stop] + rows = LOSheetColumns(self._sheet, rango.obj.Columns) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def width(self): + return self.obj.Width + @width.setter + def width(self, value): + self.obj.Width = value + + def optimal(self): + self.obj.OptimalWidth = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +class LOCalcSheet(object): + + def __init__(self, obj): + self._obj = obj + + def __getitem__(self, index): + return LOCalcRange(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __str__(self): + return f'easymacro.LOCalcSheet: {self.name}' + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + @name.setter + def name(self, value): + self._obj.Name = value + + @property + def code_name(self): + return self._obj.CodeName + @code_name.setter + def code_name(self, value): + self._obj.CodeName = value + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def is_protected(self): + return self._obj.isProtected() + + @property + def password(self): + return '' + @visible.setter + def password(self, value): + self.obj.protect(value) + + def unprotect(self, value): + try: + self.obj.unprotect(value) + return True + except: + pass + return False + + @property + def color(self): + return self._obj.TabColor + @color.setter + def color(self, value): + self._obj.TabColor = get_color(value) + + @property + def used_area(self): + cursor = self.get_cursor() + cursor.gotoEndOfUsedArea(True) + return LOCalcRange(self[cursor.AbsoluteName].obj) + + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def dp(self): + return self.draw_page + + @property + def shapes(self): + return self.draw_page + + @property + def doc(self): + return LOCalc(self.obj.DrawPage.Forms.Parent) + + @property + def charts(self): + return LOSheetCharts(self.obj.Charts, self) + + @property + def tables(self): + return LOSheetTables(self.obj.DataPilotTables, self) + + @property + def rows(self): + return LOSheetRows(self, self.obj.Rows) + + @property + def columns(self): + return LOSheetColumns(self, self.obj.Columns) + + @property + def forms(self): + return LOSheetForms(self.obj.DrawPage) + + @property + def events(self): + names = ('OnFocus', 'OnUnfocus', 'OnSelect', 'OnDoubleClick', + 'OnRightClick', 'OnChange', 'OnCalculate') + evs = self.obj.Events + events = {n: _property_to_dict(evs.getByName(n)) for n in names + if evs.getByName(n)} + return events + @events.setter + def events(self, values): + pv = '[]com.sun.star.beans.PropertyValue' + ev = self.obj.Events + for name, v in values.items(): + url = _get_url_script(v) + args = dict_to_property(dict(EventType='Script', Script=url)) + # ~ e.replaceByName(k, args) + uno.invoke(ev, 'replaceByName', (name, uno.Any(pv, args))) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + def activate(self): + self.doc.activate(self.obj) + return + + def clean(self): + doc = self.doc + sheet = doc.create_instance('com.sun.star.sheet.Spreadsheet') + doc._sheets.replaceByName(self.name, sheet) + return + + def move(self, pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.moveByName(self.name, index) + return + + def remove(self): + self.doc._sheets.removeByName(self.name) + return + + def copy(self, new_name='', pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.copyByName(self.name, new_name, index) + return LOCalcSheet(self.doc._sheets[new_name]) + + def copy_to(self, doc, target='', pos=-1): + index = pos + if pos < 0: + index = len(doc) + name = self.name + if not target: + new_name = name + + doc._sheets.importSheet(self.doc.obj, name, index) + sheet = doc[name] + 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 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): + # ~ print(isinstance(data, str), data[0]) + if data[0] in '=': + self.obj.setFormula(data) + # ~ print('Set Formula') + else: + self.obj.setString(data) + elif isinstance(data, Decimal): + self.obj.setValue(float(data)) + elif isinstance(data, (int, float, bool)): + self.obj.setValue(data) + elif isinstance(data, datetime.datetime): + d = data.toordinal() + t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY + self.obj.setValue(d - DATE_OFFSET + t) + elif isinstance(data, datetime.date): + d = data.toordinal() + self.obj.setValue(d - DATE_OFFSET) + elif isinstance(data, datetime.time): + d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY + self.obj.setValue(d) + + @property + def date(self): + value = int(self.obj.Value) + date = datetime.date.fromordinal(value + DATE_OFFSET) + return date + + @property + def time(self): + seconds = self.obj.Value * SECONDS_DAY + time_delta = datetime.timedelta(seconds=seconds) + time = (datetime.datetime.min + time_delta).time() + return time + + @property + def datetime(self): + return datetime.datetime.combine(self.date, self.time) + + @property + def data(self): + return self.obj.getDataArray() + @data.setter + def data(self, values): + if self._is_cell: + self.to_size(len(values), len(values[0])).data = values + else: + self.obj.setDataArray(values) + + @property + def dict(self): + rows = self.data + k = rows[0] + data = [dict(zip(k, r)) for r in rows[1:]] + return data + @dict.setter + def dict(self, values): + data = [tuple(values[0].keys())] + data += [tuple(d.values()) for d in values] + self.data = data + + @property + def formula(self): + return self.obj.getFormulaArray() + @formula.setter + def formula(self, values): + self.obj.setFormulaArray(values) + + @property + def array_formula(self): + return self.obj.ArrayFormula + @array_formula.setter + def array_formula(self, value): + self.obj.ArrayFormula = value + + @property + def address(self): + return self.obj.CellAddress + + @property + def range_address(self): + return self.obj.RangeAddress + + @property + def cursor(self): + cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) + return cursor + + @property + def current_region(self): + cursor = self.cursor + cursor.collapseToCurrentRegion() + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + @property + def next_cell(self): + a = self.current_region.range_address + col = a.StartColumn + row = a.EndRow + 1 + return LOCalcRange(self.sheet[row, col].obj) + + @property + def position(self): + return self.obj.Position + + @property + def size(self): + return self.obj.Size + + @property + def possize(self): + data = { + 'Width': self.size.Width, + 'Height': self.size.Height, + 'X': self.position.X, + 'Y': self.position.Y, + } + return data + + @property + def visible(self): + cursor = self.cursor + rangos = cursor.queryVisibleCells() + rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(rangos) + + @property + def merged_area(self): + cursor = self.cursor + cursor.collapseToMergedArea() + rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + return rango + + @property + def empty(self): + cursor = self.sheet.get_cursor(self.obj) + cursor = self.cursor + rangos = cursor.queryEmptyCells() + rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(rangos) + + @property + def merge(self): + return self.obj.IsMerged + @merge.setter + def merge(self, value): + self.obj.merge(value) + + @property + def style(self): + return self.obj.CellStyle + @style.setter + def style(self, value): + self.obj.CellStyle = value + + @property + def auto_format(self): + return '' + @auto_format.setter + def auto_format(self, value): + self.obj.autoFormat(value) + + @property + def validation(self): + return self.obj.Validation + @validation.setter + def validation(self, values): + current = self.validation + if not values: + current.Type = ValidationType.ANY + current.ShowInputMessage = False + else: + is_list = False + for k, v in values.items(): + if k == 'Type' and v == VT.LIST: + is_list = True + if k == 'Formula1' and is_list: + if isinstance(v, (tuple, list)): + v = ';'.join(['"{}"'.format(i) for i in v]) + setattr(current, k, v) + self.obj.Validation = current + + def select(self): + self.doc.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 copy(self, source): + self.sheet.obj.copyRange(self.address, source.range_address) + return + + def copy_to(self, cell, formula=False): + rango = cell.to_size(self.rows, self.columns) + if formula: + rango.formula = self.data + 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, 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']) + # ~ 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 + + +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' + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + for i, p in enumerate(self.obj): + if i == self._index: + obj = LOWriterTextRange(p, self._doc) + self._index += 1 + return obj + raise StopIteration + + @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 is_table(self): + return self._is_table + + @property + def text(self): + return self.obj.Text + + @property + def cursor(self): + return self.text.createTextCursorByRange(self.obj) + + @property + def dp(self): + return self._doc.dp + + def delete(self): + cursor = self.cursor + cursor.gotoStartOfParagraph(False) + cursor.gotoNextParagraph(True) + cursor.String = '' + return + + def offset(self): + cursor = self.cursor.getEnd() + return LOWriterTextRange(cursor, self._doc) + + def insert_content(self, data, cursor=None, replace=False): + if cursor is None: + cursor = self.cursor + self.text.insertTextContent(cursor, data, replace) + return + + def new_line(self, count=1): + cursor = self.cursor + for i in range(count): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + return self._doc.selection + + def insert_table(self, data): + table = self._doc.create_instance('com.sun.star.text.TextTable') + rows = len(data) + cols = len(data[0]) + table.initialize(rows, cols) + self.insert_content(table) + table.DataArray = data + name = table.Name + table = LOWriterTextTable(self._doc.tables[name], self._doc) + return table + + def insert_image(self, path, args={}): + w = args.get('Width', 1000) + h = args.get('Height', 1000) + image = self._doc.create_instance('com.sun.star.text.GraphicObject') + image.GraphicURL = _P.to_url(path) + image.AnchorType = AS_CHARACTER + image.Width = w + image.Height = h + self.insert_content(image) + return self._doc.dp.last + + +class LOWriterTextRanges(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + self._paragraphs = [LOWriterTextRange(p, doc) for p in obj] + + def __len__(self): + return len(self._paragraphs) + + def __getitem__(self, index): + return self._paragraphs[index] + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + obj = self._paragraphs[self._index] + except IndexError: + raise StopIteration + + self._index += 1 + return obj + + @property + def obj(self): + return self._obj + + +class LOWriterTextTable(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + + @property + def data(self): + return self._obj.DataArray + @data.setter + def data(self, values): + self._obj.DataArray = values + + +class LOWriterTextTables(object): + + def __init__(self, doc): + self._doc = doc + self._obj = doc.obj.TextTables + + def __getitem__(self, key): + return LOWriterTextTable(self._obj[key], self._doc) + + def __len__(self): + return self._obj.Count + + def insert(self, data, text_range=None): + if text_range is None: + text_range = self._doc.selection + text_range.insert_table(data) + return + + +class LOWriter(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = WRITER + + @property + def text(self): + return self.paragraphs + + @property + def paragraphs(self): + return LOWriterTextRanges(self.obj.Text, self) + + @property + def tables(self): + return LOWriterTextTables(self) + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName == OBJ_TEXTS: + if len(sel) == 1: + sel = LOWriterTextRanges(sel, self)[0] + else: + sel = LOWriterTextRanges(sel, self) + return sel + + if sel.ImplementationName == OBJ_SHAPES: + if len(sel) == 1: + sel = sel[0] + sel = LODrawPage(sel.Parent)[sel.Name] + return sel + + if sel.ImplementationName == OBJ_GRAPHIC: + sel = self.dp[sel.Name] + else: + debug(sel.ImplementationName) + + return sel + + @property + def dp(self): + return self.draw_page + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def cursor(self): + return self.obj.Text.createTextCursor() + + @property + def page_styles(self): + ps = self.obj.StyleFamilies['PageStyles'] + return LOWriterPageStyles(ps) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + def goto_start(self): + self.view_cursor.gotoStart(False) + return self.selection + + def goto_end(self): + self.view_cursor.gotoEnd(False) + return self.selection + + def search(self, options, find_all=True): + descriptor = self.search_descriptor + descriptor.setSearchString(options.get('Search', '')) + descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) + descriptor.SearchWords = options.get('Words', False) + if 'Attributes' in options: + attr = dict_to_property(options['Attributes']) + descriptor.setSearchAttributes(attr) + if hasattr(descriptor, 'SearchRegularExpression'): + descriptor.SearchRegularExpression = options.get('RegularExpression', False) + if hasattr(descriptor, 'SearchType') and 'Type' in options: + descriptor.SearchType = options['Type'] + + result = False + if find_all: + found = self.obj.findAll(descriptor) + if len(found): + result = [LOWriterTextRange(f, self) for f in found] + else: + found = self.obj.findFirst(descriptor) + if found: + result = LOWriterTextRange(found, self) + + return result + + def replace(self, options): + descriptor = self.replace_descriptor + descriptor.setSearchString(options['Search']) + descriptor.setReplaceString(options['Replace']) + descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) + descriptor.SearchWords = options.get('Words', False) + if 'Attributes' in options: + attr = dict_to_property(options['Attributes']) + descriptor.setSearchAttributes(attr) + if hasattr(descriptor, 'SearchRegularExpression'): + descriptor.SearchRegularExpression = options.get('RegularExpression', False) + if hasattr(descriptor, 'SearchType') and 'Type' in options: + descriptor.SearchType = options['Type'] + found = self.obj.replaceAll(descriptor) + return found + + def select(self, text): + if hasattr(text, 'obj'): + text = text.obj + self._cc.select(text) + return + + +class LOShape(LOBaseObject): + IMAGE = 'com.sun.star.drawing.GraphicObjectShape' + + def __init__(self, obj, index): + 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 is_image(self): + return self.shape_type == self.IMAGE + + @property + def name(self): + return self.obj.Name or f'{self.type}{self.index}' + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def index(self): + return self._index + + @property + def size(self): + s = self.obj.Size + a = dict(Width=s.Width, Height=s.Height) + return a + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def description(self): + return self.obj.Description + @description.setter + def description(self, value): + self.obj.Description = value + + @property + def cell(self): + return self.anchor + + @property + def anchor(self): + obj = self.obj.Anchor + if obj.ImplementationName == OBJ_CELL: + obj = LOCalcRange(obj) + elif obj.ImplementationName == OBJ_TEXT: + obj = LOWriterTextRange(obj, LODocs().active) + else: + debug('Anchor', obj.ImplementationName) + return obj + @anchor.setter + def anchor(self, value): + if hasattr(value, 'obj'): + value = value.obj + self.obj.Anchor = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def path(self): + return self.url + @property + def url(self): + url = '' + if self.is_image: + url = _P.to_system(self.obj.GraphicURL.OriginURL) + return url + + @property + def mimetype(self): + mt = '' + if self.is_image: + mt = self.obj.GraphicURL.MimeType + return mt + + @property + def linked(self): + l = False + if self.is_image: + l = self.obj.GraphicURL.Linked + return l + + def delete(self): + self.remove() + return + def remove(self): + self.obj.Parent.remove(self.obj) + return + + def save(self, path: str, mimetype=DEFAULT_MIME_TYPE): + if _P.is_dir(path): + name = self.name + ext = mimetype.lower() + else: + p = _P(path) + path = p.path + name = p.name + ext = p.ext.lower() + + path = _P.join(path, f'{name}.{ext}') + args = dict( + URL = _P.to_url(path), + MimeType = MIME_TYPE[ext], + ) + if not _export_image(self.obj, args): + path = '' + return path + + # ~ def save2(self, path: str): + # ~ size = len(self.obj.Bitmap.DIB) + # ~ data = self.obj.GraphicStream.readBytes((), size) + # ~ data = data[-1].value + # ~ path = _P.join(path, f'{self.name}.png') + # ~ _P.save_bin(path, b'') + # ~ return + + +class LODrawPage(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + shape = LOShape(self.obj[index], index) + else: + for i, o in enumerate(self.obj): + shape = self.obj[i] + name = shape.Name or f'shape{i}' + if name == index: + shape = LOShape(shape, i) + break + return shape + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + if self._index == self.count: + raise StopIteration + shape = self[self._index] + self._index += 1 + return shape + + + @property + def name(self): + return self.obj.Name + + @property + def doc(self): + return self.obj.Forms.Parent + + @property + def width(self): + return self.obj.Width + + @property + def height(self): + return self.obj.Height + + @property + def count(self): + return self.obj.Count + + @property + def last(self): + return self[self.count - 1] + + def create_instance(self, name): + return self.doc.createInstance(name) + + def add(self, type_shape, args={}): + """Insert a shape in page, type shapes: + Line + Rectangle + Ellipse + Text + """ + 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'{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) + 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, args={}): + 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') + image.GraphicURL = _P.to_url(path) + image.Size = Size(w, h) + image.Position = Point(x, y) + image.Name = name + self.obj.add(image) + 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 = { + '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') + + 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) + 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) + 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): + 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): + return LOBase(None, {'path': path}) + + +def _add_listeners(events, control, name=''): + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + 'addFocusListener': EventsFocus, + 'addItemListener': EventsItem, + 'addKeyListener': EventsKey, + 'addTabListener': EventsTab, + } + if hasattr(control, 'obj'): + control = control.obj + # ~ debug(control.ImplementationName) + is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' + is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' + is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' + is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl' + + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue + + getattr(control, key)(listeners[key](events, name)) + + if is_grid: + controllers = EventsGrid(events, name) + control.addSelectionListener(controllers) + control.Model.GridDataModel.addGridDataListener(controllers) + return + + +def _set_properties(model, properties): + if 'X' in properties: + properties['PositionX'] = properties.pop('X') + if 'Y' in properties: + properties['PositionY'] = properties.pop('Y') + keys = tuple(properties.keys()) + values = tuple(properties.values()) + model.setPropertyValues(keys, values) + return + + +class EventsListenerBase(unohelper.Base, XEventListener): + + def __init__(self, controller, name, window=None): + self._controller = controller + self._name = name + self._window = window + + @property + def name(self): + return self._name + + def disposing(self, event): + self._controller = None + if not self._window is None: + self._window.setMenuBar(None) + + +class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def mousePressed(self, event): + event_name = '{}_click'.format(self._name) + if event.ClickCount == 2: + event_name = '{}_double_click'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def mouseReleased(self, event): + pass + + def mouseEntered(self, event): + pass + + def mouseExited(self, event): + pass + + # ~ XMouseMotionListener + def mouseMoved(self, event): + pass + + def mouseDragged(self, event): + pass + + +class EventsMouseLink(EventsMouse): + + def __init__(self, controller, name): + super().__init__(controller, name) + self._text_color = 0 + + def mouseEntered(self, event): + model = event.Source.Model + self._text_color = model.TextColor or 0 + model.TextColor = get_color('blue') + return + + def mouseExited(self, event): + model = event.Source.Model + model.TextColor = self._text_color + return + + +class EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def actionPerformed(self, event): + event_name = f'{self.name}_action' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + CONTROLS = ( + 'stardiv.Toolkit.UnoControlEditModel', + ) + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + service = event.Source.Model.ImplementationName + # ~ print('Focus enter', service) + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + return + + def focusLost(self, event): + service = event.Source.Model.ImplementationName + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = -1 + return + + +class EventsKey(EventsListenerBase, XKeyListener): + """ + event.KeyChar + event.KeyCode + event.KeyFunc + event.Modifiers + """ + + def __init__(self, controller, name): + super().__init__(controller, name) + + def keyPressed(self, event): + pass + + def keyReleased(self, event): + event_name = '{}_key_released'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + # ~ else: + # ~ if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): + # ~ self._cls.close() + return + + +class EventsItem(EventsListenerBase, XItemListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def disposing(self, event): + pass + + def itemStateChanged(self, event): + event_name = '{}_item_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsItemRoadmap(EventsItem): + + def itemStateChanged(self, event): + dialog = event.Source.Context.Model + dialog.Step = event.ItemId + 1 + return + + +class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def dataChanged(self, event): + event_name = '{}_data_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def rowHeadingChanged(self, event): + pass + + def rowsInserted(self, event): + pass + + def rowsRemoved(self, evemt): + pass + + def selectionChanged(self, event): + event_name = '{}_selection_changed'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsMouseGrid(EventsMouse): + selected = False + + def mousePressed(self, event): + super().mousePressed(event) + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ print(col, row) + # ~ if col == -1 and row == -1: + # ~ if self.selected: + # ~ obj.deselectAllRows() + # ~ else: + # ~ obj.selectAllRows() + # ~ self.selected = not self.selected + return + + def mouseReleased(self, event): + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ if row == -1 and col > -1: + # ~ gdm = obj.Model.GridDataModel + # ~ for i in range(gdm.RowCount): + # ~ gdm.updateRowHeading(i, i + 1) + return + + +class EventsTab(EventsListenerBase, XTabListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def activated(self, id): + event_name = '{}_activated'.format(self.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(id) + return + + +class EventsMenu(EventsListenerBase, XMenuListener): + + def __init__(self, controller): + super().__init__(controller, '') + + def itemHighlighted(self, event): + pass + + def itemSelected(self, event): + name = event.Source.getCommand(event.MenuId) + if name.startswith('menu'): + event_name = '{}_selected'.format(name) + else: + event_name = 'menu_{}_selected'.format(name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def itemActivated(self, event): + return + + def itemDeactivated(self, event): + return + + +class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): + + def __init__(self, cls): + self._cls = cls + super().__init__(cls.events, cls.name, cls._window) + + def windowOpened(self, event): + event_name = '{}_opened'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowActivated(self, event): + control_name = '{}_activated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowDeactivated(self, event): + control_name = '{}_deactivated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowMinimized(self, event): + pass + + def windowNormalized(self, event): + pass + + def windowClosing(self, event): + if self._window: + control_name = 'window_closing' + else: + control_name = '{}_closing'.format(event.Source.Model.Name) + + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + # ~ else: + # ~ if not self._modal and not self._block: + # ~ event.Source.Visible = False + return + + def windowClosed(self, event): + control_name = '{}_closed'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + # ~ XWindowListener + def windowResized(self, event): + sb = self._cls._subcont + sb.setPosSize(0, 0, event.Width, event.Height, SIZE) + event_name = '{}_resized'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowMoved(self, event): + pass + + def windowShown(self, event): + pass + + def windowHidden(self, event): + pass + + +# ~ BorderColor = ? +# ~ FontStyleName = ? +# ~ HelpURL = ? +class UnoBaseObject(object): + + def __init__(self, obj, path=''): + self._obj = obj + self._model = obj.Model + + def __setattr__(self, name, value): + exists = hasattr(self, name) + if not exists and not name in ('_obj', '_model'): + setattr(self._model, name, value) + else: + super().__setattr__(name, value) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + @property + def m(self): + return self._model + + @property + def properties(self): + return {} + @properties.setter + def properties(self, values): + _set_properties(self.model, values) + + @property + def name(self): + return self.model.Name + + @property + def parent(self): + return self.obj.Context + + @property + def tag(self): + return self.model.Tag + @tag.setter + def tag(self, value): + self.model.Tag = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.setVisible(value) + + @property + def enabled(self): + return self.model.Enabled + @enabled.setter + def enabled(self, value): + self.model.Enabled = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def align(self): + return self.model.Align + @align.setter + def align(self, value): + self.model.Align = value + + @property + def valign(self): + return self.model.VerticalAlign + @valign.setter + def valign(self, value): + self.model.VerticalAlign = value + + @property + def font_weight(self): + return self.model.FontWeight + @font_weight.setter + def font_weight(self, value): + self.model.FontWeight = value + + @property + def font_height(self): + return self.model.FontHeight + @font_height.setter + def font_height(self, value): + self.model.FontHeight = value + + @property + def font_name(self): + return self.model.FontName + @font_name.setter + def font_name(self, value): + self.model.FontName = value + + @property + def font_underline(self): + return self.model.FontUnderline + @font_underline.setter + def font_underline(self, value): + self.model.FontUnderline = value + + @property + def text_color(self): + return self.model.TextColor + @text_color.setter + def text_color(self, value): + self.model.TextColor = value + + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + + @property + def multi_line(self): + return self.model.MultiLine + @multi_line.setter + def multi_line(self, value): + self.model.MultiLine = value + + @property + def help_text(self): + return self.model.HelpText + @help_text.setter + def help_text(self, value): + self.model.HelpText = value + + @property + def border(self): + return self.model.Border + @border.setter + def border(self, value): + # ~ Bug for report + self.model.Border = value + + @property + def width(self): + return self._model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + def _get_possize(self, name): + ps = self.obj.getPosSize() + return getattr(ps, name) + + def _set_possize(self, name, value): + ps = self.obj.getPosSize() + setattr(ps, name, value) + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + return + + @property + def x(self): + if hasattr(self.model, 'PositionX'): + return self.model.PositionX + return self._get_possize('X') + @x.setter + def x(self, value): + if hasattr(self.model, 'PositionX'): + self.model.PositionX = value + else: + self._set_possize('X', value) + + @property + def y(self): + if hasattr(self.model, 'PositionY'): + return self.model.PositionY + return self._get_possize('Y') + @y.setter + def y(self, value): + if hasattr(self.model, 'PositionY'): + self.model.PositionY = value + else: + self._set_possize('Y', value) + + @property + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value + + @property + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value + + @property + def ps(self): + ps = self.obj.getPosSize() + return ps + @ps.setter + def ps(self, ps): + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + + def set_focus(self): + self.obj.setFocus() + return + + def ps_from(self, source): + self.ps = source.ps + return + + def center(self, horizontal=True, vertical=False): + p = self.parent.Model + w = p.Width + h = p.Height + if horizontal: + x = w / 2 - self.width / 2 + self.x = x + if vertical: + y = h / 2 - self.height / 2 + self.y = y + return + + def move(self, origin, x=0, y=5, center=False): + if x: + self.x = origin.x + origin.width + x + else: + self.x = origin.x + if y: + self.y = origin.y + origin.height + y + else: + self.y = origin.y + + if center: + self.center() + return + + +class UnoLabel(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoLabelLink(UnoLabel): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'link' + + +class UnoButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoRadio(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'radio' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoCheckBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'checkbox' + + @property + def value(self): + return self.model.State + @value.setter + def value(self, value): + self.model.State = value + + @property + def label(self): + return self.model.Label + @label.setter + def label(self, value): + self.model.Label = value + + @property + def tri_state(self): + return self.model.TriState + @tri_state.setter + def tri_state(self, value): + self.model.TriState = value + + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html +class UnoText(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self.model.Text + @value.setter + def value(self, value): + self.model.Text = value + + def validate(self): + return + + +class UnoImage(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'image' + + @property + def value(self): + return self.url + @value.setter + def value(self, value): + self.url = value + + @property + def url(self): + return self.m.ImageURL + @url.setter + def url(self, value): + self.m.ImageURL = None + self.m.ImageURL = _P.to_url(value) + + +class UnoListBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._path = '' + + def __setattr__(self, name, value): + if name in ('_path',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def type(self): + return 'listbox' + + @property + def value(self): + return self.obj.getSelectedItem() + + @property + def count(self): + return len(self.data) + + @property + def data(self): + return self.model.StringItemList + @data.setter + def data(self, values): + self.model.StringItemList = list(sorted(values)) + + @property + def path(self): + return self._path + @path.setter + def path(self, value): + self._path = value + + def unselect(self): + self.obj.selectItem(self.value, False) + return + + def select(self, pos=0): + if isinstance(pos, str): + self.obj.selectItem(pos, True) + else: + self.obj.selectItemPos(pos, True) + return + + def clear(self): + self.model.removeAllItems() + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def insert(self, value, path='', pos=-1, show=True): + if pos < 0: + pos = self.count + if path: + self.model.insertItem(pos, value, self._set_image_url(path)) + else: + self.model.insertItemText(pos, value) + if show: + self.select(pos) + return + + +class UnoRoadmap(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._options = () + + def __setattr__(self, name, value): + if name in ('_options',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def options(self): + return self._options + @options.setter + def options(self, values): + self._options = values + for i, v in enumerate(values): + opt = self.model.createInstance() + opt.ID = i + opt.Label = v + self.model.insertByIndex(i, opt) + return + + @property + def enabled(self): + return True + @enabled.setter + def enabled(self, value): + for m in self.model: + m.Enabled = value + return + + def set_enabled(self, index, value): + self.model.getByIndex(index).Enabled = value + return + + +class UnoTree(UnoBaseObject): + + def __init__(self, obj, ): + super().__init__(obj) + self._tdm = None + self._data = [] + + def __setattr__(self, name, value): + if name in ('_tdm', '_data'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def selection(self): + sel = self.obj.Selection + return sel.DataValue, sel.DisplayValue + + @property + def parent(self): + parent = self.obj.Selection.Parent + if parent is None: + return () + return parent.DataValue, parent.DisplayValue + + def _get_parents(self, node): + value = (node.DisplayValue,) + parent = node.Parent + if parent is None: + return value + return self._get_parents(parent) + value + + @property + def parents(self): + values = self._get_parents(self.obj.Selection) + return values + + @property + def root(self): + if self._tdm is None: + return '' + return self._tdm.Root.DisplayValue + @root.setter + def root(self, value): + self._add_data_model(value) + + def _add_data_model(self, name): + tdm = create_instance('com.sun.star.awt.tree.MutableTreeDataModel') + root = tdm.createNode(name, True) + root.DataValue = 0 + tdm.setRoot(root) + self.model.DataModel = tdm + self._tdm = self.model.DataModel + return + + @property + def path(self): + return self.root + @path.setter + def path(self, value): + self.data = _P.walk_dir(value, True) + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = list(values) + self._add_data() + + def _add_data(self): + if not self.data: + return + + parents = {} + for node in self.data: + parent = parents.get(node[1], self._tdm.Root) + child = self._tdm.createNode(node[2], False) + child.DataValue = node[0] + parent.appendChild(child) + parents[node[0]] = child + self.obj.expandNode(self._tdm.Root) + return + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html +class UnoGrid(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._gdm = self.model.GridDataModel + self._data = [] + self._formats = () + + def __setattr__(self, name, value): + if name in ('_gdm', '_data', '_formats'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getitem__(self, key): + value = self._gdm.getCellData(key[0], key[1]) + return value + + def __setitem__(self, key, value): + self._gdm.updateCellData(key[0], key[1], value) + return + + @property + def type(self): + return 'grid' + + @property + def columns(self): + return {} + @columns.setter + def columns(self, values): + # ~ self._columns = values + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html + model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) + for properties in values: + column = create_instance('com.sun.star.awt.grid.GridColumn', True) + for k, v in properties.items(): + setattr(column, k, v) + model.addColumn(column) + self.model.ColumnModel = model + return + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = values + self.clear() + headings = tuple(range(1, len(values) + 1)) + self._gdm.addRows(headings, values) + # ~ rows = range(grid_dm.RowCount) + # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows] + # ~ grid.Model.RowBackgroundColors = tuple(colors) + return + + @property + def value(self): + if self.column == -1 or self.row == -1: + return '' + return self[self.column, self.row] + @value.setter + def value(self, value): + if self.column > -1 and self.row > -1: + self[self.column, self.row] = value + + @property + def row(self): + return self.obj.CurrentRow + + @property + def row_count(self): + return self._gdm.RowCount + + @property + def column(self): + return self.obj.CurrentColumn + + @property + def column(self): + return self.obj.CurrentColumn + + @property + def is_valid(self): + return not (self.row == -1 or self.column == -1) + + @property + def formats(self): + return self._formats + @formats.setter + def formats(self, values): + self._formats = values + + def clear(self): + self._gdm.removeAllRows() + return + + def _format_columns(self, data): + row = data + if self.formats: + for i, f in enumerate(formats): + if f: + row[i] = f.format(data[i]) + return row + + def add_row(self, data): + self._data.append(data) + row = self._format_columns(data) + self._gdm.addRow(self.row_count + 1, row) + return + + def set_cell_tooltip(self, col, row, value): + self._gdm.updateCellToolTip(col, row, value) + return + + def get_cell_tooltip(self, col, row): + value = self._gdm.getCellToolTip(col, row) + return value + + def sort(self, column, asc=True): + self._gdm.sortByColumn(column, asc) + self.update_row_heading() + return + + def update_row_heading(self): + for i in range(self.row_count): + self._gdm.updateRowHeading(i, i + 1) + return + + def remove_row(self, row): + self._gdm.removeRow(row) + del self._data[row] + self.update_row_heading() + return + + +class UnoPage(object): + + def __init__(self, obj): + self._obj = obj + self._events = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._obj.Model + + # ~ @property + # ~ def id(self): + # ~ return self.m.TabPageID + + @property + def parent(self): + return self.obj.Context + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args + + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + args = self._special_properties(tipo, args) + model = self.model.createInstance(UNO_MODELS[tipo]) + _set_properties(model, args) + name = args['Name'] + self.model.insertByName(name, model) + control = self.obj.getControl(name) + _add_listeners(self._events, control, name) + control = UNO_CLASSES[tipo](control) + + if tipo in ('listbox',): + control.path = self.path + + if tipo == 'tree' and root: + control.root = root + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + return control + + +class UnoPages(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._sheets = [] + self._events = None + + def __setattr__(self, name, value): + if name in ('_sheets', '_events'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getitem__(self, index): + name = index + if isinstance(index, int): + name = f'sheet{index}' + sheet = self.obj.getControl(name) + page = UnoPage(sheet) + page._events = self._events + return page + + @property + def type(self): + return 'pages' + + @property + def current(self): + return self.obj.ActiveTabID + @property + def active(self): + return self.current + + @property + def sheets(self): + return self._sheets + @sheets.setter + def sheets(self, values): + self._sheets = values + for i, title in enumerate(values): + sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') + sheet.Title = title + self.m.insertByName(f'sheet{i + 1}', sheet) + return + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + def insert(self, title): + self._sheets.append(title) + id = len(self._sheets) + sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') + sheet.Title = title + self.m.insertByName(f'sheet{id}', sheet) + return self[id] + + def remove(self, id): + self.obj.removeTab(id) + return + + def activate(self, id): + self.obj.activateTab(id) + return + + +UNO_CLASSES = { + 'label': UnoLabel, + 'link': UnoLabelLink, + 'button': UnoButton, + 'radio': UnoRadio, + 'checkbox': UnoCheckBox, + 'text': UnoText, + 'image': UnoImage, + 'listbox': UnoListBox, + 'roadmap': UnoRoadmap, + 'tree': UnoTree, + 'grid': UnoGrid, + 'pages': UnoPages, +} + +UNO_MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', +} +# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', +# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', +# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', +# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', +# ~ 'NumericField': 'com.sun.star.awt.UnoControlNumericFieldModel', +# ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', +# ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', +# ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', +# ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', +# ~ 'SpinButton': 'com.sun.star.awt.UnoControlSpinButtonModel', +# ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', +# ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', + + +class LODialog(object): + SEPARATION = 5 + MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + } + + def __init__(self, args): + self._obj = self._create(args) + self._model = self.obj.Model + self._events = None + self._modal = True + self._controls = {} + self._color_on_focus = COLOR_ON_FOCUS + self._id = '' + self._path = '' + self._init_controls() + + def _create(self, args): + service = 'com.sun.star.awt.DialogProvider' + path = args.pop('Path', '') + if path: + dp = create_instance(service, True) + dlg = dp.createDialog(_P.to_url(path)) + return dlg + + if 'Location' in args: + name = args['Name'] + library = args.get('Library', 'Standard') + location = args.get('Location', 'application').lower() + if location == 'user': + location = 'application' + url = f'vnd.sun.star.script:{library}.{name}?location={location}' + if location == 'document': + dp = create_instance(service, args=docs.active.obj) + else: + dp = create_instance(service, True) + # ~ uid = docs.active.uid + # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' + dlg = dp.createDialog(url) + return dlg + + dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) + model = create_instance('com.sun.star.awt.UnoControlDialogModel', True) + toolkit = create_instance('com.sun.star.awt.Toolkit', True) + _set_properties(model, args) + dlg.setModel(model) + dlg.setVisible(False) + dlg.createPeer(toolkit, None) + return dlg + + def _get_type_control(self, name): + name = name.split('.')[2] + types = { + 'UnoFixedTextControl': 'label', + 'UnoEditControl': 'text', + 'UnoButtonControl': 'button', + } + return types[name] + + def _init_controls(self): + for control in self.obj.getControls(): + tipo = self._get_type_control(control.ImplementationName) + name = control.Model.Name + control = UNO_CLASSES[tipo](control) + setattr(self, name, control) + return + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + + @property + def controls(self): + return self._controls + + @property + def path(self): + return self._path + @property + def id(self): + return self._id + @id.setter + def id(self, value): + self._id = value + self._path = _P.from_id(value) + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + @property + def width(self): + return self.model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers(self) + self._connect_listeners() + + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + self._color_on_focus = get_color(value) + + def _connect_listeners(self): + for control in self.obj.Controls: + _add_listeners(self.events, control, control.Model.Name) + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args + + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + args = self._special_properties(tipo, args) + model = self.model.createInstance(self.MODELS[tipo]) + _set_properties(model, args) + name = args['Name'] + self.model.insertByName(name, model) + control = self.obj.getControl(name) + _add_listeners(self.events, control, name) + control = UNO_CLASSES[tipo](control) + + if tipo in ('listbox',): + control.path = self.path + + if tipo == 'tree' and root: + control.root = root + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + self._controls[name] = control + return control + + def center(self, control, x=0, y=0): + w = self.width + h = self.height + + if isinstance(control, tuple): + wt = self.SEPARATION * -1 + for c in control: + wt += c.width + self.SEPARATION + x = w / 2 - wt / 2 + for c in control: + c.x = x + x = c.x + c.width + self.SEPARATION + return + + if x < 0: + x = w + x - control.width + elif x == 0: + x = w / 2 - control.width / 2 + if y < 0: + y = h + y - control.height + elif y == 0: + y = h / 2 - control.height / 2 + control.x = x + control.y = y + return + + def open(self, modal=True): + self._modal = modal + if modal: + return self.obj.execute() + else: + self.visible = True + return + + def close(self, value=0): + if self._modal: + value = self.obj.endDialog(value) + else: + self.visible = False + self.obj.dispose() + return value + + +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 + + +def create_window(args): + return LOWindow(args) + + +class classproperty: + def __init__(self, method=None): + self.fget = method + + def __get__(self, instance, cls=None): + return self.fget(cls) + + def getter(self, method): + self.fget = method + return self + + +class ClipBoard(object): + SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard' + CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' + + class TextTransferable(unohelper.Base, XTransferable): + + def __init__(self, text): + df = DataFlavor() + df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT + df.HumanPresentableName = "encoded text utf-16" + self.flavors = (df,) + self._data = text + + def getTransferData(self, flavor): + return self._data + + def getTransferDataFlavors(self): + return self.flavors + + + @classmethod + def set(cls, value): + ts = cls.TextTransferable(value) + sc = create_instance(cls.SERVICE) + sc.setContents(ts, None) + return + + @classproperty + def contents(cls): + df = None + text = '' + sc = create_instance(cls.SERVICE) + transferable = sc.getContents() + data = transferable.getTransferDataFlavors() + for df in data: + if df.MimeType == cls.CLIPBOARD_FORMAT_TEXT: + break + if df: + text = transferable.getTransferData(df) + return text +_CB = ClipBoard + + +class Paths(object): + FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' + FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' + + def __init__(self, path=''): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + self._path = Path(path) + + @property + def path(self): + return str(self._path.parent) + + @property + def file_name(self): + return self._path.name + + @property + def name(self): + return self._path.stem + + @property + def ext(self): + return self._path.suffix[1:] + + @property + def info(self): + return self.path, self.file_name, self.name, self.ext + + @property + def url(self): + return self._path.as_uri() + + @property + def size(self): + return self._path.stat().st_size + + @classproperty + def home(self): + return str(Path.home()) + + @classproperty + def documents(self): + return self.config() + + @classproperty + def temp_dir(self): + return tempfile.gettempdir() + + @classproperty + def python(self): + if IS_WIN: + path = self.join(self.config('Module'), PYTHON) + elif IS_MAC: + path = self.join(self.config('Module'), '..', 'Resources', PYTHON) + else: + path = sys.executable + return path + + @classmethod + def dir_tmp(self, only_name=False): + dt = tempfile.TemporaryDirectory() + if only_name: + dt = dt.name + return dt + + @classmethod + def tmp(cls, ext=''): + tmp = tempfile.NamedTemporaryFile(suffix=ext) + return tmp.name + + @classmethod + def save_tmp(cls, data): + path_tmp = cls.tmp() + cls.save(path_tmp, data) + return path_tmp + + @classmethod + def config(cls, name='Work'): + """ + Return de path name in 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') + return cls.to_system(getattr(path, name)) + + @classmethod + def get(cls, init_dir='', filters: str=''): + """ + 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): + """ + init_folder: folder default open + multiple: True for multiple selected + filters: 'xml' or 'xml,txt' + """ + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) + + file_picker = create_instance(cls.FILE_PICKER) + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.setMultiSelectionMode(multiple) + + if filters: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + path = '' + if file_picker.execute(): + files = file_picker.getSelectedFiles() + path = [cls.to_system(f) for f in files] + if not multiple: + path = path[0] + return path + + @classmethod + def replace_ext(cls, path, new_ext): + p = Paths(path) + name = f'{p.name}.{new_ext}' + path = cls.join(p.path, name) + return path + + @classmethod + def exists(cls, path): + result = False + if path: + path = cls.to_system(path) + result = Path(path).exists() + return result + + @classmethod + def exists_app(cls, name_app): + return bool(shutil.which(name_app)) + + @classmethod + def open(cls, path): + if IS_WIN: + os.startfile(path) + else: + pid = subprocess.Popen(['xdg-open', path]).pid + return + + @classmethod + def is_dir(cls, path): + return Path(path).is_dir() + + @classmethod + def is_file(cls, path): + return Path(path).is_file() + + @classmethod + def join(cls, *paths): + return str(Path(paths[0]).joinpath(*paths[1:])) + + @classmethod + def save(cls, path, data, encoding='utf-8'): + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path, data): + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def read(cls, path, encoding='utf-8'): + data = Path(path).read_text(encoding=encoding) + return data + + @classmethod + def read_bin(cls, path): + data = Path(path).read_bytes() + return data + + @classmethod + def to_url(cls, path): + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def to_system(cls, path): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def kill(cls, path): + result = True + p = Path(path) + + try: + if p.is_file(): + p.unlink() + elif p.is_dir(): + shutil.rmtree(path) + 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_dir(cls, path, tree=False): + 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='', pwd=''): + 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 + + @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 True + + @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): + gp = create_instance('com.sun.star.graphic.GraphicProvider') + image = gp.queryGraphic(( + PropertyValue(Name='URL', Value=cls.to_url(path)), + )) + return image + + @classmethod + def copy(cls, source, target='', name=''): + p, f, n, e = _P(source).info + if target: + p = target + if name: + e = '' + n = name + path_new = cls.join(p, f'{n}{e}') + shutil.copy(source, path_new) + return path_new +_P = Paths + + +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 == 'sheets': + return LOSheets() + if name == 'cells': + return LOCells() + if name == 'menus': + return LOMenus() + if name == 'shortcuts': + return LOShortCuts() + if name == 'clipboard': + return ClipBoard + 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 + + +# ~ From request +# ~ https://github.com/psf/requests/blob/master/requests/structures.py#L15 +class CaseInsensitiveDict(MutableMapping): + + def __init__(self, data=None, **kwargs): + self._store = OrderedDict() + if data is None: + data = {} + self.update(data, **kwargs) + + def __setitem__(self, key, value): + # Use the lowercased key for lookups, but store the actual + # key alongside the value. + self._store[key.lower()] = (key, value) + + def __getitem__(self, key): + return self._store[key.lower()][1] + + def __delitem__(self, key): + del self._store[key.lower()] + + def __iter__(self): + return (casedkey for casedkey, mappedvalue in self._store.values()) + + def __len__(self): + return len(self._store) + + def lower_items(self): + """Like iteritems(), but with all lowercase keys.""" + values = ( + (lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items() + ) + return values + + # Copy is required + def copy(self): + return CaseInsensitiveDict(self._store.values()) + + def __repr__(self): + return str(dict(self.items())) + + +# ~ 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/registration/license_en.txt b/source/registration/license_en.txt new file mode 100644 index 0000000..5ba36ef --- /dev/null +++ b/source/registration/license_en.txt @@ -0,0 +1,14 @@ +This file is part of ZazDoc. + + ZazDoc 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. + + ZazDoc 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 ZazDoc. If not, see . diff --git a/source/registration/license_es.txt b/source/registration/license_es.txt new file mode 100644 index 0000000..5ba36ef --- /dev/null +++ b/source/registration/license_es.txt @@ -0,0 +1,14 @@ +This file is part of ZazDoc. + + ZazDoc 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. + + ZazDoc 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 ZazDoc. If not, see . diff --git a/zaz.py b/zaz.py new file mode 100755 index 0000000..83e6058 --- /dev/null +++ b/zaz.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python3 + +# == Rapid Develop Macros in LibreOffice == + +# ~ This file is part of ZAZ. + +# ~ https://git.elmau.net/elmau/zaz + +# ~ ZAZ is free software: you can redistribute it and/or modify +# ~ it under the terms of the GNU General Public License as published by +# ~ the Free Software Foundation, either version 3 of the License, or +# ~ (at your option) any later version. + +# ~ ZAZ is distributed in the hope that it will be useful, +# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of +# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# ~ GNU General Public License for more details. + +# ~ You should have received a copy of the GNU General Public License +# ~ along with ZAZ. If not, see . + +import argparse +import os +import py_compile +import re +import sys +import zipfile +from datetime import datetime +from pathlib import Path +from shutil import copyfile +from subprocess import call +from xml.etree import ElementTree as ET +from xml.dom.minidom import parseString + + +from conf import ( + DATA, + DIRS, + DOMAIN, + EXTENSION, + FILES, + INFO, + PATHS, + TYPE_EXTENSION, + USE_LOCALES, + log) + + +EASYMACRO = 'easymacro.py' + + +class LiboXML(object): + CONTEXT = { + 'calc': 'com.sun.star.sheet.SpreadsheetDocument', + 'writer': 'com.sun.star.text.TextDocument', + 'impress': 'com.sun.star.presentation.PresentationDocument', + 'draw': 'com.sun.star.drawing.DrawingDocument', + 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + 'math': 'com.sun.star.formula.FormulaProperties', + 'basic': 'com.sun.star.script.BasicIDE', + } + TYPES = { + 'py': 'application/vnd.sun.star.uno-component;type=Python', + 'pyc': 'application/binary', + 'zip': 'application/binary', + 'xcu': 'application/vnd.sun.star.configuration-data', + 'rdb': 'application/vnd.sun.star.uno-typelibrary;type=RDB', + 'xcs': 'application/vnd.sun.star.configuration-schema', + 'help': 'application/vnd.sun.star.help', + 'component': 'application/vnd.sun.star.uno-components', + } + NS_MANIFEST = { + 'manifest_version': '1.2', + 'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0', + 'xmlns:loext': 'urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0', + } + NS_DESCRIPTION = { + 'xmlns': 'http://openoffice.org/extensions/description/2006', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'xmlns:d': 'http://openoffice.org/extensions/description/2006', + } + NS_ADDONS = { + 'xmlns:xs': 'http://www.w3.org/2001/XMLSchema', + 'xmlns:oor': 'http://openoffice.org/2001/registry', + } + NS_UPDATE = { + 'xmlns': 'http://openoffice.org/extensions/update/2006', + 'xmlns:d': 'http://openoffice.org/extensions/description/2006', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + } + + def __init__(self): + self._manifest = None + self._paths = [] + self._path_images = '' + self._toolbars = [] + + def _save_path(self, attr): + self._paths.append(attr['{{{}}}full-path'.format(self.NS_MANIFEST['manifest'])]) + return + + def _clean(self, name, nodes): + has_words = re.compile('\\w') + + if not re.search(has_words, str(nodes.tail)): + nodes.tail = '' + if not re.search(has_words, str(nodes.text)): + nodes.text = '' + + for node in nodes: + if name == 'manifest': + self._save_path(node.attrib) + if not re.search(has_words, str(node.tail)): + node.tail = '' + if not re.search(has_words, str(node.text)): + node.text = '' + return + + def new_manifest(self, data): + attr = { + 'manifest:version': self.NS_MANIFEST['manifest_version'], + 'xmlns:manifest': self.NS_MANIFEST['manifest'], + 'xmlns:loext': self.NS_MANIFEST['xmlns:loext'], + } + self._manifest = ET.Element('manifest:manifest', attr) + return self.add_data_manifest(data) + + def parse_manifest(self, data): + ET.register_namespace('manifest', self.NS_MANIFEST['manifest']) + self._manifest = ET.fromstring(data) + attr = {'xmlns:loext': self.NS_MANIFEST['xmlns:loext']} + self._manifest.attrib.update(**attr) + self._clean('manifest', self._manifest) + return + + def add_data_manifest(self, data): + node_name = 'manifest:file-entry' + attr = { + 'manifest:full-path': '', + 'manifest:media-type': '', + } + for path in data: + if path in self._paths: + continue + ext = path.split('.')[-1] + attr['manifest:full-path'] = path + attr['manifest:media-type'] = self.TYPES.get(ext, '') + ET.SubElement(self._manifest, node_name, attr) + return self._get_xml(self._manifest) + + def new_description(self, data): + doc = ET.Element('description', self.NS_DESCRIPTION) + + key = 'identifier' + ET.SubElement(doc, key, data[key]) + + key = 'version' + ET.SubElement(doc, key, data[key]) + + key = 'display-name' + node = ET.SubElement(doc, key) + for k, v in data[key].items(): + sn = ET.SubElement(node, 'name', {'lang': k}) + sn.text = v + + node = ET.SubElement(doc, 'extension-description') + for k in data[key].keys(): + attr = { + 'lang': k, + 'xlink:href': f'description/desc_{k}.txt', + } + ET.SubElement(node, 'src', attr) + + key = 'icon' + node = ET.SubElement(doc, key) + attr = {'xlink:href': f"images/{data[key]}"} + ET.SubElement(node, 'default', attr) + + key = 'publisher' + node = ET.SubElement(doc, key) + for k, v in data[key].items(): + attr = { + 'xlink:href': v['link'], + 'lang': k, + } + sn = ET.SubElement(node, 'name', attr) + sn.text = v['text'] + + key = 'display-name' + node = ET.SubElement(doc, 'registration') + attr = { + 'accept-by': 'user', + 'suppress-on-update': 'true', + } + node = ET.SubElement(node, 'simple-license', attr) + for k in data[key].keys(): + attr = { + 'xlink:href': f"{DIRS['registration']}/license_{k}.txt", + 'lang': k + } + ET.SubElement(node, 'license-text', attr) + + if data['update']: + node = ET.SubElement(doc, 'update-information') + ET.SubElement(node, 'src', {'xlink:href': data['update']}) + + return self._get_xml(doc) + + def _get_context(self, args): + if not args: + return '' + context = ','.join([self.CONTEXT[v] for v in args.split(',')]) + return context + + def _add_node_value(self, node, name, value='_self'): + attr = {'oor:name': name, 'oor:type': 'xs:string'} + sn = ET.SubElement(node, 'prop', attr) + sn = ET.SubElement(sn, 'value') + sn.text = value + return + + def _add_menu(self, id_extension, node, index, menu, in_menu_bar=True): + if in_menu_bar: + attr = { + 'oor:name': index, + 'oor:op': 'replace', + } + subnode = ET.SubElement(node, 'node', attr) + else: + subnode = node + + attr = {'oor:name': 'Title', 'oor:type': 'xs:string'} + sn1 = ET.SubElement(subnode, 'prop', attr) + for k, v in menu['title'].items(): + sn2 = ET.SubElement(sn1, 'value', {'xml:lang': k}) + sn2.text = v + value = self._get_context(menu['context']) + self._add_node_value(subnode, 'Context', value) + + if 'submenu' in menu: + sn = ET.SubElement(subnode, 'node', {'oor:name': 'Submenu'}) + for i, m in enumerate(menu['submenu']): + self._add_menu(id_extension, sn, f'{index}.s{i}', m) + if m.get('toolbar', False): + self._toolbars.append(m) + return + + value = f"service:{id_extension}?{menu['argument']}" + self._add_node_value(subnode, 'URL', value) + self._add_node_value(subnode, 'Target') + value = f"%origin%/{self._path_images}/{menu['icon']}" + self._add_node_value(subnode, 'ImageIdentifier', value) + return + + def new_addons(self, id_extension, data): + in_menu_bar = data['parent'] == 'OfficeMenuBar' + self._path_images = data['images'] + attr = { + 'oor:name': 'Addons', + 'oor:package': 'org.openoffice.Office', + } + attr.update(self.NS_ADDONS) + doc = ET.Element('oor:component-data', attr) + parent = ET.SubElement(doc, 'node', {'oor:name': 'AddonUI'}) + node = ET.SubElement(parent, 'node', {'oor:name': data['parent']}) + + op = 'fuse' + if in_menu_bar: + op = 'replace' + + attr = {'oor:name': id_extension, 'oor:op': op} + node = ET.SubElement(node, 'node', attr) + + if in_menu_bar: + attr = {'oor:name': 'Title', 'oor:type': 'xs:string'} + subnode = ET.SubElement(node, 'prop', attr) + for k, v in data['main'].items(): + sn = ET.SubElement(subnode, 'value', {'xml:lang': k}) + sn.text = v + + self._add_node_value(node, 'Target') + node = ET.SubElement(node, 'node', {'oor:name': 'Submenu'}) + + for i, menu in enumerate(data['menus']): + self._add_menu(id_extension, node, f'm{i}', menu, in_menu_bar) + if menu.get('toolbar', False): + self._toolbars.append(menu) + + if self._toolbars: + attr = {'oor:name': 'OfficeToolBar'} + toolbar = ET.SubElement(parent, 'node', attr) + attr = {'oor:name': id_extension, 'oor:op': 'replace'} + toolbar = ET.SubElement(toolbar, 'node', attr) + for t, menu in enumerate(self._toolbars): + self._add_menu(id_extension, toolbar, f't{t}', menu) + + return self._get_xml(doc) + + def _add_shortcut(self, node, key, id_extension, arg): + attr = {'oor:name': key, 'oor:op': 'fuse'} + subnode = ET.SubElement(node, 'node', attr) + subnode = ET.SubElement(subnode, 'prop', {'oor:name': 'Command'}) + subnode = ET.SubElement(subnode, 'value', {'xml:lang': 'en-US'}) + subnode.text = f"service:{id_extension}?{arg}" + return + + def _get_acceleartors(self, menu): + if 'submenu' in menu: + for m in menu['submenu']: + return self._get_acceleartors(m) + + if not menu.get('shortcut', ''): + return '' + + return menu + + def new_accelerators(self, id_extension, menus): + attr = { + 'oor:name': 'Accelerators', + 'oor:package': 'org.openoffice.Office', + } + attr.update(self.NS_ADDONS) + doc = ET.Element('oor:component-data', attr) + parent = ET.SubElement(doc, 'node', {'oor:name': 'PrimaryKeys'}) + + data = [] + for m in menus: + info = self._get_acceleartors(m) + if info: + data.append(info) + + node_global = None + node_modules = None + for m in data: + if m['context']: + if node_modules is None: + node_modules = ET.SubElement( + parent, 'node', {'oor:name': 'Modules'}) + for app in m['context'].split(','): + node = ET.SubElement( + node_modules, 'node', {'oor:name': self.CONTEXT[app]}) + self._add_shortcut( + node, m['shortcut'], id_extension, m['argument']) + else: + if node_global is None: + node_global = ET.SubElement( + parent, 'node', {'oor:name': 'Global'}) + self._add_shortcut( + node_global, m['shortcut'], id_extension, m['argument']) + + return self._get_xml(doc) + + def new_update(self, extension, url_oxt): + doc = ET.Element('description', self.NS_UPDATE) + ET.SubElement(doc, 'identifier', {'value': extension['id']}) + ET.SubElement(doc, 'version', {'value': extension['version']}) + node = ET.SubElement(doc, 'update-download') + ET.SubElement(node, 'src', {'xlink:href': url_oxt}) + node = ET.SubElement(doc, 'release-notes') + return self._get_xml(doc) + + def _get_xml(self, doc): + xml = parseString(ET.tostring(doc, encoding='utf-8')) + return xml.toprettyxml(indent=' ', encoding='utf-8').decode('utf-8') + + +def _exists(path): + return os.path.exists(path) + + +def _join(*paths): + return os.path.join(*paths) + + +def _mkdir(path): + return Path(path).mkdir(parents=True, exist_ok=True) + + +def _save(path, data): + with open(path, 'w') as f: + f.write(data) + return + + +def _get_files(path, filters=''): + paths = [] + if filters in ('*', '*.*'): + filters = '' + for folder, _, files in os.walk(path): + if filters: + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [_join(folder, f) for f in files if pattern.search(f)] + else: + paths += files + return paths + + +def _compress_oxt(): + log.info('Compress OXT extension...') + + path_oxt = _join(DIRS['files'], FILES['oxt']) + + z = zipfile.ZipFile(path_oxt, 'w', compression=zipfile.ZIP_DEFLATED) + root_len = len(os.path.abspath(DIRS['source'])) + for root, dirs, files in os.walk(DIRS['source']): + relative = os.path.abspath(root)[root_len:] + for f in files: + fullpath = _join(root, f) + file_name = _join(relative, f) + if file_name == FILES['idl']: + continue + z.write(fullpath, file_name, zipfile.ZIP_DEFLATED) + z.close() + + log.info('Extension OXT created successfully...') + return + + +def _install_and_test(): + path_oxt = (_join(DIRS['files'], FILES['oxt']),) + call(PATHS['install'] + path_oxt) + log.info('Install extension successfully...') + log.info('Start LibreOffice...') + call(PATHS['soffice']) + return + + +def _validate_new(): + path_source = DIRS['source'] + if not _exists(path_source): + return True + + msg = f'Path: {path_source}, exists, delete first' + log.error(msg) + return False + + +def _create_new_directories(): + path_source = DIRS['source'] + _mkdir(path_source) + path = _join(path_source, DIRS['meta']) + _mkdir(path) + path = _join(path_source, DIRS['description']) + _mkdir(path) + path = _join(path_source, DIRS['images']) + _mkdir(path) + path = _join(path_source, DIRS['registration']) + _mkdir(path) + path = _join(path_source, DIRS['office']) + _mkdir(path) + + if FILES['easymacro'] or DIRS['pythonpath']: + path = _join(path_source, 'pythonpath') + _mkdir(path) + + path = DIRS['files'] + if not _exists(path): + _mkdir(path) + + msg = 'Created directories...' + log.info(msg) + return + + +def _create_new_files(): + path_source = DIRS['source'] + + for k, v in INFO.items(): + file_name = f'license_{k}.txt' + path = _join(path_source, DIRS['registration'], file_name) + _save(path, v['license']) + + if TYPE_EXTENSION > 1: + path = _join(path_source, FILES['idl']) + _save(path, DATA['idl']) + + path = _join(path_source, FILES['py']) + _save(path, DATA['py']) + + + msg = 'Created files...' + log.info(msg) + return + + +def _validate_update(): + if TYPE_EXTENSION == 1: + return True + + if not _exists(PATHS['idlc']): + msg = 'Binary: "idlc" not found' + log.error(msg) + return False + + if not _exists(PATHS['include']): + msg = 'Directory: "include" not found' + log.error(msg) + return False + + if not _exists(PATHS['regmerge']): + msg = 'Binary: "regmerge" not found' + log.error(msg) + return False + + path = _join(DIRS['source'], FILES['idl']) + if not _exists(path): + msg = f'File: "{FILES["idl"]}" not found' + log.error(msg) + return False + + return True + + +def _compile_idl(): + if TYPE_EXTENSION == 1: + return + + log.info('Compilate IDL...') + path_rdb = _join(DIRS['source'], FILES['rdb']) + path_urd = _join(DIRS['source'], FILES['urd']) + + path = _join(DIRS['source'], FILES['idl']) + call([PATHS['idlc'], '-I', PATHS['include'], path]) + call([PATHS['regmerge'], path_rdb, '/UCR', path_urd]) + os.remove(path_urd) + + log.info('Compilate IDL successfully...') + return + + +def _update_files(): + path_files = DIRS['files'] + if not _exists(path_files): + _mkdir(path_files) + + path_source = DIRS['source'] + + for k, v in INFO.items(): + file_name = FILES['ext_desc'].format(k) + path = _join(path_source, DIRS['description'], file_name) + _save(path, v['description']) + + path_logo = EXTENSION['icon'][0] + if _exists(path_logo): + file_name = EXTENSION['icon'][1] + path = _join(path_source, DIRS['images'], file_name) + copyfile(path_logo, path) + + files = os.listdir(DIRS['images']) + for f in files: + if f[-3:].lower() == 'bmp': + source = _join(DIRS['images'], f) + target = _join(path_source, DIRS['images'], f) + copyfile(source, target) + + if FILES['easymacro']: + source = EASYMACRO + target = _join(path_source, 'pythonpath', source) + copyfile(source, target) + + xml = LiboXML() + + path = _join(path_source, DIRS['meta'], FILES['manifest']) + data = xml.new_manifest(DATA['manifest']) + _save(path, data) + + path = _join(path_source, FILES['description']) + data = xml.new_description(DATA['description']) + _save(path, data) + + if TYPE_EXTENSION == 1: + path = _join(path_source, FILES['addons']) + data = xml.new_addons(EXTENSION['id'], DATA['addons']) + _save(path, data) + + path = _join(path_source, DIRS['office']) + _mkdir(path) + path = _join(path_source, DIRS['office'], FILES['shortcut']) + data = xml.new_accelerators(EXTENSION['id'], DATA['addons']['menus']) + _save(path, data) + + + if TYPE_EXTENSION == 3: + path = _join(path_source, FILES['addin']) + _save(path, DATA['addin']) + + if USE_LOCALES: + msg = "Don't forget generate DOMAIN.pot for locales" + for lang in EXTENSION['languages']: + path = _join(path_source, DIRS['locales'], lang, 'LC_MESSAGES') + Path(path).mkdir(parents=True, exist_ok=True) + log.info(msg) + + if DATA['update']: + path_xml = _join(path_files, FILES['update']) + data = xml.new_update(EXTENSION, DATA['update']) + _save(path_xml, data) + + _compile_idl() + return + + +def _create(): + if not _validate_new(): + return + + _create_new_directories() + _create_new_files() + _update_files() + + msg = f"New extension: {EXTENSION['name']} make sucesfully...\n" + msg += '\tNow, you can install and test: zaz.py -i' + log.info(msg) + return + + +def _get_info_path(path): + path, filename = os.path.split(path) + name, extension = os.path.splitext(filename) + return (path, filename, name, extension) + + +def _zip_embed(source, files): + PATH = 'Scripts/python/' + FILE_PYC = 'easymacro.pyc' + + p, f, name, e = _get_info_path(source) + now = datetime.now().strftime('_%Y%m%d_%H%M%S') + path_source = _join(p, name + now + e) + copyfile(source, path_source) + target = source + + py_compile.compile(EASYMACRO, FILE_PYC) + xml = LiboXML() + + path_easymacro = PATH + FILE_PYC + names = [f[1] for f in files] + [path_easymacro] + nodes = [] + with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as zt: + with zipfile.ZipFile(path_source, compression=zipfile.ZIP_DEFLATED) as zs: + for name in zs.namelist(): + if FILES['manifest'] in name: + path_manifest = name + xml_manifest = zs.open(name).read() + elif name in names: + continue + else: + zt.writestr(name, zs.open(name).read()) + + data = [] + for path, name in files: + data.append(name) + zt.write(path, name) + + zt.write(FILE_PYC, path_easymacro) + data.append(path_easymacro) + + xml.parse_manifest(xml_manifest) + xml_manifest = xml.add_data_manifest(data) + zt.writestr(path_manifest, xml_manifest) + + os.unlink(FILE_PYC) + return + + +def _embed(args): + PATH = 'Scripts/python' + PYTHONPATH = 'pythonpath' + + doc = args.document + if not doc: + msg = '-d/--document Path file to embed is mandatory' + log.error(msg) + return + if not _exists(doc): + msg = 'Path file not exists' + log.error(msg) + return + + files = [] + if args.files: + files = args.files.split(',') + source = _join(PATHS['profile'], PATH) + content = os.listdir(source) + if PYTHONPATH in content: + content.remove(PYTHONPATH) + + if files: + files = [(_join(source, f), _join(PATH, f)) for f in files if f in content] + else: + files = [(_join(source, f), _join(PATH, f)) for f in content] + + _zip_embed(doc, files) + + log.info('Embedded macros successfully...') + return + + +def _locales(args): + if args.files: + files = args.files.split(',') + else: + files = _get_files(DIRS['source'], 'py') + paths = ' '.join([f for f in files if not EASYMACRO in f]) + path_pot = _join(DIRS['source'], DIRS['locales'], '{}.pot'.format(DOMAIN)) + call([PATHS['gettext'], '-o', path_pot, paths]) + log.info('POT generate successfully...') + return + + +def _update(): + path_locales = _join(DIRS['source'], DIRS['locales']) + path_pot = _join(DIRS['source'], DIRS['locales'], '{}.pot'.format(DOMAIN)) + if not _exists(path_pot): + log.error('Not exists file POT...') + return + + files = _get_files(path_locales, 'po') + if not files: + log.error('First, generate files PO...') + return + + for f in files: + call([PATHS['msgmerge'], '-U', f, path_pot]) + log.info('\tUpdate: {}'.format(f)) + + log.info('Locales update successfully...') + return + + +def _new(args): + if not args.target: + msg = 'Add argument target: -t PATH_TARGET' + log.error(msg) + return + + if not args.name: + msg = 'Add argument name: -n name-new-extension' + log.error(msg) + return + + path = _join(args.target, args.name) + _mkdir(path) + _mkdir(_join(path, 'files')) + _mkdir(_join(path, 'images')) + path_logo = 'images/pymacros.png' + copyfile(path_logo, _join(path, 'images/logo.png')) + copyfile('zaz.py', _join(path, 'zaz.py')) + copyfile(EASYMACRO, _join(path, 'easymacro.py')) + copyfile('conf.py.example', _join(path, 'conf.py')) + + msg = 'Folders and files copy successfully for new extension.' + log.info(msg) + msg = f'Change to folder: {path}' + log.info(msg) + return + + +def main(args): + if args.new: + _new(args) + return + + if args.update: + _update() + return + + if args.locales: + _locales(args) + return + + if args.embed: + _embed(args) + return + + if args.create: + _create() + return + + if not _validate_update(): + return + + if not args.only_compress: + _update_files() + + _compress_oxt() + + if args.install: + _install_and_test() + + log.info('Extension make successfully...') + return + + +def _process_command_line_arguments(): + parser = argparse.ArgumentParser( + description='Make LibreOffice extensions') + parser.add_argument('-new', '--new', dest='new', action='store_true', + default=False, required=False) + parser.add_argument('-t', '--target', dest='target', default='') + parser.add_argument('-n', '--name', dest='name', default='', required=False) + parser.add_argument('-c', '--create', dest='create', action='store_true', + default=False, required=False) + parser.add_argument('-i', '--install', dest='install', action='store_true', + default=False, required=False) + parser.add_argument('-e', '--embed', dest='embed', action='store_true', + default=False, required=False) + parser.add_argument('-d', '--document', dest='document', default='') + parser.add_argument('-f', '--files', dest='files', default='') + parser.add_argument('-l', '--locales', dest='locales', action='store_true', + default=False, required=False) + parser.add_argument('-u', '--update', dest='update', action='store_true', + default=False, required=False) + parser.add_argument('-oc', '--only_compress', dest='only_compress', + action='store_true', default=False, required=False) + return parser.parse_args() + + +if __name__ == '__main__': + args = _process_command_line_arguments() + main(args) +