From 1a5c2031f3b4c7296090d9bb12966d50f416f015 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sat, 14 Sep 2019 17:40:07 -0500 Subject: [PATCH] Add support for locales --- CHANGELOG | 4 + TODO.md | 1 + VERSION | 2 +- source/conf.py.example | 14 +- source/easymacro.py | 144 +- source/source/Addons.xcu | 58 + source/source/META-INF/manifest.xml | 5 + source/source/TestMacro.py | 33 + source/source/description.xml | 26 + source/source/description/desc_en.txt | 1 + source/source/description/desc_es.txt | 1 + source/source/images/icon_16.bmp | Bin 0 -> 2134 bytes source/source/images/testmacro.png | Bin 0 -> 26700 bytes source/source/pythonpath/easymacro.py | 1494 +++++++++++++++++++++ source/source/registration/license_en.txt | 14 + source/source/registration/license_es.txt | 14 + source/zaz.py | 13 + 17 files changed, 1812 insertions(+), 12 deletions(-) create mode 100644 source/source/Addons.xcu create mode 100644 source/source/META-INF/manifest.xml create mode 100644 source/source/TestMacro.py create mode 100644 source/source/description.xml create mode 100644 source/source/description/desc_en.txt create mode 100644 source/source/description/desc_es.txt create mode 100644 source/source/images/icon_16.bmp create mode 100644 source/source/images/testmacro.png create mode 100644 source/source/pythonpath/easymacro.py create mode 100644 source/source/registration/license_en.txt create mode 100644 source/source/registration/license_es.txt diff --git a/CHANGELOG b/CHANGELOG index 85d4b85..2e67c8b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +v 0.4.0 [14-sep-2019] +--------------------- + - Add support for locales + v 0.3.0 [10-sep-2019] --------------------- - Add support for dialogs diff --git a/TODO.md b/TODO.md index d294d43..3d28553 100644 --- a/TODO.md +++ b/TODO.md @@ -3,3 +3,4 @@ * Configuration * Option panel * Sub-menus +* Panel lateral diff --git a/VERSION b/VERSION index 0d91a54..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/source/conf.py.example b/source/conf.py.example index 713c604..7c03a49 100644 --- a/source/conf.py.example +++ b/source/conf.py.example @@ -34,6 +34,12 @@ NAME = 'TestMacro' # ~ Should be unique, used URL inverse ID = 'org.myextension.test' +# ~ If you extension will be multilanguage set: True +# ~ This feature used gettext, set pythonpath and easymacro in True +USE_LOCALES = True +DOMAIN = 'base' +PATH_LOCALES = 'locales' + PUBLISHER = { 'en': {'text': 'El Mau', 'link': 'https://elmau.net'}, 'es': {'text': 'El Mau', 'link': 'https://elmau.net'}, @@ -131,11 +137,12 @@ EXTENSION = { '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 +# ~ If used easymacro pythonpath always is True, recommended DIRS = { 'meta': 'META-INF', 'source': 'source', @@ -143,7 +150,8 @@ DIRS = { 'images': 'images', 'registration': 'registration', 'files': 'files', - 'pythonpath': False, + 'pythonpath': True, + 'locales': PATH_LOCALES, } @@ -335,6 +343,8 @@ if PARENT == 'OfficeMenuBar': def _get_context(args): + if not args: + return '' c = [] for v in args.split(','): c.append(CONTEXT[v]) diff --git a/source/easymacro.py b/source/easymacro.py index 51da1b3..622248c 100644 --- a/source/easymacro.py +++ b/source/easymacro.py @@ -18,7 +18,8 @@ # ~ along with ZAZ. If not, see . - +import ctypes +import datetime import errno import getpass import logging @@ -45,6 +46,7 @@ from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.awt.PosSize import POSSIZE, SIZE from com.sun.star.awt import Size, Point +from com.sun.star.datatransfer import XTransferable, DataFlavor from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA from com.sun.star.text.TextContentAnchorType import AS_CHARACTER @@ -79,9 +81,12 @@ log = logging.getLogger(__name__) OS = platform.system() USER = getpass.getuser() PC = platform.node() +DESKTOP = os.environ.get('DESKTOP_SESSION', '') +INFO_DEBUG = '{}\n\n{}\n\n{}'.format(sys.version, platform.platform(), '\n'.join(sys.path)) IS_WIN = OS == 'Windows' LOG_NAME = 'ZAZ' +CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' CALC = 'calc' WRITER = 'writer' @@ -119,10 +124,9 @@ def _get_config(key, node_name): LANGUAGE = _get_config('ooLocale', 'org.openoffice.Setup/L10N/') +LANG = LANGUAGE.split('-')[0] NAME = TITLE = _get_config('ooName', 'org.openoffice.Setup/Product') VERSION = _get_config('ooSetupVersion', 'org.openoffice.Setup/Product') -INFO_DEBUG = '{}\n\n{}\n\n{}'.format( - sys.version, platform.platform(), '\n'.join(sys.path)) def mri(obj): @@ -238,6 +242,10 @@ def get_desktop(): return create_instance('com.sun.star.frame.Desktop', True) +def get_dispatch(): + return create_instance('com.sun.star.frame.DispatchHelper') + + def get_temp_file(): return tempfile.NamedTemporaryFile() @@ -315,6 +323,10 @@ class LODocument(object): def title(self): return self.obj.getTitle() + @property + def frame(self): + return self._cc.getFrame() + @property def is_saved(self): return self.obj.hasLocation() @@ -368,6 +380,12 @@ class LODocument(object): w.setFocus() return + def paste(self): + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + transferable = sc.getContents() + self._cc.insertTransferable(transferable) + return self.obj.getCurrentSelection() + class LOCalc(LODocument): @@ -400,9 +418,14 @@ class LOCalc(LODocument): cell = LOCellRange(self.active[index].obj, self) return cell - # ~ def create_instance(self, name): - # ~ obj = self.obj.createInstance(name) - # ~ return obj + def select(self, rango): + r = rango + if hasattr(rango, 'obj'): + r = rango.obj + elif isinstance(rango, str): + r = self.get_cell(rango).obj + self._cc.select(r) + return class LOCalcSheet(object): @@ -551,6 +574,11 @@ class LOBasicIde(LODocument): def __init__(self, obj): super().__init__(obj) + @property + def selection(self): + sel = self._cc.getSelection() + return sel + class LOCellRange(object): @@ -674,6 +702,10 @@ class LOCellRange(object): img.setSize(Size(w, h)) return + def select(self): + self.doc._cc.select(self.obj) + return + class EventsListenerBase(unohelper.Base, XEventListener): @@ -1198,6 +1230,9 @@ def open_doc(path, **kwargs): 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 = _properties(kwargs) @@ -1252,12 +1287,13 @@ def run(command, wait=False): # ~ debug(shlex.split(command)) try: if wait: - p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) - p.wait() + # ~ p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) + # ~ p.wait() + result = subprocess.check_output(command, shell=True) else: p = subprocess.Popen(shlex.split(command), stdin=None, stdout=None, stderr=None, close_fds=True) - result, er = p.communicate() + result, er = p.communicate() except subprocess.CalledProcessError as e: msg = ("run [ERROR]: output = %s, error code = %s\n" % (e.output, e.returncode)) @@ -1366,3 +1402,93 @@ def merge_zip(target, zips): return True + +def kill(path): + p = Path(path) + if p.is_file(): + try: + p.unlink() + except: + pass + elif p.is_dir(): + p.rmdir() + return + + +def get_size_screen(): + if IS_WIN: + user32 = ctypes.windll.user32 + res = '{}x{}'.format(user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)) + else: + args = 'xrandr | grep "*" | cut -d " " -f4' + res = run(args, True) + return res.strip() + + +def get_clipboard(): + df = None + text = '' + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + transferable = sc.getContents() + data = transferable.getTransferDataFlavors() + for df in data: + if df.MimeType == CLIPBOARD_FORMAT_TEXT: + break + if df: + text = transferable.getTransferData(df) + return text + + +class TextTransferable(unohelper.Base, XTransferable): + """Keep clipboard data and provide them.""" + + def __init__(self, text): + df = DataFlavor() + df.MimeType = CLIPBOARD_FORMAT_TEXT + df.HumanPresentableName = "encoded text utf-16" + self.flavors = [df] + self.data = [text] + + def getTransferData(self, flavor): + if not flavor: + return + for i, f in enumerate(self.flavors): + if flavor.MimeType == f.MimeType: + return self.data[i] + return + + def getTransferDataFlavors(self): + return tuple(self.flavors) + + def isDataFlavorSupported(self, flavor): + if not flavor: + return False + mtype = flavor.MimeType + for f in self.flavors: + if mtype == f.MimeType: + return True + return False + + +def set_clipboard(text): + ts = TextTransferable(text) + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + sc.setContents(ts, None) + return + + +def copy(doc=None): + if doc is None: + doc = get_document() + if hasattr(doc, 'frame'): + frame = doc.frame + else: + frame = doc.getCurrentController().getFrame() + dispatch = get_dispatch() + dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ()) + return + + +def get_epoch(): + now = datetime.datetime.now() + return int(time.mktime(now.timetuple())) diff --git a/source/source/Addons.xcu b/source/source/Addons.xcu new file mode 100644 index 0000000..b726cdf --- /dev/null +++ b/source/source/Addons.xcu @@ -0,0 +1,58 @@ + + + + + + + My Extension + Mi Extensión + + + _self + + + + + Option 1 + Opción 1 + + + service:org.myextension.test?option1 + + + _self + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument + + + %origin%/images/icon + + + + + + + + + + Option 1 + Opción 1 + + + service:org.myextension.test?option1 + + + _self + + + com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument + + + %origin%/images/icon + + + + + + diff --git a/source/source/META-INF/manifest.xml b/source/source/META-INF/manifest.xml new file mode 100644 index 0000000..6c00e70 --- /dev/null +++ b/source/source/META-INF/manifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/source/source/TestMacro.py b/source/source/TestMacro.py new file mode 100644 index 0000000..79cd31f --- /dev/null +++ b/source/source/TestMacro.py @@ -0,0 +1,33 @@ +import gettext +import uno +import unohelper +from com.sun.star.task import XJobExecutor +import easymacro as app + + +ID_EXTENSION = 'org.myextension.test' +SERVICE = ('com.sun.star.task.Job',) + + +p, *_ = app.get_info_path(__file__) +path_locales = app.join(p, 'locales') +try: + lang = gettext.translation('base', path_locales, languages=[app.LANG]) + lang.install() + _ = lang.gettext +except Exception as e: + app.error(e) + + +class TestMacro(unohelper.Base, XJobExecutor): + + def __init__(self, ctx): + self.ctx = ctx + + def trigger(self, args='pyUNO'): + print('Hello World', args) + return + + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation(TestMacro, ID_EXTENSION, SERVICE) diff --git a/source/source/description.xml b/source/source/description.xml new file mode 100644 index 0000000..cf73104 --- /dev/null +++ b/source/source/description.xml @@ -0,0 +1,26 @@ + + + + + + Test Macro + Macro de Prueba + + + + + + + + + + El Mau + El Mau + + + + + + + + diff --git a/source/source/description/desc_en.txt b/source/source/description/desc_en.txt new file mode 100644 index 0000000..b667a4b --- /dev/null +++ b/source/source/description/desc_en.txt @@ -0,0 +1 @@ +My great extension \ No newline at end of file diff --git a/source/source/description/desc_es.txt b/source/source/description/desc_es.txt new file mode 100644 index 0000000..d8d8fdc --- /dev/null +++ b/source/source/description/desc_es.txt @@ -0,0 +1 @@ +Mi gran extensión \ No newline at end of file diff --git a/source/source/images/icon_16.bmp b/source/source/images/icon_16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..a954508e75c862a5b08b53f65013f0dd6de13bb4 GIT binary patch literal 2134 zcmeIyFHFNw5C-tWRaP<-4F*MmK_DxeU?vbt5M)7+1wja6X=*AF6J$YQCo3mGkbpo` zAV4e`0*RbJ!guewaxHD?{umOLug$&dy#$Y|+Tf|F3f7 zc9vAM{vOpe^_(;HEt%{&vSlt0Tj?@GV!4>dnis0ssI2 literal 0 HcmV?d00001 diff --git a/source/source/images/testmacro.png b/source/source/images/testmacro.png new file mode 100644 index 0000000000000000000000000000000000000000..2f210eda94433623e613c91ad5675eb6631dd370 GIT binary patch literal 26700 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. + + +import ctypes +import datetime +import errno +import getpass +import logging +import os +import platform +import shlex +import subprocess +import sys +import tempfile +import threading +import time +import zipfile + +from datetime import datetime +from functools import wraps +from pathlib import Path, PurePath +from pprint import pprint +from subprocess import PIPE + +import uno +import unohelper +from com.sun.star.beans import PropertyValue +from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS +from com.sun.star.awt.MessageBoxResults import YES +from com.sun.star.awt.PosSize import POSSIZE, SIZE +from com.sun.star.awt import Size, Point +from com.sun.star.datatransfer import XTransferable, DataFlavor +from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA + +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER + +from com.sun.star.lang import XEventListener +from com.sun.star.awt import XActionListener +from com.sun.star.awt import XMouseListener + + +MSG_LANG = { + 'es': { + 'OK': 'Aceptar', + 'Cancel': 'Cancelar', + 'Select file': 'Seleccionar archivo', + } +} + + +FILE_NAME_DEBUG = 'zaz-debug.log' +LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' +LOG_DATE = '%d/%m/%Y %H:%M:%S' +LEVEL_ERROR = logging.getLevelName(logging.ERROR) +LEVEL_DEBUG = logging.getLevelName(logging.DEBUG) +LEVEL_INFO = logging.getLevelName(logging.INFO) +logging.addLevelName(logging.ERROR, f'\033[1;41m{LEVEL_ERROR}\033[1;0m') +logging.addLevelName(logging.DEBUG, f'\x1b[33m{LEVEL_DEBUG}\033[1;0m') +logging.addLevelName(logging.INFO, f'\x1b[32m{LEVEL_INFO}\033[1;0m') +logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) +log = logging.getLogger(__name__) + + +OS = platform.system() +USER = getpass.getuser() +PC = platform.node() +DESKTOP = os.environ.get('DESKTOP_SESSION', '') +INFO_DEBUG = '{}\n\n{}\n\n{}'.format(sys.version, platform.platform(), '\n'.join(sys.path)) + +IS_WIN = OS == 'Windows' +LOG_NAME = 'ZAZ' +CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' + +CALC = 'calc' +WRITER = 'writer' +OBJ_CELL = 'ScCellObj' +OBJ_RANGE = 'ScCellRangeObj' +OBJ_RANGES = 'ScCellRangesObj' +OBJ_TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_RANGES) + + +CTX = uno.getComponentContext() +SM = CTX.getServiceManager() + + +def create_instance(name, with_context=False): + if with_context: + instance = SM.createInstanceWithContext(name, CTX) + else: + instance = SM.createInstance(name) + return instance + + +def _get_config(key, node_name): + 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 (ca.hasByName(key)): + data = ca.getPropertyValue(key) + return data + except Exception as e: + log.error(e) + return '' + + +LANGUAGE = _get_config('ooLocale', 'org.openoffice.Setup/L10N/') +LANG = LANGUAGE.split('-')[0] +NAME = TITLE = _get_config('ooName', 'org.openoffice.Setup/Product') +VERSION = _get_config('ooSetupVersion', 'org.openoffice.Setup/Product') + + +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 catch_exception(f): + @wraps(f) + def func(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as e: + log.error(f.__name__, exc_info=True) + return func + + +class LogWin(object): + + def __init__(self, doc): + self.doc = doc + + def write(self, info): + text = self.doc.Text + cursor = text.createTextCursor() + cursor.gotoEnd(False) + text.insertString(cursor, str(info), 0) + return + + +def info(data): + log.info(data) + return + + +def debug(info): + if IS_WIN: + # ~ app = LOApp(self.ctx, self.sm, self.desktop, self.toolkit) + # ~ doc = app.getDoc(FILE_NAME_DEBUG) + # ~ if not doc: + # ~ doc = app.newDoc(WRITER) + # ~ out = OutputDoc(doc) + # ~ sys.stdout = out + pprint(info) + return + + log.debug(info) + return + + +def error(info): + log.error(info) + return + + +def save_log(path, data): + with open(path, 'a') as out: + out.write('{} -{}- '.format(str(datetime.now())[:19], LOG_NAME)) + pprint(data, stream=out) + return + + +def run_in_thread(fn): + def run(*k, **kw): + t = threading.Thread(target=fn, args=k, kwargs=kw) + t.start() + return t + return run + + +def _(msg): + L = LANGUAGE.split('-')[0] + if L == 'en': + return msg + + if not L in MSG_LANG: + return msg + + return MSG_LANG[L][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() + mb = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message)) + return mb.execute() + + +def question(message, title=TITLE): + res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') + return res == 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_desktop(): + return create_instance('com.sun.star.frame.Desktop', True) + + +def get_dispatch(): + return create_instance('com.sun.star.frame.DispatchHelper') + + +def get_temp_file(): + return tempfile.NamedTemporaryFile() + + +def _path_url(path): + if path.startswith('file://'): + return path + return uno.systemPathToFileUrl(path) + + +def _path_system(path): + if path.startswith('file://'): + return os.path.abspath(uno.fileUrlToSystemPath(path)) + return path + + +def exists_app(name): + try: + dn = subprocess.DEVNULL + subprocess.Popen([name, ''], stdout=dn, stderr=dn).terminate() + except OSError as e: + if e.errno == errno.ENOENT: + return False + return True + + +def exists(path): + return Path(path).exists() + + +def get_type_doc(obj): + services = { + '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', + } + for k, v in services.items(): + if obj.supportsService(v): + return k + return '' + + +def _properties(values): + p = [PropertyValue(Name=n, Value=v) for n, v in values.items()] + return tuple(p) + + +# ~ Custom classes + + +class LODocument(object): + + def __init__(self, obj): + self._obj = obj + self._init_values() + + def _init_values(self): + self._type_doc = get_type_doc(self.obj) + self._cc = self.obj.getCurrentController() + return + + @property + def obj(self): + return self._obj + + @property + def type(self): + return self._type_doc + + @property + def title(self): + return self.obj.getTitle() + + @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.getURL()) + + @property + def visible(self): + w = self._cc.getFrame().getContainerWindow() + return w.Visible + @visible.setter + def visible(self, value): + w = self._cc.getFrame().getContainerWindow() + w.setVisible(value) + + @property + def zoom(self): + return self._cc.ZoomValue + @zoom.setter + def zoom(self, value): + self._cc.ZoomValue = value + + def create_instance(self, name): + obj = self.obj.createInstance(name) + return obj + + def save(self, path='', **kwargs): + opt = _properties(kwargs) + if path: + self._obj.storeAsURL(_path_url(path), opt) + else: + self._obj.store() + return True + + def close(self): + self.obj.close(True) + return + + def focus(self): + w = self._cc.getFrame().getComponentWindow() + w.setFocus() + return + + def paste(self): + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + transferable = sc.getContents() + self._cc.insertTransferable(transferable) + return self.obj.getCurrentSelection() + + +class LOCalc(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + @property + def obj(self): + return self._obj + + @property + def active(self): + return LOCalcSheet(self._cc.getActiveSheet(), self) + + @property + def selection(self): + sel = self.obj.getCurrentSelection() + if sel.ImplementationName in OBJ_TYPE_RANGES: + sel = LOCellRange(sel, self) + return sel + + def get_cell(self, index=None): + """ + index is str 'A1' + index is tuple (row, col) + """ + if index is None: + cell = self.selection.first + else: + cell = LOCellRange(self.active[index].obj, self) + return cell + + def select(self, rango): + r = rango + if hasattr(rango, 'obj'): + r = rango.obj + elif isinstance(rango, str): + r = self.get_cell(rango).obj + self._cc.select(r) + return + + +class LOCalcSheet(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + self._init_values() + + def __getitem__(self, index): + return LOCellRange(self.obj[index], self.doc) + + def _init_values(self): + return + + @property + def obj(self): + return self._obj + + @property + def doc(self): + return self._doc + + +class LOWriter(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + @property + def obj(self): + return self._obj + + @property + def string(self): + return self._obj.getText().String + + @property + def text(self): + return self._obj.getText() + + @property + def cursor(self): + return self.text.createTextCursor() + + @property + def selection(self): + sel = self._cc.getSelection() + return LOTextRange(sel[0]) + + def insert_content(self, cursor, data, replace=False): + self.text.insertTextContent(cursor, data, replace) + return + + # ~ tt = doc.createInstance('com.sun.star.text.TextTable') + # ~ tt.initialize(5, 2) + + # ~ f = doc.createInstance('com.sun.star.text.TextFrame') + # ~ f.setSize(Size(10000, 500)) + + def insert_image(self, path, **kwargs): + cursor = kwargs.get('cursor', self.selection.cursor.getEnd()) + w = kwargs.get('width', 1000) + h = kwargs.get('Height', 1000) + image = self.create_instance('com.sun.star.text.GraphicObject') + image.GraphicURL = _path_url(path) + image.AnchorType = AS_CHARACTER + image.Width = w + image.Height = h + self.insert_content(cursor, image) + return + + +class LOTextRange(object): + + def __init__(self, obj): + self._obj = obj + + @property + def obj(self): + return self._obj + + @property + def string(self): + return self.obj.String + + @property + def text(self): + return self.obj.getText() + + @property + def cursor(self): + return self.text.createTextCursorByRange(self.obj) + + +class LOBase(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + +class LODrawImpress(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + @property + def draw_page(self): + return self._cc.getCurrentPage() + + @catch_exception + def insert_image(self, path, **kwargs): + w = kwargs.get('width', 3000) + h = kwargs.get('Height', 1000) + x = kwargs.get('X', 1000) + y = kwargs.get('Y', 1000) + + image = self.create_instance('com.sun.star.drawing.GraphicObjectShape') + image.GraphicURL = _path_url(path) + image.Size = Size(w, h) + image.Position = Point(x, y) + self.draw_page.add(image) + return + + +class LOImpress(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + + +class LODraw(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + + +class LOMath(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + +class LOBasicIde(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + @property + def selection(self): + sel = self._cc.getSelection() + return sel + + +class LOCellRange(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + self._init_values() + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def __getitem__(self, index): + return LOCellRange(self.obj[index], self.doc) + + def _init_values(self): + self._type_obj = self.obj.ImplementationName + self._type_content = EMPTY + + if self._type_obj == OBJ_CELL: + self._type_content = self.obj.getType() + return + + @property + def obj(self): + return self._obj + + @property + def doc(self): + return self._doc + + @property + def type(self): + return self._type_obj + + @property + def type_content(self): + return self._type_content + + @property + def first(self): + if self.type == OBJ_RANGES: + obj = LOCellRange(self.obj[0][0,0], self.doc) + else: + obj = LOCellRange(self.obj[0,0], self.doc) + return obj + + @property + def value(self): + v = None + if self._type_content == VALUE: + v = self.obj.getValue() + elif self._type_content == TEXT: + v = self.obj.getString() + elif self._type_content == FORMULA: + v = self.obj.getFormula() + return v + @value.setter + def value(self, data): + if isinstance(data, str): + if data.startswith('='): + self.obj.setFormula(data) + else: + self.obj.setString(data) + elif isinstance(data, (int, float)): + self.obj.setValue(data) + + @property + def data(self): + return self.obj.getDataArray() + @data.setter + def data(self, values): + if isinstance(values, list): + values = tuple(values) + self.obj.setDataArray(values) + + def offset(self, col=1, row=0): + a = self.address + col = a.Column + col + row = a.Row + row + return LOCellRange(self.sheet[row,col], self.doc) + + @property + def sheet(self): + return self.obj.Spreadsheet + + @property + def draw_page(self): + return self.sheet.getDrawPage() + + @property + def name(self): + return self.obj.AbsoluteName + + @property + def address(self): + if self._type_obj == OBJ_CELL: + a = self.obj.getCellAddress() + elif self._type_obj == OBJ_RANGE: + a = self.obj.getRangeAddress() + else: + a = self.obj.getRangeAddressesAsString() + return a + + @property + def current_region(self): + cursor = self.sheet.createCursorByRange(self.obj[0,0]) + cursor.collapseToCurrentRegion() + return LOCellRange(self.sheet[cursor.AbsoluteName], self.doc) + + def insert_image(self, path, **kwargs): + s = self.obj.Size + w = kwargs.get('width', s.Width) + h = kwargs.get('Height', s.Height) + img = self.doc.create_instance('com.sun.star.drawing.GraphicObjectShape') + img.GraphicURL = _path_url(path) + self.draw_page.add(img) + img.Anchor = self.obj + img.setSize(Size(w, h)) + return + + def select(self): + self.doc._cc.select(self.obj) + return + + +class EventsListenerBase(unohelper.Base, XEventListener): + + def __init__(self, controller, window=None): + self._controller = controller + self._window = window + + def disposing(self, event): + self._controller = None + if not self._window is None: + self._window.setMenuBar(None) + + +class EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller): + super().__init__(controller) + + def actionPerformed(self, event): + name = event.Source.Model.Name + event_name = '{}_action'.format(name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsMouse(EventsListenerBase, XMouseListener): + + def __init__(self, controller): + super().__init__(controller) + + def mousePressed(self, event): + name = event.Source.Model.Name + event_name = '{}_click'.format(name) + if event.ClickCount == 2: + event_name = '{}_double_click'.format(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 + + +class UnoBaseObject(object): + + def __init__(self, obj): + self._obj = obj + self._model = self.obj.Model + self._rules = {} + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + + @property + def name(self): + return self.model.Name + + @property + def parent(self): + return self.obj.getContext() + + @property + def x(self): + return self.model.PositionX + @x.setter + def x(self, value): + self.model.PositionX = value + + @property + def y(self): + return self.model.PositionY + @y.setter + def y(self, value): + self.model.PositionY = 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 + + @property + def tag(self): + return self.model.Tag + @tag.setter + def tag(self, value): + self.model.Tag = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def rules(self): + return self._rules + @rules.setter + def rules(self, value): + self._rules = value + + def set_focus(self): + self.obj.setFocus() + return + + 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): + w = 0 + h = 0 + if x: + w = origin.width + if y: + h = origin.height + x = origin.x + x + w + y = origin.y + y + h + self.x = x + self.y = y + return + + +class UnoLabel(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoButton(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + # ~ self._set_icon() + + def _set_icon(self): + icon_name = self.tag.strip() + if icon_name: + path_icon = _file_url('{}/img/{}'.format(CURRENT_PATH, icon_name)) + self._model.ImageURL = path_icon + if self.value: + self._model.ImageAlign = 0 + return + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoText(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def value(self): + return self.model.Text + @value.setter + def value(self, value): + self.model.Text = value + + def validate(self): + + return + + +class UnoListBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._data = [] + + @property + def value(self): + return self.obj.SelectedItem + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = list(sorted(values)) + self.model.StringItemList = self.data + return + + +class LODialog(object): + + def __init__(self, properties): + self._obj = self._create(properties) + self._init_values() + + def _init_values(self): + self._model = self._obj.Model + self._init_controls() + self._events = None + # ~ self._response = None + return + + def _create(self, properties): + path = properties.pop('Path', '') + if path: + dp = create_instance('com.sun.star.awt.DialogProvider2', True) + return dp.createDialog(_path_url(path)) + + if 'Library' in properties: + location = properties['Location'] + if location == 'user': + location = 'application' + dp = create_instance('com.sun.star.awt.DialogProvider2', True) + path = 'vnd.sun.star.script:{}.{}?location={}'.format( + properties['Library'], properties['Name'], location) + return dp.createDialog(path) + + 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, properties) + dlg.setModel(model) + dlg.setVisible(False) + dlg.createPeer(toolkit, None) + + return dlg + + def _init_controls(self): + + return + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._model + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers + self._connect_listeners() + + def _connect_listeners(self): + + return + + def _add_listeners(self, control): + if self.events is None: + return + + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + } + for key, value in listeners.items(): + if hasattr(control.obj, key): + getattr(control.obj, key)(listeners[key](self.events)) + return + + def open(self): + return self.obj.execute() + + def close(self, value=0): + return self.obj.endDialog(value) + + def _get_control_model(self, control): + services = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + } + return services[control] + + def _get_custom_class(self, tipo, obj): + classes = { + 'label': UnoLabel, + 'button': UnoButton, + 'text': UnoText, + 'listbox': UnoListBox, + # ~ 'link': UnoLink, + # ~ 'tab': UnoTab, + # ~ 'roadmap': UnoRoadmap, + # ~ 'image': UnoImage, + # ~ 'radio': UnoRadio, + # ~ 'groupbox': UnoGroupBox, + # ~ 'tree': UnoTree, + # ~ 'grid': UnoGrid, + } + return classes[tipo](obj) + + @catch_exception + def add_control(self, properties): + tipo = properties.pop('Type').lower() + model = self.model.createInstance(self._get_control_model(tipo)) + set_properties(model, properties) + name = properties['Name'] + self.model.insertByName(name, model) + control = self._get_custom_class(tipo, self.obj.getControl(name)) + self._add_listeners(control) + setattr(self, name, control) + return + + +# ~ Python >= 3.7 +# ~ def __getattr__(name): + + +def _get_class_doc(obj): + classes = { + 'calc': LOCalc, + 'writer': LOWriter, + 'base': LOBase, + 'impress': LOImpress, + 'draw': LODraw, + 'math': LOMath, + 'basic': LOBasicIde, + } + type_doc = get_type_doc(obj) + return classes[type_doc](obj) + + +def get_document(): + doc = None + desktop = get_desktop() + try: + doc = _get_class_doc(desktop.getCurrentComponent()) + except Exception as e: + log.error(e) + return doc + + +def get_selection(): + return get_document().selection + + +def get_cell(*args): + if args: + index = args + if len(index) == 1: + index = args[0] + cell = get_document().get_cell(index) + else: + cell = get_selection().first + return cell + + +def active_cell(): + return get_cell() + + +def create_dialog(properties): + return LODialog(properties) + + +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 + + +def get_file(filters=(), multiple=False): + file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker') + file_picker.setTitle(_('Select file')) + file_picker.setMultiSelectionMode(multiple) + + if filters: + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + if file_picker.execute(): + if multiple: + return [_path_system(f) for f in file_picker.getSelectedFiles()] + return _path_system(file_picker.getSelectedFiles()[0]) + + return '' + + +def get_info_path(path): + path, filename = os.path.split(path) + name, extension = os.path.splitext(filename) + return (path, filename, name, extension) + + +def inputbox(message, default='', title=TITLE): + + 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(dlg) + + 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, + } + 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 new_doc(type_doc=CALC): + path = 'private:factory/s{}'.format(type_doc) + doc = get_desktop().loadComponentFromURL(path, '_default', 0, ()) + return _get_class_doc(doc) + + +def open_doc(path, **kwargs): + """ 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 = _properties(kwargs) + doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt) + if doc is None: + return + + return _get_class_doc(doc) + + +def open_file(path): + if IS_WIN: + os.startfile(path) + else: + subprocess.Popen(['xdg-open', path]) + return + + +def join(*paths): + return os.path.join(*paths) + + +def is_dir(path): + return Path(path).is_dir() + + +def is_file(path): + return Path(path).is_file() + + +def get_file_size(path): + return Path(path).stat().st_size + + +def is_created(path): + return is_file(path) and bool(get_file_size(path)) + + +def replace_ext(path, ext): + path, _, name, _ = get_info_path(path) + return '{}/{}.{}'.format(path, name, ext) + + +def zip_names(path): + with zipfile.ZipFile(path) as z: + names = z.namelist() + return names + + +def run(command, wait=False): + # ~ debug(command) + # ~ debug(shlex.split(command)) + try: + if wait: + # ~ p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) + # ~ p.wait() + result = subprocess.check_output(command, shell=True) + else: + p = subprocess.Popen(shlex.split(command), stdin=None, + stdout=None, stderr=None, close_fds=True) + result, er = p.communicate() + except subprocess.CalledProcessError as e: + msg = ("run [ERROR]: output = %s, error code = %s\n" + % (e.output, e.returncode)) + error(msg) + return False + + if result is None: + return True + + return result.decode() + + +def _zippwd(source, target, pwd): + if IS_WIN: + return False + if not exists_app('zip'): + return False + + cmd = 'zip' + opt = '-j ' + args = "{} --password {} ".format(cmd, pwd) + + if isinstance(source, (tuple, list)): + if not target: + return False + args += opt + target + ' ' + ' '.join(source) + else: + if is_file(source) and not target: + target = replace_ext(source, 'zip') + elif is_dir(source) and not target: + target = join(PurePath(source).parent, + '{}.zip'.format(PurePath(source).name)) + opt = '-r ' + args += opt + target + ' ' + source + + result = run(args, True) + if not result: + return False + + return is_created(target) + + +def zip(source, target='', mode='w', pwd=''): + if pwd: + return _zippwd(source, target, pwd) + + if isinstance(source, (tuple, list)): + if not target: + return False + + with zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) as z: + for path in source: + _, name, _, _ = get_info_path(path) + z.write(path, name) + + return is_created(target) + + if is_file(source): + if not target: + target = replace_ext(source, 'zip') + z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) + _, name, _, _ = get_info_path(source) + z.write(source, name) + z.close() + return is_created(target) + + if not target: + target = join( + PurePath(source).parent, + '{}.zip'.format(PurePath(source).name)) + z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) + root_len = len(os.path.abspath(source)) + for root, dirs, files in os.walk(source): + relative = os.path.abspath(root)[root_len:] + for f in files: + fullpath = join(root, f) + file_name = join(relative, f) + z.write(fullpath, file_name) + z.close() + + return is_created(target) + + +def unzip(source, path='', members=None, pwd=None): + if not path: + path, _, _, _ = get_info_path(source) + with zipfile.ZipFile(source) as z: + if not pwd is None: + pwd = pwd.encode() + if isinstance(members, str): + members = (members,) + z.extractall(path, members=members, pwd=pwd) + return True + + +def merge_zip(target, zips): + try: + with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: + for path in zips: + with zipfile.ZipFile(path, compression=zipfile.ZIP_DEFLATED) as s: + for name in s.namelist(): + t.writestr(name, s.open(name).read()) + except Exception as e: + error(e) + return False + + return True + + +def kill(path): + p = Path(path) + if p.is_file(): + try: + p.unlink() + except: + pass + elif p.is_dir(): + p.rmdir() + return + + +def get_size_screen(): + if IS_WIN: + user32 = ctypes.windll.user32 + res = '{}x{}'.format(user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)) + else: + args = 'xrandr | grep "*" | cut -d " " -f4' + res = run(args, True) + return res.strip() + + +def get_clipboard(): + df = None + text = '' + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + transferable = sc.getContents() + data = transferable.getTransferDataFlavors() + for df in data: + if df.MimeType == CLIPBOARD_FORMAT_TEXT: + break + if df: + text = transferable.getTransferData(df) + return text + + +class TextTransferable(unohelper.Base, XTransferable): + """Keep clipboard data and provide them.""" + + def __init__(self, text): + df = DataFlavor() + df.MimeType = CLIPBOARD_FORMAT_TEXT + df.HumanPresentableName = "encoded text utf-16" + self.flavors = [df] + self.data = [text] + + def getTransferData(self, flavor): + if not flavor: + return + for i, f in enumerate(self.flavors): + if flavor.MimeType == f.MimeType: + return self.data[i] + return + + def getTransferDataFlavors(self): + return tuple(self.flavors) + + def isDataFlavorSupported(self, flavor): + if not flavor: + return False + mtype = flavor.MimeType + for f in self.flavors: + if mtype == f.MimeType: + return True + return False + + +def set_clipboard(text): + ts = TextTransferable(text) + sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') + sc.setContents(ts, None) + return + + +def copy(doc=None): + if doc is None: + doc = get_document() + if hasattr(doc, 'frame'): + frame = doc.frame + else: + frame = doc.getCurrentController().getFrame() + dispatch = get_dispatch() + dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ()) + return + + +def get_epoch(): + now = datetime.datetime.now() + return int(time.mktime(now.timetuple())) diff --git a/source/source/registration/license_en.txt b/source/source/registration/license_en.txt new file mode 100644 index 0000000..3e7ef0e --- /dev/null +++ b/source/source/registration/license_en.txt @@ -0,0 +1,14 @@ +This file is part of TestMacro. + + TestMacro 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. + + TestMacro 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 TestMacro. If not, see . diff --git a/source/source/registration/license_es.txt b/source/source/registration/license_es.txt new file mode 100644 index 0000000..3e7ef0e --- /dev/null +++ b/source/source/registration/license_es.txt @@ -0,0 +1,14 @@ +This file is part of TestMacro. + + TestMacro 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. + + TestMacro 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 TestMacro. If not, see . diff --git a/source/zaz.py b/source/zaz.py index 7a0999f..ebbc1ff 100644 --- a/source/zaz.py +++ b/source/zaz.py @@ -20,6 +20,7 @@ import argparse import os import sys +from pathlib import Path from shutil import copyfile from subprocess import call import zipfile @@ -32,6 +33,7 @@ from conf import ( INFO, PATHS, TYPE_EXTENSION, + USE_LOCALES, log) @@ -74,6 +76,10 @@ def _compress_oxt(): z.write(fullpath, file_name, zipfile.ZIP_DEFLATED) z.close() + if DATA['update']: + path_xml = _join(path, FILES['update']) + _save(path_xml, DATA['update']) + log.info('Extension OXT created sucesfully...') return @@ -227,6 +233,13 @@ def _update_files(): path = _join(path_source, FILES['addin']) _save(path, DATA['addin']) + if USE_LOCALES: + msg = "Don't forget generate DOMAIN.pot for locales" + log.info(msg) + for lang in EXTENSION['languages']: + path = _join(path_source, DIRS['locales'], lang, 'LC_MESSAGES') + Path(path).mkdir(parents=True, exist_ok=True) + _compile_idl() return