easymacro/source/easymacro.py

443 lines
12 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 datetime
import getpass
import logging
import os
import platform
import socket
import subprocess
import sys
import time
import traceback
from functools import wraps
from pprint import pprint
from typing import Any
import uno
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
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
log = logging.getLogger(__name__)
# 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'
_info_debug = f"Python: {sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path)
SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc'
CTX = uno.getComponentContext()
SM = CTX.getServiceManager()
# UNO Enum
class MessageBoxType():
from com.sun.star.awt.MessageBoxType \
import MESSAGEBOX, INFOBOX, WARNINGBOX, ERRORBOX, QUERYBOX
MBT = MessageBoxType
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 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
def set_app_config(node_name: str, key: str, new_value: Any) -> Any:
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 _set_app_command(command: str, disable: 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,))
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()
return
class commands():
# ~ https://wiki.documentfoundation.org/Development/DispatchCommands
def __init__(self, command):
self._command = command
def disable(self):
return _set_app_command(self._command, True)
def enabled(self):
return _set_app_command(self._command, False)
# 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}"
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 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>`_
"""
mri = create_instance('mytools.Mri')
if m is None:
msg = 'Extension MRI not found'
error(msg)
return
if hasattr(obj, 'obj'):
obj = obj.obj
mri.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) -> Any:
"""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: Instance MessageBox
:rtype: pyUno
`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) -> Any:
"""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) -> Any:
"""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: Instance MessageBox
:rtype: pyUno
"""
return msgbox(message, title, type_message_box=MessageBoxType.WARNINGBOX)
def errorbox(message: Any, title: str=TITLE) -> Any:
"""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: Instance MessageBox
:rtype: pyUno
"""
return msgbox(message, title, type_message_box=MessageBoxType.ERRORBOX)
def now():
"""Current local date and time
:return: Return the current local date and time
:rtype: datetime
"""
return datetime.datetime.now()
def today():
"""Current local date
:return: Return the current local date
:rtype: date
"""
return datetime.date.today()
class LOServer(object):
HOST = 'localhost'
PORT = '8100'
ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext'
CMD = ['soffice',
'-env:SingleAppInstance=false',
'-env:UserInstallation=file:///tmp/LO_Process8100',
'--headless', '--norestore', '--invisible',
f'--accept={ARG}']
def __init__(self):
self._server = None
self._ctx = None
self._sm = None
self._start_server()
self._init_values()
def _init_values(self):
global CTX
global SM
if not self.is_running:
return
ctx = uno.getComponentContext()
service = 'com.sun.star.bridge.UnoUrlResolver'
resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx)
self._ctx = resolver.resolve('uno:{}'.format(self.ARG))
self._sm = self._ctx.getServiceManager()
CTX = self._ctx
SM = self._sm
return
@property
def is_running(self):
try:
s = socket.create_connection((self.HOST, self.PORT), 5.0)
s.close()
debug('LibreOffice is running...')
return True
except ConnectionRefusedError:
return False
def _start_server(self):
if self.is_running:
return
for i in range(3):
self._server = subprocess.Popen(self.CMD,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
time.sleep(3)
if self.is_running:
break
return
def stop(self):
if self._server is None:
print('Search pgrep soffice')
else:
self._server.terminate()
debug('LibreOffice is stop...')
return
def create_instance(self, name, with_context=True):
if with_context:
instance = self._sm.createInstanceWithContext(name, self._ctx)
else:
instance = self._sm.createInstance(name)
return instance