#!/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 base64 import csv 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 from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError 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 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.TextContentAnchorType import AS_CHARACTER from com.sun.star.awt import XActionListener from com.sun.star.lang import XEventListener from com.sun.star.awt import XMouseListener from com.sun.star.awt import XMouseMotionListener from com.sun.star.awt import XFocusListener # ~ 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 try: from peewee import Database, DateTimeField, DateField, TimeField, \ __exception_wrapper__ except ImportError as e: Database = DateField = TimeField = DateTimeField = object print('Install peewee') 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') 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 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', } DEFAULT_MIME_TYPE = 'png' 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', } 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, key=''): 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] 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, 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): m = create_instance('mytools.Mri') if m is None: msg = 'Extension MRI not found' error(msg) return 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=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') 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): 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): #~ 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, domain='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=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 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 # ~ 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'): 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 LOImage(object): TYPE = { 'png': 'image/png', 'jpg': 'image/jpeg', } def __init__(self, obj): self._obj = obj @property def obj(self): return self._obj @property def name(self): return self.obj.Name or 'img' @property def mimetype(self): return self.obj.Bitmap.MimeType def save(self, path, mimetype=DEFAULT_MIME_TYPE): p = _P(path) if _P.is_dir(path): name = self.name else: path = p.path name = p.name path = _P.join(path, f'{name}.{mimetype.lower()}') args = dict( URL = _P.to_url(path), MimeType = self.TYPE[mimetype], ) if not _export_image(self.obj, args): path = '' # ~ size = len(self.obj.Bitmap.DIB) # ~ data = self.obj.GraphicStream.readBytes((), size) # ~ data = data[-1].value # ~ data = self.obj.Bitmap.DIB.value # ~ data = self.obj.Graphic.DIB.value # ~ _P.save_bin(path, data) return path 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 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={}): 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 LOFormControl(LOBaseObject): def __init__(self, obj): self._obj = obj self._control = self.doc.CurrentController.getControl(self.obj) def __setattr__(self, name, value): if name == '_control': self.__dict__[name] = value else: super().__setattr__(name, value) @property def doc(self): return self.obj.Parent.Parent.Parent @property def name(self): return self.obj.Name @property def label(self): return self.obj.Label def set_focus(self): self._control.setFocus() return class LOForm(object): def __init__(self, obj): self._obj = obj def __getitem__(self, index): return LOFormControl(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 def __len__(self): return len(self.obj) @property def obj(self): return self._obj class LOSheetForms(object): def __init__(self, obj): self._obj = obj def __getitem__(self, index): return LOForm(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 def __len__(self): return len(self.obj) @property def obj(self): return self._obj class LOSheetRows(object): def __init__(self, sheet): self._sheet = sheet self._obj = sheet.obj.Rows def __getitem__(self, index): return LOSheetRows(self.obj[index]) @property def obj(self): return self._obj def insert(self, index, count): self.obj.insertByIndex(index, count) end = index + count return self._sheet[index:end,0:] 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 rows(self): return LOSheetRows(self) @property def forms(self): return LOSheetForms(self.obj.DrawPage.Forms) 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) class LOCalcRows(object): def __init__(self, obj): self._obj = obj def __len__(self): return self.obj.Count def __str__(self): return 'Rows' @property def obj(self): return self._obj @property def count(self): return len(self) @property def visible(self): return self.obj.IsVisible @visible.setter def visible(self, value): self.obj.IsVisible = value 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 __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 LOCalcRows(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) def select(self): self.doc.select(self.obj) return 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_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 auto_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(): 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) for cell in ranges or range(0): 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_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 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 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): return self.obj.String @string.setter def string(self, value): self.obj.String = value @property def value(self): return self.string @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 @property def is_table(self): return self._is_table def offset(self): cursor = self.cursor.getEnd() return LOWriterTextRange(cursor, self._doc) def insert_content(self, data, cursor=None, replace=False): if cursor is None: cursor = self.cursor self.text.insertTextContent(cursor, data, replace) return def insert_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 def __getitem__(self, index): for i, p in enumerate(self.obj): if i == index: obj = LOWriterTextRange(p, self._doc) break return obj 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 class LOWriter(LODocument): def __init__(self, obj): super().__init__(obj) self._type = WRITER @property def text(self): return LOWriterTextRange(self.obj.Text, self) @property def paragraphs(self): return LOWriterTextRanges(self.obj.Text, 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) class LOShape(LOBaseObject): def __init__(self, obj, index): self._index = index super().__init__(obj) @property def type(self): return 'shape' @property def name(self): return self.obj.Name or f'shape{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 def remove(self): self.obj.Parent.remove(self.obj) 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 @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 """ w = args.get('Width', 3000) h = args.get('Height', 3000) x = args.get('X', 1000) y = args.get('Y', 1000) service = f'com.sun.star.drawing.{type_shape}Shape' shape = self.create_instance(service) shape.Size = Size(w, h) shape.Position = Point(x, y) index = self.count shape.Name = f'{type_shape.lower()}{index}' 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 insert_image(self, path, args={}): w = args.get('Width', 3000) h = args.get('Height', 3000) x = args.get('X', 1000) y = args.get('Y', 1000) 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) index = self.count image.Name = f'image{index}' 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.selection 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): doc = None for i, doc in enumerate(self._desktop.Components): if isinstance(index, int) and i == index: doc = _get_class_doc(doc) break elif isinstance(index, str) and doc.Title == index: doc = _get_class_doc(doc) break return doc def __contains__(self, item): doc = self[item] return not doc is None def __iter__(self): self._i = 0 return self def __next__(self): doc = self[self._i] if doc is None: raise StopIteration self._i += 1 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' 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 # ~ BorderColor = ? # ~ FontStyleName = ? # ~ HelpURL = ? class UnoBaseObject(object): def __init__(self, obj, path=''): self._obj = obj self._model = obj.Model # ~ self._path = path 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) @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 UnoCheck(UnoBaseObject): def __init__(self, obj): super().__init__(obj) @property def type(self): return 'check' @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 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 UNO_CLASSES = { 'label': UnoLabel, 'link': UnoLabelLink, 'button': UnoButton, 'radio': UnoRadio, 'check': UnoCheck, 'text': UnoText, 'image': UnoImage, 'listbox': UnoListBox, } 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', 'check': 'com.sun.star.awt.UnoControlCheckBoxModel', 'text': 'com.sun.star.awt.UnoControlEditModel', 'image': 'com.sun.star.awt.UnoControlImageControlModel', 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', # ~ 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', # ~ 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', # ~ 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', # ~ 'tree': 'com.sun.star.awt.tree.TreeControlModel', # ~ 'pages': 'com.sun.star.awt.UnoMultiPageModel', } 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 = '' 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') 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 @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): columns = args.pop('Columns', ()) if tipo == 'link' and not 'Label' in args: args['Label'] = args['URL'] elif tipo == 'grid': args['ColumnModel'] = self._set_column_model(columns) elif tipo == 'button': if 'ImageURL' in args: args['ImageURL'] = self._set_image_url(args['ImageURL']) if not 'FocusOnClick' in args: args['FocusOnClick'] = False elif tipo == 'roadmap': if not 'Height' in args: args['Height'] = self.height if 'Title' in args: args['Text'] = args.pop('Title') elif tipo == 'tab': if not 'Width' in args: args['Width'] = self.width if not 'Height' in args: args['Height'] = self.height return args def add_control(self, args): tipo = args.pop('Type').lower() root = args.pop('Root', '') sheets = args.pop('Sheets', ()) 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 == '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 line 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 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 class Paths(object): FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' 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 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=()): """ Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html filters: Example ( ('XML', '*.xml'), ('TXT', '*.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 path')) file_picker.setDisplayDirectory(init_dir) file_picker.initialize((2,)) if filters: 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.FILE_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.getDisplayDirectory()) return path @classmethod def get_file(cls, init_dir='', filters=(), multiple=False): """ init_folder: folder default open multiple: True for multiple selected filters: Example ( ('XML', '*.xml'), ('TXT', '*.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: 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 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 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 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 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