From 9b4fd81b09e36cc1b87cfcee103a9d7306e7a270 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 29 Oct 2020 21:37:01 -0600 Subject: [PATCH] Initial version only for GNU/Linux --- README.md | 10 +- VERSION | 1 + conf.py | 432 ++++ easymacro2.py | 1 + files/ZAZLaTex2SVG_v0.1.0.oxt | Bin 0 -> 52996 bytes images/logo.png | Bin 0 -> 26700 bytes source/Addons.xcu | 54 + source/META-INF/manifest.xml | 6 + source/Office/Accelerators.xcu | 4 + source/ZAZLaTex2SVG.py | 89 + source/description.xml | 26 + source/description/desc_en.txt | 1 + source/description/desc_es.txt | 1 + source/images/zazlatex2svg.png | Bin 0 -> 26700 bytes source/pythonpath/easymacro2.py | 3549 ++++++++++++++++++++++++++++ source/registration/license_en.txt | 14 + source/registration/license_es.txt | 14 + zaz.py | 785 ++++++ 18 files changed, 4986 insertions(+), 1 deletion(-) create mode 100644 VERSION create mode 100644 conf.py create mode 120000 easymacro2.py create mode 100644 files/ZAZLaTex2SVG_v0.1.0.oxt create mode 100644 images/logo.png create mode 100644 source/Addons.xcu create mode 100644 source/META-INF/manifest.xml create mode 100644 source/Office/Accelerators.xcu create mode 100644 source/ZAZLaTex2SVG.py create mode 100644 source/description.xml create mode 100644 source/description/desc_en.txt create mode 100644 source/description/desc_es.txt create mode 100644 source/images/zazlatex2svg.png create mode 100644 source/pythonpath/easymacro2.py create mode 100644 source/registration/license_en.txt create mode 100644 source/registration/license_es.txt create mode 100755 zaz.py diff --git a/README.md b/README.md index cd418e9..5d784f0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # zaz-latex2svg -Compile Latex equations to SVG into LibreOffice \ No newline at end of file +Compile Latex equations to SVG into LibreOffice + +Requirements: + +* LibreOffice 7.0+ +* Python 3.7+ +* pdflatex +* pdfcrop +* pdf2svg diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/conf.py b/conf.py new file mode 100644 index 0000000..01db5ec --- /dev/null +++ b/conf.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python3 + +# ~ 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 or "-_" +NAME = 'ZAZLaTex2SVG' + +# ~ Should be unique, used URL inverse +ID = 'net.elmau.zaz.latex2svg' + +# ~ If you extension will be multilanguage set: True +# ~ This feature used gettext, set pythonpath and easymacro in True +# ~ You can used PoEdit for edit PO files and generate MO files. +# ~ https://poedit.net/ +USE_LOCALES = True +DOMAIN = 'base' +PATH_LOCALES = 'locales' +PATH_PYGETTEXT = '/usr/lib/python3.8/Tools/i18n/pygettext.py' +PATH_MSGMERGE = 'msgmerge' + + +# ~ Show in extension manager +PUBLISHER = { + 'en': {'text': 'El Mau', 'link': 'https://gitlab.com/mauriciobaeza'}, + 'es': {'text': 'El Mau', 'link': 'https://gitlab.com/mauriciobaeza'}, +} + +# ~ 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 = '' + +# ~ 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': NAME, + 'description': 'Generate equations in SVG from LaTex', + 'license': LICENSE_EN, + }, + 'es': { + 'display_name': NAME, + 'description': 'Genera ecuaciones en SVG desde LaTex', + 'license': LICENSE_ES, + }, +} + + +# ~ Menus, only for TYPE_EXTENSION = 1 +# ~ Parent can be: AddonMenu or OfficeMenuBar +# ~ For icons con name: NAME_16.bmp, used only NAME +# ~ PARENT = '' +# ~ MENU_MAIN = {} +# ~ Shortcut: Key + "Modifier Keys" +# ~ Important: Not used any shortcuts used for LibreOffice +# ~ SHIFT is mapped to Shift on all platforms. +# ~ MOD1 is mapped to Ctrl on Windows/Linux, while it is mapped to Cmd on Mac. +# ~ MOD2 is mapped to Alt on all platforms. +# ~ For example: Shift+Ctrl+Alt+T -> T_SHIFT_MOD1_MOD2 +PARENT = 'OfficeMenuBar' +MENU_MAIN = { + 'en': 'ZAZ LaTex2Svg', + 'es': 'ZAZ LaTex2Svg', +} +MENUS = ( + { + 'title': {'en': 'From selection', 'es': 'Desde selección'}, + 'argument': 'selection', + 'context': 'calc,writer,draw,impress', + 'icon': 'icon1', + 'toolbar': False, + 'shortcut': '', + }, + { + 'title': {'en': 'Validate applications', 'es': 'Validar aplicaciones'}, + 'argument': 'app', + 'context': 'calc,writer,draw,impress', + 'icon': 'icon2', + 'toolbar': False, + 'shortcut': '', + }, +) + + +# ~ 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), + 'languages': tuple(INFO.keys()) +} + + +# ~ If used more libraries set python path in True and copy inside +# ~ If used easymacro pythonpath always is True, recommended +DIRS = { + 'meta': 'META-INF', + 'source': 'source', + 'description': 'description', + 'images': 'images', + 'registration': 'registration', + 'files': 'files', + 'office': 'Office', + 'locales': PATH_LOCALES, + 'pythonpath': True, +} + + +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', + 'shortcut': 'Accelerators.xcu', + 'easymacro': True, +} + + +# ~ URLs for update for example +# ~ URL_XML_UPDATE = 'https://gitlab.com/USER/PROYECT/raw/BRANCH/FOLDERs/FILE_NAME' +URL_XML_UPDATE = '' +URL_OXT = '' + + +# ~ Default program for test: --calc, --writer, --draw +PROGRAM = '--calc' +# ~ Path to file for test +FILE_TEST = '' + +PATHS = { + 'idlc': '/usr/lib/libreoffice/sdk/bin/idlc', + 'include': '/usr/share/idl/libreoffice', + 'regmerge': '/usr/lib/libreoffice/program/regmerge', + 'soffice': ('soffice', PROGRAM, FILE_TEST), + 'install': ('unopkg', 'add', '-v', '-f', '-s'), + 'profile': '/home/mau/.config/libreoffice/4/user', + 'gettext': PATH_PYGETTEXT, + 'msgmerge': PATH_MSGMERGE, +} + + +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() + + +DATA_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) +""" + +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_MANIFEST = [FILES['py'], f"Office/{FILES['shortcut']}", 'Addons.xcu'] +if TYPE_EXTENSION > 1: + DATA_MANIFEST.append(FILES['rdb']) +if TYPE_EXTENSION == 3: + DATA_MANIFEST.append('CalcAddIn.xcu') + +DATA_DESCRIPTION = { + 'identifier': {'value': ID}, + 'version': {'value': VERSION}, + 'display-name': {k: v['display_name'] for k, v in INFO.items()}, + 'icon': ICON_EXT, + 'publisher': PUBLISHER, + 'update': URL_XML_UPDATE, +} + +DATA_ADDONS = { + 'parent': PARENT, + 'images': DIRS['images'], + 'main': MENU_MAIN, + 'menus': MENUS, +} + +DATA = { + 'py': DATA_PY, + 'manifest': DATA_MANIFEST, + 'description': DATA_DESCRIPTION, + 'addons': DATA_ADDONS, + 'update': URL_OXT, + 'idl': FILE_IDL, + 'addin': FILE_ADDIN, +} + + +with open('VERSION', 'w') as f: + f.write(VERSION) + + +# ~ LICENSE_ACCEPT_BY = 'user' # or admin +# ~ LICENSE_SUPPRESS_ON_UPDATE = True diff --git a/easymacro2.py b/easymacro2.py new file mode 120000 index 0000000..d2de111 --- /dev/null +++ b/easymacro2.py @@ -0,0 +1 @@ +/home/mau/Projects/libre_office/zaz/source/easymacro2.py \ No newline at end of file diff --git a/files/ZAZLaTex2SVG_v0.1.0.oxt b/files/ZAZLaTex2SVG_v0.1.0.oxt new file mode 100644 index 0000000000000000000000000000000000000000..092f1c2be743edca8ff3029159ba14d32988f79d GIT binary patch literal 52996 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 + + + + + + ZAZ LaTex2Svg + ZAZ LaTex2Svg + + + _self + + + + + From selection + Desde selección + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument,com.sun.star.drawing.DrawingDocument,com.sun.star.presentation.PresentationDocument + + + service:net.elmau.zaz.latex2svg?selection + + + _self + + + %origin%/images/icon1 + + + + + Validate applications + Validar aplicaciones + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument,com.sun.star.drawing.DrawingDocument,com.sun.star.presentation.PresentationDocument + + + service:net.elmau.zaz.latex2svg?app + + + _self + + + %origin%/images/icon2 + + + + + + + diff --git a/source/META-INF/manifest.xml b/source/META-INF/manifest.xml new file mode 100644 index 0000000..4d0b201 --- /dev/null +++ b/source/META-INF/manifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/Office/Accelerators.xcu b/source/Office/Accelerators.xcu new file mode 100644 index 0000000..ad7ae40 --- /dev/null +++ b/source/Office/Accelerators.xcu @@ -0,0 +1,4 @@ + + + + diff --git a/source/ZAZLaTex2SVG.py b/source/ZAZLaTex2SVG.py new file mode 100644 index 0000000..442884c --- /dev/null +++ b/source/ZAZLaTex2SVG.py @@ -0,0 +1,89 @@ +import uno +import unohelper +from com.sun.star.task import XJobExecutor +import easymacro2 as app + + +ID_EXTENSION = 'net.elmau.zaz.latex2svg' +SERVICE = ('com.sun.star.task.Job',) + + +class ZAZLaTex2SVG(unohelper.Base, XJobExecutor): + + def __init__(self, ctx): + self.ctx = ctx + + def trigger(self, args='pyUNO'): + if args == 'app': + self._app() + return + + self._from_selection() + return + + def _app(self): + result = 'No encontrado' + if app.paths.exists_app('pdflatex'): + result = 'Encontrado' + msg = f'pdflatex = {result}\n' + + result = 'No encontrado' + if app.paths.exists_app('pdfcrop'): + result = 'Encontrado' + msg += f'pdfcrop = {result}\n' + + result = 'No encontrado' + if app.paths.exists_app('pdf2svg'): + result = 'Encontrado' + msg += f'pdf2svg = {result}\n\n' + + msg += 'No continues hasta tener las tres aplicaciones detectadas' + + app.msgbox(msg) + return + + @app.catch_exception + def _from_selection(self): + template = """\documentclass{{article}} + \\usepackage[a5paper, landscape]{{geometry}} + \\usepackage{{xcolor}} + \\usepackage{{amssymb}} + \\usepackage{{amsmath}} + \pagestyle{{empty}} + \\begin{{document}} + + \[ {} \] + + \end{{document}} + """ + doc = app.active + sel = doc.selection + if doc.type == 'writer': + sel = sel[0] + data = sel.value + + data = template.format(data) + path_tmp = '/tmp' + path_tex = '/tmp/test.tex' + path_pdf = '/tmp/test.pdf' + path_svg = '/tmp/test.svg' + + app.paths.save(path_tex, data) + cmd = f'pdflatex --interaction=batchmode -output-directory={path_tmp} {path_tex}' + app.run(cmd) + cmd = f'pdfcrop {path_pdf} {path_pdf}' + app.run(cmd) + cmd = f'pdf2svg {path_pdf} {path_svg}' + app.run(cmd) + + sel = sel.offset() + args = {} + if doc.type == 'writer': + args = {'Width': 5000, 'Height': 2000} + sel.insert_image(path_svg, args) + + return + + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation(ZAZLaTex2SVG, ID_EXTENSION, SERVICE) diff --git a/source/description.xml b/source/description.xml new file mode 100644 index 0000000..491b052 --- /dev/null +++ b/source/description.xml @@ -0,0 +1,26 @@ + + + + + + ZAZLaTex2SVG + ZAZLaTex2SVG + + + + + + + + + + El Mau + El Mau + + + + + + + + diff --git a/source/description/desc_en.txt b/source/description/desc_en.txt new file mode 100644 index 0000000..5ef2a95 --- /dev/null +++ b/source/description/desc_en.txt @@ -0,0 +1 @@ +Generate equations in SVG from LaTex \ No newline at end of file diff --git a/source/description/desc_es.txt b/source/description/desc_es.txt new file mode 100644 index 0000000..aac48ec --- /dev/null +++ b/source/description/desc_es.txt @@ -0,0 +1 @@ +Genera ecuaciones en SVG desde LaTex \ No newline at end of file diff --git a/source/images/zazlatex2svg.png b/source/images/zazlatex2svg.png new file mode 100644 index 0000000000000000000000000000000000000000..2f210eda94433623e613c91ad5675eb6631dd370 GIT binary patch literal 26700 zcmV)9K*hg_P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3>vvRpTkrT=3UxdhC^asW7B);p->*LM&>ieySz z)oo2ld{i)odja<|xI4m~|N1}I{p(-<3cmF0a%sJeT0Ot=$Rkev(EaDv=V$Qw{r-OK zAAc8qf834o{fo#$iJ$57zx8~dKY6_T^@5hakFVb!cWr;)r+?ol{Cwm0E9sN-g~ysU;X+2xv>~lc)t`~JSimqevh9|N?b)6Y4Y>) zdy9Ic^XHr@zsHpSef`|erTd!&T<7oC&+qRu(qGH*d7~e&HP9CP`sa?+2E-7 z;xe>z`kiO7A>n@4w7m5V?(@U{`AZv{*tYVPxpRZlug_bo68?&<^!7RV;&^KIpL_|; zzHh)1@!5ro35gx>HKY=1@GZs`0_)h>pUIUI+9Pb+pmP7<2NWGf&=Y^8CpIE3UNiDyy!x`WkC)!e{4QcHM3F zJ@&L{$$$W7)tYr1HcvQ&(#fZsdfMq{oOy}0n{K}4*4u8s+PYtM?%((AZ+6Y*gWL2rNLk35 zf{=;y<4H62DL~ZEX|tDG*5M+XZT@JRbl{p=-j$1z0GoXD+2s9@{qi_L{X_!iI!va1$5oYDORMsx+XQa`r7S=D51AWPTE-w<-iIlo`4F z!uDoD`0jX#{jhAYKl``?xP~&|!6e{-_F~{-@&#DW(Wm;kPAqweCokm8%Y6Y*#$r`U zo4GIZ$K`NN>X%j2eFWCEO(T~`1G74;@#aKv%oSE_g9V$bv0HQdTeu1GsN6V@8#kI= zSa{d#*V>7+Tz(BApoP_vX>TltO+XNjcX#qR>`f-NRj_w`g@NuWUk0>#5vb4{PY(QNSaTdMN7Kwg$Qq zeB``eq*yKs&|;NH8NK;I8tPC$n?0+a5tX0YBH0oA(3y3g&jRecp|0{ zKcSN?Mzl-T4?9FHpo&J7&N$>t1L%1pA|)Iyy@1gLN@%GHpI4l`LB4?g+;)p!JzPi$ z;0}qnI%BbIX_4nAu0@J07PJ(YhIJ|DMj{1TIQ%UUL0tRZijgT*5AL!uI5&ylm`Bff z4nBxTD2~M>U7AmG>~J$NLinA-jxJ#I6aXV%@MVn|*q&vP0N$Nk$T5UZi99t z7gLk2HD!jybW>_91w=-&2R5>E$E2QKCR^I3pfF7Bmgn<5+MnwGY1e) z&a&@~Ro$B%nNuLJ=4mlqq7^1^sBZS$gPa3lTUy>E7#x=BwjOgP@4|-e&mF0vKxZ?&f zvi-aCnb~QUhYjH-n^fT#Tou^4k+~o%K@Tb9v+g8o24vF=5v=WmhtR9dBDg}}a3Q2rs>IGp; znsaLdc?_tN%j8y_AUdx|$dC7lL<2AVE(-$SB{&kN=Y)HzeG>+3?A&KHw__`(ES}lU z;Bj7x*k5A7$Pbo@Y+}{o!guu=KvILbK{-}@!y#t~y?LX&w-IXEbl^Q4s8Y%it|$w* zS>VC1(&?Xmy5Am{3U?#|H|i}`0s{}y`v!W55<|qWzD-_^O9!C13rG$K&Jbk3UhDTL^FeAqXc5jjX&;tQP_#M`g7~+mMdy8Ny*8yC6Pe8^C8;^x^(Z3az z0C}MY91#uBmE-~811R!e6RC1X8SKl0zvB+O z-!qGr7O4k}bR{ph0F;!SQ5a|uAI1fn2kJUvKNi~v4In)L>P;f(4$1Nt?x>Y({%?>*OopvXuVt~D+e z2Ryxu9{dQcKZT$92n5}OS~phil99kDYcOytQjj+$0po)nN&9)O;7?SYyw$bwk^5B zJ=Se5Zm9Zf=`J=3V)a}LH;y6!TzbIcMqdI;I;zwNFP>!D$RrQ^h`n2+!>rDrT~ACC z9(ZS*$JN51uMjVlr9=Ifj0T_N?ZC^|3;#@y(uQo4G%J2J_n&o0pB6|*FNrSZ( zW;f&wssbBFY@vjXX*UZIOQ3l6gUd@THe?OCKpY-|6Y-A(^DH&)Anq#(3z`uzU{dAT z%Hf7yC!`1D3Ck(cCC1SWF4wS9)LLfpw|ZdWsGz9&9{K^bi}c{L@H9j8W`y!W;NfnD z${wrQf{Dxlck@#<1>`pfzx1Z;gC|1U^{km>SeQ@+wjm>0F`fVm(al@Jt>+w^ngg2% zo+1#Gz&}Ledc_II51CPyA6fA}39kn9$dt&w^z*w-asGerXTCop0USHew4(*N>odhKQ5fOlR%@~9k(#U}|L$3Dd&QeK(Z~@mr zk+2ZLni=k4yCh486oI8jB!PL517TL2ZsyS=)^@=%RYSke^3Z z5XCk9a7~;Hu@TvZ*ahT>IRtmz2pr&+yda|Cu(blW#uK>W*bsDB<-(nH1K5!@`M^?K zd3z?Rc<%CUSsIi>m5fpp4sQEL3?Z5@vo99BFSuv)g~iFPA=l8T$O_%iL{6w=p(@o@ z9y zq>*R$ME7B6n0~su*Ep=nC56n^lLvl>n=}g@~Hh62JAigM)5jr`Q$n2+0MZ zLhlHDwiEZvHmTOMa41*?kOF}vq>DHooGXxp9eH;Si&w&vNbzjHngL4%4v>+sGr$HI zyR!ajED-K|aKxjq$phF8vKgiohu%DXJqH-fq2pm|;$8`aRUowyc0>e@av-ZWxdWhq z?BP>E!ur639r+riW8pRF@#bWSyWnR4fL!|NUK~NtvV?` zdDY3ae89a!XO4)}S7Qt(-8VwoinJA8G?3tcG#4;495zWM0>xhR;II%kd^Q(eN^-#P zqfN~GQKb-)l2qfK@JCq%jhoQuLY~Qf@AB0B(1-tqckX`;jO-`2^GcMBfT(zWZFz{0 zpQi&ObL)@fW5&*2!Uz%I5k7eTgmv4B2Xx9aT!i0+ELHo0_|?t|xHLA%$h89@9lwL1 zKamM|WQiWHTu<2s7=;ukp%!$9{Na^QjnbzKo3`#r8OO~GEq4ITR(p1bc}XjAy$@8(NJ_Y~F}dJiyt$X1unj2d$`_^| zm()g>KpQy>@Bxi{Q z{1wo_!lTip$m4Ybdc=Goui4hjbF-nUWS&$TwF(NW0<2dMHu{hbq#^M|R z|BkQ*w^fa`0KYuOPyGjI!>c5a;5ekNYKOjMaXqqJEljT>J=>#-$3#xS@HiYr$zI6n zuvT>#z=PWr*9WXh_s><2_L)Q{_#i zNvkP1oKVM=l5d?&O_<}ZN;moh72qQu*4YTu3z*V~$UaDda$m%j3dT-3OmzB1&Kn_5N) zC&Ej8k%UQAYeJoBwl{?!ltZ&52~^}Tau79gw`GtA#h_MOx#}wk$kywXMNkKEz#1Vi zsvx>R+FT?OQXjfdC1|dZT?raW00`x9Lk8qv)>$$T2ESC642tkwty7=}z3{dWgE8h` zEe;&LgPoJQ4RDNYwN0nCW~#)`eNx9cV?;9g~!u`pv}2br5gdz8apLAarDmI4mR0a@$dD3-s2fP@jENoof^}Kl7rB zcRsH^AG%LSe->0ronZt=QbPs}7@ZTDjwi6;O9~*+0QF%saD<%b>?!uDiq}bO(9>#H zu;IlgJ|2C9vx2*FhJ&%2dRI&djufMT{>jG@n$Y4;Wc;>(hpSP ze)$ldMNS`qB1X5%d>qxXaG~v$4F+qq5|ovJ5cV09epERm$q=IgBiznxTB(j016Y%x z6Y$~?ewPKospDh3L^7DS#pJgvl)Y;4m=eo7yE{%p50S0xY*$-hieU@dVl=@jinx-l zYNa3tJ)1^ybPOcMTTNELB$s3k(LeRVtWbHmh0aHU5C!-?VoIoGfU|hdca0CgOe70y zmc$OrEWYg5t_)OxhmGkhN~Y8l8>JrNz%gW}Oz4U+jN$DHCWRYf|h zf#~xCsUtdiHAr9$C$6^6_T(exnFIk3ZiMFx;e~tQ{;%rJi=04-b*>D&0h_t$g+3yC zc+XyzN(Wd((flM#G5^iW3!`2PB-m<@!BS!m40m1N^4gvP|zsP2^ja&pm6hy$t?48%qhbyTZo;Sg-52?v8IQ_68z%08^WNGq}#wMcG9agCoPq0)w^w|9%w%D<0 z5CL2vxovgVhy#&feTAS-*a6&JhI(57ke5coj*#*fW=T;s7-<1)rwA4$IJF?MS?l#0 zi4Wr5(m%8fKfEn6uZzWyc65ZDc zS&zR;Eg3zveDCC)HK+i$Ab1AyN#mqm#o-79v32rj$Ft(0FO{*>6eljZ1?*j)vjNWB zXps)@@owlE0SotgJg(+Ame{dK$ARIk_cJrJ4I`MtBp^z{16X!`XnII?h4mU;Rr02` za|kRC_8VBj24KHc9l;`I(sb0EGx}(TDz}6h@OV!WMH4TBd=Lw=Xo$TQ5Uc8JN9ftM zHn@C3fJpc>aFXK(3+H1B#M|U;iO*60Xu@t;#{(KbBm5WMgus3MZG;zevLQ%>AbK#2 zDYEZeZAoAqFDIMg?y$(^Dz>k@`&ZNcILgJqZ0kJ}%0 zsZk1q<9c%fp=eeUuY3eOwoxT`kEWvgr<_!1?m6lr$>RPk}2|9%={ROJz#b5#Z)j*8~UVS>sLe4WNzn!^YmEaM!%I7vfiqdJ?TI z6>rJ))ybp|mMRCY{xDlnd5%LN90AL5n z05oY^3%jUFJt&7Qk-1%p+#egIfgQ$MORs9ICtwB7^y=dVGW{2 zbb&%|8dPEp6s0IRPXe&`YHVyn4Cv4bR$T>0aN}~HPIr|fKm}xi*JuD7*`8U~qmc-V zDJcl#=GVc>`T2P=8qV`teclyCZ%%|ZUvnd>%A;w9#=2gRzM`?PhZtDo{6v+Npt8rC zyV2Tv@vP=K&{#_`jhBW}Icwnn)i<)DVSr3B>yGPc9 zHwQ2@E~?hY(R^dol%4E-SG%o1NBE(u+6vdT#m(R`%ojD+4Ez!Z*0a{B_z zl;fXDDQWM{)ipFt18N$b+b6fPMuX&4wimh;&93=3rm?MlK1@lirif-h68hJ%>QcF< z4=i$4n_1NZwoa9Dq^9a*hCl14I5QS zM$kk9qz&#rxFj9<5ox0D0x;drs|x{0YKGuG6I-PYm=a7K<1Cc0jB_9`5qa>oyQu=? zI7;yldwNQg;53cmhgWIjMp)B?!Gnbt4xtvQLddA*2{1?=)ap(GjqtCtKYXq?e`foE zSh$01%z2`TEvO9M_*}FP?Zw@rhn=V#HkJn>`y&RIqfv5uq2Jp9jdUs z(NF#kagyR1giKBTbL@;Qw8vk<%B`yIbTB&NJO+k$0 zAo?|eU#EDvO9Vw0duS|ilYbgO>T>0!G);VIKn-KBbGxtg%T2wOI4<@kjmQftb4tiG z>=5#JsUIEGW79cGv4_TL5hh4|S!1+DR~-u=-kP8u!kWa2gRwp8Za1>mkw+Tg*8H{s zK5QZ$6g9q7Rn>^P`u&;$mW#EOHwmCYS5UAAiB1p2a~~RGL{wD``jP5L?B15<+);rS z>XXliB}P&NA;k5*JWyx9^DGGawKPaW=U=1Xc%AqJUN?kiiq}`K*CA7Ukn}rpVxzzf zqmtR6qxK%j0}Y$bq82B!gJjpSENJ$s>7bEqqNnL>3ko9U98-z_OYs?vN`&yYINu%K zDJ8G6LwDTXD_rT&-XXB>;lh);S(>3QhpX1xg79;7^?bLEtYCrAvU^70f|yrjbeswz z03z(bLuQ>P11IoeNKcj+N$Ratf;sJVb*+nC~0~(a0!uks~c}JC|me!N27<>7m zc|G9LCQz@rVSDGHv8x`iv%Q_>P2jk9pg}~%ZUGdk)3Er3;wtb%dO^O`TPZSx4K3&N3j}AjYCH z@yqgwnj-tlHH_ZTA+>ktL=&_)%W@oXnN~n!7L5W%P0Mz1G#sLNr3LM4ni?@iNP)B! zI+y==a)}g-95ci+}0R(yB3BhgkRVp5NYR26V)g%D$?5ZL44*MN- zkOmA@IS+G>MQW#O$Om9lw|K0rBCsYvG2!nFj0!xpN|s=1wvJ^Y?t(n$U20jQci0l13rU;1SJCF!Xaf@n&=e_Mq`L2PbS z4rAtCw-Ui6m5)Jb`_V6BLE-8MXZx*j?r(d4YN{dCt4i7%BROgr)~gnf2hPd0Ff`MW z-@yd9f_-H_7TO3vgceaXM36}<;QP{^n(@P4%&K?Q+-O2_5ZOG16-B@VwM)5ib%T(> zRpYKI)5US#Bx6?#3O?7|D2iw$ETQTJ*iG};b=+RwBzD#&6Qs!6oqW~%lL21COMO8> z@QDR~u&lmm_^^dHoqlzuC*_Q9qmV2a+`sT{QK6^K zHaT!~@Mvpt+@x&I zv$fhwK4&cr2}PU?!H^uD=prkm=+IM%K);&bYZ@?wAg5lB=_MP~>o650b*i(XO`#Q) zEX*(X(~VUNd8(t>>SzM3noib9U6h67iAR*E8H_^HF^BXDbU|%0Eaj)-r-GjY4;m26 z13}+qpf8K~)ck8CcT_ieov~Lu#Qn9;@XpZql|mM6rK<1SH6((nkZ^<7Og-?WYRUpk zN7OJ>l3RYI!ig2n#H5Y~?(@55bJqUSY)C5q=Bu=vKp}{_WH#jd3&V!IMBF!r0Jv>y27~@x5twSM_i_d)Hx3HXGbE4b*{W_!K^9?@Sya z5ZEuxVI#4NS6Vc#n&*LI>V}jTg#Fd{5Dhx%5#yUMS8YZn1y0r25Q6qxvH{`6iw1HX zO{r6oBobZ%>OTUWf6`INCMZC2BXUDHIK&W(R4$Z+%E>_!UBvI+NyFJ{8eW~ABCVy< z*{c2_EHalcMzQe{)X)Py9fdWG#CiY|+S5clk*==}*y>P5jZLbggIiprxaJ%hpFP9H z)HGEDPu9^we!gQ;8b(!#{sw~R%5k7IFfH7|={)8sBK>9<}8Ui7suE+yHn!y>&ve-l5t`nTsAqUv3b9CxOr(}s2or_1w5~V|j zSpzBW(JuB%8j=I5>8bXpMt4@#Y~A4&A;A`3|`d>}Py*Xae$3pI%ZQBp@d>kFJ9++|&i~b$T=k zb-)jYASyR&YF{-H0)D;PV3$o$1s|joo}mm7DZU%(OEb?+$M-)6-5Xaso70euPU=DM zzzB9&gKT|}A#k%}RLO zkYLZiTSXh7epC+Inh6HIh*_Qf)dV|K9+p$r!8c@nN?1(Lg#@&!1ONQXj*xCs1L(Zd zlBU6$pxQP@scf!M{R5qBHJ4S;QVX-JtHD)pqO~^yqAxFKNM(<;FydB4Kz$ES3M!Fu z^Olob(qJX0q1gDqsuMPta~e#lEO0FlK;QSQd06L{2>TNhKM$ccq8KoW>ZN_Erm%KqAuBgYy|OC>fhi-vp#|~GL6S!RRd~!XhIJMMn39HR4z?*6E*>OO~3~6 zw~5~QnlUg^Z2G<LByLIwqa;0u7czi^vsy^1(XeskYLV5l3ns zRp6S)3c=3egCS(0%A5jujVS5M4>F=Wp%mYs&xKqNFo+b>**CB29to)oEC5Wa0=4f@ zT!4x2u;ly#vskaV$ANqwRN--krkh~rHxk9xB6{_89#sRh?8g3l-M2bD@ zt7M>nGc*yZgHxJ(G?`;KgiS$)zncVqHn4^aGkwn!7EDS?59k0YCKru~tsYnrpi*ZG zKuFKuI!!#jnlCTso(y4HOr<=1e@CcO6|btV-_Z9X#G!FUk1(_W_v>JcS0i-f!WE4v z+D(T9M4U+yLb3Wn5uISir7IC)QiBVf2N!VNTaLptEUF_xI29bE0k1koYnpz16=`4s z%4fl4*jpq8kvSSK=03_hJDbTi^p2Q71FhuUoCx*;K(B}x0dP$xpMnW($$%Rp8d>Mr zo9?9QTT#?|0D?btRvq~pW5QP`wl|u_3h+)CjilQyboxj zgZg}0)A1S>Zx$3AYFAXrt>*PiFp<8RP9F}{hkzin_P(|Sh$rP#spMloMdFwMS(;3- z`I(54Wk2nxP3cJzL@EY^yOeCv>A?Fr*zbkoYtGiAPDzj|MMJQW z*A&_c9HA4G@{2#&^{D%B$9zMvLbwdv&d2O(6Y%EkxH@ z=qe5S2sp{#DH2ELkaDn)zV-$lXh&`-OG2RLH=25AY6Vy&eH^w;vz9>zoU8OMVntsO zv6n}!W+?*Oswow9+3(WVBf-%gyOc^LI0mkoG;%-#)Tv2_qj8RkT;bAj-dnY!Bk52M z*jxg9e{{49N~pj+c1=g(-%*fIuM4qaPUBFSOWhA?E7G_ueZdx=jr!9O|Abcv{LptQ zX%H23VGT7AAIMEuStG-#Yj3$$wCGI)tcEKQo%&ZZ_Df$lfY{J?grW7O=D32U6cRgz zwn3aDkFg5*>idS?cU5>99DS$ORe$k)1JmcEg}xO=#*K{*5P=T;TC6(Uel%BttkFaZ zu^al(*Q89aU}9d{J9Y=AG~&A~m6VGP_l|qI&WjwMv#JaD2~wFg#)^8`$*%A90ODXc z6vfpMNp9td@kDSXpx3Jkw2pc<7$LApU84ws_(H5hkfJxJY!-c08WN3wmNtH8S7a7E z#@^o9kEuR!1!cB+tk~p47K>+Kk?Rg=kL7EfAEsd>^(#A@wXpR~-3pBf`_~J&8D{}2 z@f@^>q!aIeQdXbJ{Wm3Li3P*U+a>@20fcEoLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xJ z#a}<9DisF{ia2DbPF6%k9JLBXs1Ry}Rvk<({emV9Ns5c3;979-W3lSs;;gHKs~`w| zfVj9iDY{6B|4RxjVmvtR$GdxvyLW)UUS_Hp90yd*GE#}SkjbtJp;vSvgno=6C^1u? z6U8Jv$Jaf4e7%eDEbnuFjvggvGQcMg&obSxh&PC*H!Yp>K5>KfMlwl!8t44~66z#`7{DY2PB$rIC5*RraP=N}`@q_=t?{3Zfv%OftgRzYb`B$1oUnL7uPLK-UBXofPp7n zG9*U|(Ddi?!220}Qx@pE1-jSV-kSS3eE`zbRq_TnI0Qxtl)dip?#}k!{yo#|?*~RV za+L`>{<8o800v@9M??Vs0RI60puMM)00009a7bBm000N(000N(0phfhS^xk52XskI zMF-;v2Ny0F&B49P001BWNkl$6%~*o$fYSQ^d1PwCcE?g{&;7SNeB=iKo<5q z&pz4A%uadd{l2H4BT6X(bmLG<5%L1KTrPaRVn7X`GjJae4;%$H0gHfN5cvnC&cp%l z{rW3+)~y>DLzu@TJ8qG6LZ^Ab=Z)u>LwJ4rr=iV_4OyjM%UtFt7oD_&>c; zqXr3U*COx$FcZk{*c}f{2Ic@;EP#Xo1%aACFQB;Ncn{DHn4Yh3V+Q^3LjWKFfcQ7X zhyauawmLQ^0v`Zx10(~VI^Oe3ciu_jpMN6y-=~1_8AP!*CQL2ax;1Z3of-g0fc%qs z)U8Xx@4w#)tO2c?13Q36fvp*V;iMBsk22u?`|q>BQ!V~IKUAP)KI4BE8D(()>lZwVD#R-6m8iOpf!+U?J5H_uXdA<^EClT{K=C% zDneTk+K3<`B%C=DnB`Ya(;eSGQLuBIQuG-={*q5LV8#r91D5WRY6B3L%e`jVvVe$n zHK~ILhDctNdKhg^Tn?}Y_&y-wUHVHCAsXrczk;P}i-dZW@_cdyDqA;h49xmVpzc+w06dWj z4j7J7o(VqR6`gm2QkY3e3K-&;ADty2VqHCoF%0ne$^%Utn+pKbR6;_0(#DNn4Xs+W zbBxICF(Sl>&_RSR$Bqpk;XIVQXO95&9GgBcEq&rjDbM})V{s<0wdt0C4stcc0gHQ| zXLqEv5{?4?1P%c`4G8+Fb7x)@;ZGep5g?1+eg1irX@VB90RF)+PWH?u&-aIm7he}W z^`=(REenfHtI;0ZuYiLN92wSXH5f=1_$dfFjjmItM2ra0BJ>CZ(y@EW6x8X{<$z#o z)1SCp$rY+r%~r`c0FZ0+@D`sQiT3US)B@fB-bLhA;4UCDcWonJ4BA8dl|Ye%-+xC$ znECO?H?M-O{}gd-I_ybFF|+wHrUN7afLu*_=BuwT;TRBuj(WhdkgP(fr-1c98|&Ra z1&*0)4Nflo8`SlIldP&|%uUfKP!Z1wv2$ z{BtT;YRL_4dkJ!UcV7hqtQi2vHGFub6osF70;R(jDg#R$5<`6;QQ#SXls$XM+o;hQ z1si~StmCI!9K@zjAP-OqcmW;7nrMMlWD56ZecJ!GrApnqT^^&9Y*HYH%U(2yO1-M(m z8mISm*0^yjjEPCNk_z094z9Z!D2ld%<-demISxz%z6N%AVYeG-F>@vlJ^5sgeQ6zY zW~&B-m6cW&BO(O}{SkrBaGq4H}?eH-=H) zQqrCBbw5F8%GvZ}bTqL_5vvrX+qSjMEK7j?z=Md?0`3IvKqvjoK!>$li1Gp5(dSv~ zhIK%_ubz5}_97hEyEkXPw5TYBcIIgjNU1u}+53Vb)6ET~Qj)nj>AHuzcP(3%lz;vK zsB7sa!Io%LUcvrb+qchh@ZHLV3n^N5B!vgPdAG-}>FJ4CGdz|YpZSPRs6anT~` zG;A0E$n~HpAAQ6xefqfY`3|6ycR)v$)&lm0*Q&*^%kZeroic^bo`0Sw(t?&8a$YD( z6$5TZ8wgruI4l`h1@v)R(}F8hVC0r9*If^Z)<9ne*b2Iw1*T@h`-~B334CFV$A>`g z*qbz1F~=lKIZ{X00pZr>1fVH~@hhg8qJYCd_c?QD*sK{M!j;_ryU#z*?kQ6=Dh$Jg zX^uhXS7gRjW?0s=wTQ$krA~{b3(W`OuJJfp<8>Mxt^;tNf@R~f2OzdTN`QgDo3Vjt z-s?+Y)vJT{ZPc<}yq2$FDW>@w&{v>zAz()H=Gs(iO_a{Xi(l3?`%RwAIHh=CneTlWd{$9Civ_WFP_PqG0lPOZmTIo#?jPNcsD35Q#*ou|Q+Axica& zNzI~T)@1z{k-bW(1c2SZf~zpT$~5Wda$y15woT74;py?&!$Df09kK>66nHhjL2?L1 z)~kntLx_|{7e70e$zi!29A=Who6$h0LWQpQ`lEL2V5G-`QtAxw94=Q8pf>P0I#1%S zOn#OhcnSC)O05Qg-bEco`UGZGIN`JOB`X zyYVz{{Fmq`V4FS$IvoJU?%YYS(xtEZy~GX}!0P}0*S5D7utc91IJdm##VFAd<(}mH zNoE;o6NXWvNQ)N9Z+-dYW!?X$@4wH|zJ1AEr3y(KH#P)jkydBxHXr;wApj8nq9X?m zFrapAe7c^~7@(7blgyFu@F#p{&#G#D`p{+2plfnZuibYarj!jz-sZp{`o3s5U zO$q?SU&*q34YNc8o3)qo3=PqCfhA|e?^yPc#AqFj2 z5CDk3Q^M)f^oWWAco0~aQIY^!C`hc)zdtc=zn!D*{qTVUR4rPR79~pHJ9Nm9s3;YG z^r)JWqKh25-0p0@=KkHgd0XpeuLJKoxNfz7luG;wIIwpwg)h?y8vsZSq%NgPQ?btPO zB4`WS|5=;k(aCEkZSUm-vL|#W0FWF^BYN~;)39N;2E^=r*I%#Su zA`4OKbgugKd9ZtT9`?gx7Xg4|fu;=`#?L)^q?%ntN9^W9n^qfJpUfg22LS)A_|i+f zGJLpSk3q+X2vKLxer+uhS*19%rR!rKy2y0^AU6U1IBOQ;pL&XX={JX=9cO*f4Qg!P z+{cLYK&ixAnlTG!R>J0ni55Ic0&c%x1+pl%>TRU{1_qcKA9P?qvj{F@PoB9)U=c&jH zhwVj&!|XsuE$l!S2Rw{n9JbS>w*gE1H7GU!kn__U&6;s&?pzGG7oB38%dvYCI`(T3 zKt%1@4E_E08|wMzO`Aqk{`^dD+7xuC{#vr;J3E3dAFvi}FqS0!p=ML%+$B0|2=Z zX!78}{L-tJ9d7=A=(fqZ9joUY;PIO+`)=irKk`ZA#<)lqieCX;RN^@__sz&*HUX_M zj4jGEJv9al;NIrVsZ_0603iNML%Mcl@sB?blyjbV0t&0_9?YgN+U%%*Y{nT!4dI0a;GBdRXD} z=>CemVgqA(4)uNbVU8_W;7mtug)Y!nz=}>&PWKM&A z_#u_6+&gsW0aGjL1<^jHv-X0GE?BV%m?KcQV`!++UWAq+Ed2WG06_eZoGakbl)!jg zl>YO|D>R!rm5|DnZLV7mU9!pRIII~Er#zm|T+l49NcgN4Er=0eaOci!TC*k~V&!zQ zibV{xLX+wvy^wn1}w7GaX@bB{+x(pbK+dVgTg9d8w;>CEqUR*BM zRlBzU2f1>xtJ9fSITQi9G-}K^xr0OUqV3VtG;Vy{SI!eY?UwJLg9S1z?jf1&RRN3N#Is=aX_aC8)=DT>+`^qb_JtoALc;*@UfA(1bAlH+u^ws;& zl~TaUJoHyThz0o8==#7OON5C7jyfccV07uzThJD~ThO}0cW9^Sb{kw-?b)(==~71D ze}B533+X@tj7d($9Tdc*4?f`6K7H(6ozcZ7Dmo5N1frF$M#(LYKFYh_etX$t;tovn zE2i9z?>LIs>9YN*#bcnPzGJRu55;d zCNp~90>KUij7&@sJH);; z&SV&FOfv=;l|eUIgGghg)XB(8Ol%7PRo*3wT3l6h{3P4SM;X-oIvJ<6FTA-dm`c@8!Kx^FYZ%%o=z9(a2c``aW0FbLigFAO-$Jnu0 zFQ@>z>r?eurFQz$bG#rz2>grm=F_4+IB+TDeTMhw!Ny_3(v(`=HZHf7SQTPT^upHn z6BZ!O0*Qzu0g1R=iOTEsNd_Q_2HQL6h~V>)$9fktf!7rziB{rAXE^Q$po#02TNrcb zPyirTi>ydqH)-DULx@z2HO-U$@(i!ktV!PT<=OZ7=kw8dEuf1t^trQdUpl|{Ubb9k z)0#C5uT?9x!J@KQ#65!s(KgK@l_?!Pc#xh&ir7m09oGF-w7?dAfuBx5`%t1R-KDBy z-%6nVxGY%N0sy%(@v04A=ZpRU)buA4uk{So&^;0X4kA)EMX9qBbJ%mNTC#)*4?JKe zVdX-pqUhMI+~_)H&!U})8I1)01EyLQHm7d#a6BxySku#hC;c*L(2FmoN+^!av;659 z>ZF-poiXI&qZ%~Gao_-`egFNbp(wGYc{*09t@)caU88hWnKvz+=U$6D*a=xK2iF-z z`e@;DS+QjYILIZ^l#f1Q`D?FXXe-YO(oCHB6&&{$FS=#9YHh8qHswlyGt>NVPP|mD z8c7>c?IY#TL63boxBsJEDYMQASoLLqgJd6lJ9R26G&sV5FC5@_4>(?|RV%-EjyE5F z95IX_v=`c@ZorwhwNJtCXvF&Un!6}P(N?Xt14At1NgH(8p=0REGiQJUJ6;+|s zlTWI?pL}w|yfe!}XH!8-XE_NRcNHtf7~LT`0Fdn99N)HSgXk4$JGusxP4@#!{Y5!< zOqwK8BRuFFtJK)Qn?9EUu~-+N1AF(SX19`@SA6{6w^Tt za2!Kqf`|kZ1E7)pJH?s;z0d%y=lMT+q8wrw}mGslR4NM4j$0NiG6 zP5>SO4o?d>1Oos8c&Bx1ozSe*BGPiL0a`1Vo(#m^VBoMgM{bn*9j$wSj=8#D!I}br zs*eGH0CcNZk>ssg35DJca459(YI^uxcQJ15+8dEu=0d6Y8Nl%bux4Cd2}kN zy==~ssHo3F@4S=mZ?Kg#T7;B;{)s@Txejn7qpfwnKmE-&e6VOyV5SE+2&|tx*?ypJ z9N^z9VPP$O@$qWJ`t>)=v(EhZV|-pOA`)qFj@uoaqY>~+o<@y$;IYR7vpfJ0m^*ng zDLSKXzGEdl4m1e4?KU;y&p&UNm*!}*ToU-!0S>PvO8!#4e}7`$emgM30|0@St5xId zh7E|WliP%}kcwv&953!iVd)p51AF%}@b0^9XU@y$Vrr4r?i8Rgu=ti{%>v-auEogT zJ;(7UPx4ycyr9cWI<@cDfR743_80?i0LR#?_3Cl@x8D$5FLx@sMS)GHffp2fU7>4N zUL83yFtY;y(PSk%RX`VRv-<^pt)N9v@#2i$yVvhu(!C;7(qI3Dt`bs{;Um)2B}Ha-KZ64I>!ST#Bw!1#lF@D4cAXzEAxe{0nBzX@dl;gBJI{bJsD>@kjf`z-<$ zFwKeRY(SfKp$j?x7F4DT!H_z(&S>h18B%J%q?@+pR}QIMnKRqAG4!v$aJkd3n+Nvp zrEkfSRPElK9wSC%`I=pBy_Nktck-pjLr`$=H6LFQAzE~T&{3t-SC=r?I|c_+pmF1z ztAqSUgybz-K&cT9aGU{Zpwt#esEqrp)N*zW*`ogzElU8+>JfdgRQ&Yc{c zH;+S`HA^B<|eFF$YV$ z*0lWl?~#xY1*k^-`d#$+f3|GlIT1SL$%9HsA@SI;oC^>uhIq-9IL zE&M`ahgAX!#*RHE_{S%88(aI8$M)vAOM-5a15cA|o2@MNN zw*q%9SB^)4*>Aqdsw}iDb5g9w!%s;`1YZa40WhRV6O_k;D8)DLzI(xAm&}{TdzC5? z4x&z-lHhXX7_xF@&I8E6Hf@O3T(KtbieuMcV9|?<7V*}M8GZ?l=S7(F%rl@%fqqR| zzmb1{;=typd+y==g$uL&+A2+^lEQAc0BjsQ7^REKg#e*|d%#mqv3vV=9upxZG*pU$ z6a^^`GU=(O_+{x*07k$5I*mp6*^x>hA~X}BmeoPGbw~)kYS$+5>{)j2*nx=foCpVZ z?V_FtbwqeTgnheqapd4Z9u}cf!GgS6uO4@c(5*@pIz~oPHLW9Wmgw}UQ-}zK!opaj z6!Acr&`?^6@Y{LPk3aeMzcI9q8=_n;T3XyP$LSz;{aoNMy009-G~hW8Fv(BNofF=9 zht>W2155&*cN*$|h6)l&wP{1Y$&;_j^&c0p2h%VhI2n>$E|+2Wd?gS$h*BXya^8m? zN?QEmk5tnwr%<3Gt5@g5+O>q{%Ei{xr}K95Hfs1XSXpcLVek;LuX$-m2#p=MB!NJ zXYi6GD9t5vSE*4W)8ANHSp}e2o;-O{Ct8jH$R8_KaCH8BTv|4N(6OrpN*x`uf4^UH zj=rTz>FSQA`3vA|#_=veLCq8}_UKX7uzB;VelPogu&PxRa5g`n{3~C_$H~D2fLMQ!qG~ahAgCa=UfcDxXgP8UDu~sb=@wY4a=C zd*TGHhzPM%OZS=e>k(bYE6tjA{I~%WP~cKPa7YL#S~N6)(kHU0|GfRwQ=I>oevO0^CsJ{dWBeeo4H_Ui_|UnG_5^;9tXI$PfbsPw zpRk~PdqSb0RR;7fhdS^c@P?~kL1MLo@YeO*AN`3v(+?t#4spTteD!kZy(F8i88@)j5j3(6EJ-~x_$7$`OUb= z$u>9?K|!dL6nQ9reg(P(Z~?vgCx?|P#qK?OR6aPPfFh4Rs_#XqR4x)yw5XZ9Yu8bG zDx5ie8n^x@2-p)`upnQ5@(J@gbf80cI8%D{qRodNrn{e?zW&-?fx>~Uh{UH(EfIwl zDI*I3pT7S-EBp4v1cn|v#zCME95NsTuU95K{4izr?!EpZR_uTQbkKwJZ*(OwQCnadGaIzi3Z5)b?YK}awh}FUYRk2nH@Ud z21CxC4gGcE#B)qru`OC4A_j0sLPL*M?B3lIYnn;1rkPZsOBYYF#_bUo$dbN&F~D$x zGkRd9xX3ZqP3eWeH zw+^5=x>1iEk$VVu7+61!>(ij8ga~mLxOOuU{sOk7q~NwZ=|6t;6+@qVl0%j(UKp5X zDbUd&A;bZ(n>TZ7C0$Zt`=(7iQ>6+az%NSCLWIr0hNL8dLqgcKZ5w?mRN#RD1DNyn z+Z?vUg=sr?k|$q2%H+5sMK?`144=wWB^^DZ zMt~lGv(C^Dh%{G9d9D^G>{_Z6-aUKL*-X8_CxOJOY^7^%=1n+#n#iaqqP5ccf@9aK zN-4hqhogXWlMLYa1DEU3($7Ed>#JKZvvbD|-YQq_^5uD;`S|fX(Y}2e820P|?NGG? zw|)wCxgJJ4%rA@1cb{><)LpwMTB_7V&#`vJ3SMi_Ahq;k>Y@nvWBa#j&Oy3Ws6euA zeYP3wikid%9ZNm`Jg-lhl;a0&V4F7VojMhCX!=)5upiCLT5l;uGV@A z80;+C&`~SEjg-~b9avzv-H(h-Nx5u*{9A;gFo3jbG4>+*v4UM$;f5Ww!>_bJXS9jd zrhi;PL6gQLC!78RkZa08&cw&t;27xu$94?k<#DEo@&j<3Ja&v-Q>WsB8i2D9?m7(P zKjTajpD)un+V2VgvKJlAe05h#{ihE)ve!wGqHA(pki01=_BlQRT4dP$Gp0EO?L71w zx~59{%R0g7KJ;MQ%TX`WG?Vcn!nT1V8ZiTdrg96wN27ug7Od$;`7=6ygtlsbNi zq4-{9nx5{fSMye_S`-Kg5qF6aYLqq?XDi(zLLKXSv4{wwL7pp7LM_bN{Vo6=1ws_O z=7F~Fv}(nhnIxVuvNiqjO(g4ge%GWjxJ!cBduR{tI2b!=|36d_MEp zH{QtcgY-tTW{6?9F->Rjh*Q9Q3ij1}>#fU@_ea#KMSc;K$0NSu$KfiZZ`T2e+Sagy z1dP0S)k1x(tXR)}6J1rl2=GD$!)UkT%P&nb0Z81qaRfn_H!W3R9=cy*mS}M2&TJn$ z){f{+HxRZ5IR)stVg=Q&VaKp+2T14Q#qAnTWzmJ+Vlp{wF7V|SexP@>6R`(roC6#R z_z&>+mH0Ufurd}d#6>z;$KStu8dZg0IInXfVfADzW?pI%&liMmS0} zwdQvM@a=`lwssJ~(AO?&y_Ura4vbSOdjaxFjT$yMdRyXD_<7{-w@u61<8rxjEI7u! z{kAhjr*&xFhN_z7xGuyT$ zOwX)7_O=k1m7E2DwYh0tjNEli~j1GTq~8nuV>wVG3^Ai7OmDB1$} zZ5FI|F{Bj;X)1bc$sU08df)+0E?HvB=o5gaGbnmr14GfhF1RfwhS$c7$+5stO7X*t z8T_F|r;jp#qyGE3rF`F(Y?`-<$QIk`xNG}% zr*du^eI_Jd+I8|-;7A5`14pfu-)BwoqUc^JVu59aTck=CkyDd_=We-01K;gt?7n?O zYYuPQHOz|{E*w(9Ih-Y10CH-{5(EmM)M}u#W7khW2Vj>%mn)C;gT5A$R()iGV^zUY zI7fJZaew@AC6`FbxWV(+U&Neebr&F{a%D#R_17g|=&hD5$yKOO+KOGcFzvEXBPzqJ z8E-9mVFFJ^!@wcky5aSB_}}>PxWMIx)YZa--j`?|Jq~mO=8DKEuTqJhKynwYcNhjl zdZD7Blh5)4C45SK1ax>7Fb^M2x46YzfJ?6b^;Z^s_F3wCUO#dqp_xm+p$3Fs5kPv> zsYAl5RiOKluOiJ}Z=(C>4e$Z}((!5ArcDg4QU#A*&IV=x$8ZHBf{GVs@|8CReb4%2 zm2UTJ-n{v8F7(IDnW;)TK+#sME{&T4F4$n|LfSj5TQKL8ugaifQPDzPWX~}L& z65)767SHc4Q3CJYz0Cn?zWp}a7cb`2k|h#tDdm8vs#J;hZQQuZ(1xB>mgLhu8D4ga zBqUk$%OEux;2fGVcrah~>P4OmI$2QWw`E$3nD>hOAQm_(q09B>a*XK%3I>E$t;+ic z5BiNsZ%~yg7#bY&(Vp+rg;>EWDR8~I=K8MZxR`o{w`0DdEPa4c$B*MqNdaAGBaij& zXGWYaVjzgDouFj9b!ZX7Rd)Jv6OI|Vu82aMT?S+U2_jv*0c3MSFYw*L3z(}%_34x8_A{qW(fz>(=_rEl(4liot&WN6Y?aOyc@vTy{>eA)>Mw-jDV#P0Aia<$p=A6x?jsUe4>@Dpl zL5BcuwQ9xwFTX^f5=za_phIvUNlTSp4($Hs0(!t7AAG=YkB66s4NKkD_qpddG;JC#J(()`bP`=P zOL>pVAo>(>TDLC62C9G-lPB|7n>Jbg%_RY3RPWxb`|v~DIz4$a&TtF3>kJ&b|E;&^ zFknEA2gbUUD;Zh8zFj$^D!L(PF2_O>4NP@KMqZ7Ab?L@E0T_AoD0%be&yi!5Xc(X? z8@_L?;JHm`fCh-u&(-^4BNYrR9X>o2AZ)69*4?ozfZzTF9+nn$y&=gob z4p3=kz?+tmOp~aJ6-nN*Wi`4a>@5Pl?@$SW8S=33syW6AFr(hy6Ewrz#R^7+@WAkqtT;z zX~>Wq{i3H08^((w9RA@41j<`_T2}`+4goddVfB3@M)-Z(GdYh%=W;9r@BH!$F6}(C zD``|*w|8$xz@DL>rT~L}|2@m+Z6`v!o=bfln-8OXW(@=iR3bI{#QwCzVFYkpLgchW zL{RCOXQqtDl`Sjg(W46OFskC%^|*rFc^fvQRp-t*da`uC;|@H3 z{Y8*rxG>ETKu3q}rj>D#z9@C#^BW{>wQZU>r!4vXvK}l1m^*PIR_BoSMVng@7;7Ek zQSd{d#~!<6Ia9%_js#0>3XTUAo83fItxA8(2jZk? z9T9PkJ7mBYUtFAnhzJ%qMru(pm6`%SRet3aJ~&TF`Rh%G_V35%^U*;B&z3E2ped$# z2I%4dM*=#`p<|hL?M~cCt(J|nKUY+L#|?b_+H2%%+!%qMErX1c;!ODS-EY3hl5CVg z(|X(n8SsJouDg;>I*xS%9e@5gb>gGTDqZXvK2qH%MB0Ek{p+u+%+Ok5{uf`^UrStI z3^S*$Wy%n(zxE=1lGAGnzIT@{&C5fF=2#{C&0@vq>~>?oQ}}!ci*g9lMVy_clxw*liemPNx{kYGbt<$ zu=tyAaOp0p5;x9E^*jPl*zQMns5Zik=Tt&aj9b1s(xbN8NibgRQ18*ON@2 zk1ZQE5Zk^zo;`ao6B7lWPe=P2h7LG148$-%SAEw*X&0@dOWA_ZbF6Cnt}{lDW~QcUre*@AT;iJb-4Y zHhl@aY7{6SFTVbIj*~^~(DV?fq!JSP0?%bQWDoGJktfeM^W;gQ8Z@|G;7AvX6#=~N zbf{{xsvQ;KLI?2$13sm{CoAw6X{Al|;+4TuU9nm<;*K0ir>Muxn8C(ZU!_W|T9+WJ z8EiC7>esJNyj==5CRc5hhg|!HW0+>7bz*at$^2S zS|C>uSpZrlw%Yedi{pA6?NjS)EzAZ`N5OwZJ17yDsnW{Sp4aLo@ScKatxW|@*ambv z2H%BgZdKdV=H=?vd6z5g+p+BuVSwu#9mHyZR=_gn=CL;a50N_*Fl+t#94A)26dq2D zNIi6}K#XIuq@calWq=P|#fmw@oBfSaOz`@^my#C3tu8bH0bZUxo7VH^GY&9v=T;IK zsg_;BMgIbDxsaeB`Y&J3Nl?k)#qIWC7=mFKxLktUEfNtShk@4f=V$5M5jARH8pcx6 z=W7&KFgHlIN{BbK&&_TSGg#@|oC{K}di7BH)_XfP=M$LjhLb6}&B3=8IFf;XoAAF3 zl2s78`g^8Ydk@me_M1S{4N4`A`|UR>>SEJ5kv?+(TF|{l{^KrL@F`%^F&ZBuBzyz!0V9U8ZV<(&F-Tfi@RY5k6YE5)t8*sHnjS z@$qM@MKUuwrQlxRPti5bY&r|X&$@sWYlj7l6F_ZX*Ux9pP_<5-oV>!m1MEUado}`^ znsB0Y)22*S>bimBU|bv#l`Go-x!g~%($AJQGqeYJq2rhblh0F->s&c2^Ve?Or`vJP zAz@Xko?jVOD)p1b#Pqq1{Q1SQe%j}{&+GH0WIV?-r7#K<5EEV%&FMR#D^=R@YEOlW zgaOly+_?pn;C)0Z?_2!!j7=!LM^#Z!B_-J(C^jzz}k*I#Gtph37r!Z3^!<@0&R zb7g%vs*7+d5Ho8Q|7vl=lzGb0)IO~ZTRJ_(2TZ)ocRBIB_gLPqAFd4CKmluj>Fd@} zxmvaJFDC6t6KJilX7&I-T*2fT+PynlMvla^lz>FbM6nr|v}O%=)Toi;Ud}g@CNbVB zjAjBQtkom|%xQsw&^Ij^2IO`_s2f6D5Ft>wbZF=md0sb<9Em`F7lem`JFkcl2Cf>0 zAw6r>%#i~T5*9`=bR0v8v_;089+)FHHM!9RzU(E^10)g@bVVmU3*-`sM=39$YzLMy zP2S3(VJVqWGWdPKVt}rYgK)0>TuD&S70rszHE;4}Ja. + + +import datetime +import getpass +import gettext +import hashlib +import logging +import os +import platform +import re +import shlex +import shutil +import socket +import subprocess +import sys +import threading +import time + +from collections import OrderedDict +from collections.abc import MutableMapping +from decimal import Decimal +from enum import IntEnum +from functools import wraps +from pathlib import Path +from typing import Any + +import uno +import unohelper +from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS +from com.sun.star.awt.MessageBoxResults import YES +from com.sun.star.awt import Rectangle, Size, Point +from com.sun.star.awt import Key, KeyModifier, KeyEvent +from com.sun.star.container import NoSuchElementException + +from com.sun.star.beans import PropertyValue, NamedValue +from com.sun.star.sheet import TableFilterField +from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA +from com.sun.star.util import Time, Date, DateTime + +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER + +from com.sun.star.awt import XActionListener +from com.sun.star.lang import XEventListener +from com.sun.star.awt import XMouseListener +from com.sun.star.awt import XMouseMotionListener +from com.sun.star.awt import XFocusListener + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1FontUnderline.html +from com.sun.star.awt import FontUnderline +from com.sun.star.style.VerticalAlignment import TOP, MIDDLE, BOTTOM + +try: + from peewee import Database, DateTimeField, DateField, TimeField, \ + __exception_wrapper__ +except ImportError as e: + Database = DateField = TimeField = DateTimeField = object + print('Install peewee') + + +LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' +LOG_DATE = '%d/%m/%Y %H:%M:%S' +logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') +logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') +logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') +logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) +log = logging.getLogger(__name__) + + +LEFT = 0 +CENTER = 1 +RIGHT = 2 + +CALC = 'calc' +WRITER = 'writer' +DRAW = 'draw' +IMPRESS = 'impress' +BASE = 'base' +MATH = 'math' +BASIC = 'basic' +MAIN = 'main' +TYPE_DOC = { + CALC: 'com.sun.star.sheet.SpreadsheetDocument', + WRITER: 'com.sun.star.text.TextDocument', + DRAW: 'com.sun.star.drawing.DrawingDocument', + IMPRESS: 'com.sun.star.presentation.PresentationDocument', + BASE: 'com.sun.star.sdb.DocumentDataSource', + MATH: 'com.sun.star.formula.FormulaProperties', + BASIC: 'com.sun.star.script.BasicIDE', + MAIN: 'com.sun.star.frame.StartModule', +} + +OBJ_CELL = 'ScCellObj' +OBJ_RANGE = 'ScCellRangeObj' +OBJ_RANGES = 'ScCellRangesObj' +TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_RANGES) + +# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL +class FilterOperator(IntEnum): + EMPTY = 0 + NO_EMPTY = 1 + EQUAL = 2 + NOT_EQUAL = 3 + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 +class Border(IntEnum): + NO_BORDER = 0 + BORDER = 1 + SIMPLE = 2 + +OS = platform.system() +IS_WIN = OS == 'Windows' +IS_MAC = OS == 'Darwin' +USER = getpass.getuser() +PC = platform.node() +DESKTOP = os.environ.get('DESKTOP_SESSION', '') +INFO_DEBUG = f"{sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) + +_MACROS = {} + +SECONDS_DAY = 60 * 60 * 24 +DIR = { + 'images': 'images', + 'locales': 'locales', +} +DEFAULT_MIME_TYPE = 'png' +MODIFIERS = { + 'shift': KeyModifier.SHIFT, + 'ctrl': KeyModifier.MOD1, + 'alt': KeyModifier.MOD2, + 'ctrlmac': KeyModifier.MOD3, +} + +# ~ Menus +NODE_MENUBAR = 'private:resource/menubar/menubar' +MENUS = { + 'file': '.uno:PickList', + 'tools': '.uno:ToolsMenu', + 'help': '.uno:HelpMenu', + 'windows': '.uno:WindowList', + 'edit': '.uno:EditMenu', + 'view': '.uno:ViewMenu', + 'insert': '.uno:InsertMenu', + 'format': '.uno:FormatMenu', + 'styles': '.uno:FormatStylesMenu', + 'sheet': '.uno:SheetMenu', + 'data': '.uno:DataMenu', + 'table': '.uno:TableMenu', + 'form': '.uno:FormatFormMenu', + 'page': '.uno:PageMenu', + 'shape': '.uno:ShapeMenu', + 'slide': '.uno:SlideMenu', + 'show': '.uno:SlideShowMenu', +} + +MIME_TYPE = { + 'png': 'image/png', + 'jpg': 'image/jpeg', +} + +MESSAGES = { + 'es': { + 'OK': 'Aceptar', + 'Cancel': 'Cancelar', + 'Select file': 'Seleccionar archivo', + 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', + 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', + } +} + + +CTX = uno.getComponentContext() +SM = CTX.getServiceManager() + + +def create_instance(name: str, with_context: bool=False, args: Any=None) -> Any: + if with_context: + instance = SM.createInstanceWithContext(name, CTX) + elif args: + instance = SM.createInstanceWithArguments(name, (args,)) + else: + instance = SM.createInstance(name) + return instance + + +def get_app_config(node_name, key=''): + name = 'com.sun.star.configuration.ConfigurationProvider' + service = 'com.sun.star.configuration.ConfigurationAccess' + cp = create_instance(name, True) + node = PropertyValue(Name='nodepath', Value=node_name) + try: + ca = cp.createInstanceWithArguments(service, (node,)) + if ca and not key: + return ca + if ca and ca.hasByName(key): + return ca.getPropertyValue(key) + except Exception as e: + error(e) + return '' + + +LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') +LANG = LANGUAGE.split('-')[0] +NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') +VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') + +INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{INFO_DEBUG}" + +node = '/org.openoffice.Office.Calc/Calculate/Other/Date' +y = get_app_config(node, 'YY') +m = get_app_config(node, 'MM') +d = get_app_config(node, 'DD') +DATE_OFFSET = datetime.date(y, m, d).toordinal() + + +def error(info): + log.error(info) + return + + +def debug(*args): + data = [str(a) for a in args] + log.debug('\t'.join(data)) + return + + +def info(*args): + data = [str(a) for a in args] + log.info('\t'.join(data)) + return + + +def catch_exception(f): + @wraps(f) + def func(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as e: + name = f.__name__ + if IS_WIN: + debug(traceback.format_exc()) + log.error(name, exc_info=True) + return func + + +def inspect(obj: Any) -> None: + zaz = create_instance('net.elmau.zaz.inspect') + if hasattr(obj, 'obj'): + obj = obj.obj + zaz.inspect(obj) + return + + +def mri(obj): + m = create_instance('mytools.Mri') + if m is None: + msg = 'Extension MRI not found' + error(msg) + return + + m.inspect(obj) + return + + +def now(only_time=False): + now = datetime.datetime.now() + if only_time: + now = now.time() + return now + + +def today(): + return datetime.date.today() + + +def _(msg): + if LANG == 'en': + return msg + + if not LANG in MESSAGES: + return msg + + return MESSAGES[LANG][msg] + + +def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): + """ Create message box + type_msg: infobox, warningbox, errorbox, querybox, messbox + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMessageBoxFactory.html + """ + toolkit = create_instance('com.sun.star.awt.Toolkit') + parent = toolkit.getDesktopWindow() + box = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message)) + return box.execute() + + +def question(message, title=TITLE): + result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') + return result == YES + + +def warning(message, title=TITLE): + return msgbox(message, title, type_msg='warningbox') + + +def errorbox(message, title=TITLE): + return msgbox(message, title, type_msg='errorbox') + + +def get_type_doc(obj: Any) -> str: + for k, v in TYPE_DOC.items(): + if obj.supportsService(v): + return k + return '' + + +def _get_class_doc(obj: Any) -> Any: + classes = { + CALC: LOCalc, + WRITER: LOWriter, + DRAW: LODraw, + IMPRESS: LOImpress, + BASE: LOBase, + MATH: LOMath, + BASIC: LOBasic, + } + type_doc = get_type_doc(obj) + return classes[type_doc](obj) + + +def _get_class_uno(obj: Any) -> Any: + classes = dict( + SwXTextGraphicObject = LOImage, + SvxShapeText = LOImage, + ) + name = obj.ImplementationName + print(f'ImplementationName = {name}') + instance = obj + if name in classes: + instance = classes[name](obj) + return instance + + +def dict_to_property(values: dict, uno_any: bool=False): + ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) + if uno_any: + ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps) + return ps + + +def _array_to_dict(values): + d = {v[0]: v[1] for v in values} + return d + +def _property_to_dict(values): + d = {v.Name: v.Value for v in values} + return d + + +def data_to_dict(data): + if isinstance(data, tuple) and isinstance(data[0], tuple): + return _array_to_dict(data) + + if isinstance(data, tuple) and isinstance(data[0], (PropertyValue, NamedValue)): + return _property_to_dict(data) + return {} + + +def _path_url(path: str) -> str: + if path.startswith('file://'): + return path + return uno.systemPathToFileUrl(path) + + +def _path_system(path: str) -> str: + if path.startswith('file://'): + return str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + +def _get_dispatch() -> Any: + return create_instance('com.sun.star.frame.DispatchHelper') + + +def call_dispatch(frame: Any, url: str, args: dict={}) -> None: + dispatch = _get_dispatch() + opt = dict_to_property(args) + dispatch.executeDispatch(frame, url, '', 0, opt) + return + + +def get_desktop(): + return create_instance('com.sun.star.frame.Desktop', True) + + +def _date_to_struct(value): + if isinstance(value, datetime.datetime): + d = DateTime() + d.Year = value.year + d.Month = value.month + d.Day = value.day + d.Hours = value.hour + d.Minutes = value.minute + d.Seconds = value.second + elif isinstance(value, datetime.date): + d = Date() + d.Day = value.day + d.Month = value.month + d.Year = value.year + elif isinstance(value, datetime.time): + d = Time() + d.Hours = value.hour + d.Minutes = value.minute + d.Seconds = value.second + return d + + +def _struct_to_date(value): + d = None + if isinstance(value, Time): + d = datetime.time(value.Hours, value.Minutes, value.Seconds) + elif isinstance(value, Date): + if value != Date(): + d = datetime.date(value.Year, value.Month, value.Day) + elif isinstance(value, DateTime): + if value.Year > 0: + d = datetime.datetime( + value.Year, value.Month, value.Day, + value.Hours, value.Minutes, value.Seconds) + return d + + +def _get_url_script(args): + library = args['library'] + module = '.' + name = args['name'] + language = args.get('language', 'Python') + location = args.get('location', 'user') + + if language == 'Python': + module = '.py$' + elif language == 'Basic': + module = f".{module}." + if location == 'user': + location = 'application' + + url = 'vnd.sun.star.script' + url = f'{url}:{library}{module}{name}?language={language}&location={location}' + + return url + + +def _call_macro(args): + #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification + + url = _get_url_script(args) + args = args.get('args', ()) + + service = 'com.sun.star.script.provider.MasterScriptProviderFactory' + factory = create_instance(service) + script = factory.createScriptProvider('').getScript(url) + result = script.invoke(args, None, None)[0] + + return result + + +def call_macro(args, in_thread=False): + result = None + if in_thread: + t = threading.Thread(target=_call_macro, args=(args,)) + t.start() + else: + result = _call_macro(args) + return result + + +def run(command, capture=False): + cmd = shlex.split(command) + result = subprocess.run(cmd, capture_output=capture, text=True) + if capture: + result = result.stdout + else: + result = result.returncode + return result + + +# ~ def popen(command, stdin=None): + # ~ try: + # ~ proc = subprocess.Popen(shlex.split(command), shell=IS_WIN, + # ~ stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + # ~ for line in proc.stdout: + # ~ yield line.decode().rstrip() + # ~ except Exception as e: + # ~ error(e) + # ~ yield (e.errno, e.strerror) + + +def sleep(seconds): + time.sleep(seconds) + return + + +class TimerThread(threading.Thread): + + def __init__(self, event, seconds, macro): + threading.Thread.__init__(self) + self.stopped = event + self.seconds = seconds + self.macro = macro + + def run(self): + info('Timer started... {}'.format(self.macro['name'])) + while not self.stopped.wait(self.seconds): + _call_macro(self.macro) + info('Timer stopped... {}'.format(self.macro['name'])) + return + + +def start_timer(name, seconds, macro): + global _MACROS + _MACROS[name] = threading.Event() + thread = TimerThread(_MACROS[name], seconds, macro) + thread.start() + return + + +def stop_timer(name): + global _MACROS + _MACROS[name].set() + del _MACROS[name] + return + + +def install_locales(path, domain='base', dir_locales=DIR['locales']): + path_locales = _P.join(_P(path).path, dir_locales) + try: + lang = gettext.translation(domain, path_locales, languages=[LANG]) + lang.install() + _ = lang.gettext + except Exception as e: + from gettext import gettext as _ + error(e) + return _ + + +def _export_image(obj, args): + name = 'com.sun.star.drawing.GraphicExportFilter' + exporter = create_instance(name) + path = _P.to_system(args['URL']) + args = dict_to_property(args) + exporter.setSourceDocument(obj) + exporter.filter(args) + return _P.exists(path) + + +def sha256(data): + result = hashlib.sha256(data.encode()).hexdigest() + return result + +def sha512(data): + result = hashlib.sha512(data.encode()).hexdigest() + return result + + +# ~ Classes + +class LOBaseObject(object): + + def __init__(self, obj): + self._obj = obj + + def __setattr__(self, name, value): + exists = hasattr(self, name) + if not exists and not name in ('_obj', '_index'): + setattr(self._obj, name, value) + else: + super().__setattr__(name, value) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + +class LOImage(object): + TYPE = { + 'png': 'image/png', + 'jpg': 'image/jpeg', + } + + def __init__(self, obj): + self._obj = obj + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self.obj.Name or 'img' + + @property + def mimetype(self): + return self.obj.Bitmap.MimeType + + def save(self, path, mimetype=DEFAULT_MIME_TYPE): + p = _P(path) + if _P.is_dir(path): + name = self.name + else: + path = p.path + name = p.name + + path = _P.join(path, f'{name}.{mimetype.lower()}') + args = dict( + URL = _P.to_url(path), + MimeType = self.TYPE[mimetype], + ) + if not _export_image(self.obj, args): + path = '' + + # ~ size = len(self.obj.Bitmap.DIB) + # ~ data = self.obj.GraphicStream.readBytes((), size) + # ~ data = data[-1].value + + # ~ data = self.obj.Bitmap.DIB.value + # ~ data = self.obj.Graphic.DIB.value + + # ~ _P.save_bin(path, data) + return path + + +class LODocument(object): + + def __init__(self, obj): + self._obj = obj + self._cc = self.obj.getCurrentController() + self._undo = True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @property + def obj(self): + return self._obj + + @property + def title(self): + return self.obj.getTitle() + @title.setter + def title(self, value): + self.obj.setTitle(value) + + @property + def type(self): + return self._type + + @property + def uid(self): + return self.obj.RuntimeUID + + @property + def frame(self): + return self._cc.getFrame() + + @property + def is_saved(self): + return self.obj.hasLocation() + + @property + def is_modified(self): + return self.obj.isModified() + + @property + def is_read_only(self): + return self.obj.isReadOnly() + + @property + def path(self): + return _path_system(self.obj.URL) + + @property + def status_bar(self): + return self._cc.getStatusIndicator() + + @property + def visible(self): + w = self.frame.ContainerWindow + return w.isVisible() + @visible.setter + def visible(self, value): + w = self.frame.ContainerWindow + w.setVisible(value) + + @property + def zoom(self): + return self._cc.ZoomValue + @zoom.setter + def zoom(self, value): + self._cc.ZoomValue = value + + @property + def undo(self): + return self._undo + @undo.setter + def undo(self, value): + self._undo = value + um = self.obj.UndoManager + if value: + try: + um.leaveUndoContext() + except: + pass + else: + um.enterHiddenUndoContext() + + @property + def selection(self): + sel = self.obj.CurrentSelection + return _get_class_uno(sel) + + def create_instance(self, name): + obj = self.obj.createInstance(name) + return obj + + def set_focus(self): + w = self.frame.ComponentWindow + w.setFocus() + return + + def copy(self): + call_dispatch(self.frame, '.uno:Copy') + return + + def paste(self): + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + transferable = sc.getContents() + self._cc.insertTransferable(transferable) + return + + def select(self, obj): + self._cc.select(obj) + return + + def to_pdf(self, path: str='', args: dict={}): + path_pdf = path + filter_name = '{}_pdf_Export'.format(self.type) + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + opt = dict_to_property(args) + try: + self.obj.storeToURL(_P.to_url(path), opt) + except Exception as e: + error(e) + path_pdf = '' + + return _P.exists(path_pdf) + + def save(self, path: str='', args: dict={}) -> bool: + result = True + opt = dict_to_property(args) + if path: + try: + self.obj.storeAsURL(_path_url(path), opt) + except Exception as e: + error(e) + result = False + else: + self.obj.store() + return result + + def close(self): + self.obj.close(True) + return + + +class LOCalc(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = CALC + self._sheets = obj.Sheets + + def __getitem__(self, index): + return LOCalcSheet(self._sheets[index]) + + def __len__(self): + return self._sheets.Count + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName in TYPE_RANGES: + sel = LOCalcRange(sel) + return sel + + @property + def active(self): + return LOCalcSheet(self._cc.ActiveSheet) + + @property + def db_ranges(self): + # ~ return LOCalcDataBaseRanges(self.obj.DataBaseRanges) + return self.obj.DatabaseRanges + + def render(self, data, sheet=None, clean=True): + if sheet is None: + sheet = self.active + return sheet.render(data, clean=clean) + + +class LOChart(object): + + def __init__(self, name, obj, draw_page): + self._name = name + self._obj = obj + self._eobj = self._obj.EmbeddedObject + self._type = 'Column' + self._cell = None + self._shape = self._get_shape(draw_page) + self._pos = self._shape.Position + + def __getitem__(self, index): + return LOBaseObject(self.diagram.getDataRowProperties(index)) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def diagram(self): + return self._eobj.Diagram + + @property + def type(self): + return self._type + @type.setter + def type(self, value): + self._type = value + if value == 'Bar': + self.diagram.Vertical = True + return + type_chart = f'com.sun.star.chart.{value}Diagram' + self._eobj.setDiagram(self._eobj.createInstance(type_chart)) + + @property + def cell(self): + return self._cell + @cell.setter + def cell(self, value): + self._cell = value + self._shape.Anchor = value.obj + + @property + def position(self): + return self._pos + @position.setter + def position(self, value): + self._pos = value + self._shape.Position = value + + def _get_shape(self, draw_page): + for shape in draw_page: + if shape.PersistName == self.name: + break + return shape + + +class LOSheetCharts(object): + + def __init__(self, obj, sheet): + self._obj = obj + self._sheet = sheet + + def __getitem__(self, index): + return LOChart(index, self.obj[index], self._sheet.draw_page) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + def new(self, name, pos_size, data): + self.obj.addNewByName(name, pos_size, data, True, True) + return LOChart(name, self.obj[name], self._sheet.draw_page) + + +class LOFormControl(LOBaseObject): + + def __init__(self, obj): + self._obj = obj + self._control = self.doc.CurrentController.getControl(self.obj) + + def __setattr__(self, name, value): + if name == '_control': + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def doc(self): + return self.obj.Parent.Parent.Parent + + @property + def name(self): + return self.obj.Name + + @property + def label(self): + return self.obj.Label + + def set_focus(self): + self._control.setFocus() + return + + +class LOForm(object): + + def __init__(self, obj): + self._obj = obj + + def __getitem__(self, index): + return LOFormControl(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + +class LOSheetForms(object): + + def __init__(self, obj): + self._obj = obj + + def __getitem__(self, index): + return LOForm(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + +class LOSheetRows(object): + + def __init__(self, sheet): + self._sheet = sheet + self._obj = sheet.obj.Rows + + def __getitem__(self, index): + return LOSheetRows(self.obj[index]) + + @property + def obj(self): + return self._obj + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + end = index + count + return self._sheet[index:end,0:] + + +class LOCalcSheet(object): + + def __init__(self, obj): + self._obj = obj + + def __getitem__(self, index): + return LOCalcRange(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + @name.setter + def name(self, value): + self._obj.Name = value + + @property + def used_area(self): + cursor = self.get_cursor() + cursor.gotoEndOfUsedArea(True) + return LOCalcRange(self[cursor.AbsoluteName].obj) + + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def dp(self): + return self.draw_page + + @property + def shapes(self): + return self.draw_page + + @property + def doc(self): + return self.obj.DrawPage.Forms.Parent + + @property + def charts(self): + return LOSheetCharts(self.obj.Charts, self) + + @property + def rows(self): + return LOSheetRows(self) + + @property + def forms(self): + return LOSheetForms(self.obj.DrawPage.Forms) + + def get_cursor(self, cell=None): + if cell is None: + cursor = self.obj.createCursor() + else: + cursor = self.obj.createCursorByRange(cell) + return cursor + + def render(self, data, rango=None, clean=True): + if rango is None: + rango = self.used_area + return rango.render(data, clean) + + +class LOCalcRange(object): + + def __init__(self, obj): + self._obj = obj + self._sd = None + + def __getitem__(self, index): + return LOCalcRange(self.obj[index]) + + def __iter__(self): + self._r = 0 + self._c = 0 + return self + + def __next__(self): + try: + rango = self[self._r, self._c] + except Exception as e: + raise StopIteration + self._c += 1 + if self._c == self.columns: + self._c = 0 + self._r +=1 + return rango + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __str__(self): + if self.is_none: + s = 'Range: None' + else: + s = f'Range: {self.name}' + return s + + @property + def obj(self): + return self._obj + + @property + def is_none(self): + return self.obj is None + + @property + def sheet(self): + return LOCalcSheet(self.obj.Spreadsheet) + + @property + def doc(self): + doc = self.obj.Spreadsheet.DrawPage.Forms.Parent + return LODocument(doc) + + @property + def name(self): + return self.obj.AbsoluteName + + @property + def code_name(self): + name = self.name.replace('$', '').replace('.', '_').replace(':', '') + return name + + @property + def columns(self): + return self.obj.Columns.Count + + @property + def rows(self): + return self.obj.Rows.Count + + @property + def row(self): + r1 = self.address.Row + r2 = r1 + 1 + ra = self.current_region.range_address + c1 = ra.StartColumn + c2 = ra.EndColumn + 1 + return LOCalcRange(self.sheet[r1:r2, c1:c2].obj) + + @property + def type(self): + return self.obj.Type + + @property + def value(self): + v = None + if self.type == VALUE: + v = self.obj.getValue() + elif self.type == TEXT: + v = self.obj.getString() + elif self.type == FORMULA: + v = self.obj.getFormula() + return v + @value.setter + def value(self, data): + if isinstance(data, str): + # ~ print(isinstance(data, str), data[0]) + if data[0] in '=': + self.obj.setFormula(data) + # ~ print('Set Formula') + else: + self.obj.setString(data) + elif isinstance(data, Decimal): + self.obj.setValue(float(data)) + elif isinstance(data, (int, float, bool)): + self.obj.setValue(data) + elif isinstance(data, datetime.datetime): + d = data.toordinal() + t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY + self.obj.setValue(d - DATE_OFFSET + t) + elif isinstance(data, datetime.date): + d = data.toordinal() + self.obj.setValue(d - DATE_OFFSET) + elif isinstance(data, datetime.time): + d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY + self.obj.setValue(d) + + @property + def date(self): + value = int(self.obj.Value) + date = datetime.date.fromordinal(value + DATE_OFFSET) + return date + + @property + def time(self): + seconds = self.obj.Value * SECONDS_DAY + time_delta = datetime.timedelta(seconds=seconds) + time = (datetime.datetime.min + time_delta).time() + return time + + @property + def datetime(self): + return datetime.datetime.combine(self.date, self.time) + + @property + def data(self): + return self.obj.getDataArray() + @data.setter + def data(self, values): + self.obj.setDataArray(values) + + @property + def formula(self): + return self.obj.getFormulaArray() + @formula.setter + def formula(self, values): + self.obj.setFormulaArray(values) + + @property + def address(self): + return self.obj.CellAddress + + @property + def range_address(self): + return self.obj.RangeAddress + + @property + def cursor(self): + cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) + return cursor + + @property + def current_region(self): + cursor = self.cursor + cursor.collapseToCurrentRegion() + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + @property + def next_cell(self): + a = self.current_region.range_address + col = a.StartColumn + row = a.EndRow + 1 + return LOCalcRange(self.sheet[row, col].obj) + + @property + def position(self): + return self.obj.Position + + @property + def size(self): + return self.obj.Size + + @property + def possize(self): + data = { + 'Width': self.size.Width, + 'Height': self.size.Height, + 'X': self.position.X, + 'Y': self.position.Y, + } + return data + + def select(self): + self.doc.select(self.obj) + return + + def offset(self, rows=0, cols=1): + ra = self.range_address + col = ra.EndColumn + cols + row = ra.EndRow + rows + return LOCalcRange(self.sheet[row, col].obj) + + def to_size(self, rows, cols): + cursor = self.cursor + cursor.collapseToSize(cols, rows) + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + def copy_to(self, cell, formula=False): + rango = cell.to_size(self.rows, self.columns) + if formula: + rango.formula = self.data + else: + rango.data = self.data + return + + def auto_width(self): + self.obj.Columns.OptimalWidth = True + return + + def render(self, data, clean=True): + self._sd = self.sheet.obj.createSearchDescriptor() + self._sd.SearchCaseSensitive = False + for k, v in data.items(): + cell = self._render_value(k, v) + return cell + + def _render_value(self, key, value, parent=''): + cell = None + if isinstance(value, dict): + for k, v in value.items(): + cell = self._render_value(k, v, key) + return cell + elif isinstance(value, (list, tuple)): + self._render_list(key, value) + return + + search = f'{{{key}}}' + if parent: + search = f'{{{parent}.{key}}}' + ranges = self.find_all(search) + + for cell in ranges or range(0): + self._set_new_value(cell, search, value) + return LOCalcRange(cell) + + def _set_new_value(self, cell, search, value): + if not cell.ImplementationName == 'ScCellObj': + return + + if isinstance(value, str): + pattern = re.compile(search, re.IGNORECASE) + new_value = pattern.sub(value, cell.String) + cell.String = new_value + else: + LOCalcRange(cell).value = value + return + + def _render_list(self, key, rows): + for row in rows: + for k, v in row.items(): + self._render_value(k, v) + return + + def find_all(self, search_string): + if self._sd is None: + self._sd = self.sheet.obj.createSearchDescriptor() + self._sd.SearchCaseSensitive = False + + self._sd.setSearchString(search_string) + ranges = self.obj.findAll(self._sd) + return ranges + + def filter(self, args, with_headers=True): + ff = TableFilterField() + ff.Field = args['Field'] + ff.Operator = args['Operator'] + if isinstance(args['Value'], str): + ff.IsNumeric = False + ff.StringValue = args['Value'] + else: + ff.IsNumeric = True + ff.NumericValue = args['Value'] + + fd = self.obj.createFilterDescriptor(True) + fd.ContainsHeader = with_headers + fd.FilterFields = ((ff,)) + # ~ self.obj.AutoFilter = True + self.obj.filter(fd) + return + + def copy_format_from(self, rango): + rango.select() + self.doc.copy() + self.select() + args = { + 'Flags': 'T', + 'MoveMode': 4, + } + url = '.uno:InsertContents' + call_dispatch(self.doc.frame, url, args) + return + + def to_image(self): + self.select() + self.doc.copy() + args = {'SelectedFormat': 141} + url = '.uno:ClipboardFormatItems' + call_dispatch(self.doc.frame, url, args) + return self.sheet.shapes[-1] + + def insert_image(self, path, args={}): + ps = self.possize + args['Width'] = args.get('Width', ps['Width']) + args['Height'] = args.get('Height', ps['Height']) + args['X'] = args.get('X', ps['X']) + args['Y'] = args.get('Y', ps['Y']) + # ~ img.ResizeWithCell = True + img = self.sheet.dp.insert_image(path, args) + img.Anchor = self.obj + args.clear() + return img + + +class LOWriterPageStyle(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + def __str__(self): + return f'Page Style: {self.name}' + + @property + def name(self): + return self._obj.Name + + +class LOWriterPageStyles(object): + + def __init__(self, styles): + self._styles = styles + + def __getitem__(self, index): + return LOWriterPageStyle(self._styles[index]) + + @property + def names(self): + return self._styles.ElementNames + + def __str__(self): + return '\n'.join(self.names) + + +class LOWriterTextRange(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph' + self._is_table = self.obj.ImplementationName == 'SwXTextTable' + + @property + def obj(self): + return self._obj + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def value(self): + return self.string + + @property + def is_table(self): + return self._is_table + + @property + def text(self): + return self.obj.getText() + + @property + def cursor(self): + return self.text.createTextCursorByRange(self.obj) + + def offset(self): + cursor = self.cursor.getEnd() + return LOWriterTextRange(cursor, self._doc) + + def insert_content(self, data, cursor=None, replace=False): + if cursor is None: + cursor = self.cursor + self.text.insertTextContent(cursor, data, replace) + return + + def insert_image(self, path, args={}): + w = args.get('Width', 1000) + h = args.get('Height', 1000) + image = self._doc.create_instance('com.sun.star.text.GraphicObject') + image.GraphicURL = _P.to_url(path) + image.AnchorType = AS_CHARACTER + image.Width = w + image.Height = h + self.insert_content(image) + return + + +class LOWriterTextRanges(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + def __getitem__(self, index): + return LOWriterTextRange(self.obj[index], self._doc) + + @property + def obj(self): + return self._obj + + +class LOWriter(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = WRITER + + @property + def selection(self): + sel = self.obj.CurrentSelection + # ~ print(sel.ImplementationName) + if sel.ImplementationName == 'SwXTextRanges': + sel = LOWriterTextRanges(sel, self) + return sel + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def cursor(self): + return self.obj.Text.createTextCursor() + + @property + def page_styles(self): + ps = self.obj.StyleFamilies['PageStyles'] + return LOWriterPageStyles(ps) + + +class LOShape(LOBaseObject): + + def __init__(self, obj, index): + self._index = index + super().__init__(obj) + + @property + def name(self): + return self.obj.Name or f'shape{self.index}' + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def index(self): + return self._index + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def anchor(self): + return self.obj.Anchor + @anchor.setter + def anchor(self, value): + if hasattr(value, 'obj'): + value = value.obj + self.obj.Anchor = value + + def remove(self): + self.obj.Parent.remove(self.obj) + return + + +class LODrawPage(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + shape = LOShape(self.obj[index], index) + else: + for i, o in enumerate(self.obj): + shape = self.obj[i] + name = shape.Name or f'shape{i}' + if name == index: + shape = LOShape(shape, i) + break + return shape + + @property + def name(self): + return self.obj.Name + + @property + def doc(self): + return self.obj.Forms.Parent + + @property + def width(self): + return self.obj.Width + + @property + def height(self): + return self.obj.Height + + @property + def count(self): + return self.obj.Count + + def create_instance(self, name): + return self.doc.createInstance(name) + + def add(self, type_shape, args={}): + """Insert a shape in page, type shapes: + Line + Rectangle + Ellipse + Text + """ + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + + service = f'com.sun.star.drawing.{type_shape}Shape' + shape = self.create_instance(service) + shape.Size = Size(w, h) + shape.Position = Point(x, y) + index = self.count + shape.Name = f'{type_shape.lower()}{index}' + self.obj.add(shape) + return LOShape(self.obj[index], index) + + def insert_image(self, path, args={}): + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + + image = self.create_instance('com.sun.star.drawing.GraphicObjectShape') + image.GraphicURL = _P.to_url(path) + image.Size = Size(w, h) + image.Position = Point(x, y) + index = self.count + image.Name = f'image{index}' + self.obj.add(image) + return LOShape(self.obj[index], index) + + +class LODrawImpress(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + page = self.obj.DrawPages[index] + else: + page = self.obj.DrawPages.getByName(index) + return LODrawPage(page) + + @property + def selection(self): + sel = self.obj.CurrentSelection[0] + return _get_class_uno(sel) + + @property + def current_page(self): + return LODrawPage(self._cc.getCurrentPage()) + + def paste(self): + call_dispatch(self.frame, '.uno:Paste') + return self.selection + + def add(self, type_shape, args={}): + return self.current_page.add(type_shape, args) + + def insert_image(self, path, args={}): + self.current_page.insert_image(path, args) + return + + # ~ def export(self, path, mimetype='png'): + # ~ args = dict( + # ~ URL = _P.to_url(path), + # ~ MimeType = MIME_TYPE[mimetype], + # ~ ) + # ~ result = _export_image(self.obj, args) + # ~ return result + + +class LODraw(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + self._type = DRAW + + +class LOImpress(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + self._type = IMPRESS + + +class BaseDateField(DateField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class BaseTimeField(TimeField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class BaseDateTimeField(DateTimeField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class FirebirdDatabase(Database): + field_types = {'BOOL': 'BOOLEAN', 'DATETIME': 'TIMESTAMP'} + + def __init__(self, database, **kwargs): + super().__init__(database, **kwargs) + self._db = database + + def _connect(self): + return self._db + + def create_tables(self, models, **options): + options['safe'] = False + tables = self._db.tables + models = [m for m in models if not m.__name__.lower() in tables] + super().create_tables(models, **options) + + def execute_sql(self, sql, params=None, commit=True): + with __exception_wrapper__: + cursor = self._db.execute(sql, params) + return cursor + + def last_insert_id(self, cursor, query_type=None): + # ~ debug('LAST_ID', cursor) + return 0 + + def rows_affected(self, cursor): + return self._db.rows_affected + + @property + def path(self): + return self._db.path + + +class BaseRow: + pass + + +class BaseQuery(object): + PY_TYPES = { + 'SQL_LONG': 'getLong', + 'SQL_VARYING': 'getString', + 'SQL_FLOAT': 'getFloat', + 'SQL_BOOLEAN': 'getBoolean', + 'SQL_TYPE_DATE': 'getDate', + 'SQL_TYPE_TIME': 'getTime', + 'SQL_TIMESTAMP': 'getTimestamp', + } + TYPES_DATE = ('SQL_TYPE_DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP') + + def __init__(self, query): + self._query = query + self._meta = query.MetaData + self._cols = self._meta.ColumnCount + self._names = query.Columns.ElementNames + self._data = self._get_data() + + def __getitem__(self, index): + return self._data[index] + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + row = self._data[self._index] + except IndexError: + raise StopIteration + self._index += 1 + return row + + def _to_python(self, index): + type_field = self._meta.getColumnTypeName(index) + value = getattr(self._query, self.PY_TYPES[type_field])(index) + if type_field in self.TYPES_DATE: + value = _struct_to_date(value) + return value + + def _get_row(self): + row = BaseRow() + for i in range(1, self._cols + 1): + column_name = self._meta.getColumnName(i) + value = self._to_python(i) + setattr(row, column_name, value) + return row + + def _get_data(self): + data = [] + while self._query.next(): + row = self._get_row() + data.append(row) + return data + + @property + def tuples(self): + data = [tuple(r.__dict__.values()) for r in self._data] + return tuple(data) + + @property + def dicts(self): + data = [r.__dict__ for r in self._data] + return tuple(data) + + +class LOBase(object): + DB_TYPES = { + str: 'setString', + int: 'setInt', + float: 'setFloat', + bool: 'setBoolean', + Date: 'setDate', + Time: 'setTime', + DateTime: 'setTimestamp', + } + # ~ setArray + # ~ setBinaryStream + # ~ setBlob + # ~ setByte + # ~ setBytes + # ~ setCharacterStream + # ~ setClob + # ~ setNull + # ~ setObject + # ~ setObjectNull + # ~ setObjectWithInfo + # ~ setPropertyValue + # ~ setRef + + def __init__(self, obj, args={}): + self._obj = obj + self._type = BASE + self._dbc = create_instance('com.sun.star.sdb.DatabaseContext') + self._rows_affected = 0 + path = args.get('path', '') + self._path = _P(path) + self._name = self._path.name + if _P.exists(path): + if not self.is_registered: + self.register() + db = self._dbc.getByName(self.name) + else: + db = self._dbc.createInstance() + db.URL = 'sdbc:embedded:firebird' + db.DatabaseDocument.storeAsURL(self._path.url, ()) + self.register() + self._obj = db + self._con = db.getConnection('', '') + + def __contains__(self, item): + return item in self.tables + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def path(self): + return str(self._path) + + @property + def is_registered(self): + return self._dbc.hasRegisteredDatabase(self.name) + + @property + def tables(self): + tables = [t.Name.lower() for t in self._con.getTables()] + return tables + + @property + def rows_affected(self): + return self._rows_affected + + def register(self): + if not self.is_registered: + self._dbc.registerDatabaseLocation(self.name, self._path.url) + return + + def revoke(self, name): + self._dbc.revokeDatabaseLocation(name) + return True + + def save(self): + self.obj.DatabaseDocument.store() + self.refresh() + return + + def close(self): + self._con.close() + return + + def refresh(self): + self._con.getTables().refresh() + return + + def initialize(self, database_proxy, tables): + db = FirebirdDatabase(self) + database_proxy.initialize(db) + db.create_tables(tables) + return + + def _validate_sql(self, sql, params): + limit = ' LIMIT ' + for p in params: + sql = sql.replace('?', f"'{p}'", 1) + if limit in sql: + sql = sql.split(limit)[0] + sql = sql.replace('SELECT', f'SELECT FIRST {params[-1]}') + return sql + + def cursor(self, sql, params): + if sql.startswith('SELECT'): + sql = self._validate_sql(sql, params) + cursor = self._con.prepareStatement(sql) + return cursor + + if not params: + cursor = self._con.createStatement() + return cursor + + cursor = self._con.prepareStatement(sql) + for i, v in enumerate(params, 1): + t = type(v) + if not t in self.DB_TYPES: + error('Type not support') + debug((i, t, v, self.DB_TYPES[t])) + getattr(cursor, self.DB_TYPES[t])(i, v) + return cursor + + def execute(self, sql, params): + debug(sql, params) + cursor = self.cursor(sql, params) + + if sql.startswith('SELECT'): + result = cursor.executeQuery() + elif params: + result = cursor.executeUpdate() + self._rows_affected = result + self.save() + else: + result = cursor.execute(sql) + self.save() + + return result + + def select(self, sql): + debug('SELECT', sql) + if not sql.startswith('SELECT'): + return () + + cursor = self._con.prepareStatement(sql) + query = cursor.executeQuery() + return BaseQuery(query) + + def get_query(self, query): + sql, args = query.sql() + sql = self._validate_sql(sql, args) + return self.select(sql) + + +class LOMath(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = MATH + + +class LOBasic(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = BASIC + + +class LODocs(object): + _desktop = None + + def __init__(self): + self._desktop = get_desktop() + LODocs._desktop = self._desktop + + def __getitem__(self, index): + doc = None + for i, doc in enumerate(self._desktop.Components): + if isinstance(index, int) and i == index: + doc = _get_class_doc(doc) + break + elif isinstance(index, str) and doc.Title == index: + doc = _get_class_doc(doc) + break + return doc + + def __contains__(self, item): + doc = self[item] + return not doc is None + + def __iter__(self): + self._i = 0 + return self + + def __next__(self): + doc = self[self._i] + if doc is None: + raise StopIteration + self._i += 1 + return doc + + def __len__(self): + for i, _ in enumerate(self._desktop.Components): + pass + return i + 1 + + @property + def active(self): + return _get_class_doc(self._desktop.getCurrentComponent()) + + @classmethod + def new(cls, type_doc=CALC, args={}): + if type_doc == BASE: + return LOBase(None, args) + + path = f'private:factory/s{type_doc}' + opt = dict_to_property(args) + doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) + return _get_class_doc(doc) + + @classmethod + def open(cls, path, args={}): + """ Open document in path + Usually options: + Hidden: True or False + AsTemplate: True or False + ReadOnly: True or False + Password: super_secret + MacroExecutionMode: 4 = Activate macros + Preview: True or False + + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1frame_1_1XComponentLoader.html + http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html + """ + path = _path_url(path) + opt = dict_to_property(args) + doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) + if doc is None: + return + + return _get_class_doc(doc) + + def connect(self, path): + return LOBase(None, {'path': path}) + + +def _add_listeners(events, control, name=''): + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + 'addFocusListener': EventsFocus, + # ~ 'addItemListener': EventsItem, + # ~ 'addKeyListener': EventsKey, + # ~ 'addTabListener': EventsTab, + } + if hasattr(control, 'obj'): + control = control.obj + # ~ debug(control.ImplementationName) + is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' + is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' + is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' + + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue + + getattr(control, key)(listeners[key](events, name)) + + # ~ if is_grid: + # ~ controllers = EventsGrid(events, name) + # ~ control.addSelectionListener(controllers) + # ~ control.Model.GridDataModel.addGridDataListener(controllers) + return + + +def _set_properties(model, properties): + if 'X' in properties: + properties['PositionX'] = properties.pop('X') + if 'Y' in properties: + properties['PositionY'] = properties.pop('Y') + keys = tuple(properties.keys()) + values = tuple(properties.values()) + model.setPropertyValues(keys, values) + return + + +class EventsListenerBase(unohelper.Base, XEventListener): + + def __init__(self, controller, name, window=None): + self._controller = controller + self._name = name + self._window = window + + @property + def name(self): + return self._name + + def disposing(self, event): + self._controller = None + if not self._window is None: + self._window.setMenuBar(None) + + +class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def mousePressed(self, event): + event_name = '{}_click'.format(self._name) + if event.ClickCount == 2: + event_name = '{}_double_click'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def mouseReleased(self, event): + pass + + def mouseEntered(self, event): + pass + + def mouseExited(self, event): + pass + + # ~ XMouseMotionListener + def mouseMoved(self, event): + pass + + def mouseDragged(self, event): + pass + + +class EventsMouseLink(EventsMouse): + + def __init__(self, controller, name): + super().__init__(controller, name) + self._text_color = 0 + + def mouseEntered(self, event): + model = event.Source.Model + self._text_color = model.TextColor or 0 + model.TextColor = get_color('blue') + return + + def mouseExited(self, event): + model = event.Source.Model + model.TextColor = self._text_color + return + + +class EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def actionPerformed(self, event): + event_name = f'{self.name}_action' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + CONTROLS = ( + 'stardiv.Toolkit.UnoControlEditModel', + ) + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + service = event.Source.Model.ImplementationName + # ~ print('Focus enter', service) + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + return + + def focusLost(self, event): + service = event.Source.Model.ImplementationName + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = -1 + return + + +# ~ BorderColor = ? +# ~ FontStyleName = ? +# ~ HelpURL = ? +class UnoBaseObject(object): + + def __init__(self, obj): + self._obj = obj + self._model = obj.Model + + def __setattr__(self, name, value): + exists = hasattr(self, name) + if not exists and not name in ('_obj', '_model'): + setattr(self._model, name, value) + else: + super().__setattr__(name, value) + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + @property + def m(self): + return self._model + + @property + def properties(self): + return {} + @properties.setter + def properties(self, values): + _set_properties(self.model, values) + + @property + def name(self): + return self.model.Name + + @property + def parent(self): + return self.obj.Context + + @property + def tag(self): + return self.model.Tag + @tag.setter + def tag(self, value): + self.model.Tag = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.setVisible(value) + + @property + def enabled(self): + return self.model.Enabled + @enabled.setter + def enabled(self, value): + self.model.Enabled = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def align(self): + return self.model.Align + @align.setter + def align(self, value): + self.model.Align = value + + @property + def valign(self): + return self.model.VerticalAlign + @valign.setter + def valign(self, value): + self.model.VerticalAlign = value + + @property + def font_weight(self): + return self.model.FontWeight + @font_weight.setter + def font_weight(self, value): + self.model.FontWeight = value + + @property + def font_height(self): + return self.model.FontHeight + @font_height.setter + def font_height(self, value): + self.model.FontHeight = value + + @property + def font_name(self): + return self.model.FontName + @font_name.setter + def font_name(self, value): + self.model.FontName = value + + @property + def font_underline(self): + return self.model.FontUnderline + @font_underline.setter + def font_underline(self, value): + self.model.FontUnderline = value + + @property + def text_color(self): + return self.model.TextColor + @text_color.setter + def text_color(self, value): + self.model.TextColor = value + + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + + @property + def multi_line(self): + return self.model.MultiLine + @multi_line.setter + def multi_line(self, value): + self.model.MultiLine = value + + @property + def help_text(self): + return self.model.HelpText + @help_text.setter + def help_text(self, value): + self.model.HelpText = value + + @property + def border(self): + return self.model.Border + @border.setter + def border(self, value): + # ~ Bug for report + self.model.Border = value + + @property + def width(self): + return self._model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + def _get_possize(self, name): + ps = self.obj.getPosSize() + return getattr(ps, name) + + def _set_possize(self, name, value): + ps = self.obj.getPosSize() + setattr(ps, name, value) + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + return + + @property + def x(self): + if hasattr(self.model, 'PositionX'): + return self.model.PositionX + return self._get_possize('X') + @x.setter + def x(self, value): + if hasattr(self.model, 'PositionX'): + self.model.PositionX = value + else: + self._set_possize('X', value) + + @property + def y(self): + if hasattr(self.model, 'PositionY'): + return self.model.PositionY + return self._get_possize('Y') + @y.setter + def y(self, value): + if hasattr(self.model, 'PositionY'): + self.model.PositionY = value + else: + self._set_possize('Y', value) + + @property + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value + + @property + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value + + def center(self, horizontal=True, vertical=False): + p = self.parent.Model + w = p.Width + h = p.Height + if horizontal: + x = w / 2 - self.width / 2 + self.x = x + if vertical: + y = h / 2 - self.height / 2 + self.y = y + return + + def move(self, origin, x=0, y=5, center=False): + if x: + self.x = origin.x + origin.width + x + else: + self.x = origin.x + if y: + self.y = origin.y + origin.height + y + else: + self.y = origin.y + + if center: + self.center() + return + + +class UnoLabel(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'label' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoLabelLink(UnoLabel): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'link' + + +class UnoButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoRadio(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'radio' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoCheck(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'check' + + @property + def value(self): + return self.model.State + @value.setter + def value(self, value): + self.model.State = value + + @property + def label(self): + return self.model.Label + @label.setter + def label(self, value): + self.model.Label = value + + @property + def tri_state(self): + return self.model.TriState + @tri_state.setter + def tri_state(self, value): + self.model.TriState = value + + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html +class UnoText(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self.model.Text + @value.setter + def value(self, value): + self.model.Text = value + + +UNO_CLASSES = { + 'label': UnoLabel, + 'link': UnoLabelLink, + 'button': UnoButton, + 'radio': UnoRadio, + 'check': UnoCheck, + 'text': UnoText, +} + + +class LODialog(object): + MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'check': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + # ~ 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + # ~ 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + # ~ 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + # ~ 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + # ~ 'tree': 'com.sun.star.awt.tree.TreeControlModel', + # ~ 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + # ~ 'image': 'com.sun.star.awt.UnoControlImageControlModel', + # ~ 'pages': 'com.sun.star.awt.UnoMultiPageModel', + } + + def __init__(self, args): + self._obj = self._create(args) + self._model = self.obj.Model + self._events = None + self._modal = True + self._controls = {} + self._color_on_focus = COLOR_ON_FOCUS + + def _create(self, args): + service = 'com.sun.star.awt.DialogProvider' + path = args.pop('Path', '') + if path: + dp = create_instance(service, True) + dlg = dp.createDialog(_path_url(path)) + return dlg + + if 'Location' in args: + name = args['Name'] + library = args.get('Library', 'Standard') + location = args.get('Location', 'application') + if location == 'user': + location = 'application' + url = f'vnd.sun.star.script:{library}.{name}?location={location}' + if location == 'document': + dp = create_instance(service, args=docs.active.obj) + else: + dp = create_instance(service, True) + # ~ uid = docs.active.uid + # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' + dlg = dp.createDialog(url) + return dlg + + dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) + model = create_instance('com.sun.star.awt.UnoControlDialogModel', True) + toolkit = create_instance('com.sun.star.awt.Toolkit', True) + _set_properties(model, args) + dlg.setModel(model) + dlg.setVisible(False) + dlg.createPeer(toolkit, None) + return dlg + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + + @property + def controls(self): + return self._controls + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers(self) + self._connect_listeners() + + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + self._color_on_focus = get_color(value) + + def _connect_listeners(self): + for control in self.obj.Controls: + _add_listeners(self.events, control, control.Model.Name) + return + + def _special_properties(self, tipo, args): + columns = args.pop('Columns', ()) + + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + elif tipo == 'grid': + args['ColumnModel'] = self._set_column_model(columns) + elif tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + if not 'FocusOnClick' in args: + args['FocusOnClick'] = False + elif tipo == 'roadmap': + if not 'Height' in args: + args['Height'] = self.height + if 'Title' in args: + args['Text'] = args.pop('Title') + elif tipo == 'tab': + if not 'Width' in args: + args['Width'] = self.width + if not 'Height' in args: + args['Height'] = self.height + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + + args = self._special_properties(tipo, args) + model = self.model.createInstance(self.MODELS[tipo]) + _set_properties(model, args) + name = args['Name'] + self.model.insertByName(name, model) + control = self.obj.getControl(name) + _add_listeners(self.events, control, name) + control = UNO_CLASSES[tipo](control) + + if tipo == 'tree' and root: + control.root = root + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + self._controls[name] = control + return control + + def open(self, modal=True): + self._modal = modal + if modal: + return self.obj.execute() + else: + self.visible = True + return + + def close(self, value=0): + if self._modal: + value = self.obj.endDialog(value) + else: + self.visible = False + self.obj.dispose() + return value + + +class LOSheets(object): + + def __getitem__(self, index): + return LODocs().active[index] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +class LOCells(object): + + def __getitem__(self, index): + return LODocs().active.active[index] + + +class LOShortCut(object): +# ~ getKeyEventsByCommand + + def __init__(self, app): + self._app = app + self._scm = None + self._init_values() + + def _init_values(self): + name = 'com.sun.star.ui.GlobalAcceleratorConfiguration' + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + uicm = manager.getUIConfigurationManager(service) + self._scm = uicm.ShortCutManager + return + + def __contains__(self, item): + cmd = self._get_command(item) + return bool(cmd) + + def _get_key_event(self, command): + events = self._scm.AllKeyEvents + for event in events: + cmd = self._scm.getCommandByKeyEvent(event) + if cmd == command: + break + return event + + def _to_key_event(self, shortcut): + key_event = KeyEvent() + keys = shortcut.split('+') + for v in keys[:-1]: + key_event.Modifiers += MODIFIERS[v.lower()] + key_event.KeyCode = getattr(Key, keys[-1].upper()) + return key_event + + def _get_command(self, shortcut): + command = '' + key_event = self._to_key_event(shortcut) + try: + command = self._scm.getCommandByKeyEvent(key_event) + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return command + + def add(self, shortcut, command): + if isinstance(command, dict): + command = _get_url_script(command) + key_event = self._to_key_event(shortcut) + self._scm.setKeyEvent(key_event, command) + self._scm.store() + return + + def reset(self): + self._scm.reset() + self._scm.store() + return + + def remove(self, shortcut): + key_event = self._to_key_event(shortcut) + try: + self._scm.removeKeyEvent(key_event) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return + + def remove_by_command(self, command): + if isinstance(command, dict): + command = _get_url_script(command) + try: + self._scm.removeCommandFromAllKeyEvents(command) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {command}') + return + + +class LOShortCuts(object): + + def __getitem__(self, index): + return LOShortCut(index) + + +class LOMenu(object): + + def __init__(self, app): + self._app = app + self._ui = None + self._pymenus = None + self._menu = None + self._menus = self._get_menus() + + def __getitem__(self, index): + if isinstance(index, int): + self._menu = self._menus[index] + else: + for menu in self._menus: + cmd = menu.get('CommandURL', '') + if MENUS[index.lower()] == cmd: + self._menu = menu + break + line = self._menu.get('CommandURL', '') + line += self._get_submenus(self._menu['ItemDescriptorContainer']) + return line + + def _get_menus(self): + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + self._ui = manager.getUIConfigurationManager(service) + self._pymenus = self._ui.getSettings(NODE_MENUBAR, True) + data = [] + for menu in self._pymenus: + data.append(data_to_dict(menu)) + return data + + def _get_info(self, menu): + line = menu.get('CommandURL', '') + line += self._get_submenus(menu['ItemDescriptorContainer']) + return line + + def _get_submenus(self, menu, level=1): + line = '' + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '----------') + line += f'\n{" " * level}├─ ({i}) {cmd}' + submenu = data.get('ItemDescriptorContainer', None) + if not submenu is None: + line += self._get_submenus(submenu, level + 1) + return line + + def __str__(self): + info = '\n'.join([self._get_info(m) for m in self._menus]) + return info + + def _get_index_menu(self, menu, command): + index = -1 + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '') + if cmd == command: + index = i + break + return index + + def insert(self, name, args): + idc = None + replace = False + command = args['CommandURL'] + label = args['Label'] + + self[name] + menu = self._menu['ItemDescriptorContainer'] + submenu = args.get('Submenu', False) + if submenu: + idc = self._ui.createSettings() + + index = self._get_index_menu(menu, command) + if index == -1: + if 'Index' in args: + index = args['Index'] + else: + index = self._get_index_menu(menu, args['After']) + 1 + else: + replace = True + + data = dict ( + CommandURL = command, + Label = label, + Style = 0, + Type = 0, + ItemDescriptorContainer = idc, + ) + self._save(menu, data, index, replace) + self._insert_submenu(idc, submenu) + return + + def _get_command(self, args): + shortcut = args.get('ShortCut', '') + cmd = args['CommandURL'] + if isinstance(cmd, dict): + cmd = _get_url_script(cmd) + if shortcut: + LOShortCut(self._app).add(shortcut, cmd) + return cmd + + def _insert_submenu(self, parent, menus): + for i, v in enumerate(menus): + submenu = v.pop('Submenu', False) + if submenu: + idc = self._ui.createSettings() + v['ItemDescriptorContainer'] = idc + v['Type'] = 0 + if v['Label'] == '-': + v['Type'] = 1 + else: + v['CommandURL'] = self._get_command(v) + self._save(parent, v, i) + if submenu: + self._insert_submenu(idc, submenu) + return + + def remove(self, name, command): + self[name] + menu = self._menu['ItemDescriptorContainer'] + index = self._get_index_menu(menu, command) + if index > -1: + uno.invoke(menu, 'removeByIndex', (index,)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + def _save(self, menu, properties, index, replace=False): + properties = dict_to_property(properties, True) + if replace: + uno.invoke(menu, 'replaceByIndex', (index, properties)) + else: + uno.invoke(menu, 'insertByIndex', (index, properties)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + +class LOMenus(object): + + def __getitem__(self, index): + return LOMenu(index) + + +class classproperty: + def __init__(self, method=None): + self.fget = method + + def __get__(self, instance, cls=None): + return self.fget(cls) + + def getter(self, method): + self.fget = method + return self + + +class Paths(object): + + def __init__(self, path=''): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + self._path = Path(path) + + @property + def path(self): + return str(self._path.parent) + + @property + def file_name(self): + return self._path.name + + @property + def name(self): + return self._path.stem + + @property + def ext(self): + return self._path.suffix + + @property + def info(self): + return self.path, self.file_name, self.name, self.ext + + @property + def url(self): + return self._path.as_uri() + + @classproperty + def home(self): + return str(Path.home()) + + @classmethod + def replace_ext(cls, path, new_ext): + p = Paths(path) + name = f'{p.name}.{new_ext}' + path = cls.join(p.path, name) + return path + + @classmethod + def exists(cls, path): + result = False + if path: + path = cls.to_system(path) + result = Path(path).exists() + return result + + @classmethod + def exists_app(cls, name_app): + return bool(shutil.which(name_app)) + + @classmethod + def open(cls, path): + if IS_WIN: + os.startfile(path) + else: + pid = subprocess.Popen(['xdg-open', path]).pid + return + + @classmethod + def is_dir(cls, path): + return Path(path).is_dir() + + @classmethod + def join(cls, *paths): + return str(Path(paths[0]).joinpath(*paths[1:])) + + @classmethod + def save(cls, path, data, encoding='utf-8'): + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path, data): + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def to_url(cls, path): + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def to_system(cls, path): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def walk(cls, path, filters=''): + paths = [] + if filters in ('*', '*.*'): + filters = '' + for folder, _, files in os.walk(path): + if filters: + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [cls.join(folder, f) for f in files if pattern.search(f)] + else: + paths += files + return paths + + @classmethod + def copy(cls, source, target='', name=''): + p, f, n, e = _P(source).info + if target: + p = target + if name: + e = '' + n = name + path_new = cls.join(p, f'{n}{e}') + shutil.copy(source, path_new) + return path_new +_P = Paths + + +def __getattr__(name): + if name == 'active': + return LODocs().active + if name == 'active_sheet': + return LODocs().active.active + if name == 'selection': + return LODocs().active.selection + if name in ('rectangle', 'pos_size'): + return Rectangle() + if name == 'paths': + return Paths + if name == 'docs': + return LODocs() + if name == 'sheets': + return LOSheets() + if name == 'cells': + return LOCells() + if name == 'menus': + return LOMenus() + if name == 'shortcuts': + return LOShortCuts() + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +def create_dialog(args): + return LODialog(args) + + +def inputbox(message, default='', title=TITLE, echochar=''): + + class ControllersInput(object): + + def __init__(self, dlg): + self.d = dlg + + def cmd_ok_action(self, event): + self.d.close(1) + return + + args = { + 'Title': title, + 'Width': 200, + 'Height': 80, + } + dlg = LODialog(args) + dlg.events = ControllersInput + + args = { + 'Type': 'Label', + 'Name': 'lbl_msg', + 'Label': message, + 'Width': 140, + 'Height': 50, + 'X': 5, + 'Y': 5, + 'MultiLine': True, + 'Border': 1, + } + dlg.add_control(args) + + args = { + 'Type': 'Text', + 'Name': 'txt_value', + 'Text': default, + 'Width': 190, + 'Height': 15, + } + if echochar: + args['EchoChar'] = ord(echochar[0]) + dlg.add_control(args) + dlg.txt_value.move(dlg.lbl_msg) + + args = { + 'Type': 'button', + 'Name': 'cmd_ok', + 'Label': _('OK'), + 'Width': 40, + 'Height': 15, + 'DefaultButton': True, + 'PushButtonType': 1, + } + dlg.add_control(args) + dlg.cmd_ok.move(dlg.lbl_msg, 10, 0) + + args = { + 'Type': 'button', + 'Name': 'cmd_cancel', + 'Label': _('Cancel'), + 'Width': 40, + 'Height': 15, + 'PushButtonType': 2, + } + dlg.add_control(args) + dlg.cmd_cancel.move(dlg.cmd_ok) + + if dlg.open(): + return dlg.txt_value.value + + return '' + + +def get_fonts(): + toolkit = create_instance('com.sun.star.awt.Toolkit') + device = toolkit.createScreenCompatibleDevice(0, 0) + return device.FontDescriptors + + +# ~ From request +# ~ https://github.com/psf/requests/blob/master/requests/structures.py#L15 +class CaseInsensitiveDict(MutableMapping): + + def __init__(self, data=None, **kwargs): + self._store = OrderedDict() + if data is None: + data = {} + self.update(data, **kwargs) + + def __setitem__(self, key, value): + # Use the lowercased key for lookups, but store the actual + # key alongside the value. + self._store[key.lower()] = (key, value) + + def __getitem__(self, key): + return self._store[key.lower()][1] + + def __delitem__(self, key): + del self._store[key.lower()] + + def __iter__(self): + return (casedkey for casedkey, mappedvalue in self._store.values()) + + def __len__(self): + return len(self._store) + + def lower_items(self): + """Like iteritems(), but with all lowercase keys.""" + values = ( + (lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items() + ) + return values + + # Copy is required + def copy(self): + return CaseInsensitiveDict(self._store.values()) + + def __repr__(self): + return str(dict(self.items())) + + +# ~ https://en.wikipedia.org/wiki/Web_colors +def get_color(value): + COLORS = { + 'aliceblue': 15792383, + 'antiquewhite': 16444375, + 'aqua': 65535, + 'aquamarine': 8388564, + 'azure': 15794175, + 'beige': 16119260, + 'bisque': 16770244, + 'black': 0, + 'blanchedalmond': 16772045, + 'blue': 255, + 'blueviolet': 9055202, + 'brown': 10824234, + 'burlywood': 14596231, + 'cadetblue': 6266528, + 'chartreuse': 8388352, + 'chocolate': 13789470, + 'coral': 16744272, + 'cornflowerblue': 6591981, + 'cornsilk': 16775388, + 'crimson': 14423100, + 'cyan': 65535, + 'darkblue': 139, + 'darkcyan': 35723, + 'darkgoldenrod': 12092939, + 'darkgray': 11119017, + 'darkgreen': 25600, + 'darkgrey': 11119017, + 'darkkhaki': 12433259, + 'darkmagenta': 9109643, + 'darkolivegreen': 5597999, + 'darkorange': 16747520, + 'darkorchid': 10040012, + 'darkred': 9109504, + 'darksalmon': 15308410, + 'darkseagreen': 9419919, + 'darkslateblue': 4734347, + 'darkslategray': 3100495, + 'darkslategrey': 3100495, + 'darkturquoise': 52945, + 'darkviolet': 9699539, + 'deeppink': 16716947, + 'deepskyblue': 49151, + 'dimgray': 6908265, + 'dimgrey': 6908265, + 'dodgerblue': 2003199, + 'firebrick': 11674146, + 'floralwhite': 16775920, + 'forestgreen': 2263842, + 'fuchsia': 16711935, + 'gainsboro': 14474460, + 'ghostwhite': 16316671, + 'gold': 16766720, + 'goldenrod': 14329120, + 'gray': 8421504, + 'grey': 8421504, + 'green': 32768, + 'greenyellow': 11403055, + 'honeydew': 15794160, + 'hotpink': 16738740, + 'indianred': 13458524, + 'indigo': 4915330, + 'ivory': 16777200, + 'khaki': 15787660, + 'lavender': 15132410, + 'lavenderblush': 16773365, + 'lawngreen': 8190976, + 'lemonchiffon': 16775885, + 'lightblue': 11393254, + 'lightcoral': 15761536, + 'lightcyan': 14745599, + 'lightgoldenrodyellow': 16448210, + 'lightgray': 13882323, + 'lightgreen': 9498256, + 'lightgrey': 13882323, + 'lightpink': 16758465, + 'lightsalmon': 16752762, + 'lightseagreen': 2142890, + 'lightskyblue': 8900346, + 'lightslategray': 7833753, + 'lightslategrey': 7833753, + 'lightsteelblue': 11584734, + 'lightyellow': 16777184, + 'lime': 65280, + 'limegreen': 3329330, + 'linen': 16445670, + 'magenta': 16711935, + 'maroon': 8388608, + 'mediumaquamarine': 6737322, + 'mediumblue': 205, + 'mediumorchid': 12211667, + 'mediumpurple': 9662683, + 'mediumseagreen': 3978097, + 'mediumslateblue': 8087790, + 'mediumspringgreen': 64154, + 'mediumturquoise': 4772300, + 'mediumvioletred': 13047173, + 'midnightblue': 1644912, + 'mintcream': 16121850, + 'mistyrose': 16770273, + 'moccasin': 16770229, + 'navajowhite': 16768685, + 'navy': 128, + 'oldlace': 16643558, + 'olive': 8421376, + 'olivedrab': 7048739, + 'orange': 16753920, + 'orangered': 16729344, + 'orchid': 14315734, + 'palegoldenrod': 15657130, + 'palegreen': 10025880, + 'paleturquoise': 11529966, + 'palevioletred': 14381203, + 'papayawhip': 16773077, + 'peachpuff': 16767673, + 'peru': 13468991, + 'pink': 16761035, + 'plum': 14524637, + 'powderblue': 11591910, + 'purple': 8388736, + 'red': 16711680, + 'rosybrown': 12357519, + 'royalblue': 4286945, + 'saddlebrown': 9127187, + 'salmon': 16416882, + 'sandybrown': 16032864, + 'seagreen': 3050327, + 'seashell': 16774638, + 'sienna': 10506797, + 'silver': 12632256, + 'skyblue': 8900331, + 'slateblue': 6970061, + 'slategray': 7372944, + 'slategrey': 7372944, + 'snow': 16775930, + 'springgreen': 65407, + 'steelblue': 4620980, + 'tan': 13808780, + 'teal': 32896, + 'thistle': 14204888, + 'tomato': 16737095, + 'turquoise': 4251856, + 'violet': 15631086, + 'wheat': 16113331, + 'white': 16777215, + 'whitesmoke': 16119285, + 'yellow': 16776960, + 'yellowgreen': 10145074, + } + + if isinstance(value, tuple): + color = (value[0] << 16) + (value[1] << 8) + value[2] + else: + if value[0] == '#': + r, g, b = bytes.fromhex(value[1:]) + color = (r << 16) + (g << 8) + b + else: + color = COLORS.get(value.lower(), -1) + return color + + +COLOR_ON_FOCUS = get_color('LightYellow') + + +class LOServer(object): + HOST = 'localhost' + PORT = '8100' + ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext' + CMD = ['soffice', + '-env:SingleAppInstance=false', + '-env:UserInstallation=file:///tmp/LO_Process8100', + '--headless', '--norestore', '--invisible', + f'--accept={ARG}'] + + def __init__(self): + self._server = None + self._ctx = None + self._sm = None + self._start_server() + self._init_values() + + def _init_values(self): + global CTX + global SM + + if not self.is_running: + return + + ctx = uno.getComponentContext() + service = 'com.sun.star.bridge.UnoUrlResolver' + resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx) + self._ctx = resolver.resolve('uno:{}'.format(self.ARG)) + self._sm = self._ctx.getServiceManager() + CTX = self._ctx + SM = self._sm + return + + @property + def is_running(self): + try: + s = socket.create_connection((self.HOST, self.PORT), 5.0) + s.close() + debug('LibreOffice is running...') + return True + except ConnectionRefusedError: + return False + + def _start_server(self): + if self.is_running: + return + + for i in range(3): + self._server = subprocess.Popen(self.CMD, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + time.sleep(3) + if self.is_running: + break + return + + def stop(self): + if self._server is None: + print('Search pgrep soffice') + else: + self._server.terminate() + debug('LibreOffice is stop...') + return + + def create_instance(self, name, with_context=True): + if with_context: + instance = self._sm.createInstanceWithContext(name, self._ctx) + else: + instance = self._sm.createInstance(name) + return instance + diff --git a/source/registration/license_en.txt b/source/registration/license_en.txt new file mode 100644 index 0000000..04e32b3 --- /dev/null +++ b/source/registration/license_en.txt @@ -0,0 +1,14 @@ +This file is part of ZAZLaTex2SVG. + + ZAZLaTex2SVG 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. + + ZAZLaTex2SVG 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 ZAZLaTex2SVG. If not, see . diff --git a/source/registration/license_es.txt b/source/registration/license_es.txt new file mode 100644 index 0000000..04e32b3 --- /dev/null +++ b/source/registration/license_es.txt @@ -0,0 +1,14 @@ +This file is part of ZAZLaTex2SVG. + + ZAZLaTex2SVG 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. + + ZAZLaTex2SVG 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 ZAZLaTex2SVG. If not, see . diff --git a/zaz.py b/zaz.py new file mode 100755 index 0000000..0953e17 --- /dev/null +++ b/zaz.py @@ -0,0 +1,785 @@ +#!/usr/bin/env python3 + +# == Rapid Develop Macros in LibreOffice == + +# ~ 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 py_compile +import re +import sys +import zipfile +from datetime import datetime +from pathlib import Path +from shutil import copyfile +from subprocess import call +from xml.etree import ElementTree as ET +from xml.dom.minidom import parseString + + +from conf import ( + DATA, + DIRS, + DOMAIN, + EXTENSION, + FILES, + INFO, + PATHS, + TYPE_EXTENSION, + USE_LOCALES, + log) + + +class LiboXML(object): + CONTEXT = { + 'calc': 'com.sun.star.sheet.SpreadsheetDocument', + 'writer': 'com.sun.star.text.TextDocument', + 'impress': 'com.sun.star.presentation.PresentationDocument', + 'draw': 'com.sun.star.drawing.DrawingDocument', + 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + 'math': 'com.sun.star.formula.FormulaProperties', + 'basic': 'com.sun.star.script.BasicIDE', + } + TYPES = { + 'py': 'application/vnd.sun.star.uno-component;type=Python', + 'pyc': 'application/binary', + 'zip': 'application/binary', + 'xcu': 'application/vnd.sun.star.configuration-data', + 'rdb': 'application/vnd.sun.star.uno-typelibrary;type=RDB', + 'xcs': 'application/vnd.sun.star.configuration-schema', + 'help': 'application/vnd.sun.star.help', + 'component': 'application/vnd.sun.star.uno-components', + } + NS_MANIFEST = { + 'manifest_version': '1.2', + 'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0', + 'xmlns:loext': 'urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0', + } + NS_DESCRIPTION = { + 'xmlns': 'http://openoffice.org/extensions/description/2006', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'xmlns:d': 'http://openoffice.org/extensions/description/2006', + } + NS_ADDONS = { + 'xmlns:xs': 'http://www.w3.org/2001/XMLSchema', + 'xmlns:oor': 'http://openoffice.org/2001/registry', + } + NS_UPDATE = { + 'xmlns': 'http://openoffice.org/extensions/update/2006', + 'xmlns:d': 'http://openoffice.org/extensions/description/2006', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + } + + def __init__(self): + self._manifest = None + self._paths = [] + self._path_images = '' + self._toolbars = [] + + def _save_path(self, attr): + self._paths.append(attr['{{{}}}full-path'.format(self.NS_MANIFEST['manifest'])]) + return + + def _clean(self, name, nodes): + has_words = re.compile('\\w') + + if not re.search(has_words, str(nodes.tail)): + nodes.tail = '' + if not re.search(has_words, str(nodes.text)): + nodes.text = '' + + for node in nodes: + if name == 'manifest': + self._save_path(node.attrib) + if not re.search(has_words, str(node.tail)): + node.tail = '' + if not re.search(has_words, str(node.text)): + node.text = '' + return + + def new_manifest(self, data): + attr = { + 'manifest:version': self.NS_MANIFEST['manifest_version'], + 'xmlns:manifest': self.NS_MANIFEST['manifest'], + 'xmlns:loext': self.NS_MANIFEST['xmlns:loext'], + } + self._manifest = ET.Element('manifest:manifest', attr) + return self.add_data_manifest(data) + + def parse_manifest(self, data): + ET.register_namespace('manifest', self.NS_MANIFEST['manifest']) + self._manifest = ET.fromstring(data) + attr = {'xmlns:loext': self.NS_MANIFEST['xmlns:loext']} + self._manifest.attrib.update(**attr) + self._clean('manifest', self._manifest) + return + + def add_data_manifest(self, data): + node_name = 'manifest:file-entry' + attr = { + 'manifest:full-path': '', + 'manifest:media-type': '', + } + for path in data: + if path in self._paths: + continue + ext = path.split('.')[-1] + attr['manifest:full-path'] = path + attr['manifest:media-type'] = self.TYPES.get(ext, '') + ET.SubElement(self._manifest, node_name, attr) + return self._get_xml(self._manifest) + + def new_description(self, data): + doc = ET.Element('description', self.NS_DESCRIPTION) + + key = 'identifier' + ET.SubElement(doc, key, data[key]) + + key = 'version' + ET.SubElement(doc, key, data[key]) + + key = 'display-name' + node = ET.SubElement(doc, key) + for k, v in data[key].items(): + sn = ET.SubElement(node, 'name', {'lang': k}) + sn.text = v + + node = ET.SubElement(doc, 'extension-description') + for k in data[key].keys(): + attr = { + 'lang': k, + 'xlink:href': f'description/desc_{k}.txt', + } + ET.SubElement(node, 'src', attr) + + key = 'icon' + node = ET.SubElement(doc, key) + attr = {'xlink:href': f"images/{data[key]}"} + ET.SubElement(node, 'default', attr) + + key = 'publisher' + node = ET.SubElement(doc, key) + for k, v in data[key].items(): + attr = { + 'xlink:href': v['link'], + 'lang': k, + } + sn = ET.SubElement(node, 'name', attr) + sn.text = v['text'] + + key = 'display-name' + node = ET.SubElement(doc, 'registration') + attr = { + 'accept-by': 'user', + 'suppress-on-update': 'true', + } + node = ET.SubElement(node, 'simple-license', attr) + for k in data[key].keys(): + attr = { + 'xlink:href': f"{DIRS['registration']}/license_{k}.txt", + 'lang': k + } + ET.SubElement(node, 'license-text', attr) + + if data['update']: + node = ET.SubElement(doc, 'update-information') + ET.SubElement(node, 'src', {'xlink:href': data['update']}) + + return self._get_xml(doc) + + def _get_context(self, args): + if not args: + return '' + context = ','.join([self.CONTEXT[v] for v in args.split(',')]) + return context + + def _add_node_value(self, node, name, value='_self'): + attr = {'oor:name': name, 'oor:type': 'xs:string'} + sn = ET.SubElement(node, 'prop', attr) + sn = ET.SubElement(sn, 'value') + sn.text = value + return + + def _add_menu(self, id_extension, node, index, menu, in_menu_bar=True): + if in_menu_bar: + attr = { + 'oor:name': index, + 'oor:op': 'replace', + } + subnode = ET.SubElement(node, 'node', attr) + else: + subnode = node + + attr = {'oor:name': 'Title', 'oor:type': 'xs:string'} + sn1 = ET.SubElement(subnode, 'prop', attr) + for k, v in menu['title'].items(): + sn2 = ET.SubElement(sn1, 'value', {'xml:lang': k}) + sn2.text = v + value = self._get_context(menu['context']) + self._add_node_value(subnode, 'Context', value) + + if 'submenu' in menu: + sn = ET.SubElement(subnode, 'node', {'oor:name': 'Submenu'}) + for i, m in enumerate(menu['submenu']): + self._add_menu(id_extension, sn, f'{index}.s{i}', m) + if m.get('toolbar', False): + self._toolbars.append(m) + return + + value = f"service:{id_extension}?{menu['argument']}" + self._add_node_value(subnode, 'URL', value) + self._add_node_value(subnode, 'Target') + value = f"%origin%/{self._path_images}/{menu['icon']}" + self._add_node_value(subnode, 'ImageIdentifier', value) + return + + def new_addons(self, id_extension, data): + in_menu_bar = data['parent'] == 'OfficeMenuBar' + self._path_images = data['images'] + attr = { + 'oor:name': 'Addons', + 'oor:package': 'org.openoffice.Office', + } + attr.update(self.NS_ADDONS) + doc = ET.Element('oor:component-data', attr) + parent = ET.SubElement(doc, 'node', {'oor:name': 'AddonUI'}) + node = ET.SubElement(parent, 'node', {'oor:name': data['parent']}) + + op = 'fuse' + if in_menu_bar: + op = 'replace' + + attr = {'oor:name': id_extension, 'oor:op': op} + node = ET.SubElement(node, 'node', attr) + + if in_menu_bar: + attr = {'oor:name': 'Title', 'oor:type': 'xs:string'} + subnode = ET.SubElement(node, 'prop', attr) + for k, v in data['main'].items(): + sn = ET.SubElement(subnode, 'value', {'xml:lang': k}) + sn.text = v + + self._add_node_value(node, 'Target') + node = ET.SubElement(node, 'node', {'oor:name': 'Submenu'}) + + for i, menu in enumerate(data['menus']): + self._add_menu(id_extension, node, f'm{i}', menu, in_menu_bar) + if menu.get('toolbar', False): + self._toolbars.append(menu) + + if self._toolbars: + attr = {'oor:name': 'OfficeToolBar'} + toolbar = ET.SubElement(parent, 'node', attr) + attr = {'oor:name': id_extension, 'oor:op': 'replace'} + toolbar = ET.SubElement(toolbar, 'node', attr) + for t, menu in enumerate(self._toolbars): + self._add_menu(id_extension, toolbar, f't{t}', menu) + + return self._get_xml(doc) + + def _add_shortcut(self, node, key, id_extension, arg): + attr = {'oor:name': key, 'oor:op': 'fuse'} + subnode = ET.SubElement(node, 'node', attr) + subnode = ET.SubElement(subnode, 'prop', {'oor:name': 'Command'}) + subnode = ET.SubElement(subnode, 'value', {'xml:lang': 'en-US'}) + subnode.text = f"service:{id_extension}?{arg}" + return + + def _get_acceleartors(self, menu): + if 'submenu' in menu: + for m in menu['submenu']: + return self._get_acceleartors(m) + + if not menu.get('shortcut', ''): + return '' + + return menu + + def new_accelerators(self, id_extension, menus): + attr = { + 'oor:name': 'Accelerators', + 'oor:package': 'org.openoffice.Office', + } + attr.update(self.NS_ADDONS) + doc = ET.Element('oor:component-data', attr) + parent = ET.SubElement(doc, 'node', {'oor:name': 'PrimaryKeys'}) + + data = [] + for m in menus: + info = self._get_acceleartors(m) + if info: + data.append(info) + + node_global = None + node_modules = None + for m in data: + if m['context']: + if node_modules is None: + node_modules = ET.SubElement( + parent, 'node', {'oor:name': 'Modules'}) + for app in m['context'].split(','): + node = ET.SubElement( + node_modules, 'node', {'oor:name': self.CONTEXT[app]}) + self._add_shortcut( + node, m['shortcut'], id_extension, m['argument']) + else: + if node_global is None: + node_global = ET.SubElement( + parent, 'node', {'oor:name': 'Global'}) + self._add_shortcut( + node_global, m['shortcut'], id_extension, m['argument']) + + return self._get_xml(doc) + + def new_update(self, extension, url_oxt): + doc = ET.Element('description', self.NS_UPDATE) + ET.SubElement(doc, 'identifier', {'value': extension['id']}) + ET.SubElement(doc, 'version', {'value': extension['version']}) + node = ET.SubElement(doc, 'update-download') + ET.SubElement(node, 'src', {'xlink:href': url_oxt}) + node = ET.SubElement(doc, 'release-notes') + return self._get_xml(doc) + + def _get_xml(self, doc): + xml = parseString(ET.tostring(doc, encoding='utf-8')) + return xml.toprettyxml(indent=' ', encoding='utf-8').decode('utf-8') + + +def _exists(path): + return os.path.exists(path) + + +def _join(*paths): + return os.path.join(*paths) + + +def _mkdir(path): + return Path(path).mkdir(parents=True, exist_ok=True) + + +def _save(path, data): + with open(path, 'w') as f: + f.write(data) + return + + +def _get_files(path, filters=''): + paths = [] + if filters in ('*', '*.*'): + filters = '' + for folder, _, files in os.walk(path): + if filters: + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [_join(folder, f) for f in files if pattern.search(f)] + else: + paths += files + return paths + + +def _compress_oxt(): + log.info('Compress OXT extension...') + + path_oxt = _join(DIRS['files'], 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) + path = _join(path_source, DIRS['office']) + _mkdir(path) + + if FILES['easymacro'] or DIRS['pythonpath']: + path = _join(path_source, '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_files = DIRS['files'] + if not _exists(path_files): + _mkdir(path_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) + + if FILES['easymacro']: + source = 'easymacro2.py' + target = _join(path_source, 'pythonpath', source) + copyfile(source, target) + + xml = LiboXML() + + path = _join(path_source, DIRS['meta'], FILES['manifest']) + data = xml.new_manifest(DATA['manifest']) + _save(path, data) + + path = _join(path_source, FILES['description']) + data = xml.new_description(DATA['description']) + _save(path, data) + + if TYPE_EXTENSION == 1: + path = _join(path_source, FILES['addons']) + data = xml.new_addons(EXTENSION['id'], DATA['addons']) + _save(path, data) + + path = _join(path_source, DIRS['office']) + _mkdir(path) + path = _join(path_source, DIRS['office'], FILES['shortcut']) + data = xml.new_accelerators(EXTENSION['id'], DATA['addons']['menus']) + _save(path, data) + + + if TYPE_EXTENSION == 3: + path = _join(path_source, FILES['addin']) + _save(path, DATA['addin']) + + if USE_LOCALES: + msg = "Don't forget generate DOMAIN.pot for locales" + for lang in EXTENSION['languages']: + path = _join(path_source, DIRS['locales'], lang, 'LC_MESSAGES') + Path(path).mkdir(parents=True, exist_ok=True) + log.info(msg) + + if DATA['update']: + path_xml = _join(path_files, FILES['update']) + data = xml.new_update(EXTENSION, DATA['update']) + _save(path_xml, data) + + _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 _get_info_path(path): + path, filename = os.path.split(path) + name, extension = os.path.splitext(filename) + return (path, filename, name, extension) + + +def _zip_embed(source, files): + PATH = 'Scripts/python/' + EASYMACRO = 'easymacro2.py' + FILE_PYC = 'easymacro.pyc' + + p, f, name, e = _get_info_path(source) + now = datetime.now().strftime('_%Y%m%d_%H%M%S') + path_source = _join(p, name + now + e) + copyfile(source, path_source) + target = source + + py_compile.compile(EASYMACRO, FILE_PYC) + xml = LiboXML() + + path_easymacro = PATH + FILE_PYC + names = [f[1] for f in files] + [path_easymacro] + nodes = [] + with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as zt: + with zipfile.ZipFile(path_source, compression=zipfile.ZIP_DEFLATED) as zs: + for name in zs.namelist(): + if FILES['manifest'] in name: + path_manifest = name + xml_manifest = zs.open(name).read() + elif name in names: + continue + else: + zt.writestr(name, zs.open(name).read()) + + data = [] + for path, name in files: + data.append(name) + zt.write(path, name) + + zt.write(FILE_PYC, path_easymacro) + data.append(path_easymacro) + + xml.parse_manifest(xml_manifest) + xml_manifest = xml.add_data_manifest(data) + zt.writestr(path_manifest, xml_manifest) + + os.unlink(FILE_PYC) + return + + +def _embed(args): + PATH = 'Scripts/python' + PYTHONPATH = 'pythonpath' + + doc = args.document + if not doc: + msg = '-d/--document Path file to embed is mandatory' + log.error(msg) + return + if not _exists(doc): + msg = 'Path file not exists' + log.error(msg) + return + + files = [] + if args.files: + files = args.files.split(',') + source = _join(PATHS['profile'], PATH) + content = os.listdir(source) + if PYTHONPATH in content: + content.remove(PYTHONPATH) + + if files: + files = [(_join(source, f), _join(PATH, f)) for f in files if f in content] + else: + files = [(_join(source, f), _join(PATH, f)) for f in content] + + _zip_embed(doc, files) + + log.info('Embedded macros successfully...') + return + + +def _locales(args): + EASYMACRO = 'easymacro2.py' + + if args.files: + files = args.files.split(',') + else: + files = _get_files(DIRS['source'], 'py') + paths = ' '.join([f for f in files if not EASYMACRO in f]) + path_pot = _join(DIRS['source'], DIRS['locales'], '{}.pot'.format(DOMAIN)) + call([PATHS['gettext'], '-o', path_pot, paths]) + log.info('POT generate successfully...') + return + + +def _update(): + path_locales = _join(DIRS['source'], DIRS['locales']) + path_pot = _join(DIRS['source'], DIRS['locales'], '{}.pot'.format(DOMAIN)) + if not _exists(path_pot): + log.error('Not exists file POT...') + return + + files = _get_files(path_locales, 'po') + if not files: + log.error('First, generate files PO...') + return + + for f in files: + call([PATHS['msgmerge'], '-U', f, path_pot]) + log.info('\tUpdate: {}'.format(f)) + + log.info('Locales update successfully...') + return + + + +def main(args): + if args.update: + _update() + return + + if args.locales: + _locales(args) + return + + if args.embed: + _embed(args) + return + + if args.new: + _new() + return + + if not _validate_update(): + return + + if not args.only_compress: + _update_files() + + _compress_oxt() + + if args.install: + _install_and_test() + + log.info('Extension make successfully...') + 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) + parser.add_argument('-e', '--embed', dest='embed', action='store_true', + default=False, required=False) + parser.add_argument('-d', '--document', dest='document', default='') + parser.add_argument('-f', '--files', dest='files', default='') + parser.add_argument('-l', '--locales', dest='locales', action='store_true', + default=False, required=False) + parser.add_argument('-u', '--update', dest='update', action='store_true', + default=False, required=False) + parser.add_argument('-oc', '--only_compress', dest='only_compress', + action='store_true', default=False, required=False) + return parser.parse_args() + + +if __name__ == '__main__': + args = _process_command_line_arguments() + main(args) +