4333 lines
118 KiB
Python
4333 lines
118 KiB
Python
#!/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 <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
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 <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt.html#ad249d76933bdf54c35f4eaf51a5b7965>`_
|
|
"""
|
|
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 <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1configuration_1_1ConfigurationProvider.html>`_
|
|
"""
|
|
|
|
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 <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1configuration_1_1ConfigurationUpdateAccess.html>`_
|
|
"""
|
|
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 <https://github.com/hanya/MRI/releases>`_
|
|
"""
|
|
|
|
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 <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1MessageBoxButtons.html>`_
|
|
:type buttons: long
|
|
:param type_message_box: The `message box type <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt.html#ad249d76933bdf54c35f4eaf51a5b7965>`_
|
|
:type type_message_box: enum
|
|
:return: `MessageBoxResult <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1MessageBoxResults.html>`_
|
|
:rtype: int
|
|
|
|
`See Api XMessageBoxFactory <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_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 <https://github.com/hanya/MRI/releases>`_
|
|
"""
|
|
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 <https://en.wikipedia.org/wiki/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 <https://docs.python.org/3/library/datetime.html#date-objects>`_
|
|
"""
|
|
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 <https://docs.python.org/3/library/datetime.html#datetime.datetime.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 <https://docs.python.org/3/library/datetime.html#datetime.datetime.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 <https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification>`_
|
|
"""
|
|
@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 <https://docs.python.org/3.7/library/subprocess.html>`_
|
|
"""
|
|
@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 <http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html>`_
|
|
"""
|
|
path = create_instance('com.sun.star.util.PathSettings')
|
|
path = cls.to_system(getattr(path, name)).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 <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html>`_
|
|
"""
|
|
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 <https://docs.python.org/3.8/library/csv.html#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 <https://docs.python.org/3.8/library/csv.html#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', '<BR>')
|
|
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 <https://en.wikipedia.org/wiki/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 <https://wiki.documentfoundation.org/Development/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 <https://wiki.documentfoundation.org/Development/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 <https://api.libreoffice.org/docs/idl/ref/structcom_1_1sun_1_1star_1_1awt_1_1FontDescriptor.html>`_
|
|
"""
|
|
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 <https://help.libreoffice.org/latest/en-US/text/shared/guide/convertfilters.html>`_
|
|
`See API FilterFactory <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1FilterFactory.html>`_
|
|
"""
|
|
factory = create_instance('com.sun.star.document.FilterFactory')
|
|
rows = [data_to_dict(factory[name]) for name in factory]
|
|
for row in rows:
|
|
row['UINames'] = data_to_dict(row['UINames'])
|
|
return rows
|
|
|
|
|
|
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 <https://wiki.documentfoundation.org/Macros/Python_Guide/PDF_export_filter_data>`_
|
|
"""
|
|
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 <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1sheet_1_1RangeSelectionArguments.html>`_
|
|
"""
|
|
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 <http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1frame_1_1XComponentLoader.html>`_
|
|
`See API MediaDescriptor <http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html>`_
|
|
"""
|
|
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
|