diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58bd977 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] + +*.log +conf.py +files/ + +docs/ + +# Virtualenv +.env/ +virtual/ + + diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..155a043 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,4 @@ +v 0.1.0 [06-sep-2019] +--------------------- + - Initial version + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..45e2060 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +* Used zaz and easymacro +* Report errors +* Add examples +* Add to list of proyects develop with zaz or easymacro +* Send me a postal card +* Pay for support + + +**BCH**: `pzkuydh70v4r52hl3nqs66gy9wqdtrgej5ewv48xyq` + +**BTC**: `3FhiXcXmAesmQzrNEngjHFnvaJRhU1AGWV` + +**LTC**: `MBcgQ3LQJA4W2wsXknTdm2fxRSysLaBJHS` + +**ETH**: `0x61a4f614a30ff686445751ed8328b82b77ecfc69` + + diff --git a/README.md b/README.md index 8eca20b..7993ed5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ ZAZ Scripts and library for develop macros in LibreOffice with Python. + +Develop in pure Python, not need any dependence. + +For Python 3.6+ diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..d294d43 --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +* Automatic update +* Help +* Configuration +* Option panel +* Sub-menus diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..f3e862a --- /dev/null +++ b/VERSION @@ -0,0 +1,2 @@ +zaz 0.1.0 +easymacro 0.1.0 diff --git a/source/conf.py.example b/source/conf.py.example new file mode 100644 index 0000000..13b19b8 --- /dev/null +++ b/source/conf.py.example @@ -0,0 +1,541 @@ +# ~ This file is part of ZAZ. + +# ~ ZAZ is free software: you can redistribute it and/or modify +# ~ it under the terms of the GNU General Public License as published by +# ~ the Free Software Foundation, either version 3 of the License, or +# ~ (at your option) any later version. + +# ~ ZAZ is distributed in the hope that it will be useful, +# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of +# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# ~ GNU General Public License for more details. + +# ~ You should have received a copy of the GNU General Public License +# ~ along with ZAZ. If not, see . + + +import logging + + +# ~ Type extension: +# ~ 1 = normal extension +# ~ 2 = new component +# ~ 3 = Calc addin +TYPE_EXTENSION = 1 + +# ~ https://semver.org/ +VERSION = '0.1.0' + +# ~ Your great extension name, not used spaces +NAME = 'TestMacro' + +# ~ Should be unique, used URL inverse +ID = 'org.myextension.test' + +PUBLISHER = { + 'en': {'text': 'El Mau', 'link': 'https://elmau.net'}, + 'es': {'text': 'El Mau', 'link': 'https://elmau.net'}, +} + +# ~ Name in this folder for copy +ICON = 'images/logo.png' +# ~ Name inside extensions +ICON_EXT = f'{NAME.lower()}.png' + +# ~ For example +# ~ DEPENDENCIES_MINIMAL = '6.0' +DEPENDENCIES_MINIMAL = '' + +LICENSE_ACCEPT_BY = 'user' # or admin +LICENSE_SUPPRESS_ON_UPDATE = True +# ~ Change for you favorite license +LICENSE_EN = f"""This file is part of {NAME}. + + {NAME} 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. + + {NAME} 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 {NAME}. If not, see . +""" +LICENSE_ES = LICENSE_EN + +INFO = { + 'en': { + 'display_name': 'Test Macro', + 'description': 'My great extension', + 'license': LICENSE_EN, + }, + 'es': { + 'display_name': 'Macro de Prueba', + 'description': 'Mi gran extensión', + 'license': LICENSE_ES, + }, +} + + +CONTEXT = { + 'calc': 'com.sun.star.sheet.SpreadsheetDocument', +} + + +# ~ Menus, only for TYPE_EXTENSION = 1 +# ~ Parent can be: AddonMenu or OfficeMenuBar +# ~ For icons con name: NAME_16.bmp, used only NAME +# ~ PARENT = 'AddonMenu' +# ~ MENU_MAIN = '' +PARENT = 'OfficeMenuBar' +MENU_MAIN = { + 'en': 'My Extension', + 'es': 'Mi Extensión', +} +MENUS = ( + { + 'title': {'en': 'Option 1', 'es': 'Opción 1'}, + 'argument': 'option1', + 'context': '', + 'icon': 'icon', + 'toolbar': True, + }, +) + +# ~ Functions, only for TYPE_EXTENSION = 3 +FUNCTIONS = { + 'test': { + 'displayname': {'en': 'test', 'es': 'prueba'}, + 'description': {'en': 'My test', 'es': 'Mi prueba'}, + 'parameters': { + 'value': { + 'displayname': {'en': 'value', 'es': 'valor'}, + 'description': {'en': 'The value', 'es': 'El valor'}, + }, + }, + }, +} +# ~ FUNCTIONS = {} + +EXTENSION = { + 'version': VERSION, + 'name': NAME, + 'id': ID, + 'icon': (ICON, ICON_EXT), +} + + +# If used more libraries set python path in True and copy inside +DIRS = { + 'meta': 'META-INF', + 'source': 'source', + 'description': 'description', + 'images': 'images', + 'registration': 'registration', + 'files': 'files', + 'pythonpath': False, +} + + +FILES = { + 'oxt': f'{NAME}_v{VERSION}.oxt', + 'py': f'{NAME}.py', + 'ext_desc': 'desc_{}.txt', + 'manifest': 'manifest.xml', + 'description': 'description.xml', + 'idl': f'X{NAME}.idl', + 'addons': 'Addons.xcu', + 'urd': f'X{NAME}.urd', + 'rdb': f'X{NAME}.rdb', + 'update': f'{NAME.lower()}.update.xml', + 'addin': 'CalcAddIn.xcu', +} + + +# ~ URLs for update for example +# ~ URL_XML_UPDATE = 'https://gitlab.com/USER/PROYECT/raw/BRANCH/FOLDERs/FILE_NAME' +URL_XML_UPDATE = '' +URL_OXT = '' + + +# ~ If used user profile for develop +# ~ PATH_DEV = '-env:UserInstallation=file:///home/mau/.temp/develop' +# ~ unopkg not support (or I not know how) other user profile +PATH_DEV = '' +PATHS = { + 'idlc': '/usr/lib/libreoffice/sdk/bin/idlc', + 'include': '/usr/share/idl/libreoffice', + 'regmerge': '/usr/lib/libreoffice/program/regmerge', + 'soffice': ('soffice', '--calc'), + 'install': ('unopkg', 'add', '-v', '-f', '-s'), +} + + +SERVICES = { + 'job': "('com.sun.star.task.Job',)", + 'addin': "('com.sun.star.sheet.AddIn',)", +} + + +FORMAT = '%(asctime)s - %(levelname)s - %(message)s' +DATE = '%d/%m/%Y %H:%M:%S' +LEVEL_ERROR = logging.getLevelName(logging.ERROR) +LEVEL_INFO = logging.getLevelName(logging.INFO) +logging.addLevelName(logging.ERROR, f'\033[1;41m{LEVEL_ERROR}\033[1;0m') +logging.addLevelName(logging.INFO, f'\x1b[32m{LEVEL_INFO}\033[1;0m') +logging.basicConfig(level=logging.DEBUG, format=FORMAT, datefmt=DATE) +log = logging.getLogger(NAME) + + +def _methods(): + template = """ def {0}(self, {1}): + print({1}) + return 'ok'\n""" + functions = '' + for k, v in FUNCTIONS.items(): + args = ','.join(v['parameters'].keys()) + functions += template.format(k, args) + return functions + + +SRV = SERVICES['job'] +XSRV = 'XJobExecutor' +SRV_IMPORT = f'from com.sun.star.task import {XSRV}' +METHODS = """ def trigger(self, args='pyUNO'): + print('Hello World', args) + return\n""" + +if TYPE_EXTENSION > 1: + MENUS = () + XSRV = f'X{NAME}' + SRV_IMPORT = f'from {ID} import {XSRV}' +if TYPE_EXTENSION == 2: + SRV = f"('{ID}',)" + METHODS = """ def test(self, args='pyUNO'): + print('Hello World', args) + return\n""" +elif TYPE_EXTENSION == 3: + SRV = SERVICES['addin'] + METHODS = _methods() + + +FILE_PY = f"""import uno +import unohelper +{SRV_IMPORT} + + +ID_EXTENSION = '{ID}' +SERVICE = {SRV} + + +class {NAME}(unohelper.Base, {XSRV}): + + def __init__(self, ctx): + self.ctx = ctx + +{METHODS} + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation({NAME}, ID_EXTENSION, SERVICE) +""" + + +tmp = ' {}' +node = [tmp.format(k, v['display_name']) for k, v in INFO.items()] +NODE_DISPLAY_NAME = '\n'.join(node) + +tmp = ' ' +node = [tmp.format(k) for k, v in INFO.items()] +NODE_EXTENSION_DESCRIPTION = '\n'.join(node) + +NODE_ICON = '' +if ICON: + NODE_ICON = f' ' + +NODE_PUBLISHER = '' +if PUBLISHER: + tmp = ' {}' + node = [tmp.format(v['link'], k, v['text']) for k, v in PUBLISHER.items()] + NODE_PUBLISHER = '\n'.join(node) + +NODE_DEPENDENCIES_MINIMAL = '' +if DEPENDENCIES_MINIMAL: + NODE_DEPENDENCIES_MINIMAL = f"""\n + + """ + +tmp = ' ' +node = [tmp.format(DIRS['registration'], k) for k in INFO.keys()] +NODE_LICENSE = '\n'.join(node) + +NODE_UPDATE = '' +if URL_XML_UPDATE: + NODE_UPDATE = f""" + + + """ + +FILE_DESCRIPTION = f""" + + + + +{NODE_DISPLAY_NAME} + + +{NODE_EXTENSION_DESCRIPTION} + + +{NODE_ICON} + + +{NODE_PUBLISHER} + + + +{NODE_LICENSE} + + {NODE_DEPENDENCIES_MINIMAL}{NODE_UPDATE} + +""" + +NODE_MENU = """ + +{titles} + + + service:{id}?{argument} + + + _self + + + {context} + + + %origin%/{folder}/{icon} + + """ + +opt = 'fuse' +if PARENT == 'OfficeMenuBar': + opt = 'replace' + +menus = [] +toolbar = [] +tmp = ' {}' +for i, m in enumerate(MENUS): + titles = [tmp.format(k, v) for k, v in m['title'].items()] + values = { + 'id': ID, + 'index': i+101, + 'opt': opt, + 'titles': '\n'.join(titles), + 'argument': m['argument'], + 'context': m['context'], + 'folder': DIRS['images'], + 'icon': m['icon'], + } + menus.append(NODE_MENU.format(**values)) + if m['toolbar']: + values['index'] = f't{i+1}' + toolbar.append(NODE_MENU.format(**values)) + +NODE_TOOLBAR = '' +if PARENT == 'AddonMenu': + NODE_MENUS = '\n'.join(menus) +else: + tmp = ' {}' + titles = '\n'.join([tmp.format(k, v) for k, v in MENU_MAIN.items()]) + SUBMENUS = '\n ' + '\n'.join(menus) + '\n ' + NODE_MENUS = f""" + +{titles} + + + _self + + {SUBMENUS} + """ + + if toolbar: + node_toolbars = '\n'.join(toolbar) + NODE_TOOLBAR = f""" + +{node_toolbars} + + """ + +FILE_ADDONS = f""" + + + +{NODE_MENUS} + +{NODE_TOOLBAR} + + +""" + + +NODE_ADDONS = '\n ' +if TYPE_EXTENSION > 1: + NODE_ADDONS = f'\n ' +if TYPE_EXTENSION == 3: + NODE_ADDONS += '\n ' + +FILE_MANIFEST = f""" + + {NODE_ADDONS} + +""" + + +FILE_UPDATE = '' +if URL_XML_UPDATE: + FILE_UPDATE = f""" + + + + + + + + + + + +""" + + +def _functions(): + a = '[in] any {}' + t = ' any {}({});' + f = '' + for k, v in FUNCTIONS.items(): + args = ','.join([a.format(k) for k, v in v['parameters'].items()]) + f += t.format(k, args) + return f + + +FILE_IDL = '' +if TYPE_EXTENSION > 1: + id_ext = ID.replace('.', '_') + interface = f'X{NAME}' + module = '' + for i, P in enumerate(ID.split('.')): + module += f'module {P} {{ ' + close_module = '}; ' * (i + 1) + functions = ' void test([in] any argument);' + if TYPE_EXTENSION == 3: + functions = _functions() + + FILE_IDL = f"""#ifndef __{id_ext}_idl__ +#define __{id_ext}_idl__ + +#include + +{module} + + interface {interface} : com::sun::star::uno::XInterface + {{ +{functions} + }}; + + service {P} {{ + interface {interface}; + }}; + +{close_module} +#endif +""" + + +def _parameters(args): + NODE = """ + +{displayname} + + +{description} + + """ + line = '{}{}' + node = '' + for k, v in args.items(): + displayname = '\n'.join( + [line.format(' ' * 16, k, v) for k, v in v['displayname'].items()]) + description = '\n'.join( + [line.format(' ' * 16, k, v) for k, v in v['description'].items()]) + values = { + 'name': k, + 'displayname': displayname, + 'description': description, + } + node += NODE.format(**values) + return node + + +NODE_FUNCTIONS = '' +if TYPE_EXTENSION == 3: + tmp = '{}{}' + NODE_FUNCTION = """ + +{displayname} + + +{description} + + + Add-In + + + AutoAddIn.{name} + + +{parameters} + + """ + + for k, v in FUNCTIONS.items(): + displayname = '\n'.join( + [tmp.format(' ' * 12, k, v) for k, v in v['displayname'].items()]) + description = '\n'.join( + [tmp.format(' ' * 12, k, v) for k, v in v['description'].items()]) + parameters = _parameters(v['parameters']) + values = { + 'name': k, + 'displayname': displayname, + 'description': description, + 'parameters': parameters, + } + NODE_FUNCTIONS += NODE_FUNCTION.format(**values) + + +FILE_ADDIN = f""" + + + + +{NODE_FUNCTIONS} + + + +""" + + +DATA = { + 'py': FILE_PY, + 'manifest': FILE_MANIFEST, + 'description': FILE_DESCRIPTION, + 'addons': FILE_ADDONS, + 'update': FILE_UPDATE, + 'idl': FILE_IDL, + 'addin': FILE_ADDIN, +} diff --git a/source/images/icon_16.bmp b/source/images/icon_16.bmp new file mode 100644 index 0000000..a954508 Binary files /dev/null and b/source/images/icon_16.bmp differ diff --git a/source/images/logo.png b/source/images/logo.png new file mode 100644 index 0000000..eccb275 Binary files /dev/null and b/source/images/logo.png differ diff --git a/source/zaz.py b/source/zaz.py new file mode 100644 index 0000000..917d7a0 --- /dev/null +++ b/source/zaz.py @@ -0,0 +1,269 @@ +# ~ This file is part of ZAZ. + +# ~ ZAZ is free software: you can redistribute it and/or modify +# ~ it under the terms of the GNU General Public License as published by +# ~ the Free Software Foundation, either version 3 of the License, or +# ~ (at your option) any later version. + +# ~ ZAZ is distributed in the hope that it will be useful, +# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of +# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# ~ GNU General Public License for more details. + +# ~ You should have received a copy of the GNU General Public License +# ~ along with ZAZ. If not, see . + +import argparse +import os +import sys +from shutil import copyfile +from subprocess import call +import zipfile + +from conf import ( + DATA, + DIRS, + EXTENSION, + FILES, + INFO, + PATHS, + TYPE_EXTENSION, + log) + + +def _exists(path): + return os.path.exists(path) + + +def _join(*paths): + return os.path.join(*paths) + + +def _mkdir(path): + return os.mkdir(path) + + +def _save(path, data): + with open(path, 'w') as f: + f.write(data) + return + + +def _compress_oxt(): + log.info('Compress OXT extension...') + + path = DIRS['files'] + if not _exists(path): + _mkdir(path) + + path_oxt = _join(path, FILES['oxt']) + + z = zipfile.ZipFile(path_oxt, 'w', compression=zipfile.ZIP_DEFLATED) + root_len = len(os.path.abspath(DIRS['source'])) + for root, dirs, files in os.walk(DIRS['source']): + relative = os.path.abspath(root)[root_len:] + for f in files: + fullpath = _join(root, f) + file_name = _join(relative, f) + if file_name == FILES['idl']: + continue + z.write(fullpath, file_name, zipfile.ZIP_DEFLATED) + z.close() + + log.info('Extension OXT created sucesfully...') + return + + +def _install_and_test(): + path_oxt = (_join(DIRS['files'], FILES['oxt']),) + call(PATHS['install'] + path_oxt) + log.info('Install extension sucesfully...') + log.info('Start LibreOffice...') + call(PATHS['soffice']) + return + + +def _validate_new(): + path_source = DIRS['source'] + if not _exists(path_source): + return True + + msg = f'Path: {path_source}, exists, delete first' + log.error(msg) + return False + + +def _create_new_directories(): + path_source = DIRS['source'] + _mkdir(path_source) + path = _join(path_source, DIRS['meta']) + _mkdir(path) + path = _join(path_source, DIRS['description']) + _mkdir(path) + path = _join(path_source, DIRS['images']) + _mkdir(path) + path = _join(path_source, DIRS['registration']) + _mkdir(path) + + if DIRS['pythonpath']: + path = _join(path_source, DIRS['pythonpath']) + _mkdir(path) + + path = DIRS['files'] + if not _exists(path): + _mkdir(path) + + msg = 'Created directories...' + log.info(msg) + return + + +def _create_new_files(): + path_source = DIRS['source'] + + for k, v in INFO.items(): + file_name = f'license_{k}.txt' + path = _join(path_source, DIRS['registration'], file_name) + _save(path, v['license']) + + if TYPE_EXTENSION > 1: + path = _join(path_source, FILES['idl']) + _save(path, DATA['idl']) + + path = _join(path_source, FILES['py']) + _save(path, DATA['py']) + + msg = 'Created files...' + log.info(msg) + return + + +def _validate_update(): + if TYPE_EXTENSION == 1: + return True + + if not _exists(PATHS['idlc']): + msg = 'Binary: "idlc" not found' + log.error(msg) + return False + + if not _exists(PATHS['include']): + msg = 'Directory: "include" not found' + log.error(msg) + return False + + if not _exists(PATHS['regmerge']): + msg = 'Binary: "regmerge" not found' + log.error(msg) + return False + + path = _join(DIRS['source'], FILES['idl']) + if not _exists(path): + msg = f'File: "{FILES["idl"]}" not found' + log.error(msg) + return False + + return True + + +def _compile_idl(): + if TYPE_EXTENSION == 1: + return + + log.info('Compilate IDL...') + path_rdb = _join(DIRS['source'], FILES['rdb']) + path_urd = _join(DIRS['source'], FILES['urd']) + + path = _join(DIRS['source'], FILES['idl']) + call([PATHS['idlc'], '-I', PATHS['include'], path]) + call([PATHS['regmerge'], path_rdb, '/UCR', path_urd]) + os.remove(path_urd) + + log.info('Compilate IDL sucesfully...') + return + + +def _update_files(): + path_source = DIRS['source'] + + for k, v in INFO.items(): + file_name = FILES['ext_desc'].format(k) + path = _join(path_source, DIRS['description'], file_name) + _save(path, v['description']) + + path_logo = EXTENSION['icon'][0] + if _exists(path_logo): + file_name = EXTENSION['icon'][1] + path = _join(path_source, DIRS['images'], file_name) + copyfile(path_logo, path) + + files = os.listdir(DIRS['images']) + for f in files: + if f[-3:].lower() == 'bmp': + source = _join(DIRS['images'], f) + target = _join(path_source, DIRS['images'], f) + copyfile(source, target) + + path = _join(path_source, DIRS['meta'], FILES['manifest']) + _save(path, DATA['manifest']) + + path = _join(path_source, FILES['addons']) + _save(path, DATA['addons']) + + path = _join(path_source, FILES['description']) + _save(path, DATA['description']) + + if TYPE_EXTENSION == 3: + path = _join(path_source, FILES['addin']) + _save(path, DATA['addin']) + + _compile_idl() + return + + +def _new(): + if not _validate_new(): + return + + _create_new_directories() + _create_new_files() + _update_files() + + msg = f"New extension: {EXTENSION['name']} make sucesfully...\n" + msg += '\tNow, you can install and test: zaz.py -i' + log.info(msg) + return + + +def main(args): + if args.new: + _new() + return + + if not _validate_update(): + return + + _update_files() + _compress_oxt() + + if args.install: + _install_and_test() + + log.info('Extension make sucesfully...') + return + + +def _process_command_line_arguments(): + parser = argparse.ArgumentParser( + description='Make LibreOffice extensions') + parser.add_argument('-i', '--install', dest='install', action='store_true', + default=False, required=False) + parser.add_argument('-n', '--new', dest='new', action='store_true', + default=False, required=False) + return parser.parse_args() + + +if __name__ == '__main__': + args = _process_command_line_arguments() + main(args) +