Merge branch 'develop'

Add support for locales
This commit is contained in:
Mauricio Baeza 2019-09-14 17:40:23 -05:00
commit 51158ba8ea
17 changed files with 2054 additions and 26 deletions

View File

@ -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

View File

@ -3,3 +3,4 @@
* Configuration
* Option panel
* Sub-menus
* Panel lateral

View File

@ -1 +1 @@
0.3.0
0.4.0

View File

@ -23,7 +23,7 @@ import logging
# ~ 1 = normal extension
# ~ 2 = new component
# ~ 3 = Calc addin
TYPE_EXTENSION = 2
TYPE_EXTENSION = 1
# ~ https://semver.org/
VERSION = '0.1.0'
@ -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])

View File

@ -18,15 +18,26 @@
# ~ along with ZAZ. If not, see <https://www.gnu.org/licenses/>.
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
@ -35,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
@ -66,11 +78,18 @@ logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
log = logging.getLogger(__name__)
OS = sys.platform
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))
WIN = 'win32'
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'
@ -105,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):
@ -122,17 +140,6 @@ def mri(obj):
return
def debug(*info):
for i in info:
log.debug(i)
return
def error(info):
log.error(info)
return
def catch_exception(f):
@wraps(f)
def func(*args, **kwargs):
@ -143,6 +150,51 @@ def catch_exception(f):
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)
@ -190,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()
@ -206,6 +262,20 @@ def _path_system(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',
@ -253,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()
@ -289,10 +363,29 @@ class LODocument(object):
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):
@ -325,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):
@ -476,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):
@ -599,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):
@ -1002,7 +1109,6 @@ def create_dialog(properties):
return LODialog(properties)
@catch_exception
def set_properties(model, properties):
if 'X' in properties:
properties['PositionX'] = properties.pop('X')
@ -1109,7 +1215,13 @@ def inputbox(message, default='', title=TITLE):
return ''
def open(path, **kwargs):
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
@ -1118,6 +1230,9 @@ def open(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)
@ -1129,12 +1244,251 @@ def open(path, **kwargs):
def open_file(path):
if OS == WIN:
if IS_WIN:
os.startfile(path)
else:
subprocess.call(['xdg-open', path])
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()))

58
source/source/Addons.xcu Normal file
View File

@ -0,0 +1,58 @@
<?xml version='1.0' encoding='UTF-8'?>
<oor:component-data xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" oor:name="Addons" oor:package="org.openoffice.Office">
<node oor:name="AddonUI">
<node oor:name="OfficeMenuBar">
<node oor:name="org.myextension.test" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">My Extension</value>
<value xml:lang="es">Mi Extensión</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<node oor:name="Submenu">
<node oor:name="org.myextension.test.101" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Option 1</value>
<value xml:lang="es">Opción 1</value>
</prop>
<prop oor:name="URL" oor:type="xs:string">
<value>service:org.myextension.test?option1</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<prop oor:name="Context" oor:type="xs:string">
<value>com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument</value>
</prop>
<prop oor:name="ImageIdentifier" oor:type="xs:string">
<value>%origin%/images/icon</value>
</prop>
</node>
</node>
</node>
</node>
<node oor:name="OfficeToolBar">
<node oor:name="org.myextension.test" oor:op="replace">
<node oor:name="org.myextension.test.t1" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Option 1</value>
<value xml:lang="es">Opción 1</value>
</prop>
<prop oor:name="URL" oor:type="xs:string">
<value>service:org.myextension.test?option1</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<prop oor:name="Context" oor:type="xs:string">
<value>com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument</value>
</prop>
<prop oor:name="ImageIdentifier" oor:type="xs:string">
<value>%origin%/images/icon</value>
</prop>
</node>
</node>
</node>
</node>
</oor:component-data>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest>
<manifest:file-entry manifest:full-path="TestMacro.py" manifest:media-type="application/vnd.sun.star.uno-component;type=Python"/>
<manifest:file-entry manifest:full-path="Addons.xcu" manifest:media-type="application/vnd.sun.star.configuration-data"/>
</manifest:manifest>

View File

@ -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)

View File

@ -0,0 +1,26 @@
<?xml version='1.0' encoding='UTF-8'?>
<description xmlns="http://openoffice.org/extensions/description/2006" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:d="http://openoffice.org/extensions/description/2006">
<identifier value="org.myextension.test" />
<version value="0.1.0" />
<display-name>
<name lang="en">Test Macro</name>
<name lang="es">Macro de Prueba</name>
</display-name>
<extension-description>
<src lang="en" xlink:href="description/desc_en.txt" />
<src lang="es" xlink:href="description/desc_es.txt" />
</extension-description>
<icon>
<default xlink:href="images/testmacro.png" />
</icon>
<publisher>
<name xlink:href="https://elmau.net" lang="en">El Mau</name>
<name xlink:href="https://elmau.net" lang="es">El Mau</name>
</publisher>
<registration>
<simple-license accept-by="user" suppress-on-update="true" >
<license-text xlink:href="registration/license_en.txt" lang="en" />
<license-text xlink:href="registration/license_es.txt" lang="es" />
</simple-license>
</registration>
</description>

View File

@ -0,0 +1 @@
My great extension

View File

@ -0,0 +1 @@
Mi gran extensión

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because it is too large Load Diff

View File

@ -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 <https://www.gnu.org/licenses/>.

View File

@ -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 <https://www.gnu.org/licenses/>.

View File

@ -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