#!/usr/bin/env python3 # == Rapid Develop Macros in LibreOffice == # ~ https://git.cuates.net/elmau/easymacro # ~ easymacro is free software: you can redistribute it and/or modify # ~ it under the terms of the GNU General Public License as published by # ~ the Free Software Foundation, either version 3 of the License, or # ~ (at your option) any later version. # ~ easymacro is distributed in the hope that it will be useful, # ~ but WITHOUT ANY WARRANTY; without even the implied warranty of # ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # ~ GNU General Public License for more details. # ~ You should have received a copy of the GNU General Public License # ~ along with easymacro. If not, see . import csv import datetime import getpass import hashlib import io import json import logging import os import platform import re import shlex import shutil import socket import ssl import subprocess import sys import tempfile import threading import time import traceback from functools import wraps from pathlib import Path from pprint import pprint from string import Template from typing import Any, Union from socket import timeout from urllib import parse from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError import mailbox import smtplib from smtplib import SMTPException, SMTPAuthenticationError from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.mime.text import MIMEText from email.utils import formatdate from email import encoders import uno import unohelper from com.sun.star.awt import Rectangle, Size, Point from com.sun.star.awt import Key, KeyEvent, KeyModifier from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.beans import PropertyValue, NamedValue from com.sun.star.beans.PropertyConcept import ALL from com.sun.star.datatransfer import XTransferable, DataFlavor from com.sun.star.io import IOException, XOutputStream from com.sun.star.ui.dialogs import TemplateDescription from com.sun.star.sheet import XRangeSelectionListener from com.sun.star.lang import XEventListener from com.sun.star.container import NoSuchElementException # Global variables OS = platform.system() DESKTOP = os.environ.get('DESKTOP_SESSION', '') PC = platform.node() USER = getpass.getuser() IS_WIN = OS == 'Windows' IS_MAC = OS == 'Darwin' LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' LOG_DATE = '%d/%m/%Y %H:%M:%S' if IS_WIN: logging.addLevelName(logging.ERROR, 'ERROR') logging.addLevelName(logging.DEBUG, 'DEBUG') logging.addLevelName(logging.INFO, 'INFO') else: logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) log = logging.getLogger(__name__) _info_debug = f"Python: {sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) TIMEOUT = 10 SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc' _EVENTS = {} PYTHON = 'python' if IS_WIN: PYTHON = 'python.exe' FILES = { 'CONFIG': 'zaz-{}.json', } DIRS = {} MESSAGES = { 'es': { 'OK': 'Aceptar', 'Cancel': 'Cancelar', 'Select path': 'Seleccionar ruta', 'Select directory': 'Seleccionar directorio', 'Select file': 'Seleccionar archivo', 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', } } CTX = uno.getComponentContext() SM = CTX.getServiceManager() # UNO Enum class MessageBoxType(): """Class for import enum `See Api MessageBoxType `_ """ from com.sun.star.awt.MessageBoxType \ import MESSAGEBOX, INFOBOX, WARNINGBOX, ERRORBOX, QUERYBOX MBT = MessageBoxType def create_instance(name: str, with_context: bool=False, argument: Any=None) -> Any: """Create a service instance :param name: Name of service :type name: str :param with_context: If used context :type with_context: bool :param argument: If needed some argument :type argument: Any :return: PyUno instance :rtype: PyUno Object """ if with_context: instance = SM.createInstanceWithContext(name, CTX) elif argument: instance = SM.createInstanceWithArguments(name, (argument,)) else: instance = SM.createInstance(name) return instance def get_app_config(node_name: str, key: str='') -> Any: """Get any key from any node from LibreOffice configuration. :param node_name: Name of node :type name: str :param key: Name of key :type key: str :return: Any value :rtype: Any `See Api ConfigurationProvider `_ """ name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationAccess' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) value = '' try: value = cp.createInstanceWithArguments(service, (node,)) if value and value.hasByName(key): value = value.getPropertyValue(key) except Exception as e: error(e) value = '' return value # Get info LibO NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') LANG = LANGUAGE.split('-')[0] INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{_info_debug}" # Get start date from Calc configuration node = '/org.openoffice.Office.Calc/Calculate/Other/Date' year = get_app_config(node, 'YY') month = get_app_config(node, 'MM') day = get_app_config(node, 'DD') DATE_OFFSET = datetime.date(year, month, day).toordinal() def _(msg): if LANG == 'en': return msg if not LANG in MESSAGES: return msg return MESSAGES[LANG][msg] def set_app_config(node_name: str, key: str, new_value: Any) -> Any: """Update value for key in node name. :param node_name: Name of node :type name: str :param key: Name of key :type key: str :return: True if update sucesfully :rtype: bool `See Api ConfigurationUpdateAccess `_ """ result = True current_value = '' name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationUpdateAccess' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) update = cp.createInstanceWithArguments(service, (node,)) try: current_value = update.getPropertyValue(key) update.setPropertyValue(key, new_value) update.commitChanges() except Exception as e: error(e) if update.hasByName(key) and current_value: update.setPropertyValue(key, current_value) update.commitChanges() result = False return result def debug(*messages) -> None: """Show messages debug :param messages: List of messages to debug :type messages: list[Any] """ data = [str(m) for m in messages] log.debug('\t'.join(data)) return def error(message: Any) -> None: """Show message error :param message: The message error :type message: Any """ log.error(message) return def info(*messages) -> None: """Show messages info :param messages: List of messages to debug :type messages: list[Any] """ data = [str(m) for m in messages] log.info('\t'.join(data)) return def save_log(path: str, data: Any) -> None: """Save data in file, data append to end and automatic add current time. :param path: Path to save log :type path: str :param data: Data to save in file log :type data: Any """ with open(path, 'a') as f: f.write(f'{str(now())[:19]} - ') pprint(data, stream=f) return def mri(obj: Any) -> None: """Inspect object with MRI Extension :param obj: Any pyUno object :type obj: Any `See MRI `_ """ m = create_instance('mytools.Mri') if m is None: msg = 'Extension MRI not found' error(msg) return if hasattr(obj, 'obj'): obj = obj.obj m.inspect(obj) return def catch_exception(f): """Catch exception for any function :param f: Any Python function :type f: Function instance """ @wraps(f) def func(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: name = f.__name__ if IS_WIN: msgbox(traceback.format_exc()) log.error(name, exc_info=True) return func def msgbox(message: Any, title: str=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, \ type_message_box=MessageBoxType.INFOBOX) -> int: """Create message box :param message: Any type message, all is converted to string. :type message: Any :param title: The title for message box :type title: str :param buttons: A combination of `com::sun::star::awt::MessageBoxButtons `_ :type buttons: long :param type_message_box: The `message box type `_ :type type_message_box: enum :return: `MessageBoxResult `_ :rtype: int `See Api XMessageBoxFactory `_ """ toolkit = create_instance('com.sun.star.awt.Toolkit') parent = toolkit.getDesktopWindow() box = toolkit.createMessageBox(parent, type_message_box, buttons, title, str(message)) return box.execute() def question(message: str, title: str=TITLE) -> bool: """Create message box question, show buttons YES and NO :param message: Message question :type message: str :param title: The title for message box :type title: str :return: True if user click YES and False if click NO :rtype: bool """ result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, MessageBoxType.QUERYBOX) return result == YES def warning(message: Any, title: str=TITLE) -> int: """Create message box with icon warning :param message: Any type message, all is converted to string. :type message: Any :param title: The title for message box :type title: str :return: MessageBoxResult :rtype: int """ return msgbox(message, title, type_message_box=MessageBoxType.WARNINGBOX) def errorbox(message: Any, title: str=TITLE) -> int: """Create message box with icon error :param message: Any type message, all is converted to string. :type message: Any :param title: The title for message box :type title: str :return: MessageBoxResult :rtype: int """ return msgbox(message, title, type_message_box=MessageBoxType.ERRORBOX) def sleep(seconds: int): """Sleep """ time.sleep(seconds) return def run_in_thread(fn): """Run any function in thread :param fn: Any Python function (macro) :type fn: Function instance """ def run(*k, **kw): t = threading.Thread(target=fn, args=k, kwargs=kw) t.start() return t return run def dict_to_property(values: dict, uno_any: bool=False): """Convert dictionary to array of PropertyValue :param values: Dictionary of values :type values: dict :param uno_any: If return like array uno.Any :type uno_any: bool :return: Tuple of PropertyValue or array uno.Any :rtype: tuples or uno.Any """ ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) if uno_any: ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps) return ps def _property_to_dict(values): d = {v.Name: v.Value for v in values} return d def data_to_dict(data) -> dict: """Convert tuples, list, PropertyValue, NamedValue to dictionary :param data: Dictionary of values :type data: array of tuples, list, PropertyValue or NamedValue :return: Dictionary :rtype: dict """ d = {} if not isinstance(data, (tuple, list)): return d if isinstance(data[0], (tuple, list)): d = {r[0]: r[1] for r in data} elif isinstance(data[0], (PropertyValue, NamedValue)): d = _property_to_dict(data) return d def render(template, data): s = Template(template) return s.safe_substitute(**data) def _set_properties(model, properties): # ~ if 'X' in properties: # ~ properties['PositionX'] = properties.pop('X') # ~ if 'Y' in properties: # ~ properties['PositionY'] = properties.pop('Y') keys = tuple(properties.keys()) values = tuple(properties.values()) model.setPropertyValues(keys, values) return # Classes class LOInspect(): """Classe inspect Inspired by `MRI `_ """ TYPE_CLASSES = { 'INTERFACE': '-Interface-', 'SEQUENCE': '-Sequence-', 'STRUCT': '-Struct-', } def __init__(self, obj: Any, to_doc: bool=False): """Introspection objects pyUno :param obj: Object to inspect :type obj: Any pyUno :param to_doc: If show info in new doc Calc :type to_doc: bool """ self._obj = obj if hasattr(obj, 'obj'): self._obj = obj.obj self._properties = () self._methods = () self._interfaces = () self._services = () self._listeners = () introspection = create_instance('com.sun.star.beans.Introspection') result = introspection.inspect(self._obj) if result: self._properties = self._get_properties(result) self._methods = self._get_methods(result) self._interfaces = self._get_interfaces(result) self._services = self._get_services(self._obj) self._listeners = self._get_listeners(result) self._to_doc(to_doc) def _to_doc(self, to_doc: bool): if not to_doc: return doc = LODocuments().new() sheet = doc[0] sheet.name = 'Properties' sheet['A1'].data = self.properties sheet = doc.insert('Methods') sheet['A1'].data = self.methods sheet = doc.insert('Interfaces') sheet['A1'].data = self.interfaces sheet = doc.insert('Services') sheet['A1'].data = self.services sheet = doc.insert('Listeners') sheet['A1'].data = self.listeners return def _get_value(self, p: Any): type_class = p.Type.typeClass.value if type_class in self.TYPE_CLASSES: return self.TYPE_CLASSES[type_class] value = '' try: value = getattr(self._obj, p.Name) if type_class == 'ENUM' and value: value = value.value elif type_class == 'TYPE': value = value.typeName elif value is None: value = '-void-' except: value = '-error-' return str(value) def _get_attributes(self, a: Any): PA = {1 : 'Maybe Void', 16 : 'Read Only'} attr = ', '.join([PA.get(k, '') for k in PA.keys() if a & k]) return attr def _get_property(self, p: Any): name = p.Name tipo = p.Type.typeName value = self._get_value(p) attr = self._get_attributes(p.Attributes) return name, tipo, value, attr def _get_properties(self, result: Any): properties = result.getProperties(ALL) data = [('Name', 'Type', 'Value', 'Attributes')] data += [self._get_property(p) for p in properties] return data def _get_arguments(self, m: Any): arguments = '( {} )'.format(', '.join( [' '.join(( f'[{p.aMode.value.lower()}]', p.aName, p.aType.Name)) for p in m.ParameterInfos] )) return arguments def _get_method(self, m: Any): name = m.Name arguments = self._get_arguments(m) return_type = m.ReturnType.Name class_name = m.DeclaringClass.Name return name, arguments, return_type, class_name def _get_methods(self, result: Any): methods = result.getMethods(ALL) data = [('Name', 'Arguments', 'Return Type', 'Class')] data += [self._get_method(m) for m in methods] return data def _get_interfaces(self, result: Any): methods = result.getMethods(ALL) interfaces = {m.DeclaringClass.Name for m in methods} return tuple(zip(interfaces)) def _get_services(self, obj: Any): try: data = [str(s) for s in obj.getSupportedServiceNames()] data = tuple(zip(data)) except: data = () return data def _get_listeners(self, result: Any): data = [l.typeName for l in result.getSupportedListeners()] return tuple(zip(data)) @property def properties(self): return self._properties @property def methods(self): return self._methods @property def interfaces(self): return self._interfaces @property def services(self): return self._services @property def listeners(self): return self._listeners # ~ https://github.com/django/django/blob/main/django/utils/functional.py#L61 class classproperty: def __init__(self, method=None): self.fget = method def __get__(self, instance, cls=None): return self.fget(cls) def getter(self, method): self.fget = method return self class Dates(object): """Class for datetimes """ _start = None @classproperty def now(cls): """Current local date and time :return: Return the current local date and time :rtype: datetime """ return datetime.datetime.now().replace(microsecond=0) @classproperty def today(cls): """Current local date :return: Return the current local date :rtype: date """ return datetime.date.today() @classproperty def time(cls): """Current local time :return: Return the current local time :rtype: datetime.time """ t = cls.now.time().replace(microsecond=0) return t @classproperty def epoch(cls): """Get unix time :return: Return unix time :rtype: int `See Unix Time `_ """ n = cls.now e = int(time.mktime(n.timetuple())) return e @classmethod def date(cls, year: int, month: int, day: int): """Get date from year, month, day :param year: Year of date :type year: int :param month: Month of date :type month: int :param day: Day of day :type day: int :return: Return the date :rtype: date `See Python date `_ """ d = datetime.date(year, month, day) return d @classmethod def str_to_date(cls, str_date: str, template: str, to_calc: bool=False): """Get date from string :param str_date: Date in string :type str_date: str :param template: Formato of date string :type template: str :param to_calc: If date is for used in Calc cell :type to_calc: bool :return: Return date or int if used in Calc :rtype: date or int `See Python strptime `_ """ d = datetime.datetime.strptime(str_date, template).date() if to_calc: d = d.toordinal() - DATE_OFFSET return d @classmethod def calc_to_date(cls, value: float): """Get date from calc value :param value: Float value from cell :type value: float :return: Return the current local date :rtype: date `See Python fromordinal `_ """ d = datetime.date.fromordinal(int(value) + DATE_OFFSET) return d @classmethod def start(cls): """Start counter """ cls._start = cls.now info('Start: ', cls._start) return @classmethod def end(cls, get_seconds: bool=True): """End counter :param get_seconds: If return value in total seconds :type get_seconds: bool :return: Return the timedelta or total seconds :rtype: timedelta or int """ e = cls.now td = e - cls._start result = str(td) if get_seconds: result = td.total_seconds() info('End: ', e) return result class Json(object): """Class for json data """ @classmethod def dumps(cls, data: Any) -> str: """Dumps :param data: Any data :type data: Any :return: Return string json :rtype: str """ return json.dumps(data, indent=4, sort_keys=True) @classmethod def loads(cls, data: str) -> Any: """Loads :param data: String data :type data: str :return: Return any object :rtype: Any """ return json.loads(data) class Macro(object): """Class for call macro `See Scripting Framework `_ """ @classmethod def call(cls, args: dict, in_thread: bool=False): """Call any macro :param args: Dictionary with macro location :type args: dict :param in_thread: If execute in thread :type in_thread: bool :return: Return None or result of call macro :rtype: Any """ result = None if in_thread: t = threading.Thread(target=cls._call, args=(args,)) t.start() else: result = cls._call(args) return result @classmethod def get_url_script(cls, args: dict): library = args['library'] name = args['name'] language = args.get('language', 'Python') location = args.get('location', 'user') module = args.get('module', '.') if language == 'Python': module = '.py$' elif language == 'Basic': module = f".{module}." if location == 'user': location = 'application' url = 'vnd.sun.star.script' url = f'{url}:{library}{module}{name}?language={language}&location={location}' return url @classmethod def _call(cls, args: dict): url = cls.get_url_script(args) args = args.get('args', ()) service = 'com.sun.star.script.provider.MasterScriptProviderFactory' factory = create_instance(service) script = factory.createScriptProvider('').getScript(url) result = script.invoke(args, None, None)[0] return result class Shell(object): """Class for subprocess `See Subprocess `_ """ @classmethod def run(cls, command, capture=False, split=False): """Execute commands :param command: Command to run :type command: str :param capture: If capture result of command :type capture: bool :param split: Some commands need split. :type split: bool :return: Result of command :rtype: Any """ if split: cmd = shlex.split(command) result = subprocess.run(cmd, capture_output=capture, text=True, shell=IS_WIN) if capture: result = result.stdout else: result = result.returncode else: if capture: result = subprocess.check_output(command, shell=True).decode() else: result = subprocess.Popen(command) return result @classmethod def popen(cls, command): """Execute commands and return line by line :param command: Command to run :type command: str :return: Result of command :rtype: Any """ try: proc = subprocess.Popen(shlex.split(command), shell=IS_WIN, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) for line in proc.stdout: yield line.decode().rstrip() except Exception as e: error(e) yield (e.errno, e.strerror) class Timer(object): """Class for timer thread""" class TimerThread(threading.Thread): def __init__(self, event, seconds, macro): threading.Thread.__init__(self) self._event = event self._seconds = seconds self._macro = macro def run(self): while not self._event.wait(self._seconds): Macro.call(self._macro) info('\tTimer stopped... ') return @classmethod def exists(cls, name): """Validate in timer **name** exists :param name: Timer name, it must be unique :type name: str :return: True if exists timer name :rtype: bool """ global _EVENTS return name in _EVENTS @classmethod def start(cls, name: str, seconds: float, macro: dict): """Start timer **name** every **seconds** and execute **macro** :param name: Timer name, it must be unique :type name: str :param seconds: Seconds for wait :type seconds: float :param macro: Macro for execute :type macro: dict """ global _EVENTS _EVENTS[name] = threading.Event() info(f"Timer '{name}' started, execute macro: '{macro['name']}'") thread = cls.TimerThread(_EVENTS[name], seconds, macro) thread.start() return @classmethod def stop(cls, name: str): """Stop timer **name** :param name: Timer name :type name: str """ global _EVENTS _EVENTS[name].set() del _EVENTS[name] return @classmethod def once(cls, name: str, seconds: float, macro: dict): """Start timer **name** only once in **seconds** and execute **macro** :param name: Timer name, it must be unique :type name: str :param seconds: Seconds for wait before execute macro :type seconds: float :param macro: Macro for execute :type macro: dict """ global _EVENTS _EVENTS[name] = threading.Timer(seconds, Macro.call, (macro,)) _EVENTS[name].start() info(f'Event: "{name}", started... execute in {seconds} seconds') return @classmethod def cancel(cls, name: str): """Cancel timer **name** only once events. :param name: Timer name, it must be unique :type name: str """ global _EVENTS if name in _EVENTS: try: _EVENTS[name].cancel() del _EVENTS[name] info(f'Cancel event: "{name}", ok...') except Exception as e: error(e) else: debug(f'Cancel event: "{name}", not exists...') return class Hash(object): """Class for hash """ @classmethod def digest(cls, method: str, data: str, in_hex: bool=True): """Get digest from data with method :param method: Digest method: md5, sha1, sha256, sha512, etc... :type method: str :param data: Data for get digest :type data: str :param in_hex: If True, get digest in hexadecimal, if False, get bytes :type in_hex: bool :return: bytes or hex digest :rtype: bytes or str """ result = '' obj = getattr(hashlib, method)(data.encode()) if in_hex: result = obj.hexdigest() else: result = obj.digest() return result class Paths(object): """Class for paths """ FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' REMOTE_FILE_PICKER = 'com.sun.star.ui.dialogs.RemoteFilePicker' OFFICE_FILE_PICKER = 'com.sun.star.ui.dialogs.OfficeFilePicker' def __init__(self, path=''): if path.startswith('file://'): path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) self._path = Path(path) @property def path(self): """Get base path""" return str(self._path.parent) @property def file_name(self): """Get file name""" return self._path.name @property def name(self): """Get name""" return self._path.stem @property def ext(self): """Get extension""" return self._path.suffix[1:] @property def size(self): """Get size""" return self._path.stat().st_size @property def url(self): """Get like URL""" return self._path.as_uri() @property def info(self): """Get all info like tuple""" i = (self.path, self.file_name, self.name, self.ext, self.size, self.url) return i @property def dict(self): """Get all info like dict""" data = { 'path': self.path, 'file_name': self.file_name, 'name': self.name, 'ext': self.ext, 'size': self.size, 'url': self.url, } return data @classproperty def home(self): """Get user home""" return str(Path.home()) @classproperty def documents(self): """Get user save documents""" return self.config() @classproperty def user_profile(self): """Get path user profile""" path = self.config('UserConfig') path = str(Path(path).parent) return path @classproperty def user_config(self): """Get path config in user profile""" path = self.config('UserConfig') return path @classproperty def python(self): """Get path executable python""" if IS_WIN: path = self.join(self.config('Module'), PYTHON) elif IS_MAC: path = self.join(self.config('Module'), '..', 'Resources', PYTHON) else: path = sys.executable return path @classmethod def to_system(cls, path:str) -> str: """Convert paths in URL to system :param path: Path to convert :type path: str :return: Path system format :rtype: str """ if path.startswith('file://'): path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) return path @classmethod def to_url(cls, path: str) -> str: """Convert paths in format system to URL :param path: Path to convert :type path: str :return: Path in URL :rtype: str """ if not path.startswith('file://'): path = Path(path).as_uri() return path @classmethod def config(cls, name: str='Work') -> Union[str, list]: """Return path from config :param name: Name in service PathSettings, default get path documents :type name: str :return: Path in config, if exists. :rtype: str or list `See Api XPathSettings `_ """ path = create_instance('com.sun.star.util.PathSettings') path = cls.to_system(getattr(path, name)).split(';') if len(path) == 1: path = path[0] return path @classmethod def join(cls, *paths: str) -> str: """Join paths :param paths: Paths to join :type paths: list :return: New path with joins :rtype: str """ path = str(Path(paths[0]).joinpath(*paths[1:])) return path @classmethod def exists(cls, path: str) -> bool: """If exists path :param path: Path for validate :type path: str :return: True if path exists, False if not. :rtype: bool """ path = cls.to_system(path) result = Path(path).exists() return result @classmethod def exists_app(cls, name_app: str) -> bool: """If exists app in system :param name_app: Name of application :type name_app: str :return: True if app exists, False if not. :rtype: bool """ result = bool(shutil.which(name_app)) return result @classmethod def is_dir(cls, path: str): """Validate if path is directory :param path: Path for validate :type path: str :return: True if path is directory, False if not. :rtype: bool """ return Path(path).is_dir() @classmethod def is_file(cls, path: str): """Validate if path is a file :param path: Path for validate :type path: str :return: True if path is a file, False if not. :rtype: bool """ return Path(path).is_file() @classmethod def temp_file(self): """Make temporary file""" return tempfile.NamedTemporaryFile(mode='w') @classmethod def temp_dir(self): """Make temporary directory""" return tempfile.TemporaryDirectory(ignore_cleanup_errors=True) @classmethod def get(cls, init_dir: str='', filters: str='') -> str: """Get path for save :param init_dir: Initial default path :type init_dir: str :param filters: Filter for show type files: 'xml' or 'txt,xml' :type filters: str :return: Selected path :rtype: str `See API `_ """ if not init_dir: init_dir = cls.documents init_dir = cls.to_url(init_dir) file_picker = create_instance(cls.FILE_PICKER) file_picker.setTitle(_('Select path')) file_picker.setDisplayDirectory(init_dir) file_picker.initialize((TemplateDescription.FILEOPEN_SIMPLE,)) if filters: for f in filters.split(','): file_picker.appendFilter(f.upper(), f'*.{f.lower()}') path = '' if file_picker.execute(): path = cls.to_system(file_picker.getSelectedFiles()[0]) return path @classmethod def get_dir(cls, init_dir: str='') -> str: """Get path dir :param init_dir: Initial default path :type init_dir: str :return: Selected path :rtype: str """ folder_picker = create_instance(cls.FOLDER_PICKER) if not init_dir: init_dir = cls.documents init_dir = cls.to_url(init_dir) folder_picker.setTitle(_('Select directory')) folder_picker.setDisplayDirectory(init_dir) path = '' if folder_picker.execute(): path = cls.to_system(folder_picker.getDirectory()) return path @classmethod def get_file(cls, init_dir: str='', filters: str='', multiple: bool=False): """Get path exists file :param init_dir: Initial default path :type init_dir: str :param filters: Filter for show type files: 'xml' or 'txt,xml' :type filters: str :param multiple: If user can selected multiple files :type multiple: bool :return: Selected path :rtype: str """ if not init_dir: init_dir = cls.documents init_dir = cls.to_url(init_dir) file_picker = create_instance(cls.FILE_PICKER) file_picker.setTitle(_('Select file')) file_picker.setDisplayDirectory(init_dir) file_picker.setMultiSelectionMode(multiple) if filters: for f in filters.split(','): file_picker.appendFilter(f.upper(), f'*.{f.lower()}') path = '' if file_picker.execute(): files = file_picker.getSelectedFiles() path = [cls.to_system(f) for f in files] if not multiple: path = path[0] return path @classmethod def files(cls, path: str, pattern: str='*'): """Get all files in path :param path: Path with files :type path: str :param pattern: For filter files, default get all. :type pattern: str :return: Files in path :rtype: list """ files = [str(p) for p in Path(path).glob(pattern) if p.is_file()] return files @classmethod def walk(cls, path, filters=''): """Get all files in path recursively :param path: Path with files :type path: str :param filters: For filter files, default get all. :type filters: str :return: Files in path :rtype: list """ paths = [] for folder, _, files in os.walk(path): if filters: pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) paths += [cls.join(folder, f) for f in files if pattern.search(f)] else: paths += [cls.join(folder, f) for f in files] return paths @classmethod def dirs(cls, path): """Get directories in path :param path: Path to scan :type path: str :return: Directories in path :rtype: list """ dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()] return dirs @classmethod def walk_dirs(cls, path, tree=False): """Get directories recursively :param path: Path to scan :type path: str :param tree: get info in a tuple (ID_FOLDER, ID_PARENT, NAME) :type tree: bool :return: Directories in path :rtype: list """ folders = [] if tree: i = 0 parents = {path: 0} for root, dirs, _ in os.walk(path): for name in dirs: i += 1 rn = cls.join(root, name) if not rn in parents: parents[rn] = i folders.append((i, parents[root], name)) else: for root, dirs, _ in os.walk(path): folders += [cls.join(root, name) for name in dirs] return folders @classmethod def extension(cls, id_ext: str): """Get path extension install from id :param id_ext: ID extension :type id_ext: str :return: Path extension :rtype: str """ pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') path = Paths.to_system(pip.getPackageLocation(id_ext)) return path @classmethod def replace_ext(cls, path: str, new_ext: str): """Replace extension in file path :param path: Path to file :type path: str :param new_ext: New extension :type new_ext: str :return: Path with new extension :rtype: str """ p = Paths(path) name = f'{p.name}.{new_ext}' path = cls.join(p.path, name) return path @classmethod def open(cls, path: str): """Open any file with default program in systema :param path: Path to file :type path: str :return: PID file, only Linux :rtype: int """ pid = 0 if IS_WIN: os.startfile(path) else: pid = subprocess.Popen(['xdg-open', path]).pid return pid # ~ Save/read data @classmethod def save(cls, path: str, data: str, encoding: str='utf-8') -> bool: """Save data in path with encoding :param path: Path to file save :type path: str :param data: Data to save :type data: str :param encoding: Encoding for save data, default utf-8 :type encoding: str :return: True, if save corrrectly :rtype: bool """ result = bool(Path(path).write_text(data, encoding=encoding)) return result @classmethod def save_bin(cls, path: str, data: bytes) -> bool: """Save binary data in path :param path: Path to file save :type path: str :param data: Data to save :type data: bytes :return: True, if save corrrectly :rtype: bool """ result = bool(Path(path).write_bytes(data)) return result @classmethod def read(cls, path: str, get_lines: bool=False, encoding: str='utf-8') -> Union[str, list]: """Read data in path :param path: Path to file read :type path: str :param get_lines: If read file line by line :type get_lines: bool :return: File content :rtype: str or list """ if get_lines: with Path(path).open(encoding=encoding) as f: data = f.readlines() else: data = Path(path).read_text(encoding=encoding) return data @classmethod def read_bin(cls, path: str) -> bytes: """Read binary data in path :param path: Path to file read :type path: str :return: File content :rtype: bytes """ data = Path(path).read_bytes() return data # ~ Import/export data @classmethod def from_json(cls, path: str) -> Any: """Read path file and load json data :param path: Path to file :type path: str :return: Any data :rtype: Any """ data = json.loads(cls.read(path)) return data @classmethod def to_json(cls, path: str, data: str): """Save data in path file like json :param path: Path to file :type path: str :return: True if save correctly :rtype: bool """ data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) return cls.save(path, data) @classmethod def from_csv(cls, path: str, args: dict={}) -> tuple: """Read CSV :param path: Path to file csv :type path: str :param args: Any argument support for Python library :type args: dict :return: Data csv like tuple :rtype: tuple `See CSV Reader `_ """ with open(path) as f: rows = tuple(csv.reader(f, **args)) return rows @classmethod def to_csv(cls, path: str, data: Any, args: dict={}): """Write CSV :param path: Path to file write csv :type path: str :param data: Data to write :type data: Iterable :param args: Any argument support for Python library :type args: dict `See CSV Writer `_ """ with open(path, 'w') as f: writer = csv.writer(f, **args) writer.writerows(data) return @classmethod def zip(cls, source: Union[str, tuple, list], target='') -> str: path_zip = target if not isinstance(source, (tuple, list)): path, _, name, _ = _P(source).info start = len(path) + 1 if not target: path_zip = f'{path}/{name}.zip' if isinstance(source, (tuple, list)): files = [(f, f[len(_P(f).path)+1:]) for f in source] elif _P.is_file(source): files = ((source, source[start:]),) else: files = [(f, f[start:]) for f in _P.walk(source)] compression = zipfile.ZIP_DEFLATED with zipfile.ZipFile(path_zip, 'w', compression=compression) as z: for f in files: z.write(f[0], f[1]) return path_zip @classmethod def unzip(cls, source, target='', members=None, pwd=None): path = target if not target: path = _P(source).path with zipfile.ZipFile(source) as z: if not pwd is None: pwd = pwd.encode() if isinstance(members, str): members = (members,) z.extractall(path, members=members, pwd=pwd) return @classmethod def zip_content(cls, path: str): with zipfile.ZipFile(path) as z: names = z.namelist() return names @classmethod def merge_zip(cls, target, zips): try: with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: for path in zips: with zipfile.ZipFile(path, compression=zipfile.ZIP_DEFLATED) as s: for name in s.namelist(): t.writestr(name, s.open(name).read()) except Exception as e: error(e) return False return True @classmethod def kill(cls, path: str): """Delete path :param path: Path to file or directory :type path: str :return: True if delete correctly :rtype: bool """ p = Path(path) try: if p.is_file(): p.unlink() elif p.is_dir(): shutil.rmtree(path) result = True except OSError as e: log.error(e) result = False return result @classmethod def copy(cls, source: str, target: str='', name: str=''): p, f, n, e, _, _ = Paths(source).info if target: p = target e = f'.{e}' if name: e = '' n = name path_new = cls.join(p, f'{n}{e}') shutil.copy(source, path_new) return path_new class Config(object): """Class for set and get configurations """ @classmethod def set(cls, prefix: str, value: Any, key: str='') -> bool: """Save data config in user config like json :param prefix: Unique prefix for this data :type prefix: str :param value: Value for save :type value: Any :param key: Key for value :type key: str :return: True if save correctly :rtype: bool """ name_file = FILES['CONFIG'].format(prefix) path = Paths.join(Paths.user_config, name_file) data = value if key: data = cls.get(prefix) data[key] = value result = Paths.to_json(path, data) return result @classmethod def get(cls, prefix: str, key: str='', default: Any={}) -> Any: """Get data config from user config like json :param prefix: Unique prefix for this data :type prefix: str :param key: Key for value :type key: str :param default: Get if not exists key :type default: Any :return: data :rtype: Any """ data = {} name_file = FILES['CONFIG'].format(prefix) path = Paths.join(Paths.user_config, name_file) if not Paths.exists(path): return data data = Paths.from_json(path) if key: data = data.get(key, default) return data class Url(object): """Class for simple url open """ @classmethod def _open(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ json: bool=False, timeout: int=TIMEOUT, method: str='GET') -> tuple: """URL Open""" debug(url) result = None context = None rheaders = {} err = '' if verify: if not data is None: if isinstance(data, str): data = data.encode() elif isinstance(data, dict): data = parse.urlencode(data).encode('ascii') else: context = ssl._create_unverified_context() try: req = Request(url, data=data, headers=headers, method=method) response = urlopen(req, timeout=timeout, context=context) except HTTPError as e: error(e) err = str(e) except URLError as e: error(e.reason) err = str(e.reason) # ToDo # ~ except timeout: # ~ err = 'timeout' # ~ error(err) else: rheaders = dict(response.info()) result = response.read().decode() if json: result = Json.loads(result) return result, rheaders, err @classmethod def get(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ json: bool=False, timeout: int=TIMEOUT) -> tuple: """Method GET :param url: Url to open :type url: str :return: result, headers and error :rtype: tuple """ return cls._open(url, data, headers, verify, json, timeout) # ToDo @classmethod def post(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ json: bool=False, timeout: int=TIMEOUT) -> tuple: """Method POST """ data = parse.urlencode(data).encode('ascii') return cls._open(url, data, headers, verify, json, timeout, 'POST') class Email(object): """Class for send email """ class _SmtpServer(object): def __init__(self, config): self._server = None self._error = '' self._sender = '' self._is_connect = self._login(config) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() @property def is_connect(self): return self._is_connect @property def error(self): return self._error def _login(self, config): name = config['server'] port = config['port'] is_ssl = config['ssl'] starttls = config.get('starttls', False) self._sender = config['user'] try: if starttls: self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) self._server.ehlo() self._server.starttls() self._server.ehlo() elif is_ssl: self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) self._server.ehlo() else: self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) self._server.login(self._sender, config['password']) msg = 'Connect to: {}'.format(name) debug(msg) return True except smtplib.SMTPAuthenticationError as e: if '535' in str(e): self._error = _('Incorrect user or password') return False if '534' in str(e) and 'gmail' in name: self._error = _('Allow less secure apps in GMail') return False except smtplib.SMTPException as e: self._error = str(e) return False except Exception as e: self._error = str(e) return False return False def _body(self, msg): body = msg.replace('\n', '
') return body def send(self, message): # ~ file_name = 'attachment; filename={}' email = MIMEMultipart() email['From'] = self._sender email['To'] = message['to'] email['Cc'] = message.get('cc', '') email['Subject'] = message['subject'] email['Date'] = formatdate(localtime=True) if message.get('confirm', False): email['Disposition-Notification-To'] = email['From'] email.attach(MIMEText(self._body(message['body']), 'html')) paths = message.get('files', ()) if isinstance(paths, str): paths = (paths,) for path in paths: fn = _P(path).file_name # ~ print('NAME', fn) part = MIMEBase('application', 'octet-stream') part.set_payload(_P.read_bin(path)) encoders.encode_base64(part) part.add_header('Content-Disposition', f'attachment; filename="{fn}"') email.attach(part) receivers = ( email['To'].split(',') + email['CC'].split(',') + message.get('bcc', '').split(',')) try: self._server.sendmail(self._sender, receivers, email.as_string()) msg = 'Email sent...' debug(msg) if message.get('path', ''): self.save_message(email, message['path']) return True except Exception as e: self._error = str(e) return False return False def save_message(self, email, path): mbox = mailbox.mbox(path, create=True) mbox.lock() try: msg = mailbox.mboxMessage(email) mbox.add(msg) mbox.flush() finally: mbox.unlock() return def close(self): try: self._server.quit() msg = 'Close connection...' debug(msg) except: pass return @classmethod def _send_email(cls, server, messages): with cls._SmtpServer(server) as server: if server.is_connect: for msg in messages: server.send(msg) else: error(server.error) return server.error @classmethod def send(cls, server: dict, messages: Union[dict, tuple, list]): """Send email with config server, emails send in thread. :param server: Configuration for send emails :type server: dict :param server: Dictionary con message or list of messages :type server: dict or iterator """ if isinstance(messages, dict): messages = (messages,) t = threading.Thread(target=cls._send_email, args=(server, messages)) t.start() return class Color(object): """Class for colors `See Web Colors `_ """ COLORS = { 'aliceblue': 15792383, 'antiquewhite': 16444375, 'aqua': 65535, 'aquamarine': 8388564, 'azure': 15794175, 'beige': 16119260, 'bisque': 16770244, 'black': 0, 'blanchedalmond': 16772045, 'blue': 255, 'blueviolet': 9055202, 'brown': 10824234, 'burlywood': 14596231, 'cadetblue': 6266528, 'chartreuse': 8388352, 'chocolate': 13789470, 'coral': 16744272, 'cornflowerblue': 6591981, 'cornsilk': 16775388, 'crimson': 14423100, 'cyan': 65535, 'darkblue': 139, 'darkcyan': 35723, 'darkgoldenrod': 12092939, 'darkgray': 11119017, 'darkgreen': 25600, 'darkgrey': 11119017, 'darkkhaki': 12433259, 'darkmagenta': 9109643, 'darkolivegreen': 5597999, 'darkorange': 16747520, 'darkorchid': 10040012, 'darkred': 9109504, 'darksalmon': 15308410, 'darkseagreen': 9419919, 'darkslateblue': 4734347, 'darkslategray': 3100495, 'darkslategrey': 3100495, 'darkturquoise': 52945, 'darkviolet': 9699539, 'deeppink': 16716947, 'deepskyblue': 49151, 'dimgray': 6908265, 'dimgrey': 6908265, 'dodgerblue': 2003199, 'firebrick': 11674146, 'floralwhite': 16775920, 'forestgreen': 2263842, 'fuchsia': 16711935, 'gainsboro': 14474460, 'ghostwhite': 16316671, 'gold': 16766720, 'goldenrod': 14329120, 'gray': 8421504, 'grey': 8421504, 'green': 32768, 'greenyellow': 11403055, 'honeydew': 15794160, 'hotpink': 16738740, 'indianred': 13458524, 'indigo': 4915330, 'ivory': 16777200, 'khaki': 15787660, 'lavender': 15132410, 'lavenderblush': 16773365, 'lawngreen': 8190976, 'lemonchiffon': 16775885, 'lightblue': 11393254, 'lightcoral': 15761536, 'lightcyan': 14745599, 'lightgoldenrodyellow': 16448210, 'lightgray': 13882323, 'lightgreen': 9498256, 'lightgrey': 13882323, 'lightpink': 16758465, 'lightsalmon': 16752762, 'lightseagreen': 2142890, 'lightskyblue': 8900346, 'lightslategray': 7833753, 'lightslategrey': 7833753, 'lightsteelblue': 11584734, 'lightyellow': 16777184, 'lime': 65280, 'limegreen': 3329330, 'linen': 16445670, 'magenta': 16711935, 'maroon': 8388608, 'mediumaquamarine': 6737322, 'mediumblue': 205, 'mediumorchid': 12211667, 'mediumpurple': 9662683, 'mediumseagreen': 3978097, 'mediumslateblue': 8087790, 'mediumspringgreen': 64154, 'mediumturquoise': 4772300, 'mediumvioletred': 13047173, 'midnightblue': 1644912, 'mintcream': 16121850, 'mistyrose': 16770273, 'moccasin': 16770229, 'navajowhite': 16768685, 'navy': 128, 'oldlace': 16643558, 'olive': 8421376, 'olivedrab': 7048739, 'orange': 16753920, 'orangered': 16729344, 'orchid': 14315734, 'palegoldenrod': 15657130, 'palegreen': 10025880, 'paleturquoise': 11529966, 'palevioletred': 14381203, 'papayawhip': 16773077, 'peachpuff': 16767673, 'peru': 13468991, 'pink': 16761035, 'plum': 14524637, 'powderblue': 11591910, 'purple': 8388736, 'red': 16711680, 'rosybrown': 12357519, 'royalblue': 4286945, 'saddlebrown': 9127187, 'salmon': 16416882, 'sandybrown': 16032864, 'seagreen': 3050327, 'seashell': 16774638, 'sienna': 10506797, 'silver': 12632256, 'skyblue': 8900331, 'slateblue': 6970061, 'slategray': 7372944, 'slategrey': 7372944, 'snow': 16775930, 'springgreen': 65407, 'steelblue': 4620980, 'tan': 13808780, 'teal': 32896, 'thistle': 14204888, 'tomato': 16737095, 'turquoise': 4251856, 'violet': 15631086, 'wheat': 16113331, 'white': 16777215, 'whitesmoke': 16119285, 'yellow': 16776960, 'yellowgreen': 10145074, } def _get_color(self, index): if isinstance(index, tuple): color = (index[0] << 16) + (index[1] << 8) + index[2] else: if index[0] == '#': r, g, b = bytes.fromhex(index[1:]) color = (r << 16) + (g << 8) + b else: color = self.COLORS.get(index.lower(), -1) return color def __call__(self, index): return self._get_color(index) def __getitem__(self, index): return self._get_color(index) COLOR_ON_FOCUS = Color()('LightYellow') class ClipBoard(object): SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard' CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' class TextTransferable(unohelper.Base, XTransferable): def __init__(self, text): df = DataFlavor() df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT df.HumanPresentableName = 'encoded text utf-16' self.flavors = (df,) self._data = text def getTransferData(self, flavor): return self._data def getTransferDataFlavors(self): return self.flavors @classmethod def set(cls, value): ts = cls.TextTransferable(value) sc = create_instance(cls.SERVICE) sc.setContents(ts, None) return @classproperty def contents(cls): df = None text = '' sc = create_instance(cls.SERVICE) transferable = sc.getContents() data = transferable.getTransferDataFlavors() for df in data: if df.MimeType == cls.CLIPBOARD_FORMAT_TEXT: break if df: text = transferable.getTransferData(df) return text class IOStream(object): """Classe for input/output stream""" class OutputStream(unohelper.Base, XOutputStream): def __init__(self): self._buffer = b'' self.closed = 0 @property def buffer(self): return self._buffer def closeOutput(self): self.closed = 1 def writeBytes(self, seq): if seq.value: self._buffer = seq.value def flush(self): pass @classmethod def buffer(cls): return io.BytesIO() @classmethod def input(cls, buffer): service = 'com.sun.star.io.SequenceInputStream' stream = create_instance(service, True) stream.initialize((uno.ByteSequence(buffer.getvalue()),)) return stream @classmethod def output(cls): return cls.OutputStream() class EventsListenerBase(unohelper.Base, XEventListener): def __init__(self, controller, name, window=None): self._controller = controller self._name = name self._window = window @property def name(self): return self._name def disposing(self, event): self._controller = None if not self._window is None: self._window.setMenuBar(None) class EventsRangeSelectionListener(EventsListenerBase, XRangeSelectionListener): def __init__(self, controller): super().__init__(controller, '') def done(self, event): range_selection = event.RangeDescriptor event_name = 'range_selection_done' if hasattr(self._controller, event_name): getattr(self._controller, event_name)(range_selection) return def aborted(self, event): range_selection = event.RangeDescriptor event_name = 'range_selection_aborted' if hasattr(self._controller, event_name): getattr(self._controller, event_name)() return class LOShapes(object): _type = 'ShapeCollection' def __init__(self, obj): self._obj = obj def __len__(self): return self.obj.Count def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __iter__(self): self._index = 0 return self def __next__(self): try: s = self.obj[self._index] shape = LOShape(s) except IndexError: raise StopIteration self._index += 1 return shape def __str__(self): return 'Shapes' @property def obj(self): return self._obj class LOShape(object): IMAGE = 'com.sun.star.drawing.GraphicObjectShape' def __init__(self, obj): self._obj = obj def __str__(self): return f'Shape: {self.name}' @property def obj(self): return self._obj @property def properties(self): # ~ properties = self.obj.PropertySetInfo.Properties # ~ data = {p.Name: getattr(self.obj, p.Name) for p in properties} data = self.obj.PropertySetInfo.Properties keys = [p.Name for p in data] values = self.obj.getPropertyValues(keys) data = dict(zip(keys, values)) return data @properties.setter def properties(self, values): _set_properties(self.obj, values) @property def shape_type(self): return self.obj.ShapeType @property def name(self): return self.obj.Name @name.setter def name(self, value): self.obj.Name = value @property def is_image(self): return self.shape_type == self.IMAGE @property def is_shape(self): return self.shape_type != self.IMAGE @property def size(self): s = self.obj.Size return s @size.setter def size(self, value): self.obj.Size = value @property def width(self): s = self.obj.Size return s.Width @width.setter def width(self, value): s = self.size s.Width = value self.size = s @property def height(self): s = self.obj.Size return s.Height @height.setter def height(self, value): s = self.size s.Height = value self.size = s @property def position(self): return self.obj.Position @property def x(self): return self.position.X @property def y(self): return self.position.Y @property def string(self): return self.obj.String @string.setter def string(self, value): self.obj.String = value @property def title(self): return self.obj.Title @title.setter def title(self, value): self.obj.Title = value @property def description(self): return self.obj.Description @description.setter def description(self, value): self.obj.Description = value class LOShortCuts(object): """Classe for manager shortcuts""" KEYS = {getattr(Key, k): k for k in dir(Key)} MODIFIERS = { 'shift': KeyModifier.SHIFT, 'ctrl': KeyModifier.MOD1, 'alt': KeyModifier.MOD2, 'ctrlmac': KeyModifier.MOD3, } COMBINATIONS = { 0: '', 1: 'shift', 2: 'ctrl', 4: 'alt', 8: 'ctrlmac', 3: 'shift+ctrl', 5: 'shift+alt', 9: 'shift+ctrlmac', 6: 'ctrl+alt', 10: 'ctrl+ctrlmac', 12: 'alt+ctrlmac', 7: 'shift+ctrl+alt', 11: 'shift+ctrl+ctrlmac', 13: 'shift+alt+ctrlmac', 14: 'ctrl+alt+ctrlmac', 15: 'shift+ctrl+alt+ctrlmac', } def __init__(self, app: str=''): self._app = app service = 'com.sun.star.ui.GlobalAcceleratorConfiguration' if app: service = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' type_app = LODocuments.TYPES[app] manager = create_instance(service, True) uicm = manager.getUIConfigurationManager(type_app) self._config = uicm.ShortCutManager else: self._config = create_instance(service) def __getitem__(self, index): return LOShortCuts(index) def __contains__(self, item): cmd = self.get_by_shortcut(item) return bool(cmd) def __iter__(self): self._i = -1 return self def __next__(self): self._i += 1 try: event = self._config.AllKeyEvents[self._i] event = self._get_info(event) except IndexError: raise StopIteration return event @classmethod def to_key_event(cls, shortcut: str): """Convert from string shortcut (Shift+Ctrl+Alt+LETTER) to KeyEvent""" key_event = KeyEvent() keys = shortcut.split('+') try: for m in keys[:-1]: key_event.Modifiers += cls.MODIFIERS[m.lower()] key_event.KeyCode = getattr(Key, keys[-1].upper()) except Exception as e: error(e) key_event = None return key_event @classmethod def get_url_script(cls, command: Union[str, dict]) -> str: """Get uno command or url for macro""" url = command if isinstance(url, str) and not url.startswith('.uno:'): url = f'.uno:{command}' elif isinstance(url, dict): url = Macro.get_url_script(command) return url def _get_shortcut(self, k): """Get shortcut for key event""" # ~ print(k.KeyCode, str(k.KeyChar), k.KeyFunc, k.Modifiers) shortcut = f'{self.COMBINATIONS[k.Modifiers]}+{self.KEYS[k.KeyCode]}' return shortcut def _get_info(self, key): """Get shortcut and command""" cmd = self._config.getCommandByKeyEvent(key) shortcut = self._get_shortcut(key) return shortcut, cmd def get_all(self): """Get all events key""" events = [(self._get_info(k)) for k in self._config.AllKeyEvents] return events def get_by_command(self, command: Union[str, dict]): """Get shortcuts by command""" url = LOShortCuts.get_url_script(command) key_events = self._config.getKeyEventsByCommand(url) shortcuts = [self._get_shortcut(k) for k in key_events] return shortcuts def get_by_shortcut(self, shortcut: str): """Get command by shortcut""" command = '' key_event = LOShortCuts.to_key_event(shortcut) if key_event: command = self._config.getCommandByKeyEvent(key_event) return command def set(self, shortcut: str, command: Union[str, dict]) -> bool: """Set shortcut to command :param shortcut: Shortcut like Shift+Ctrl+Alt+LETTER :type shortcut: str :param command: Command tu assign, 'UNOCOMMAND' or dict with macro info :type command: str or dict :return: True if set sucesfully :rtype: bool """ result = True url = LOShortCuts.get_url_script(command) key_event = LOShortCuts.to_key_event(shortcut) try: self._config.setKeyEvent(key_event, url) self._config.store() except Exception as e: error(e) result = False return result def remove_by_shortcut(self, shortcut: str): """Remove by shortcut""" key_event = LOShortCuts.to_key_event(shortcut) try: self._config.removeKeyEvent(key_event) result = True except NoSuchElementException: debug(f'No exists: {shortcut}') result = False return result def remove_by_command(self, command: Union[str, dict]): """Remove by shortcut""" url = LOShortCuts.get_url_script(command) self._config.removeCommandFromAllKeyEvents(url) return def reset(self): """Reset configuration""" self._config.reset() self._config.store() return class LOMenuDebug(): """Classe for debug info menu""" @classmethod def _get_info(cls, menu, index): """Get every option menu""" line = f"({index}) {menu.get('CommandURL', '----------')}" submenu = menu.get('ItemDescriptorContainer', None) if not submenu is None: line += cls._get_submenus(submenu) return line @classmethod def _get_submenus(cls, menu, level=1): """Get submenus""" line = '' for i, v in enumerate(menu): data = data_to_dict(v) cmd = data.get('CommandURL', '----------') line += f'\n{" " * level}├─ ({i}) {cmd}' submenu = data.get('ItemDescriptorContainer', None) if not submenu is None: line += cls._get_submenus(submenu, level + 1) return line def __call__(cls, menu): for i, m in enumerate(menu): data = data_to_dict(m) print(cls._get_info(data, i)) return class LOMenuBase(): """Classe base for menus""" NODE = 'private:resource/menubar/menubar' config = None menus = None app = '' @classmethod def _get_index(cls, parent: Any, name: Union[int, str]=''): """Get index menu from name :param parent: Menu parent :type parent: pyUno :param name: Menu name for search if is str :type name: int or str :return: Index of menu :rtype: int """ index = None if isinstance(name, str) and name: for i, m in enumerate(parent): menu = data_to_dict(m) if menu.get('CommandURL', '') == name: index = i break elif isinstance(name, str): index = len(parent) - 1 elif isinstance(name, int): index = name return index @classmethod def _get_command_url(cls, menu: dict): """Get url from command and set shortcut :param menu: Menu data :type menu: dict :return: URL command :rtype: str """ shortcut = menu.pop('ShortCut', '') command = menu['CommandURL'] url = LOShortCuts.get_url_script(command) if shortcut: LOShortCuts(cls.app).set(shortcut, command) return url @classmethod def _save(cls, parent: Any, menu: dict, index: int): """Insert menu :param parent: Menu parent :type parent: pyUno :param menu: New menu data :type menu: dict :param index: Position to insert :type index: int """ # ~ Some day # ~ self._menus.insertByIndex(index, new_menu) properties = dict_to_property(menu, True) uno.invoke(parent, 'insertByIndex', (index, properties)) cls.config.replaceSettings(cls.NODE, cls.menus) return @classmethod def _insert_submenu(cls, parent: Any, menus: list): """Insert submenus recursively :param parent: Menu parent :type parent: pyUno :param menus: List of menus :type menus: list """ for i, menu in enumerate(menus): submenu = menu.pop('Submenu', False) if submenu: idc = cls.config.createSettings() menu['ItemDescriptorContainer'] = idc menu['Type'] = 0 if menu['Label'][0] == '-': menu['Type'] = 1 else: menu['CommandURL'] = cls._get_command_url(menu) cls._save(parent, menu, i) if submenu: cls._insert_submenu(idc, submenu) return @classmethod def _get_first_command(cls, command): url = command if isinstance(command, dict): url = Macro.get_url_script(command) return url @classmethod def insert(cls, parent: Any, menu: dict, after: Union[int, str]=''): """Insert new menu :param parent: Menu parent :type parent: pyUno :param menu: New menu data :type menu: dict :param after: After menu insert :type after: int or str """ index = cls._get_index(parent, after) + 1 submenu = menu.pop('Submenu', False) menu['Type'] = 0 idc = cls.config.createSettings() menu['ItemDescriptorContainer'] = idc menu['CommandURL'] = cls._get_first_command(menu['CommandURL']) cls._save(parent, menu, index) if submenu: cls._insert_submenu(idc, submenu) return @classmethod def remove(cls, parent: Any, name: Union[str, dict]): """Remove name in parent :param parent: Menu parent :type parent: pyUno :param menu: Menu name :type menu: str """ if isinstance(name, dict): name = Macro.get_url_script(name) index = cls._get_index(parent, name) if index is None: debug(f'Not found: {name}') return uno.invoke(parent, 'removeByIndex', (index,)) cls.config.replaceSettings(cls.NODE, cls.menus) cls.config.store() return class LOMenu(object): """Classe for individual menu""" def __init__(self, config: Any, menus: Any, app: str, menu: Any): """ :param config: Configuration Mananer :type config: pyUno :param menus: Menu bar main :type menus: pyUno :param app: Name LibreOffice module :type app: str :para menu: Particular menu :type menu: pyUno """ self._config = config self._menus = menus self._app = app self._parent = menu def __contains__(self, name): """If exists name in menu""" exists = False for m in self._parent: menu = data_to_dict(m) cmd = menu.get('CommandURL', '') if name == cmd: exists = True break return exists def __getitem__(self, index): """Index access""" if isinstance(index, int): menu = data_to_dict(self._parent[index]) else: for m in self._parent: menu = data_to_dict(m) cmd = menu.get('CommandURL', '') if cmd == index: break obj = LOMenu(self._config, self._menus, self._app, menu['ItemDescriptorContainer']) return obj def debug(self): """Debug menu""" LOMenuDebug()(self._parent) return def insert(self, menu: dict, after: Union[int, str]='', save: bool=True): """Insert new menu :param menu: New menu data :type menu: dict :param after: Insert in after menu :type after: int or str :param save: For persistente save :type save: bool """ LOMenuBase.config = self._config LOMenuBase.menus = self._menus LOMenuBase.app = self._app LOMenuBase.insert(self._parent, menu, after) if save: self._config.store() return def remove(self, menu: str): """Remove menu :param menu: Menu name :type menu: str """ LOMenuBase.config = self._config LOMenuBase.menus = self._menus LOMenuBase.remove(self._parent, menu) return class LOMenuApp(object): """Classe for manager menu by LibreOffice module""" NODE = 'private:resource/menubar/menubar' MENUS = { 'file': '.uno:PickList', 'picklist': '.uno:PickList', 'tools': '.uno:ToolsMenu', 'help': '.uno:HelpMenu', 'window': '.uno:WindowList', 'edit': '.uno:EditMenu', 'view': '.uno:ViewMenu', 'insert': '.uno:InsertMenu', 'format': '.uno:FormatMenu', 'styles': '.uno:FormatStylesMenu', 'formatstyles': '.uno:FormatStylesMenu', 'sheet': '.uno:SheetMenu', 'data': '.uno:DataMenu', 'table': '.uno:TableMenu', 'formatform': '.uno:FormatFormMenu', 'page': '.uno:PageMenu', 'shape': '.uno:ShapeMenu', 'slide': '.uno:SlideMenu', 'slideshow': '.uno:SlideShowMenu', } def __init__(self, app: str): """ :param app: LibreOffice Module: calc, writer, draw, impress, math, main :type app: str """ self._app = app self._config = self._get_config() self._menus = self._config.getSettings(self.NODE, True) def _get_config(self): """Get config manager""" service = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' type_app = LODocuments.TYPES[self._app] manager = create_instance(service, True) config = manager.getUIConfigurationManager(type_app) return config def debug(self): """Debug menu""" LOMenuDebug()(self._menus) return def __contains__(self, name): """If exists name in menu""" exists = False for m in self._menus: menu = data_to_dict(m) cmd = menu.get('CommandURL', '') if name == cmd: exists = True break return exists def __getitem__(self, index): """Index access""" if isinstance(index, int): menu = data_to_dict(self._menus[index]) else: for m in self._menus: menu = data_to_dict(m) cmd = menu.get('CommandURL', '') if cmd == index or cmd == self.MENUS[index.lower()]: break obj = LOMenu(self._config, self._menus, self._app, menu['ItemDescriptorContainer']) return obj def insert(self, menu: dict, after: Union[int, str]='', save: bool=True): """Insert new menu :param menu: New menu data :type menu: dict :param after: Insert in after menu :type after: int or str :param save: For persistente save :type save: bool """ LOMenuBase.config = self._config LOMenuBase.menus = self._menus LOMenuBase.app = self._app LOMenuBase.insert(self._menus, menu, after) if save: self._config.store() return def remove(self, menu: str): """Remove menu :param menu: Menu name :type menu: str """ LOMenuBase.config = self._config LOMenuBase.menus = self._menus LOMenuBase.remove(self._menus, menu) return class LOMenus(object): """Classe for manager menus""" def __getitem__(self, index): """Index access""" return LOMenuApp(index) class LOEvents(): def __init__(self, obj): self._obj = obj def __contains__(self, item): return self.obj.hasByName(item) def __getitem__(self, index): """Index access""" return self.obj.getByName(index) def __setitem__(self, name: str, macro: dict): """Set macro to event :param name: Event name :type name: str :param macro: Macro execute in event :type name: dict """ pv = '[]com.sun.star.beans.PropertyValue' args = () if macro: url = Macro.get_url_script(macro) args = dict_to_property(dict(EventType='Script', Script=url)) uno.invoke(self.obj, 'replaceByName', (name, uno.Any(pv, args))) @property def obj(self): return self._obj @property def names(self): return self.obj.ElementNames def remove(self, name): pv = '[]com.sun.star.beans.PropertyValue' uno.invoke(self.obj, 'replaceByName', (name, uno.Any(pv, ()))) return class LOMain(): """Classe for LibreOffice""" class commands(): """Class for disable and enable commands `See DispatchCommands `_ """ @classmethod def _set_app_command(cls, command: str, disable: bool): """Disable or enabled UNO command :param command: UNO command to disable or enabled :type command: str :param disable: True if disable, False if active :type disable: bool :return: True if correctly update, False if not. :rtype: bool """ NEW_NODE_NAME = f'zaz_disable_command_{command.lower()}' name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationUpdateAccess' node_name = '/org.openoffice.Office.Commands/Execute/Disabled' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) update = cp.createInstanceWithArguments(service, (node,)) result = True try: if disable: new_node = update.createInstanceWithArguments(()) new_node.setPropertyValue('Command', command) update.insertByName(NEW_NODE_NAME, new_node) else: update.removeByName(NEW_NODE_NAME) update.commitChanges() except Exception as e: result = False return result @classmethod def disable(cls, command: str): """Disable UNO command :param command: UNO command to disable :type command: str :return: True if correctly disable, False if not. :rtype: bool """ return cls._set_app_command(command, True) @classmethod def enabled(cls, command): """Enabled UNO command :param command: UNO command to enabled :type command: str :return: True if correctly disable, False if not. :rtype: bool """ return cls._set_app_command(command, False) @classproperty def cmd(cls): """Disable or enable commands""" return cls.commands @classproperty def desktop(cls): """Create desktop instance :return: Desktop instance :rtype: pyUno """ obj = create_instance('com.sun.star.frame.Desktop', True) return obj @classmethod def dispatch(cls, frame: Any, command: str, args: dict={}) -> None: """Call dispatch, used only if not exists directly in API :param frame: doc or frame instance :type frame: pyUno :param command: Command to execute :type command: str :param args: Extra argument for command :type args: dict `See DispatchCommands <`See DispatchCommands `_>`_ """ dispatch = create_instance('com.sun.star.frame.DispatchHelper') if hasattr(frame, 'frame'): frame = frame.frame url = command if not command.startswith('.uno:'): url = f'.uno:{command}' opt = dict_to_property(args) dispatch.executeDispatch(frame, url, '', 0, opt) return @classmethod def fonts(cls): """Get all font visibles in LibreOffice :return: tuple of FontDescriptors :rtype: tuple `See API FontDescriptor `_ """ toolkit = create_instance('com.sun.star.awt.Toolkit') device = toolkit.createScreenCompatibleDevice(0, 0) return device.FontDescriptors @classmethod def filters(cls): """Get all support filters `See Help ConvertFilters `_ `See API FilterFactory `_ """ factory = create_instance('com.sun.star.document.FilterFactory') rows = [data_to_dict(factory[name]) for name in factory] for row in rows: row['UINames'] = data_to_dict(row['UINames']) return rows class LODocument(): def __init__(self, obj): self._obj = obj self._cc = obj.getCurrentController() self._undo = True def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() @property def obj(self): """Return original pyUno object""" return self._obj @property def type(self): """Get type document""" return self._type @property def frame(self): """Get frame document""" return self._cc.getFrame() @property def title(self): """Get title document""" return self.obj.getTitle() @title.setter def title(self, value): self.obj.setTitle(value) @property def uid(self): """Get Runtime UID""" return self.obj.RuntimeUID @property def is_saved(self): """Get is saved""" return self.obj.hasLocation() @property def is_modified(self): """Get is modified""" return self.obj.isModified() @property def is_read_only(self): """Get is read only""" return self.obj.isReadonly() @property def path(self): """Get path in system files""" return Paths.to_system(self.obj.URL) @property def dir(self): """Get directory from path""" return Paths(self.path).path @property def file_name(self): """Get only file name""" return Paths(self.path).file_name @property def name(self): """Get name without extension""" return Paths(self.path).name @property def visible(self): """Get windows visible""" w = self.frame.ContainerWindow return w.isVisible() @visible.setter def visible(self, value): w = self.frame.ContainerWindow w.setVisible(value) @property def zoom(self): """Get current zoom value""" return self._cc.ZoomValue @zoom.setter def zoom(self, value): self._cc.ZoomValue = value @property def status_bar(self): """Get status bar""" bar = self._cc.getStatusIndicator() return bar @property def selection(self): """Get current selecction""" sel = self.obj.CurrentSelection return sel @property def table_auto_formats(self): taf = create_instance('com.sun.star.sheet.TableAutoFormats') return taf.ElementNames def save(self, path: str='', args: dict={}) -> bool: """Save document :param path: Path to save document :type path: str :param args: Optional: Extra argument for save :type args: dict :return: True if save correctly, False if not :rtype: bool """ if not path: self.obj.store() return True path_save = Paths.to_url(path) opt = dict_to_property(args) try: self.obj.storeAsURL(path_save, opt) except Exception as e: error(e) return False return True def close(self): """Close document""" self.obj.close(True) return def to_pdf(self, path: str='', args: dict={}): """Export to PDF :param path: Path to export document :type path: str :param args: Optional: Extra argument for export :type args: dict :return: None if path or stream in memory :rtype: bytes or None `See PDF Export `_ """ stream = None path_pdf = 'private:stream' filter_name = f'{self.type}_pdf_Export' filter_data = dict_to_property(args, True) filters = { 'FilterName': filter_name, 'FilterData': filter_data, } if path: path_pdf = Paths.to_url(path) else: stream = IOStream.output() filters['OutputStream'] = stream opt = dict_to_property(filters) try: self.obj.storeToURL(path_pdf, opt) except Exception as e: error(e) if not stream is None: stream = stream.buffer return stream def export(self, path: str='', filter_name: str='', args: dict={}): """Export to others formats :param path: Path to export document :type path: str :param filter_name: Filter name to export :type filter_name: str :param args: Optional: Extra argument for export :type args: dict :return: None if path or stream in memory :rtype: bytes or None """ FILTERS = { 'xlsx': 'Calc MS Excel 2007 XML', 'xls': 'MS Excel 97', 'docx': 'MS Word 2007 XML', 'doc': 'MS Word 97', 'rtf': 'Rich Text Format', } stream = None path_target = 'private:stream' filter_name = FILTERS.get(filter_name, filter_name) filter_data = dict_to_property(args, True) filters = { 'FilterName': filter_name, 'FilterData': filter_data, } if path: path_target = Paths.to_url(path) else: stream = IOStream.output() filters['OutputStream'] = stream opt = dict_to_property(filters) try: self.obj.storeToURL(path_target, opt) except Exception as e: error(e) if not stream is None: stream = stream.buffer return stream def _create_instance(self, name): obj = self.obj.createInstance(name) return obj def set_focus(self): """Send focus to windows""" w = self.frame.ComponentWindow w.setFocus() return def copy(self): """Copy current selection""" LOMain.dispatch(self.frame, 'Copy') return def paste(self): """Paste current content in clipboard""" sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') transferable = sc.getContents() self._cc.insertTransferable(transferable) return def paste_special(self): """Insert contents, show dialog box Paste Special""" LOMain.dispatch(self.frame, 'InsertContents') return def paste_values(self): """Paste only values""" args = { 'Flags': 'SVDT', # ~ 'FormulaCommand': 0, # ~ 'SkipEmptyCells': False, # ~ 'Transpose': False, # ~ 'AsLink': False, # ~ 'MoveMode': 4, } LOMain.dispatch(self.frame, 'InsertContents', args) return def clear_undo(self): """Clear history undo""" self.obj.getUndoManager().clear() return class LODocMain(): """Classe for start module""" _type = 'main' def __init__(self, obj): self._obj = obj @property def obj(self): return self._obj @property def type(self): return self._type class LOCellStyle(): def __init__(self, obj): self._obj = obj def __str__(self): return f'CellStyle: {self.name}' @property def obj(self): return self._obj @property def name(self): return self.obj.Name @property def properties(self): properties = self.obj.PropertySetInfo.Properties data = {p.Name: getattr(self.obj, p.Name) for p in properties} return data @properties.setter def properties(self, values): _set_properties(self.obj, values) class LOCellStyles(): def __init__(self, obj, doc): self._obj = obj self._doc = doc def __len__(self): return len(self.obj) def __getitem__(self, index): return LOCellStyle(self.obj[index]) def __setitem__(self, key, value): self.obj[key] = value def __delitem__(self, key): if not isinstance(key, str): key = key.Name del self.obj[key] def __contains__(self, item): return item in self.obj @property def obj(self): return self._obj @property def names(self): return self.obj.ElementNames def new(self, name: str): obj = self._doc.create_instance('com.sun.star.style.CellStyle') self.obj[name] = obj return LOCellStyle(obj) class LODocCalc(LODocument): """Classe for Calc module""" TYPE_RANGES = ('ScCellObj', 'ScCellRangeObj') RANGES = 'ScCellRangesObj' SHAPE = 'com.sun.star.drawing.SvxShapeCollection' _type = 'calc' def __init__(self, obj): super().__init__(obj) self._sheets = obj.Sheets self._listener_range_selection = None def __getitem__(self, index): """Index access""" return LOCalcSheet(self._sheets[index]) def __setitem__(self, key: str, value: Any): """Insert new sheet""" self._sheets[key] = value def __len__(self): return self._sheets.Count def __contains__(self, item): return item in self._sheets def __iter__(self): self._i = 0 return self def __next__(self): try: sheet = LOCalcSheet(self._sheets[self._i]) except Exception as e: raise StopIteration self._i += 1 return sheet def __str__(self): return f'Calc: {self.title}' @property def selection(self): sel = self.obj.CurrentSelection type_obj = sel.ImplementationName if type_obj in self.TYPE_RANGES: sel = LOCalcRange(sel) elif type_obj == self.RANGES: sel = LOCalcRanges(sel) elif type_obj == self.SHAPE: if len(sel) == 1: sel = LOShape(sel[0]) else: sel = LOShapes(sel) else: debug(type_obj) return sel @property def headers(self): """Get true if is visible columns/rows headers""" return self._cc.ColumnRowHeaders @headers.setter def headers(self, value): """Set visible columns/rows headers""" self._cc.ColumnRowHeaders = value @property def tabs(self): """Get true if is visible tab sheets""" return self._cc.SheetTabs @tabs.setter def tabs(self, value): """Set visible tab sheets""" self._cc.SheetTabs = value @property def names(self): """Get all sheet names""" names = self.obj.Sheets.ElementNames return names @property def active(self): """Get active sheet""" return LOCalcSheet(self._cc.ActiveSheet) @property def new_sheet(self): sheet = self._create_instance('com.sun.star.sheet.Spreadsheet') return sheet @property def events(self): return LOEvents(self.obj.Events) @property def cs(self): return self.cell_styles @property def cell_styles(self): obj = self.obj.StyleFamilies['CellStyles'] return LOCellStyles(obj, self) def activate(self, sheet: Any): """Activate sheet :param sheet: Sheet to activate :type sheet: str, pyUno or LOCalcSheet """ obj = sheet if isinstance(sheet, LOCalcSheet): obj = sheet.obj elif isinstance(sheet, str): obj = self._sheets[sheet] self._cc.setActiveSheet(obj) return def insert(self, name: Union[str, list, tuple]): """Insert new sheet :param name: Name new sheet, or iterable with names. :type name: str, list or tuple :return: New last instance sheet. :rtype: LOCalcSheet """ names = name if isinstance(name, str): names = (name,) for n in names: self._sheets[n] = self._create_instance('com.sun.star.sheet.Spreadsheet') return LOCalcSheet(self._sheets[n]) def remove(self, name: str): """Remove sheet by name :param name: Name sheet will remove :type name: str """ if isinstance(name, LOCalcSheet): name = name.name self._sheets.removeByName(name) return def move(self, name:str, pos: int=-1): """Move sheet name to position :param name: Name sheet to move :type name: str :param pos: New position, if pos=-1 move to end :type pos: int """ index = pos if pos < 0: index = len(self) if isinstance(name, LOCalcSheet): name = name.name self._sheets.moveByName(name, index) return def _get_new_name_sheet(self, name): i = 1 new_name = f'{name}_{i}' while new_name in self: i += 1 new_name = f'{name}_{i}' return new_name def copy_sheet(self, name: Any, new_name: str='', pos: int=-1): """Copy sheet by name """ if isinstance(name, LOCalcSheet): name = name.name index = pos if pos < 0: index = len(self) if not new_name: new_name = self._get_new_name_sheet(name) self._sheets.copyByName(name, new_name, index) return LOCalcSheet(self._sheets[new_name]) def copy_from(self, doc: Any, source: Any=None, target: Any=None, pos: int=-1): """Copy sheet from document """ index = pos if pos < 0: index = len(self) names = source if not source: names = doc.names elif isinstance(source, str): names = (source,) elif isinstance(source, LOCalcSheet): names = (source.name,) new_names = target if not target: new_names = names elif isinstance(target, str): new_names = (target,) for i, name in enumerate(names): self._sheets.importSheet(doc.obj, name, index + i) self[index + i].name = new_names[i] return LOCalcSheet(self._sheets[index]) def sort(self, reverse=False): """Sort sheets by name :param reverse: For order in reverse :type reverse: bool """ names = sorted(self.names, reverse=reverse) for i, n in enumerate(names): self.move(n, i) return @run_in_thread def start_range_selection(self, controllers: Any, args: dict={}): """Start select range selection by user `See Api RangeSelectionArguments `_ """ if args: args['CloseOnMouseRelease'] = args.get('CloseOnMouseRelease', True) else: args = dict( Title = 'Please select a range', CloseOnMouseRelease = True) properties = dict_to_property(args) self._listener_range_selection = EventsRangeSelectionListener(controllers(self)) self._cc.addRangeSelectionListener(self._listener_range_selection) self._cc.startRangeSelection(properties) return def remove_range_selection_listener(self): if not self._listener_range_selection is None: self._cc.removeRangeSelectionListener(self._listener_range_selection) return def select(self, rango: Any): obj = rango if hasattr(rango, 'obj'): obj = rango.obj self._cc.select(obj) return @property def ranges(self): obj = self._create_instance('com.sun.star.sheet.SheetCellRanges') return LOCalcRanges(obj) def get_ranges(self, address: str): ranges = self.ranges ranges.add([sheet[address] for sheet in self]) return ranges class LOCalcSheet(object): def __init__(self, obj): self._obj = obj def __getitem__(self, index): return LOCalcRange(self.obj[index]) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __str__(self): return f'Sheet: {self.name}' @property def obj(self): return self._obj @property def doc(self): return LODocCalc(self.obj.DrawPage.Forms.Parent) @property def name(self): return self._obj.Name @name.setter def name(self, value): self._obj.Name = value @property def code_name(self): return self._obj.CodeName @code_name.setter def code_name(self, value): self._obj.CodeName = value @property def visible(self): return self._obj.IsVisible @visible.setter def visible(self, value): self._obj.IsVisible = value @property def color(self): return self._obj.TabColor @color.setter def color(self, value): self._obj.TabColor = Color()(value) @property def events(self): return LOEvents(self.obj.Events) @property def used_area(self): cursor = self.create_cursor() cursor.gotoEndOfUsedArea(True) return self[cursor.AbsoluteName] @property def is_protected(self): return self._obj.isProtected() @property def password(self): return '' @password.setter def password(self, value): self.obj.protect(value) def unprotect(self, value): try: self.obj.unprotect(value) return True except: pass return False def move(self, pos: int=-1): index = pos if pos < 0: index = len(self.doc) self.doc.move(self.name, index) return def remove(self): self.doc.remove(self.name) return def copy(self, new_name: str='', pos: int=-1): index = pos if pos < 0: index = len(self.doc) new_sheet = self.doc.copy_sheet(self.name, new_name, index) return new_sheet def copy_to(self, doc: Any, target: str='', pos: int=-1): index = pos if pos < 0: index = len(doc) new_name = target or self.name sheet = doc.copy_from(self.doc, self.name, new_name, index) return sheet def activate(self): self.doc.activate(self.obj) return def create_cursor(self, rango: Any=None): if rango is None: cursor = self.obj.createCursor() else: obj = rango if hasattr(rango, 'obj'): obj = rango.obj cursor = self.obj.createCursorByRange(obj) return cursor class LOCalcRanges(object): def __init__(self, obj): self._obj = obj def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __len__(self): return self._obj.Count def __iter__(self): self._index = 0 return self def __next__(self): try: r = self.obj[self._index] rango = LOCalcRange(r) except IndexError: raise StopIteration self._index += 1 return rango def __contains__(self, item): return self._obj.hasByName(item.name) def __getitem__(self, index): r = self.obj[index] rango = LOCalcRange(r) return rango def __str__(self): s = f'Ranges: {self.names}' return s @property def obj(self): return self._obj @property def names(self): return self.obj.ElementNames @property def data(self): rows = [r.data for r in self] return rows @data.setter def data(self, values): for i, data in enumerate(values): self[i].data = data @property def style(self): return '' @style.setter def style(self, value): for r in self: r.style = value def add(self, rangos: Any): if isinstance(rangos, LOCalcRange): rangos = (rangos,) for r in rangos: self.obj.addRangeAddress(r.range_address, False) return def remove(self, rangos: Any): if isinstance(rangos, LOCalcRange): rangos = (rangos,) for r in rangos: self.obj.removeRangeAddress(r.range_address) return class LOCalcRange(object): CELL = 'ScCellObj' def __init__(self, obj): self._obj = obj self._is_cell = obj.ImplementationName == self.CELL def __getitem__(self, index): return LOCalcRange(self.obj[index]) def __iter__(self): self._r = 0 self._c = 0 return self def __next__(self): try: rango = self[self._r, self._c] except Exception as e: raise StopIteration self._c += 1 if self._c == self.obj.Columns.Count: self._c = 0 self._r +=1 return rango def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __len__(self): ra = self.range_address rows = ra.EndRow - ra.StartRow + 1 cols = ra.EndColumn - ra.StartColumn + 1 return rows, cols def __str__(self): s = f'Range: {self.name}' if self.is_cell: s = f'Cell: {self.name}' return s @property def obj(self): return self._obj @property def is_cell(self): return self._is_cell @property def name(self): return self.obj.AbsoluteName @property def address(self): return self.obj.CellAddress @property def range_address(self): return self.obj.RangeAddress @property def sheet(self): return LOCalcSheet(self.obj.Spreadsheet) @property def doc(self): doc = self.obj.Spreadsheet.DrawPage.Forms.Parent return LODocCalc(doc) @property def cursor(self): cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) return cursor def offset(self, rows=0, cols=1): ra = self.range_address col = ra.EndColumn + cols row = ra.EndRow + rows return LOCalcRange(self.sheet[row, col].obj) @property def style(self): return self.obj.CellStyle @style.setter def style(self, value): self.obj.CellStyle = value @property def string(self): return self.obj.String @string.setter def string(self, value): self.obj.setString(value) @property def data(self): return self.obj.getDataArray() @data.setter def data(self, values): if self._is_cell: self.to_size(len(values[0]), len(values)).data = values else: self.obj.setDataArray(values) @property def current_region(self): cursor = self.cursor cursor.collapseToCurrentRegion() rango = self.obj.Spreadsheet[cursor.AbsoluteName] return LOCalcRange(rango) def to_size(self, cols: int, rows: int): cursor = self.cursor cursor.collapseToSize(cols, rows) rango = self.obj.Spreadsheet[cursor.AbsoluteName] return LOCalcRange(rango) class LOWriterTextRange(object): def __init__(self, obj, doc): self._obj = obj self._doc = doc @property def obj(self): return self._obj @property def text(self): return self.obj.Text @property def cursor(self): return self.text.createTextCursorByRange(self.obj) def insert_comment(self, content: str, author: str='', dt: Any=None): # ~ range.Text.insertTextContent(cursor, comment, False) comment = self._doc._create_instance('com.sun.star.text.textfield.Annotation') comment.Content = content comment.Author = author comment.attach(self.cursor.End) return class LODocWriter(LODocument): _type = 'writer' TEXT_RANGES = 'SwXTextRanges' def __init__(self, obj): super().__init__(obj) self._view_settings = self._cc.ViewSettings @property def selection(self): sel = self.obj.CurrentSelection type_obj = sel.ImplementationName if type_obj == self.TEXT_RANGES: if len(sel) == 1: sel = LOWriterTextRange(sel[0], self) return sel @property def zoom(self): return self._view_settings.ZoomValue @zoom.setter def zoom(self, value): self._view_settings.ZoomValue = value class LODocDrawImpress(LODocument): def __init__(self, obj): super().__init__(obj) class LODocDraw(LODocDrawImpress): _type = 'draw' def __init__(self, obj): super().__init__(obj) class LODocImpress(LODocDrawImpress): _type = 'impress' def __init__(self, obj): super().__init__(obj) class LODocMath(LODocDrawImpress): _type = 'math' def __init__(self, obj): super().__init__(obj) class LODocBase(LODocument): _type = 'base' def __init__(self, obj): super().__init__(obj) class LODocIDE(LODocument): _type = 'basicide' def __init__(self, obj): super().__init__(obj) class LODocuments(): """Classe for documents """ TYPES = { 'calc': 'com.sun.star.sheet.SpreadsheetDocument', 'writerr': 'com.sun.star.text.TextDocument', 'draw': 'com.sun.star.drawing.DrawingDocument', 'impress': 'com.sun.star.presentation.PresentationDocument', 'math': 'com.sun.star.formula.FormulaProperties', 'ide': 'com.sun.star.script.BasicIDE', 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', 'main': 'com.sun.star.frame.StartModule', } _classes = { 'com.sun.star.sheet.SpreadsheetDocument': LODocCalc, 'com.sun.star.text.TextDocument': LODocWriter, 'com.sun.star.drawing.DrawingDocument': LODocDraw, 'com.sun.star.presentation.PresentationDocument': LODocImpress, 'com.sun.star.formula.FormulaProperties': LODocMath, 'com.sun.star.script.BasicIDE': LODocIDE, 'com.sun.star.sdb.OfficeDatabaseDocument': LODocBase, 'com.sun.star.frame.StartModule': LODocMain } # ~ BASE: 'com.sun.star.sdb.DocumentDataSource', def __init__(self): self._desktop = LOMain.desktop def __len__(self): # ~ len(self._desktop.Components) for i, _ in enumerate(self._desktop.Components): pass return i + 1 def __getitem__(self, index): # ~ self._desktop.Components[index] obj = None for i, doc in enumerate(self._desktop.Components): if isinstance(index, int) and i == index: obj = self._get_class_doc(doc) break elif isinstance(index, str) and doc.Title == index: obj = self._get_class_doc(doc) break return obj def __contains__(self, item): doc = self[item] return not doc is None def __iter__(self): self._i = -1 return self def __next__(self): self._i += 1 doc = self[self._i] if doc is None: raise StopIteration else: return doc def _get_class_doc(self, doc): """Identify type doc""" main = 'com.sun.star.frame.StartModule' if doc.supportsService(main): return self._classes[main](doc) mm = create_instance('com.sun.star.frame.ModuleManager') type_module = mm.identify(doc) return self._classes[type_module](doc) @property def active(self): """Get active doc""" doc = self._desktop.getCurrentComponent() obj = self._get_class_doc(doc) return obj def new(self, type_doc: str='calc', args: dict={}): """Create new document :param type_doc: The type doc to create, default is Calc :type type_doc: str :param args: Extra argument :type args: dict :return: New document :rtype: Custom classe """ url = f'private:factory/s{type_doc}' opt = dict_to_property(args) doc = self._desktop.loadComponentFromURL(url, '_default', 0, opt) obj = self._get_class_doc(doc) return obj def open(self, path: str, args: dict={}): """ Open document from path :param path: Path to document :type path: str :param args: Extra argument Usually options: Hidden: True or False AsTemplate: True or False ReadOnly: True or False Password: super_secret MacroExecutionMode: 4 = Activate macros Preview: True or False :type args: dict `See API XComponentLoader `_ `See API MediaDescriptor `_ """ url = Paths.to_url(path) opt = dict_to_property(args) doc = self._desktop.loadComponentFromURL(url, '_default', 0, opt) if doc is None: return obj = self._get_class_doc(doc) return obj def __getattr__(name): classes = { 'inspect': LOInspect, 'dates': Dates, 'json': Json, 'macro': Macro, 'shell': Shell, 'timer': Timer, 'hash': Hash, 'path': Paths, 'config': Config, 'url': Url, 'email': Email, 'color': Color(), 'io': IOStream, 'clipboard': ClipBoard, 'shortcuts': LOShortCuts(), 'menus': LOMenus(), 'lo': LOMain, 'command': LOMain.cmd, 'docs': LODocuments(), 'active': LODocuments().active, } if name in classes: return classes[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") class LOServer(object): """Started LibeOffice like server """ HOST = 'localhost' PORT = '8100' ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext' CMD = ['soffice', '-env:SingleAppInstance=false', '-env:UserInstallation=file:///tmp/LO_Process8100', '--headless', '--norestore', '--invisible', f'--accept={ARG}'] def __init__(self): self._server = None self._ctx = None self._sm = None self._start_server() self._init_values() def _init_values(self): global CTX global SM if not self.is_running: return ctx = uno.getComponentContext() service = 'com.sun.star.bridge.UnoUrlResolver' resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx) self._ctx = resolver.resolve('uno:{}'.format(self.ARG)) self._sm = self._ctx.getServiceManager() CTX = self._ctx SM = self._sm return @property def is_running(self): try: s = socket.create_connection((self.HOST, self.PORT), 5.0) s.close() debug('LibreOffice is running...') return True except ConnectionRefusedError: return False def _start_server(self): if self.is_running: return for i in range(3): self._server = subprocess.Popen(self.CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(3) if self.is_running: break return def stop(self): """Stop server """ if self._server is None: print('Search pgrep soffice') else: self._server.terminate() debug('LibreOffice is stop...') return def _create_instance(self, name, with_context=True): if with_context: instance = self._sm.createInstanceWithContext(name, self._ctx) else: instance = self._sm.createInstance(name) return instance