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 0000000..a954508 Binary files /dev/null and b/source/source/images/icon_16.bmp differ diff --git a/source/source/images/testmacro.png b/source/source/images/testmacro.png new file mode 100644 index 0000000..2f210ed Binary files /dev/null and b/source/source/images/testmacro.png differ diff --git a/source/source/pythonpath/easymacro.py b/source/source/pythonpath/easymacro.py new file mode 100644 index 0000000..622248c --- /dev/null +++ b/source/source/pythonpath/easymacro.py @@ -0,0 +1,1494 @@ +#!/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 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