From 41681330d3d5c556535ed06d2fdacc10f3871407 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 12 Nov 2020 19:35:29 -0600 Subject: [PATCH] Add install button --- VERSION | 2 +- conf.py | 4 +- easymacro.py | 140 +- .../{ZAZPip_v0.5.0.oxt => ZAZPip_v0.6.0.oxt} | Bin 81277 -> 77469 bytes source/Addons.xcu | 2 +- source/Office/Accelerators.xcu | 2 +- source/ZAZPip.py | 492 +- source/description.xml | 2 +- source/pythonpath/easymacro.py | 7535 ++++++++--------- source/pythonpath/main.py | 517 ++ 10 files changed, 3993 insertions(+), 4703 deletions(-) rename files/{ZAZPip_v0.5.0.oxt => ZAZPip_v0.6.0.oxt} (50%) create mode 100644 source/pythonpath/main.py diff --git a/VERSION b/VERSION index 79a2734..09a3acf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 \ No newline at end of file +0.6.0 \ No newline at end of file diff --git a/conf.py b/conf.py index b3dca5c..ae67910 100644 --- a/conf.py +++ b/conf.py @@ -26,7 +26,7 @@ import logging TYPE_EXTENSION = 1 # ~ https://semver.org/ -VERSION = '0.5.0' +VERSION = '0.6.0' # ~ Your great extension name, not used spaces NAME = 'ZAZPip' @@ -112,7 +112,7 @@ MENU_MAIN = {} MENUS = ( { 'title': {'en': 'Open Pip', 'es': 'Abrir Pip'}, - 'argument': 'open', + 'argument': 'open_dialog_pip', 'context': '', 'icon': 'icon', 'toolbar': False, diff --git a/easymacro.py b/easymacro.py index cdef714..29d4a17 100644 --- a/easymacro.py +++ b/easymacro.py @@ -67,6 +67,7 @@ import unohelper from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.awt import Rectangle, Size, Point +from com.sun.star.awt.PosSize import POSSIZE from com.sun.star.awt import Key, KeyModifier, KeyEvent from com.sun.star.container import NoSuchElementException from com.sun.star.datatransfer import XTransferable, DataFlavor @@ -399,19 +400,6 @@ def _get_class_doc(obj: Any) -> Any: return classes[type_doc](obj) -# ~ def _get_class_uno(obj: Any) -> Any: - # ~ classes = dict( - # ~ SwXTextGraphicObject = LOImage, - # ~ SvxShapeText = LOImage, - # ~ ) - # ~ name = obj.ImplementationName - # ~ print(f'ImplementationName = {name}') - # ~ instance = obj - # ~ if name in classes: - # ~ instance = classes[name](obj) - # ~ return instance - - def dict_to_property(values: dict, uno_any: bool=False): ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) if uno_any: @@ -429,6 +417,14 @@ def _property_to_dict(values): return d +def json_dumps(data): + return json.dumps(data, indent=4, sort_keys=True) + + +def json_loads(data): + return json.loads(data) + + def data_to_dict(data): if isinstance(data, tuple) and isinstance(data[0], tuple): return _array_to_dict(data) @@ -438,12 +434,6 @@ def data_to_dict(data): return {} -def _path_url(path: str) -> str: - if path.startswith('file://'): - return path - return uno.systemPathToFileUrl(path) - - def _get_dispatch() -> Any: return create_instance('com.sun.star.frame.DispatchHelper') @@ -1145,7 +1135,7 @@ class LODocument(object): opt = dict_to_property(args) if path: try: - self.obj.storeAsURL(_path_url(path), opt) + self.obj.storeAsURL(_P.to_url(path), opt) except Exception as e: error(e) result = False @@ -2828,7 +2818,7 @@ class LODocs(object): 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) + path = _P.to_url(path) opt = dict_to_property(args) doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) if doc is None: @@ -2994,9 +2984,10 @@ class EventsFocus(EventsListenerBase, XFocusListener): # ~ HelpURL = ? class UnoBaseObject(object): - def __init__(self, obj): + def __init__(self, obj, path=''): self._obj = obj self._model = obj.Model + # ~ self._path = path def __setattr__(self, name, value): exists = hasattr(self, name) @@ -3199,6 +3190,22 @@ class UnoBaseObject(object): def tab_stop(self, value): self.model.Tabstop = value + @property + def ps(self): + ps = self.obj.getPosSize() + return ps + @ps.setter + def ps(self, ps): + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + + def set_focus(self): + self.obj.setFocus() + return + + def ps_from(self, source): + self.ps = source.ps + return + def center(self, horizontal=True, vertical=False): p = self.parent.Model w = p.Width @@ -3361,6 +3368,78 @@ class UnoImage(UnoBaseObject): self.m.ImageURL = _P.to_url(value) +class UnoListBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._path = '' + + def __setattr__(self, name, value): + if name in ('_path',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def type(self): + return 'listbox' + + @property + def value(self): + return self.obj.getSelectedItem() + + @property + def count(self): + return len(self.data) + + @property + def data(self): + return self.model.StringItemList + @data.setter + def data(self, values): + self.model.StringItemList = list(sorted(values)) + + @property + def path(self): + return self._path + @path.setter + def path(self, value): + self._path = value + + def unselect(self): + self.obj.selectItem(self.value, False) + return + + def select(self, pos=0): + if isinstance(pos, str): + self.obj.selectItem(pos, True) + else: + self.obj.selectItemPos(pos, True) + return + + def clear(self): + self.model.removeAllItems() + return + + def _set_image_url(self, image): + if _P.exists(image): + return _P.to_url(image) + + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) + + def insert(self, value, path='', pos=-1, show=True): + if pos < 0: + pos = self.count + if path: + self.model.insertItem(pos, value, self._set_image_url(path)) + else: + self.model.insertItemText(pos, value) + if show: + self.select(pos) + return + + UNO_CLASSES = { 'label': UnoLabel, 'link': UnoLabelLink, @@ -3369,6 +3448,7 @@ UNO_CLASSES = { 'check': UnoCheck, 'text': UnoText, 'image': UnoImage, + 'listbox': UnoListBox, } @@ -3382,9 +3462,9 @@ class LODialog(object): 'check': 'com.sun.star.awt.UnoControlCheckBoxModel', 'text': 'com.sun.star.awt.UnoControlEditModel', 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', # ~ 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', # ~ 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - # ~ 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', # ~ 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', # ~ 'tree': 'com.sun.star.awt.tree.TreeControlModel', # ~ 'pages': 'com.sun.star.awt.UnoMultiPageModel', @@ -3405,7 +3485,7 @@ class LODialog(object): path = args.pop('Path', '') if path: dp = create_instance(service, True) - dlg = dp.createDialog(_path_url(path)) + dlg = dp.createDialog(_P.to_url(path)) return dlg if 'Location' in args: @@ -3446,6 +3526,9 @@ class LODialog(object): return self._controls @property + def path(self): + return self._path + @property def id(self): return self._id @id.setter @@ -3474,6 +3557,13 @@ class LODialog(object): def visible(self, value): self.obj.Visible = value + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + @property def events(self): return self._events @@ -3539,6 +3629,8 @@ class LODialog(object): control = self.obj.getControl(name) _add_listeners(self.events, control, name) control = UNO_CLASSES[tipo](control) + if tipo in ('listbox',): + control.path = self.path if tipo == 'tree' and root: control.root = root diff --git a/files/ZAZPip_v0.5.0.oxt b/files/ZAZPip_v0.6.0.oxt similarity index 50% rename from files/ZAZPip_v0.5.0.oxt rename to files/ZAZPip_v0.6.0.oxt index b1410964df62f365d058b748f5bebd6417e57327..08a54e7687c03cca60224acdc21e8b453cbc354f 100644 GIT binary patch delta 33767 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 delta 37628 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

Usr9oC&M$`48@MESs6oZJuk*9rE_9S3rEr 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 VQnJ4bvtHhqD*(W-#O1_*{{R^n^+Nyv diff --git a/source/Addons.xcu b/source/Addons.xcu index 691020b..dca21e5 100644 --- a/source/Addons.xcu +++ b/source/Addons.xcu @@ -11,7 +11,7 @@ - service:net.elmau.zaz.pip?open + service:net.elmau.zaz.pip?open_dialog_pip _self diff --git a/source/Office/Accelerators.xcu b/source/Office/Accelerators.xcu index c5ac0c1..92fcc4c 100644 --- a/source/Office/Accelerators.xcu +++ b/source/Office/Accelerators.xcu @@ -4,7 +4,7 @@ - service:net.elmau.zaz.pip?open + service:net.elmau.zaz.pip?open_dialog_pip diff --git a/source/ZAZPip.py b/source/ZAZPip.py index 4f98bc9..79382e1 100644 --- a/source/ZAZPip.py +++ b/source/ZAZPip.py @@ -1,507 +1,23 @@ import uno import unohelper from com.sun.star.task import XJobExecutor -import easymacro as app +import main + ID_EXTENSION = 'net.elmau.zaz.pip' SERVICE = ('com.sun.star.task.Job',) -TITLE = 'ZAZ-PIP' -URL_PIP = 'https://bootstrap.pypa.io/get-pip.py' -PIP = 'pip' - - -PACKAGES = { - 'cffi': 'ok.png', - 'cryptography': 'ok.png', - 'httpx': 'ok.png', - 'lxml': 'ok.png', - 'numpy': 'ok.png', - 'pandas': 'ok.png', - 'psycopg2-binary': 'ok.png', - 'peewee': 'ok.png', - 'pillow': 'ok.png', - 'pytesseract': 'ok.png', - 'sounddevice': 'ok.png', -} - - -_ = app.install_locales(__file__) - - -class Controllers(object): - OK1 = 'Successfully installed' - OK2 = 'Requirement already' - OK3 = 'Successfully uninstalled' - - def __init__(self, dialog): - self.d = dialog - self.path_python = app.get_path_python() - self._states = { - 'list': False, - 'install': False, - 'search': False, - 'versions': False, - } - - def _set_state(self, state): - for k in self._states.keys(): - self._states[k] = False - self._states[state] = True - return - - def cmd_install_pip_action(self, event): - msg = _('Do you want install PIP?') - if not app.question(msg, 'ZAZ-Pip'): - return - - self._install_pip() - return - - @app.run_in_thread - def _install_pip(self): - self.d.link_proyect.visible = False - self.d.lst_log.visible = True - path_pip = app.get_temp_file(True) - - self.d.lst_log.insert('Download PIP...') - data, err = app.url_open(URL_PIP, verify=False) - if err: - msg = _('Do you have internet connection?') - app.errorbox('{}\n\n{}'.format(msg, err)) - return - - app.save_file(path_pip, 'wb', data) - if not app.is_created(path_pip): - msg = _('File PIP not save') - app.errorbox(msg) - return - self.d.lst_log.insert(_('PIP save correctly...')) - - try: - - self.d.lst_log.insert(_('Start installing PIP...')) - cmd = '"{}" "{}" --user'.format(self.path_python, path_pip) - for line in app.popen(cmd): - if isinstance(line, tuple): - app.errorbox(line) - break - self.d.lst_log.insert(line) - - cmd = self._cmd_pip('-V') - label = app.run(cmd, True) - if label: - self.d.lbl_pip.value = label - self.d.cmd_install_pip.visible = False - self.d.cmd_admin_pip.visible = True - msg = _('PIP installed sucesfully') - app.msgbox(msg) - else: - msg = _('PIP not installed, see log') - app.warning(msg) - except Exception as e: - app.errorbox(e) - - return - - def _cmd_pip(self, args): - cmd = '"{}" -m pip {}'.format(self.path_python, args) - return cmd - - def cmd_admin_pip_action(self, event): - self.d.lst_log.possize(self.d.lst_package) - self.d.lst_log.step = 1 - self.d.step = 1 - self.cmd_home_action(None) - return - - def cmd_close_action(self, event): - self.d.close() - return - - def cmd_home_action(self, event): - self.d.txt_search.value = '' - self._list() - return - - def txt_search_key_released(self, event): - if event.KeyCode == app.KEY['enter']: - self.cmd_search_action(None) - return - - if not self.d.txt_search.value.strip(): - self.cmd_home_action(None) - return - - @app.run_in_thread - def _list(self): - self._set_state('list') - self.d.lst_log.visible = False - self.d.lst_package.visible = True - - cmd = self._cmd_pip(' list --format=json') - self.d.lst_package.clear() - result = app.run(cmd, True) - packages = app.json_loads(result) - - for p in packages: - t = '{} - ({})'.format(p['name'], p['version']) - self.d.lst_package.insert(t, 'ok.png') - self.d.lst_package.select() - self.d.txt_search.set_focus() - return - - @app.run_in_thread - def _search(self, value): - self._set_state('search') - line = '' - cmd = self._cmd_pip(' search {}'.format(value)) - self.d.lst_package.clear() - for line in app.popen(cmd): - parts = line.split(')') - name = parts[0].strip() + ')' - description = parts[-1].strip() - parts = name.split('(') - name_verify = parts[0].strip() - package = '{} {}'.format(name, description) - image = PACKAGES.get(name_verify, 'question.png') - self.d.lst_package.insert(package, image) - - if line: - self.d.lst_package.select() - else: - self.d.lst_package.insert(_('Not found...'), 'error.png', show=False) - return - - def cmd_search_action(self, event): - search = self.d.txt_search.value.strip() - if not search: - self.d.txt_search.set_focus() - return - - self._search(search) - return - - @app.run_in_thread - def _install(self, value): - self._set_state('install') - self.d.lst_package.visible = False - self.d.lst_log.visible = True - - line = '' - name = value.split(' ')[0].strip() - cmd = self._cmd_pip(' install --upgrade --user {}'.format(name)) - self.d.lst_log.clear() - for line in app.popen(cmd): - if self.OK1 in line or self.OK2 in line: - self.d.lst_log.insert(line, 'ok.png') - else: - self.d.lst_log.insert(line) - return - - @app.catch_exception - def lst_package_double_click(self, event): - opt = 'install' - if self._states['list']: - opt = 'upgrade' - - name = self.d.lst_package.value - msg = _('Do you want {}:\n\n{} ?').format(opt, name) - if not app.question(msg, TITLE): - return - - self._install(name) - return - - @app.run_in_thread - def _uninstall(self, value): - self._set_state('install') - self.d.lst_package.visible = False - self.d.lst_log.visible = True - - line = '' - name = value.split(' ')[0].strip() - cmd = self._cmd_pip(' uninstall -y {}'.format(name)) - self.d.lst_log.clear() - for line in app.popen(cmd): - if self.OK3 in line: - self.d.lst_log.insert(line, 'ok.png') - else: - self.d.lst_log.insert(line) - return - - def cmd_uninstall_action(self, event): - if not self._states['list']: - msg = _('Select installed package') - app.warning(msg) - return - - name = self.d.lst_package.value - msg = _('Do you want uninstall:\n\n{} ?').format(name) - if not app.question(msg): - return - - self._uninstall(name) - return - - @app.catch_exception - def cmd_shell_action(self, name): - if app.IS_WIN: - cmd = '"{}"'.format(self.path_python) - app.open_file(cmd) - else: - cmd = 'exec "{}"' - if app.IS_MAC: - cmd = 'open "{}"' - elif app.DESKTOP == 'gnome': - cmd = 'gnome-terminal -- {}' - - cmd = cmd.format(self.path_python) - app.run(cmd) - return - - class ZAZPip(unohelper.Base, XJobExecutor): def __init__(self, ctx): self.ctx = ctx def trigger(self, args): - dialog = self._create_dialog() - dialog.open() + main.ID_EXTENSION = ID_EXTENSION + main.run(args, __file__) return - def _create_dialog(self): - args= { - 'Name': 'dialog', - 'Title': 'Zaz-Pip', - 'Width': 200, - 'Height': 220, - } - dialog = app.create_dialog(args) - dialog.id_extension = ID_EXTENSION - dialog.events = Controllers(dialog) - - lbl_title = '{} {} - {}'.format(app.NAME, app.VERSION, app.OS) - args = { - 'Type': 'Label', - 'Name': 'lbl_title', - 'Label': lbl_title, - 'Width': 100, - 'Height': 15, - 'Border': 1, - 'Align': 1, - 'VerticalAlign': 1, - 'Step': 10, - 'FontHeight': 12, - } - dialog.add_control(args) - dialog.center(dialog.lbl_title, y=5) - - path_python = app.get_path_python() - cmd = '"{}" -V'.format(path_python) - label = app.run(cmd, True) - - args = { - 'Type': 'Label', - 'Name': 'lbl_python', - 'Label': str(label), - 'Width': 100, - 'Height': 15, - 'Border': 1, - 'Align': 1, - 'VerticalAlign': 1, - 'Step': 10, - 'FontHeight': 11, - } - dialog.add_control(args) - dialog.center(dialog.lbl_python, y=25) - - cmd = '"{}" -m pip -V'.format(path_python) - label = app.run(cmd, True) - exists_pip = True - if not label: - exists_pip = False - label = _('PIP not installed') - args = { - 'Type': 'Label', - 'Name': 'lbl_pip', - 'Label': label, - 'Width': 160, - 'Height': 30, - 'Border': 1, - 'Align': 1, - 'VerticalAlign': 1, - 'Step': 10, - 'MultiLine': True, - } - dialog.add_control(args) - dialog.center(dialog.lbl_pip, y=45) - - args = { - 'Type': 'Button', - 'Name': 'cmd_admin_pip', - 'Label': _('Admin PIP'), - 'Width': 60, - 'Height': 18, - 'Step': 10, - 'ImageURL': 'python.png', - 'ImagePosition': 1, - } - dialog.add_control(args) - dialog.center(dialog.cmd_admin_pip, y=80) - - args = { - 'Type': 'Button', - 'Name': 'cmd_install_pip', - 'Label': _('Install PIP'), - 'Width': 60, - 'Height': 18, - 'Step': 10, - 'ImageURL': 'install.png', - 'ImagePosition': 1, - } - dialog.add_control(args) - dialog.center(dialog.cmd_install_pip, y=80) - - args = { - 'Type': 'Link', - 'Name': 'link_proyect', - 'URL': 'https://gitlab.com/mauriciobaeza/', - 'Label': 'https://gitlab.com/mauriciobaeza/', - 'Border': 1, - 'Width': 130, - 'Height': 15, - 'Align': 1, - 'VerticalAlign': 1, - 'Step': 10, - } - dialog.add_control(args) - dialog.center(dialog.link_proyect, y=-5) - - args = { - 'Type': 'Listbox', - 'Name': 'lst_log', - 'Width': 160, - 'Height': 105, - 'Step': 10, - } - dialog.add_control(args) - dialog.center(dialog.lst_log, y=105) - - args = { - 'Type': 'Label', - 'Name': 'lbl_package', - 'Label': _('Packages'), - 'Width': 100, - 'Height': 15, - 'Border': 1, - 'Align': 1, - 'VerticalAlign': 1, - 'Step': 1, - } - dialog.add_control(args) - - args = { - 'Type': 'Text', - 'Name': 'txt_search', - 'Width': 180, - 'Height': 12, - 'Step': 1, - 'Border': 0, - } - dialog.add_control(args) - - args = { - 'Type': 'Listbox', - 'Name': 'lst_package', - 'Width': 180, - 'Height': 128, - 'Step': 1, - } - dialog.add_control(args) - - args = { - 'Type': 'Button', - 'Name': 'cmd_close', - 'Label': _('~Close'), - 'Width': 60, - 'Height': 18, - 'Step': 1, - 'ImageURL': 'close.png', - 'ImagePosition': 1, - # ~ 'PushButtonType': 2, - } - dialog.add_control(args) - dialog.center(dialog.cmd_close, y=-5) - - args = { - 'Type': 'Button', - 'Name': 'cmd_home', - 'Width': 18, - 'Height': 18, - 'Step': 1, - 'ImageURL': 'home.png', - 'FocusOnClick': False, - 'Y': 2, - } - dialog.add_control(args) - - args = { - 'Type': 'Button', - 'Name': 'cmd_search', - 'Width': 18, - 'Height': 18, - 'Step': 1, - 'ImageURL': 'search.png', - 'FocusOnClick': False, - 'Y': 2, - } - dialog.add_control(args) - - args = { - 'Type': 'Button', - 'Name': 'cmd_uninstall', - 'Width': 18, - 'Height': 18, - 'Step': 1, - 'ImageURL': 'uninstall.png', - 'FocusOnClick': False, - 'Y': 2, - } - dialog.add_control(args) - - args = { - 'Type': 'Button', - 'Name': 'cmd_shell', - 'Width': 18, - 'Height': 18, - 'Step': 1, - 'ImageURL': 'shell.png', - 'FocusOnClick': False, - 'Y': 2, - } - dialog.add_control(args) - - controls = (dialog.cmd_home, dialog.cmd_search, - dialog.cmd_uninstall, dialog.cmd_shell) - dialog.lbl_package.move(dialog.cmd_home) - dialog.txt_search.move(dialog.lbl_package) - dialog.lst_package.move(dialog.txt_search) - dialog.lbl_package.center() - dialog.lst_package.center() - dialog.txt_search.center() - dialog.center(controls) - - dialog.step = 10 - - dialog.cmd_install_pip.visible = not exists_pip - dialog.cmd_admin_pip.visible = exists_pip - dialog.lst_log.visible = False - - return dialog - g_ImplementationHelper = unohelper.ImplementationHelper() g_ImplementationHelper.addImplementation(ZAZPip, ID_EXTENSION, SERVICE) diff --git a/source/description.xml b/source/description.xml index 3b7128f..f2e52ef 100644 --- a/source/description.xml +++ b/source/description.xml @@ -1,7 +1,7 @@ - + ZAZ Pip ZAZ Pip diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py index 30ea714..29d4a17 100644 --- a/source/pythonpath/easymacro.py +++ b/source/pythonpath/easymacro.py @@ -4,6 +4,8 @@ # ~ This file is part of ZAZ. +# ~ https://git.elmau.net/elmau/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 @@ -19,11 +21,9 @@ import base64 import csv -import ctypes import datetime -import errno -import gettext import getpass +import gettext import hashlib import json import logging @@ -33,22 +33,25 @@ import re import shlex import shutil import socket -import subprocess import ssl +import subprocess import sys import tempfile import threading import time -import traceback import zipfile +from collections import OrderedDict +from collections.abc import MutableMapping +from decimal import Decimal +from enum import IntEnum from functools import wraps -from pathlib import Path, PurePath +from pathlib import Path from pprint import pprint +from string import Template +from typing import Any from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError -from string import Template -from subprocess import PIPE import smtplib from smtplib import SMTPException, SMTPAuthenticationError @@ -61,150 +64,39 @@ import mailbox import uno import unohelper -from com.sun.star.util import Time, Date, DateTime -from com.sun.star.beans import PropertyValue, NamedValue 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.awt import Rectangle -from com.sun.star.awt import KeyEvent -from com.sun.star.awt.KeyFunction import QUIT +from com.sun.star.awt import Rectangle, Size, Point +from com.sun.star.awt.PosSize import POSSIZE +from com.sun.star.awt import Key, KeyModifier, KeyEvent +from com.sun.star.container import NoSuchElementException from com.sun.star.datatransfer import XTransferable, DataFlavor -from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA -from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.beans import PropertyValue, NamedValue +from com.sun.star.sheet import TableFilterField +from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA +from com.sun.star.util import Time, Date, DateTime + from com.sun.star.text.TextContentAnchorType import AS_CHARACTER -from com.sun.star.script import ScriptEventDescriptor -from com.sun.star.lang import XEventListener from com.sun.star.awt import XActionListener +from com.sun.star.lang import XEventListener from com.sun.star.awt import XMouseListener from com.sun.star.awt import XMouseMotionListener -from com.sun.star.util import XModifyListener -from com.sun.star.awt import XTopWindowListener -from com.sun.star.awt import XWindowListener -from com.sun.star.awt import XMenuListener -from com.sun.star.awt import XKeyListener -from com.sun.star.awt import XItemListener from com.sun.star.awt import XFocusListener -from com.sun.star.awt import XTabListener -from com.sun.star.awt.grid import XGridDataListener -from com.sun.star.awt.grid import XGridSelectionListener +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1FontUnderline.html +from com.sun.star.awt import FontUnderline +from com.sun.star.style.VerticalAlignment import TOP, MIDDLE, BOTTOM try: - from fernet import Fernet, InvalidToken -except ImportError: - pass + from peewee import Database, DateTimeField, DateField, TimeField, \ + __exception_wrapper__ +except ImportError as e: + Database = DateField = TimeField = DateTimeField = object + print('Install peewee') -ID_EXTENSION = '' - -DIR = { - 'images': 'images', - 'locales': 'locales', -} - -KEY = { - 'enter': 1280, -} - -SEPARATION = 5 - -MSG_LANG = { - 'es': { - 'OK': 'Aceptar', - 'Cancel': 'Cancelar', - 'Select file': 'Seleccionar archivo', - 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', - 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', - } -} - -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' -IS_MAC = OS == 'Darwin' - -LOG_NAME = 'ZAZ' -CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' - -PYTHON = 'python' -if IS_WIN: - PYTHON = 'python.exe' - -CALC = 'calc' -WRITER = 'writer' - -OBJ_CELL = 'ScCellObj' -OBJ_RANGE = 'ScCellRangeObj' -OBJ_RANGES = 'ScCellRangesObj' -OBJ_TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_RANGES) - -TEXT_RANGE = 'SwXTextRange' -TEXT_RANGES = 'SwXTextRanges' -TEXT_TYPE_RANGES = (TEXT_RANGE, TEXT_RANGES) - -TYPE_DOC = { - '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.DocumentDataSource', - 'math': 'com.sun.star.formula.FormulaProperties', - 'basic': 'com.sun.star.script.BasicIDE', - 'main': 'com.sun.star.frame.StartModule', -} - -NODE_MENUBAR = 'private:resource/menubar/menubar' -MENUS_MAIN = { - 'file': '.uno:PickList', - 'tools': '.uno:ToolsMenu', - 'help': '.uno:HelpMenu', -} -MENUS_CALC = { - 'file': '.uno:PickList', - 'edit': '.uno:EditMenu', - 'view': '.uno:ViewMenu', - 'insert': '.uno:InsertMenu', - 'format': '.uno:FormatMenu', - 'styles': '.uno:FormatStylesMenu', - 'sheet': '.uno:SheetMenu', - 'data': '.uno:DataMenu', - 'tools': '.uno:ToolsMenu', - 'windows': '.uno:WindowList', - 'help': '.uno:HelpMenu', -} -MENUS_WRITER = { - 'file': '.uno:PickList', - 'edit': '.uno:EditMenu', - 'view': '.uno:ViewMenu', - 'insert': '.uno:InsertMenu', - 'format': '.uno:FormatMenu', - 'styles': '.uno:FormatStylesMenu', - 'sheet': '.uno:TableMenu', - 'data': '.uno:FormatFormMenu', - 'tools': '.uno:ToolsMenu', - 'windows': '.uno:WindowList', - 'help': '.uno:HelpMenu', -} -MENUS_APP = { - 'main': MENUS_MAIN, - 'calc': MENUS_CALC, - 'writer': MENUS_WRITER, -} - -EXT = { - 'pdf': 'pdf', -} - -FILE_NAME_DEBUG = 'debug.odt' -FILE_NAME_CONFIG = 'zaz-{}.json' LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' LOG_DATE = '%d/%m/%Y %H:%M:%S' logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') @@ -214,19 +106,135 @@ logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) log = logging.getLogger(__name__) -_start = 0 -_stop_thread = {} +# ~ You can get custom salt +# ~ codecs.encode(os.urandom(16), 'hex') +SALT = b'c9548699d4e432dfd2b46adddafbb06d' + TIMEOUT = 10 +LOG_NAME = 'ZAZ' +FILE_NAME_CONFIG = 'zaz-{}.json' + +LEFT = 0 +CENTER = 1 +RIGHT = 2 + +CALC = 'calc' +WRITER = 'writer' +DRAW = 'draw' +IMPRESS = 'impress' +BASE = 'base' +MATH = 'math' +BASIC = 'basic' +MAIN = 'main' +TYPE_DOC = { + CALC: 'com.sun.star.sheet.SpreadsheetDocument', + WRITER: 'com.sun.star.text.TextDocument', + DRAW: 'com.sun.star.drawing.DrawingDocument', + IMPRESS: 'com.sun.star.presentation.PresentationDocument', + BASE: 'com.sun.star.sdb.DocumentDataSource', + MATH: 'com.sun.star.formula.FormulaProperties', + BASIC: 'com.sun.star.script.BasicIDE', + MAIN: 'com.sun.star.frame.StartModule', +} + +OBJ_CELL = 'ScCellObj' +OBJ_RANGE = 'ScCellRangeObj' +OBJ_RANGES = 'ScCellRangesObj' +TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_RANGES) + +OBJ_SHAPES = 'com.sun.star.drawing.SvxShapeCollection' +OBJ_SHAPE = 'com.sun.star.comp.sc.ScShapeObj' +OBJ_GRAPHIC = 'SwXTextGraphicObject' + +OBJ_TEXTS = 'SwXTextRanges' +OBJ_TEXT = 'SwXTextRange' + +# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL +class FilterOperator(IntEnum): + EMPTY = 0 + NO_EMPTY = 1 + EQUAL = 2 + NOT_EQUAL = 3 + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 +class Border(IntEnum): + NO_BORDER = 0 + BORDER = 1 + SIMPLE = 2 + +OS = platform.system() +IS_WIN = OS == 'Windows' +IS_MAC = OS == 'Darwin' +USER = getpass.getuser() +PC = platform.node() +DESKTOP = os.environ.get('DESKTOP_SESSION', '') +INFO_DEBUG = f"{sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) + +_MACROS = {} +_start = 0 + SECONDS_DAY = 60 * 60 * 24 +DIR = { + 'images': 'images', + 'locales': 'locales', +} +DEFAULT_MIME_TYPE = 'png' +MODIFIERS = { + 'shift': KeyModifier.SHIFT, + 'ctrl': KeyModifier.MOD1, + 'alt': KeyModifier.MOD2, + 'ctrlmac': KeyModifier.MOD3, +} + +# ~ Menus +NODE_MENUBAR = 'private:resource/menubar/menubar' +MENUS = { + 'file': '.uno:PickList', + 'tools': '.uno:ToolsMenu', + 'help': '.uno:HelpMenu', + 'windows': '.uno:WindowList', + 'edit': '.uno:EditMenu', + 'view': '.uno:ViewMenu', + 'insert': '.uno:InsertMenu', + 'format': '.uno:FormatMenu', + 'styles': '.uno:FormatStylesMenu', + 'sheet': '.uno:SheetMenu', + 'data': '.uno:DataMenu', + 'table': '.uno:TableMenu', + 'form': '.uno:FormatFormMenu', + 'page': '.uno:PageMenu', + 'shape': '.uno:ShapeMenu', + 'slide': '.uno:SlideMenu', + 'show': '.uno:SlideShowMenu', +} + +MIME_TYPE = { + 'png': 'image/png', + 'jpg': 'image/jpeg', +} + +MESSAGES = { + 'es': { + 'OK': 'Aceptar', + 'Cancel': 'Cancelar', + 'Select path': 'Seleccionar ruta', + 'Select directory': 'Seleccionar directorio', + 'Select file': 'Seleccionar archivo', + 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', + 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', + } +} CTX = uno.getComponentContext() SM = CTX.getServiceManager() -def create_instance(name, with_context=False): +def create_instance(name: str, with_context: bool=False, args: Any=None) -> Any: if with_context: instance = SM.createInstanceWithContext(name, CTX) + elif args: + instance = SM.createInstanceWithArguments(name, (args,)) else: instance = SM.createInstance(name) return instance @@ -248,33 +256,41 @@ def get_app_config(node_name, key=''): return '' -# ~ FILTER_PDF = '/org.openoffice.Office.Common/Filter/PDF/Export/' LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') LANG = LANGUAGE.split('-')[0] NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') -nd = '/org.openoffice.Office.Calc/Calculate/Other/Date' -d = get_app_config(nd, 'DD') -m = get_app_config(nd, 'MM') -y = get_app_config(nd, 'YY') +INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{INFO_DEBUG}" + +node = '/org.openoffice.Office.Calc/Calculate/Other/Date' +y = get_app_config(node, 'YY') +m = get_app_config(node, 'MM') +d = get_app_config(node, 'DD') DATE_OFFSET = datetime.date(y, m, d).toordinal() -def mri(obj): - m = create_instance('mytools.Mri') - if m is None: - msg = 'Extension MRI not found' - error(msg) - return - - m.inspect(obj) +def error(info): + log.error(info) return -def inspect(obj): - zaz = create_instance('net.elmau.zaz.inspect') - zaz.inspect(obj) +def debug(*args): + data = [str(a) for a in args] + log.debug('\t'.join(data)) + return + + +def info(*args): + data = [str(a) for a in args] + log.info('\t'.join(data)) + return + + +def save_log(path, data): + with open(path, 'a') as f: + f.write(f'{str(now())[:19]} -{LOG_NAME}- ') + pprint(data, stream=f) return @@ -291,47 +307,22 @@ def catch_exception(f): return func -class LogWin(object): +def inspect(obj: Any) -> None: + zaz = create_instance('net.elmau.zaz.inspect') + if hasattr(obj, 'obj'): + obj = obj.obj + zaz.inspect(obj) + return - 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) + '\n\n', 0) +def mri(obj): + m = create_instance('mytools.Mri') + if m is None: + msg = 'Extension MRI not found' + error(msg) return - -def info(data): - log.info(data) - return - - -def debug(*info): - if IS_WIN: - doc = get_document(FILE_NAME_DEBUG) - if doc is None: - return - doc = LogWin(doc.obj) - doc.write(str(info)) - return - - data = [str(d) for d in info] - log.debug('\t'.join(data)) - return - - -def error(info): - log.error(info) - return - - -def save_log(path, data): - with open(path, 'a') as out: - out.write('{} -{}- '.format(str(now())[:19], LOG_NAME)) - pprint(data, stream=out) + m.inspect(obj) return @@ -346,7 +337,7 @@ def run_in_thread(fn): def now(only_time=False): now = datetime.datetime.now() if only_time: - return now.time() + now = now.time() return now @@ -354,64 +345,14 @@ def today(): return datetime.date.today() -def get_date(year, month, day, hour=-1, minute=-1, second=-1): - if hour > -1 or minute > -1 or second > -1: - h = hour - m = minute - s = second - if h == -1: - h = 0 - if m == -1: - m = 0 - if s == -1: - s = 0 - d = datetime.datetime(year, month, day, h, m, s) - else: - d = datetime.date(year, month, day) - return d - - -def get_config(key='', default=None, prefix='config'): - path_json = FILE_NAME_CONFIG.format(prefix) - values = None - path = join(get_config_path('UserConfig'), path_json) - if not exists_path(path): - return default - - with open(path, 'r', encoding='utf-8') as fh: - data = fh.read() - values = json.loads(data) - - if key: - return values.get(key, default) - - return values - - -def set_config(key, value, prefix='config'): - path_json = FILE_NAME_CONFIG.format(prefix) - path = join(get_config_path('UserConfig'), path_json) - values = get_config(default={}, prefix=prefix) - values[key] = value - with open(path, 'w', encoding='utf-8') as fh: - json.dump(values, fh, ensure_ascii=False, sort_keys=True, indent=4) - return True - - -def sleep(seconds): - time.sleep(seconds) - return - - def _(msg): - L = LANGUAGE.split('-')[0] - if L == 'en': + if LANG == 'en': return msg - if not L in MSG_LANG: + if not LANG in MESSAGES: return msg - return MSG_LANG[L][msg] + return MESSAGES[LANG][msg] def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): @@ -421,13 +362,13 @@ def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infob """ toolkit = create_instance('com.sun.star.awt.Toolkit') parent = toolkit.getDesktopWindow() - mb = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message)) - return mb.execute() + box = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message)) + return box.execute() def question(message, title=TITLE): - res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') - return res == YES + result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') + return result == YES def warning(message, title=TITLE): @@ -438,183 +379,597 @@ 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 call_dispatch(url, args=()): - frame = get_document().frame - dispatch = get_dispatch() - dispatch.executeDispatch(frame, url, '', 0, args) - return - - -def get_temp_file(only_name=False): - delete = True - if IS_WIN: - delete = False - tmp = tempfile.NamedTemporaryFile(delete=delete) - if only_name: - tmp = tmp.name - return tmp - -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(path): - return Path(path).exists() - - -def get_type_doc(obj): +def get_type_doc(obj: Any) -> str: for k, v in TYPE_DOC.items(): if obj.supportsService(v): return k return '' -def dict_to_property(values, uno_any=False): +def _get_class_doc(obj: Any) -> Any: + classes = { + CALC: LOCalc, + WRITER: LOWriter, + DRAW: LODraw, + IMPRESS: LOImpress, + BASE: LOBase, + MATH: LOMath, + BASIC: LOBasic, + } + type_doc = get_type_doc(obj) + return classes[type_doc](obj) + + +def dict_to_property(values: dict, uno_any: bool=False): ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) if uno_any: ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps) return ps -def dict_to_named(values): - ps = tuple([NamedValue(n, v) for n, v in values.items()]) - return ps - - -def property_to_dict(values): - d = {i.Name: i.Value for i in values} +def _array_to_dict(values): + d = {v[0]: v[1] for v in values} return d -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) +def _property_to_dict(values): + d = {v.Name: v.Value for v in values} + return d + + +def json_dumps(data): + return json.dumps(data, indent=4, sort_keys=True) + + +def json_loads(data): + return json.loads(data) + + +def data_to_dict(data): + if isinstance(data, tuple) and isinstance(data[0], tuple): + return _array_to_dict(data) + + if isinstance(data, tuple) and isinstance(data[0], (PropertyValue, NamedValue)): + return _property_to_dict(data) + return {} + + +def _get_dispatch() -> Any: + return create_instance('com.sun.star.frame.DispatchHelper') + + +def call_dispatch(frame: Any, url: str, args: dict={}) -> None: + dispatch = _get_dispatch() + opt = dict_to_property(args) + dispatch.executeDispatch(frame, url, '', 0, opt) return -def array_to_dict(values): - d = {r[0]: r[1] for r in values} +def get_desktop(): + return create_instance('com.sun.star.frame.Desktop', True) + + +def _date_to_struct(value): + if isinstance(value, datetime.datetime): + d = DateTime() + d.Year = value.year + d.Month = value.month + d.Day = value.day + d.Hours = value.hour + d.Minutes = value.minute + d.Seconds = value.second + elif isinstance(value, datetime.date): + d = Date() + d.Day = value.day + d.Month = value.month + d.Year = value.year + elif isinstance(value, datetime.time): + d = Time() + d.Hours = value.hour + d.Minutes = value.minute + d.Seconds = value.second return d -# ~ Custom classes -class ObjectBase(object): +def _struct_to_date(value): + d = None + if isinstance(value, Time): + d = datetime.time(value.Hours, value.Minutes, value.Seconds) + elif isinstance(value, Date): + if value != Date(): + d = datetime.date(value.Year, value.Month, value.Day) + elif isinstance(value, DateTime): + if value.Year > 0: + d = datetime.datetime( + value.Year, value.Month, value.Day, + value.Hours, value.Minutes, value.Seconds) + return d + + +def _get_url_script(args): + library = args['library'] + module = '.' + name = args['name'] + language = args.get('language', 'Python') + location = args.get('location', 'user') + + if language == 'Python': + module = '.py$' + elif language == 'Basic': + module = f".{module}." + if location == 'user': + location = 'application' + + url = 'vnd.sun.star.script' + url = f'{url}:{library}{module}{name}?language={language}&location={location}' + + return url + + +def _call_macro(args): + #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification + + url = _get_url_script(args) + args = args.get('args', ()) + + service = 'com.sun.star.script.provider.MasterScriptProviderFactory' + factory = create_instance(service) + script = factory.createScriptProvider('').getScript(url) + result = script.invoke(args, None, None)[0] + + return result + + +def call_macro(args, in_thread=False): + result = None + if in_thread: + t = threading.Thread(target=_call_macro, args=(args,)) + t.start() + else: + result = _call_macro(args) + return result + + +def run(command, capture=False, split=True): + if not split: + return subprocess.check_output(command, shell=True).decode() + + cmd = shlex.split(command) + result = subprocess.run(cmd, capture_output=capture, text=True) + if capture: + result = result.stdout + else: + result = result.returncode + return result + + +# ~ def popen(command, stdin=None): + # ~ try: + # ~ proc = subprocess.Popen(shlex.split(command), shell=IS_WIN, + # ~ stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + # ~ for line in proc.stdout: + # ~ yield line.decode().rstrip() + # ~ except Exception as e: + # ~ error(e) + # ~ yield (e.errno, e.strerror) + + +def sleep(seconds): + time.sleep(seconds) + return + + +class TimerThread(threading.Thread): + + def __init__(self, event, seconds, macro): + threading.Thread.__init__(self) + self.stopped = event + self.seconds = seconds + self.macro = macro + + def run(self): + info('Timer started... {}'.format(self.macro['name'])) + while not self.stopped.wait(self.seconds): + _call_macro(self.macro) + info('Timer stopped... {}'.format(self.macro['name'])) + return + + +def start_timer(name, seconds, macro): + global _MACROS + _MACROS[name] = threading.Event() + thread = TimerThread(_MACROS[name], seconds, macro) + thread.start() + return + + +def stop_timer(name): + global _MACROS + _MACROS[name].set() + del _MACROS[name] + return + + +def install_locales(path, domain='base', dir_locales=DIR['locales']): + path_locales = _P.join(_P(path).path, dir_locales) + try: + lang = gettext.translation(domain, path_locales, languages=[LANG]) + lang.install() + _ = lang.gettext + except Exception as e: + from gettext import gettext as _ + error(e) + return _ + + +def _export_image(obj, args): + name = 'com.sun.star.drawing.GraphicExportFilter' + exporter = create_instance(name) + path = _P.to_system(args['URL']) + args = dict_to_property(args) + exporter.setSourceDocument(obj) + exporter.filter(args) + return _P.exists(path) + + +def sha256(data): + result = hashlib.sha256(data.encode()).hexdigest() + return result + +def sha512(data): + result = hashlib.sha512(data.encode()).hexdigest() + return result + + +def get_config(key='', default={}, prefix='conf'): + name_file = FILE_NAME_CONFIG.format(prefix) + values = None + path = _P.join(_P.config('UserConfig'), name_file) + if not _P.exists(path): + return default + + values = _P.from_json(path) + if key: + values = values.get(key, default) + + return values + + +def set_config(key, value, prefix='conf'): + name_file = FILE_NAME_CONFIG.format(prefix) + path = _P.join(_P.config('UserConfig'), name_file) + values = get_config(default={}, prefix=prefix) + values[key] = value + result = _P.to_json(path, values) + return result + + +def start(): + global _start + _start = now() + info(_start) + return + + +def end(get_seconds=False): + global _start + e = now() + td = e - _start + result = str(td) + if get_seconds: + result = td.total_seconds() + return result + + +def get_epoch(): + n = now() + return int(time.mktime(n.timetuple())) + + +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +def get_size_screen(): + if IS_WIN: + user32 = ctypes.windll.user32 + res = f'{user32.GetSystemMetrics(0)}x{user32.GetSystemMetrics(1)}' + else: + args = 'xrandr | grep "*" | cut -d " " -f4' + res = run(args, split=False) + return res.strip() + + +def url_open(url, data=None, headers={}, verify=True, get_json=False): + err = '' + req = Request(url) + for k, v in headers.items(): + req.add_header(k, v) + try: + # ~ debug(url) + if verify: + if not data is None and isinstance(data, str): + data = data.encode() + response = urlopen(req, data=data) + else: + context = ssl._create_unverified_context() + response = urlopen(req, context=context) + except HTTPError as e: + error(e) + err = str(e) + except URLError as e: + error(e.reason) + err = str(e.reason) + else: + headers = dict(response.info()) + result = response.read() + if get_json: + result = json.loads(result) + + return result, headers, err + + +def _get_key(password): + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=SALT, + iterations=100000) + key = base64.urlsafe_b64encode(kdf.derive(password.encode())) + return key + + +def encrypt(data, password): + from cryptography.fernet import Fernet + + f = Fernet(_get_key(password)) + if isinstance(data, str): + data = data.encode() + token = f.encrypt(data).decode() + return token + + +def decrypt(token, password): + from cryptography.fernet import Fernet, InvalidToken + + data = '' + f = Fernet(_get_key(password)) + try: + data = f.decrypt(token.encode()).decode() + except InvalidToken as e: + error('Invalid Token') + return data + + +class SmtpServer(object): + + def __init__(self, config): + self._server = None + self._error = '' + self._sender = '' + self._is_connect = self._login(config) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @property + def is_connect(self): + return self._is_connect + + @property + def error(self): + return self._error + + def _login(self, config): + name = config['server'] + port = config['port'] + is_ssl = config['ssl'] + self._sender = config['user'] + hosts = ('gmail' in name or 'outlook' in name) + try: + if is_ssl and hosts: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + self._server.ehlo() + self._server.starttls() + self._server.ehlo() + elif is_ssl: + self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) + self._server.ehlo() + else: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + + self._server.login(self._sender, config['password']) + msg = 'Connect to: {}'.format(name) + debug(msg) + return True + except smtplib.SMTPAuthenticationError as e: + if '535' in str(e): + self._error = _('Incorrect user or password') + return False + if '534' in str(e) and 'gmail' in name: + self._error = _('Allow less secure apps in GMail') + return False + except smtplib.SMTPException as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return False + + def _body(self, msg): + body = msg.replace('\\n', '
') + return body + + def send(self, message): + file_name = 'attachment; filename={}' + email = MIMEMultipart() + email['From'] = self._sender + email['To'] = message['to'] + email['Cc'] = message.get('cc', '') + email['Subject'] = message['subject'] + email['Date'] = formatdate(localtime=True) + if message.get('confirm', False): + email['Disposition-Notification-To'] = email['From'] + email.attach(MIMEText(self._body(message['body']), 'html')) + + for path in message.get('files', ()): + fn = _P(path).file_name + part = MIMEBase('application', 'octet-stream') + part.set_payload(_P.read_bin(path)) + encoders.encode_base64(part) + part.add_header('Content-Disposition', f'attachment; filename={fn}') + email.attach(part) + + receivers = ( + email['To'].split(',') + + email['CC'].split(',') + + message.get('bcc', '').split(',')) + try: + self._server.sendmail(self._sender, receivers, email.as_string()) + msg = 'Email sent...' + debug(msg) + if message.get('path', ''): + self.save_message(email, message['path']) + return True + except Exception as e: + self._error = str(e) + return False + return False + + def save_message(self, email, path): + mbox = mailbox.mbox(path, create=True) + mbox.lock() + try: + msg = mailbox.mboxMessage(email) + mbox.add(msg) + mbox.flush() + finally: + mbox.unlock() + return + + def close(self): + try: + self._server.quit() + msg = 'Close connection...' + debug(msg) + except: + pass + return + + +def _send_email(server, messages): + with SmtpServer(server) as server: + if server.is_connect: + for msg in messages: + server.send(msg) + else: + error(server.error) + return server.error + + +def send_email(server, message): + messages = message + if isinstance(message, dict): + messages = (message,) + t = threading.Thread(target=_send_email, args=(server, messages)) + t.start() + return + + +# ~ Classes + +class LOBaseObject(object): def __init__(self, obj): self._obj = obj + def __setattr__(self, name, value): + exists = hasattr(self, name) + if not exists and not name in ('_obj', '_index'): + setattr(self._obj, name, value) + else: + super().__setattr__(name, value) + def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass - def __getitem__(self, index): - return self.obj[index] + @property + def obj(self): + return self._obj - def __getattr__(self, name): - a = None - if name == 'obj': - a = super().__getattr__(name) - else: - if hasattr(self.obj, name): - a = getattr(self.obj, name) - return a + +class LOImage(object): + TYPE = { + 'png': 'image/png', + 'jpg': 'image/jpeg', + } + + def __init__(self, obj): + self._obj = obj @property def obj(self): return self._obj - @obj.setter - def obj(self, value): - self._obj = value + + @property + def name(self): + return self.obj.Name or 'img' + + @property + def mimetype(self): + return self.obj.Bitmap.MimeType + + def save(self, path, mimetype=DEFAULT_MIME_TYPE): + p = _P(path) + if _P.is_dir(path): + name = self.name + else: + path = p.path + name = p.name + + path = _P.join(path, f'{name}.{mimetype.lower()}') + args = dict( + URL = _P.to_url(path), + MimeType = self.TYPE[mimetype], + ) + if not _export_image(self.obj, args): + path = '' + + # ~ size = len(self.obj.Bitmap.DIB) + # ~ data = self.obj.GraphicStream.readBytes((), size) + # ~ data = data[-1].value + + # ~ data = self.obj.Bitmap.DIB.value + # ~ data = self.obj.Graphic.DIB.value + + # ~ _P.save_bin(path, data) + return path -class LOObjectBase(object): +class LODocument(object): + FILTERS = { + 'doc': 'MS Word 97', + 'docx': 'MS Word 2007 XML', + } def __init__(self, obj): - self.__dict__['_obj'] = obj + self._obj = obj + self._cc = self.obj.getCurrentController() + self._undo = True def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - return True - - def __setattr__(self, name, value): - print('BASE__setattr__', name) - if name == '_obj': - super().__setattr__(name, value) - else: - self.obj.setPropertyValue(name, value) - - # ~ def _try_for_method(self, name): - # ~ a = None - # ~ m = 'get{}'.format(name) - # ~ if hasattr(self.obj, m): - # ~ a = getattr(self.obj, m)() - # ~ else: - # ~ a = getattr(self.obj, name) - # ~ return a - - def __getattr__(self, name): - print('BASE__getattr__', name) - if name == 'obj': - a = super().__getattr__(name) - else: - a = self.obj.getPropertyValue(name) - # ~ Bug - if a is None: - msg = 'Error get: {} - {}'.format(self.obj.ImplementationName, name) - error(msg) - raise Exception(msg) - return a - - @property - def obj(self): - return self._obj - - -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 + self.close() @property def obj(self): @@ -628,12 +983,12 @@ class LODocument(object): self.obj.setTitle(value) @property - def uid(self): - return self.obj.RuntimeUID + def type(self): + return self._type @property - def type(self): - return self._type_doc + def uid(self): + return self.obj.RuntimeUID @property def frame(self): @@ -653,19 +1008,31 @@ class LODocument(object): @property def path(self): - return _path_system(self.obj.getURL()) + return _P.to_system(self.obj.URL) @property - def statusbar(self): + def dir(self): + return _P(self.path).path + + @property + def file_name(self): + return _P(self.path).file_name + + @property + def name(self): + return _P(self.path).name + + @property + def status_bar(self): return self._cc.getStatusIndicator() @property def visible(self): - w = self._cc.getFrame().getContainerWindow() + w = self.frame.ContainerWindow return w.isVisible() @visible.setter def visible(self, value): - w = self._cc.getFrame().getContainerWindow() + w = self.frame.ContainerWindow w.setVisible(value) @property @@ -675,6 +1042,31 @@ class LODocument(object): def zoom(self, value): self._cc.ZoomValue = value + @property + def undo(self): + return self._undo + @undo.setter + def undo(self, value): + self._undo = value + um = self.obj.UndoManager + if value: + try: + um.leaveUndoContext() + except: + pass + else: + um.enterHiddenUndoContext() + + def clear_undo(self): + self.obj.getUndoManager().clear() + return + + @property + def selection(self): + sel = self.obj.CurrentSelection + # ~ return _get_class_uno(sel) + return sel + @property def table_auto_formats(self): taf = create_instance('com.sun.star.sheet.TableAutoFormats') @@ -684,344 +1076,118 @@ class LODocument(object): obj = self.obj.createInstance(name) return obj - def save(self, path='', **kwargs): - # ~ opt = _properties(kwargs) - opt = dict_to_property(kwargs) - if path: - self._obj.storeAsURL(_path_url(path), opt) - else: - self._obj.store() - return True - - def close(self): - self.obj.close(True) + def set_focus(self): + w = self.frame.ComponentWindow + w.setFocus() return - def focus(self): - w = self._cc.getFrame().getComponentWindow() - w.setFocus() + def copy(self): + call_dispatch(self.frame, '.uno:Copy') return def paste(self): sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') transferable = sc.getContents() self._cc.insertTransferable(transferable) - return self.obj.getCurrentSelection() + # ~ return self.obj.getCurrentSelection() + return - def to_pdf(self, path, **kwargs): + def select(self, obj): + self._cc.select(obj) + return + + def to_pdf(self, path: str='', args: dict={}): path_pdf = path - if path: - if is_dir(path): - _, _, n, _ = get_info_path(self.path) - path_pdf = join(path, '{}.{}'.format(n, EXT['pdf'])) - else: - path_pdf = replace_ext(self.path, EXT['pdf']) - filter_name = '{}_pdf_Export'.format(self.type) - filter_data = dict_to_property(kwargs, True) + filter_data = dict_to_property(args, True) args = { 'FilterName': filter_name, 'FilterData': filter_data, } - args = dict_to_property(args) + opt = dict_to_property(args) try: - self.obj.storeToURL(_path_url(path_pdf), args) + self.obj.storeToURL(_P.to_url(path), opt) except Exception as e: error(e) path_pdf = '' - return path_pdf + return _P.exists(path_pdf) - # ~ If location="document" Then - # ~ sp = ThisComponent.getScriptProvider() - - -class FormControlBase(object): - EVENTS = { - 'action': 'actionPerformed', - 'click': 'mousePressed', - } - TYPES = { - 'actionPerformed': 'XActionListener', - 'mousePressed': 'XMouseListener', - } - - def __init__(self, obj): - self._obj = obj - self._index = -1 - self._rules = {} - - @property - def obj(self): - return self._obj - - @property - def name(self): - return self.obj.Name - - @property - def form(self): - return self.obj.getParent() - - @property - def index(self): - return self._index - @index.setter - def index(self, value): - self._index = value - - @property - def events(self): - return self.form.getScriptEvents(self.index) - - def remove_event(self, name=''): - for ev in self.events: - if name and \ - ev.EventMethod == self.EVENTS[name] and \ - ev.ListenerType == self.TYPES[ev.EventMethod]: - self.form.revokeScriptEvent(self.index, - ev.ListenerType, ev.EventMethod, ev.AddListenerParam) - break - else: - self.form.revokeScriptEvent(self.index, - ev.ListenerType, ev.EventMethod, ev.AddListenerParam) - return - - def add_event(self, name, macro): - if not 'name' in macro: - macro['name'] = '{}_{}'.format(self.name, name) - - event = ScriptEventDescriptor() - event.AddListenerParam = '' - event.EventMethod = self.EVENTS[name] - event.ListenerType = self.TYPES[event.EventMethod] - event.ScriptCode = _get_url_script(macro) - event.ScriptType = 'Script' - - for ev in self.events: - if ev.EventMethod == event.EventMethod and \ - ev.ListenerType == event.ListenerType: - self.form.revokeScriptEvent(self.index, - event.ListenerType, event.EventMethod, event.AddListenerParam) - break - - self.form.registerScriptEvent(self.index, event) - return - - -class FormButton(FormControlBase): - - def __init__(self, obj): - super().__init__(obj) - - - -class LOForm(ObjectBase): - - def __init__(self, obj): - super().__init__(obj) - self._init_controls() - - def __getitem__(self, index): - if isinstance(index, int): - return self._controls[index] - else: - return getattr(self, index) - - def _get_type_control(self, name): - types = { - # ~ 'stardiv.Toolkit.UnoFixedTextControl': 'label', - 'com.sun.star.form.OButtonModel': 'formbutton', - # ~ 'stardiv.Toolkit.UnoEditControl': 'text', - # ~ 'stardiv.Toolkit.UnoRoadmapControl': 'roadmap', - # ~ 'stardiv.Toolkit.UnoFixedHyperlinkControl': 'link', - # ~ 'stardiv.Toolkit.UnoListBoxControl': 'listbox', + def export(self, path: str, ext: str='', args: dict={}): + if not ext: + ext = _P(path).ext + filter_name = self.FILTERS[ext] + filter_data = dict_to_property(args, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, } - return types[name] + opt = dict_to_property(args) + try: + self.obj.storeToURL(_P.to_url(path), opt) + except Exception as e: + error(e) + path = '' + return _P.exists(path) - def _init_controls(self): - self._controls = [] - for i, c in enumerate(self.obj.ControlModels): - tipo = self._get_type_control(c.ImplementationName) - control = get_custom_class(tipo, c) - control.index = i - self._controls.append(control) - setattr(self, c.Name, control) - - @property - def name(self): - return self._obj.getName() - @name.setter - def name(self, value): - self._obj.setName(value) - - -class LOForms(ObjectBase): - - def __init__(self, obj, doc): - self._doc = doc - super().__init__(obj) - - def __getitem__(self, index): - form = super().__getitem__(index) - return LOForm(form) - - @property - def doc(self): - return self._doc - - @property - def count(self): - return self.obj.getCount() - - @property - def names(self): - return self.obj.getElementNames() - - def exists(self, name): - return name in self.names - - def insert(self, name): - form = self.doc.create_instance('com.sun.star.form.component.Form') - self.obj.insertByName(name, form) - return self[name] - - def remove(self, index): - if isinstance(index, int): - self.obj.removeByIndex(index) + def save(self, path: str='', args: dict={}) -> bool: + result = True + opt = dict_to_property(args) + if path: + try: + self.obj.storeAsURL(_P.to_url(path), opt) + except Exception as e: + error(e) + result = False else: - self.obj.removeByName(index) + self.obj.store() + return result + + def close(self): + self.obj.close(True) return -class LOCellStyle(LOObjectBase): - - def __init__(self, obj): - super().__init__(obj) - - @property - def name(self): - return self.obj.Name - - def apply(self, properties): - set_properties(self.obj, properties) - return - - -class LOCellStyles(object): - - def __init__(self, obj): - self._obj = obj - - def __len__(self): - return len(self.obj) - - def __getitem__(self, index): - return LOCellStyle(self.obj[index]) - - def __setitem__(self, key, value): - self.obj[key] = value - - def __delitem__(self, key): - if not isinstance(key, str): - key = key.Name - del self.obj[key] - - def __contains__(self, item): - return item in self.obj - - @property - def obj(self): - return self._obj - - @property - def names(self): - return self.obj.ElementNames - - def apply(self, style, properties): - set_properties(style, properties) - return - - -class LOImage(object): - TYPES = { - 'image/png': 'png', - 'image/jpeg': 'jpg', - } - - def __init__(self, obj): - self._obj = obj - - @property - def obj(self): - return self._obj - - @property - def address(self): - return self.obj.Anchor.AbsoluteName - - @property - def name(self): - return self.obj.Name - - @property - def mimetype(self): - return self.obj.Bitmap.MimeType - - @property - def url(self): - return _path_system(self.obj.URL) - @url.setter - def url(self, value): - self.obj.URL = _path_url(value) - - @property - def path(self): - return _path_system(self.obj.GraphicURL) - @path.setter - def path(self, value): - self.obj.GraphicURL = _path_url(value) - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self_obj.Visible = value - - def save(self, path): - if is_dir(path): - p = path - n = self.name - else: - p, fn, n, e = get_info_path(path) - ext = self.TYPES[self.mimetype] - path = join(p, '{}.{}'.format(n, ext)) - size = len(self.obj.Bitmap.DIB) - data = self.obj.GraphicStream.readBytes((), size) - data = data[-1].value - save_file(path, 'wb', data) - return path - - class LOCalc(LODocument): def __init__(self, obj): super().__init__(obj) - self._sheets = obj.getSheets() + self._type = CALC + self._sheets = obj.Sheets def __getitem__(self, index): - if isinstance(index, str): - code_name = [s.Name for s in self._sheets if s.CodeName == index] - if code_name: - index = code_name[0] - return LOCalcSheet(self._sheets[index], self) + return LOCalcSheet(self._sheets[index]) def __setitem__(self, key, value): self._sheets[key] = value + def __len__(self): + return self._sheets.Count + def __contains__(self, item): - return item in self.obj.Sheets + return item in self._sheets + + @property + def names(self): + names = self.obj.Sheets.ElementNames + return names + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName in TYPE_RANGES: + sel = LOCalcRange(sel) + elif sel.ImplementationName == OBJ_SHAPES: + if len(sel) == 1: + sel = sel[0] + sel = LODrawPage(sel.Parent)[sel.Name] + else: + debug(sel.ImplementationName) + return sel + + @property + def active(self): + return LOCalcSheet(self._cc.ActiveSheet) @property def headers(self): @@ -1038,90 +1204,62 @@ class LOCalc(LODocument): self._cc.SheetTabs = value @property - def active(self): - return LOCalcSheet(self._cc.getActiveSheet(), self) + def db_ranges(self): + # ~ return LOCalcDataBaseRanges(self.obj.DataBaseRanges) + return self.obj.DatabaseRanges def activate(self, sheet): obj = sheet if isinstance(sheet, LOCalcSheet): obj = sheet.obj elif isinstance(sheet, str): - obj = self[sheet].obj + obj = self._sheets[sheet] self._cc.setActiveSheet(obj) return - @property - def selection(self): - sel = self.obj.getCurrentSelection() - if sel.ImplementationName in OBJ_TYPE_RANGES: - sel = LOCellRange(sel, self) - return sel + def new_sheet(self): + s = self.create_instance('com.sun.star.sheet.Spreadsheet') + return s - @property - def sheets(self): - return LOCalcSheets(self._sheets, self) - - @property - def names(self): - return self.sheets.names - - @property - def cell_style(self): - obj = self.obj.getStyleFamilies()['CellStyles'] - return LOCellStyles(obj) - - def create(self): - return self.obj.createInstance('com.sun.star.sheet.Spreadsheet') - - def insert(self, name, pos=-1): - # ~ sheet = obj.createInstance('com.sun.star.sheet.Spreadsheet') - # ~ obj.Sheets['New'] = sheet - index = pos - if pos < 0: - index = self._sheets.Count + pos + 1 + def insert(self, name): + names = name if isinstance(name, str): - self._sheets.insertNewByName(name, index) - else: - for n in name: - self._sheets.insertNewByName(n, index) - name = n - return LOCalcSheet(self._sheets[name], self) + names = (name,) + for n in names: + self._sheets[n] = self.new_sheet() + return LOCalcSheet(self._sheets[n]) def move(self, name, pos=-1): - return self.sheets.move(name, pos) - - def remove(self, name): - return self.sheets.remove(name) - - def copy(self, source='', target='', pos=-1): index = pos if pos < 0: - index = self._sheets.Count + pos + 1 + index = len(self) + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.moveByName(name, index) + return - names = source - if not names: - names = self.names - elif isinstance(source, str): - names = (source,) + def remove(self, name): + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.removeByName(name) + return - new_names = target - if not target: - new_names = [n + '_2' for n in names] - elif isinstance(target, str): - new_names = (target,) - - for i, ns in enumerate(names): - self.sheets.copy(ns, new_names[i], index + i) - - return LOCalcSheet(self._sheets[index], self) + def copy(self, name, new_name='', pos=-1): + if isinstance(name, LOCalcSheet): + name = name.name + index = pos + if pos < 0: + index = len(self) + self._sheets.copyByName(name, new_name, index) + return LOCalcSheet(self._sheets[new_name]) def copy_from(self, doc, source='', target='', pos=-1): index = pos if pos < 0: - index = self._sheets.Count + pos + 1 + index = len(self) names = source - if not names: + if not source: names = doc.names elif isinstance(source, str): names = (source,) @@ -1132,118 +1270,37 @@ class LOCalc(LODocument): elif isinstance(target, str): new_names = (target,) - for i, n in enumerate(names): - self._sheets.importSheet(doc.obj, n, index + i) - self.sheets[index + i].name = new_names[i] + for i, name in enumerate(names): + self._sheets.importSheet(doc.obj, name, index + i) + self[index + i].name = new_names[i] - # ~ doc.getCurrentController().setActiveSheet(sheet) - # ~ For controls in sheet - # ~ doc.getCurrentController().setFormDesignMode(False) - - return LOCalcSheet(self._sheets[index], self) + return LOCalcSheet(self._sheets[index]) def sort(self, reverse=False): names = sorted(self.names, reverse=reverse) for i, n in enumerate(names): - self.sheets.move(n, i) + self.move(n, i) return - 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 - - def create_cell_style(self, name=''): - obj = self.create_instance('com.sun.star.style.CellStyle') - if name: - self.cell_style[name] = obj - return LOCellStyle(obj) - - def clear_undo(self): - self.obj.getUndoManager().clear() - return - - def filter_by_color(self, cell=None): - if cell is None: - cell = self.selection.first - cr = cell.current_region - col = cell.column - cr.column - rangos = cell.get_column(col).visible - for r in rangos: - for row in range(r.rows): - c = r[row, 0] - if c.back_color != cell.back_color: - c.rows_visible = False - return + def render(self, data, sheet=None, clean=True): + if sheet is None: + sheet = self.active + return sheet.render(data, clean=clean) -class LOCalcSheets(object): +class LOChart(object): - def __init__(self, obj, doc): + def __init__(self, name, obj, draw_page): + self._name = name self._obj = obj - self._doc = doc + self._eobj = self._obj.EmbeddedObject + self._type = 'Column' + self._cell = None + self._shape = self._get_shape(draw_page) + self._pos = self._shape.Position def __getitem__(self, index): - return LOCalcSheet(self.obj[index], self.doc) - - @property - def obj(self): - return self._obj - - @property - def doc(self): - return self._doc - - @property - def count(self): - return self.obj.Count - - @property - def names(self): - return self.obj.ElementNames - - def copy(self, name, new_name, pos): - self.obj.copyByName(name, new_name, pos) - return - - def move(self, name, pos): - index = pos - if pos < 0: - index = self.count + pos + 1 - sheet = self.obj[name] - self.obj.moveByName(sheet.Name, index) - return - - def remove(self, name): - sheet = self.obj[name] - self.obj.removeByName(sheet.Name) - 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) + return LOBaseObject(self.diagram.getDataRowProperties(index)) def __enter__(self): return self @@ -1251,22 +1308,201 @@ class LOCalcSheet(object): def __exit__(self, exc_type, exc_value, traceback): pass - def _init_values(self): - self._events = None - self._dp = self.obj.getDrawPage() - self._images = {i.Name: LOImage(i) for i in self._dp} - @property def obj(self): return self._obj @property - def doc(self): - return self._doc + def name(self): + return self._name @property - def images(self): - return self._images + def diagram(self): + return self._eobj.Diagram + + @property + def type(self): + return self._type + @type.setter + def type(self, value): + self._type = value + if value == 'Bar': + self.diagram.Vertical = True + return + type_chart = f'com.sun.star.chart.{value}Diagram' + self._eobj.setDiagram(self._eobj.createInstance(type_chart)) + + @property + def cell(self): + return self._cell + @cell.setter + def cell(self, value): + self._cell = value + self._shape.Anchor = value.obj + + @property + def position(self): + return self._pos + @position.setter + def position(self, value): + self._pos = value + self._shape.Position = value + + def _get_shape(self, draw_page): + for shape in draw_page: + if shape.PersistName == self.name: + break + return shape + + +class LOSheetCharts(object): + + def __init__(self, obj, sheet): + self._obj = obj + self._sheet = sheet + + def __getitem__(self, index): + return LOChart(index, self.obj[index], self._sheet.draw_page) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + def new(self, name, pos_size, data): + self.obj.addNewByName(name, pos_size, data, True, True) + return LOChart(name, self.obj[name], self._sheet.draw_page) + + +class LOFormControl(LOBaseObject): + + def __init__(self, obj): + self._obj = obj + self._control = self.doc.CurrentController.getControl(self.obj) + + def __setattr__(self, name, value): + if name == '_control': + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + @property + def doc(self): + return self.obj.Parent.Parent.Parent + + @property + def name(self): + return self.obj.Name + + @property + def label(self): + return self.obj.Label + + def set_focus(self): + self._control.setFocus() + return + + +class LOForm(object): + + def __init__(self, obj): + self._obj = obj + + def __getitem__(self, index): + return LOFormControl(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + +class LOSheetForms(object): + + def __init__(self, obj): + self._obj = obj + + def __getitem__(self, index): + return LOForm(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + +class LOSheetRows(object): + + def __init__(self, sheet): + self._sheet = sheet + self._obj = sheet.obj.Rows + + def __getitem__(self, index): + return LOSheetRows(self.obj[index]) + + @property + def obj(self): + return self._obj + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + end = index + count + return self._sheet[index:end,0:] + + +class LOCalcSheet(object): + + def __init__(self, obj): + self._obj = obj + + def __getitem__(self, index): + return LOCalcRange(self.obj[index]) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __str__(self): + return f'easymacro.LOCalcSheet: {self.name}' + + @property + def obj(self): + return self._obj @property def name(self): @@ -1282,27 +1518,12 @@ class LOCalcSheet(object): def code_name(self, value): self._obj.CodeName = value - @property - def color(self): - return self._obj.TabColor - @color.setter - def color(self, value): - self._obj.TabColor = get_color(value) - - @property - def active(self): - return self.doc.selection.first - - def activate(self): - self.doc.activate(self.obj) - return - @property def visible(self): - return self.obj.IsVisible + return self._obj.IsVisible @visible.setter def visible(self, value): - self.obj.IsVisible = value + self._obj.IsVisible = value @property def is_protected(self): @@ -1323,172 +1544,575 @@ class LOCalcSheet(object): pass return False - def get_cursor(self, cell): - return self.obj.createCursorByRange(cell) + @property + def color(self): + return self._obj.TabColor + @color.setter + def color(self, value): + self._obj.TabColor = get_color(value) - def exists_chart(self, name): - return name in self.obj.Charts.ElementNames + @property + def used_area(self): + cursor = self.get_cursor() + cursor.gotoEndOfUsedArea(True) + return LOCalcRange(self[cursor.AbsoluteName].obj) + + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def dp(self): + return self.draw_page + + @property + def shapes(self): + return self.draw_page + + @property + def doc(self): + return LOCalc(self.obj.DrawPage.Forms.Parent) + + @property + def charts(self): + return LOSheetCharts(self.obj.Charts, self) + + @property + def rows(self): + return LOSheetRows(self) @property def forms(self): - return LOForms(self._dp.getForms(), self.doc) + return LOSheetForms(self.obj.DrawPage.Forms) - @property - def events(self): - return self._events - @events.setter - def events(self, controllers): - self._events = controllers - self._connect_listeners() - - def _connect_listeners(self): - if self.events is None: - return - - listeners = { - 'addModifyListener': EventsModify, - } - for key, value in listeners.items(): - getattr(self.obj, key)(listeners[key](self.events)) - print('add_listener') + def activate(self): + self.doc.activate(self._obj) return + def clean(self): + doc = self.doc + sheet = doc.create_instance('com.sun.star.sheet.Spreadsheet') + doc._sheets.replaceByName(self.name, sheet) + return -class LOWriter(LODocument): + def move(self, pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.moveByName(self.name, index) + return + + def remove(self): + self.doc._sheets.removeByName(self.name) + return + + def copy(self, new_name='', pos=-1): + index = pos + if pos < 0: + index = len(self.doc) + self.doc._sheets.copyByName(self.name, new_name, index) + return LOCalcSheet(self.doc._sheets[new_name]) + + def copy_to(self, doc, target='', pos=-1): + index = pos + if pos < 0: + index = len(doc) + name = self.name + if not target: + new_name = name + + doc._sheets.importSheet(self.doc.obj, name, index) + sheet = doc[name] + sheet.name = new_name + return sheet + + def get_cursor(self, cell=None): + if cell is None: + cursor = self.obj.createCursor() + else: + cursor = self.obj.createCursorByRange(cell) + return cursor + + def render(self, data, rango=None, clean=True): + if rango is None: + rango = self.used_area + return rango.render(data, clean) + + +class LOCalcRows(object): def __init__(self, obj): - super().__init__(obj) + self._obj = obj + + def __len__(self): + return self.obj.Count + + def __str__(self): + return 'Rows' @property def obj(self): return self._obj @property - def string(self): - return self._obj.getText().String + def count(self): + return len(self) @property - def text(self): - return self._obj.getText() - - @property - def cursor(self): - return self.text.createTextCursor() - - @property - def paragraphs(self): - return [LOTextRange(p) for p in self.text] - - @property - def selection(self): - sel = self.obj.getCurrentSelection() - if sel.ImplementationName == TEXT_RANGES: - return LOTextRange(sel[0]) - elif sel.ImplementationName == TEXT_RANGE: - return LOTextRange(sel) - return sel - - def write(self, data, cursor=None): - cursor = cursor or self.selection.cursor.getEnd() - if data.startswith('\n'): - c = data.split('\n') - for i in range(len(c)-1): - self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) - else: - self.text.insertString(cursor, data, False) - return - - def insert_table(self, data, cursor=None): - cursor = cursor or self.selection.cursor.getEnd() - table = self.obj.createInstance('com.sun.star.text.TextTable') - rows = len(data) - cols = len(data[0]) - table.initialize(rows, cols) - self.insert_content(cursor, table) - table.DataArray = data - return WriterTable(table) - - def create_chart(self, tipo, cursor=None): - cursor = cursor or self.selection.cursor.getEnd() - chart = LOChart(None, tipo) - chart.cursor = cursor - chart.doc = self - return chart - - def insert_content(self, cursor, data, replace=False): - self.text.insertTextContent(cursor, data, replace) - return - - # ~ 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 - - def go_start(self): - cursor = self._cc.getViewCursor() - cursor.gotoStart(False) - return cursor - - def go_end(self): - cursor = self._cc.getViewCursor() - cursor.gotoEnd(False) - return cursor - - def select(self, text): - self._cc.select(text) - return - - def search(self, options): - descriptor = self.obj.createSearchDescriptor() - descriptor.setSearchString(options.get('Search', '')) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if 'Attributes' in options: - attr = dict_to_property(options['Attributes']) - descriptor.setSearchAttributes(attr) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - - if options.get('First', False): - found = self.obj.findFirst(descriptor) - else: - found = self.obj.findAll(descriptor) - - return found - - def replace(self, options): - descriptor = self.obj.createReplaceDescriptor() - descriptor.setSearchString(options['Search']) - descriptor.setReplaceString(options['Replace']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if 'Attributes' in options: - attr = dict_to_property(options['Attributes']) - descriptor.setSearchAttributes(attr) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - found = self.obj.replaceAll(descriptor) - return found + def visible(self): + return self.obj.IsVisible + @visible.setter + def visible(self, value): + self.obj.IsVisible = value -class LOTextRange(object): +class LOCalcRange(object): def __init__(self, obj): self._obj = obj + self._sd = None + self._is_cell = obj.ImplementationName == OBJ_CELL + + def __getitem__(self, index): + return LOCalcRange(self.obj[index]) + + def __iter__(self): + self._r = 0 + self._c = 0 + return self + + def __next__(self): + try: + rango = self[self._r, self._c] + except Exception as e: + raise StopIteration + self._c += 1 + if self._c == self.columns: + self._c = 0 + self._r +=1 + return rango + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __str__(self): + if self.is_none: + s = 'Range: None' + else: + s = f'Range: {self.name}' + return s + + @property + def obj(self): + return self._obj + + @property + def is_none(self): + return self.obj is None + + @property + def is_cell(self): + return self._is_cell + + @property + def back_color(self): + return self._obj.CellBackColor + @back_color.setter + def back_color(self, value): + self._obj.CellBackColor = get_color(value) + + @property + def dp(self): + return self.sheet.dp + + @property + def sheet(self): + return LOCalcSheet(self.obj.Spreadsheet) + + @property + def doc(self): + doc = self.obj.Spreadsheet.DrawPage.Forms.Parent + return LODocument(doc) + + @property + def name(self): + return self.obj.AbsoluteName + + @property + def code_name(self): + name = self.name.replace('$', '').replace('.', '_').replace(':', '') + return name + + @property + def columns(self): + return self.obj.Columns.Count + + @property + def column(self): + c1 = self.address.Column + c2 = c1 + 1 + ra = self.current_region.range_address + r1 = ra.StartRow + r2 = ra.EndRow + 1 + return LOCalcRange(self.sheet[r1:r2, c1:c2].obj) + + @property + def rows(self): + return LOCalcRows(self.obj.Rows) + + @property + def row(self): + r1 = self.address.Row + r2 = r1 + 1 + ra = self.current_region.range_address + c1 = ra.StartColumn + c2 = ra.EndColumn + 1 + return LOCalcRange(self.sheet[r1:r2, c1:c2].obj) + + @property + def type(self): + return self.obj.Type + + @property + def value(self): + v = None + if self.type == VALUE: + v = self.obj.getValue() + elif self.type == TEXT: + v = self.obj.getString() + elif self.type == FORMULA: + v = self.obj.getFormula() + return v + @value.setter + def value(self, data): + if isinstance(data, str): + # ~ print(isinstance(data, str), data[0]) + if data[0] in '=': + self.obj.setFormula(data) + # ~ print('Set Formula') + else: + self.obj.setString(data) + elif isinstance(data, Decimal): + self.obj.setValue(float(data)) + elif isinstance(data, (int, float, bool)): + self.obj.setValue(data) + elif isinstance(data, datetime.datetime): + d = data.toordinal() + t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY + self.obj.setValue(d - DATE_OFFSET + t) + elif isinstance(data, datetime.date): + d = data.toordinal() + self.obj.setValue(d - DATE_OFFSET) + elif isinstance(data, datetime.time): + d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY + self.obj.setValue(d) + + @property + def date(self): + value = int(self.obj.Value) + date = datetime.date.fromordinal(value + DATE_OFFSET) + return date + + @property + def time(self): + seconds = self.obj.Value * SECONDS_DAY + time_delta = datetime.timedelta(seconds=seconds) + time = (datetime.datetime.min + time_delta).time() + return time + + @property + def datetime(self): + return datetime.datetime.combine(self.date, self.time) + + @property + def data(self): + return self.obj.getDataArray() + @data.setter + def data(self, values): + if self._is_cell: + self.to_size(len(values), len(values[0])).data = values + else: + self.obj.setDataArray(values) + + @property + def dict(self): + rows = self.data + k = rows[0] + data = [dict(zip(k, r)) for r in rows[1:]] + return data + @dict.setter + def dict(self, values): + data = [tuple(values[0].keys())] + data += [tuple(d.values()) for d in values] + self.data = data + + @property + def formula(self): + return self.obj.getFormulaArray() + @formula.setter + def formula(self, values): + self.obj.setFormulaArray(values) + + @property + def array_formula(self): + return self.obj.ArrayFormula + @array_formula.setter + def array_formula(self, value): + self.obj.ArrayFormula = value + + @property + def address(self): + return self.obj.CellAddress + + @property + def range_address(self): + return self.obj.RangeAddress + + @property + def cursor(self): + cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) + return cursor + + @property + def current_region(self): + cursor = self.cursor + cursor.collapseToCurrentRegion() + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + @property + def next_cell(self): + a = self.current_region.range_address + col = a.StartColumn + row = a.EndRow + 1 + return LOCalcRange(self.sheet[row, col].obj) + + @property + def position(self): + return self.obj.Position + + @property + def size(self): + return self.obj.Size + + @property + def possize(self): + data = { + 'Width': self.size.Width, + 'Height': self.size.Height, + 'X': self.position.X, + 'Y': self.position.Y, + } + return data + + @property + def visible(self): + cursor = self.cursor + rangos = cursor.queryVisibleCells() + rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(rangos) + + def select(self): + self.doc.select(self.obj) + return + + def offset(self, rows=0, cols=1): + ra = self.range_address + col = ra.EndColumn + cols + row = ra.EndRow + rows + return LOCalcRange(self.sheet[row, col].obj) + + def to_size(self, rows, cols): + cursor = self.cursor + cursor.collapseToSize(cols, rows) + return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + + def copy_to(self, cell, formula=False): + rango = cell.to_size(self.rows, self.columns) + if formula: + rango.formula = self.data + else: + rango.data = self.data + return + + def copy_from(self, rango, formula=False): + data = rango + if isinstance(rango, LOCalcRange): + if formula: + data = rango.formula + else: + data = rango.data + rows = len(data) + cols = len(data[0]) + if formula: + self.to_size(rows, cols).formula = data + else: + self.to_size(rows, cols).data = data + return + + def auto_width(self): + self.obj.Columns.OptimalWidth = True + return + + def clean_render(self, template='\{(\w.+)\}'): + self._sd.SearchRegularExpression = True + self._sd.setSearchString(template) + self.obj.replaceAll(self._sd) + return + + def render(self, data, clean=True): + self._sd = self.sheet.obj.createSearchDescriptor() + self._sd.SearchCaseSensitive = False + for k, v in data.items(): + cell = self._render_value(k, v) + return cell + + def _render_value(self, key, value, parent=''): + cell = None + if isinstance(value, dict): + for k, v in value.items(): + cell = self._render_value(k, v, key) + return cell + elif isinstance(value, (list, tuple)): + self._render_list(key, value) + return + + search = f'{{{key}}}' + if parent: + search = f'{{{parent}.{key}}}' + ranges = self.find_all(search) + + for cell in ranges or range(0): + self._set_new_value(cell, search, value) + return LOCalcRange(cell) + + def _set_new_value(self, cell, search, value): + if not cell.ImplementationName == 'ScCellObj': + return + + if isinstance(value, str): + pattern = re.compile(search, re.IGNORECASE) + new_value = pattern.sub(value, cell.String) + cell.String = new_value + else: + LOCalcRange(cell).value = value + return + + def _render_list(self, key, rows): + for row in rows: + for k, v in row.items(): + self._render_value(k, v) + return + + def find_all(self, search_string): + if self._sd is None: + self._sd = self.sheet.obj.createSearchDescriptor() + self._sd.SearchCaseSensitive = False + + self._sd.setSearchString(search_string) + ranges = self.obj.findAll(self._sd) + return ranges + + def filter(self, args, with_headers=True): + ff = TableFilterField() + ff.Field = args['Field'] + ff.Operator = args['Operator'] + if isinstance(args['Value'], str): + ff.IsNumeric = False + ff.StringValue = args['Value'] + else: + ff.IsNumeric = True + ff.NumericValue = args['Value'] + + fd = self.obj.createFilterDescriptor(True) + fd.ContainsHeader = with_headers + fd.FilterFields = ((ff,)) + # ~ self.obj.AutoFilter = True + self.obj.filter(fd) + return + + def copy_format_from(self, rango): + rango.select() + self.doc.copy() + self.select() + args = { + 'Flags': 'T', + 'MoveMode': 4, + } + url = '.uno:InsertContents' + call_dispatch(self.doc.frame, url, args) + return + + def to_image(self): + self.select() + self.doc.copy() + args = {'SelectedFormat': 141} + url = '.uno:ClipboardFormatItems' + call_dispatch(self.doc.frame, url, args) + return self.sheet.shapes[-1] + + def insert_image(self, path, args={}): + ps = self.possize + args['Width'] = args.get('Width', ps['Width']) + args['Height'] = args.get('Height', ps['Height']) + args['X'] = args.get('X', ps['X']) + args['Y'] = args.get('Y', ps['Y']) + # ~ img.ResizeWithCell = True + img = self.sheet.dp.insert_image(path, args) + img.Anchor = self.obj + args.clear() + return img + + def filter_by_color(self, cell): + rangos = cell.column[1:,:].visible + for r in rangos: + for c in r: + if c.back_color != cell.back_color: + c.rows.visible = False + return + + def clear(self, what=1023): + # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html + self.obj.clearContents(what) + return + + +class LOWriterPageStyle(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + def __str__(self): + return f'Page Style: {self.name}' + + @property + def name(self): + return self._obj.Name + + +class LOWriterPageStyles(object): + + def __init__(self, styles): + self._styles = styles + + def __getitem__(self, index): + return LOWriterPageStyle(self._styles[index]) + + @property + def names(self): + return self._styles.ElementNames + + def __str__(self): + return '\n'.join(self.names) + + +class LOWriterTextRange(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph' self._is_table = self.obj.ImplementationName == 'SwXTextTable' @@ -1497,17 +2121,20 @@ class LOTextRange(object): return self._obj @property - def is_paragraph(self): - return self._is_paragraph + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def value(self): + return self.string @property def is_table(self): return self._is_table - @property - def string(self): - return self.obj.String - @property def text(self): return self.obj.getText() @@ -1516,9 +2143,444 @@ class LOTextRange(object): def cursor(self): return self.text.createTextCursorByRange(self.obj) + @property + def dp(self): + return self._doc.dp + + def offset(self): + cursor = self.cursor.getEnd() + return LOWriterTextRange(cursor, self._doc) + + def insert_content(self, data, cursor=None, replace=False): + if cursor is None: + cursor = self.cursor + self.text.insertTextContent(cursor, data, replace) + return + + def insert_image(self, path, args={}): + w = args.get('Width', 1000) + h = args.get('Height', 1000) + image = self._doc.create_instance('com.sun.star.text.GraphicObject') + image.GraphicURL = _P.to_url(path) + image.AnchorType = AS_CHARACTER + image.Width = w + image.Height = h + self.insert_content(image) + return self._doc.dp.last + + +class LOWriterTextRanges(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + def __getitem__(self, index): + return LOWriterTextRange(self.obj[index], self._doc) + + @property + def obj(self): + return self._obj + + +class LOWriter(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = WRITER + + @property + def selection(self): + sel = self.obj.CurrentSelection + if sel.ImplementationName == OBJ_TEXTS: + if len(sel) == 1: + sel = LOWriterTextRanges(sel, self)[0] + else: + sel = LOWriterTextRanges(sel, self) + return sel + + if sel.ImplementationName == OBJ_SHAPES: + if len(sel) == 1: + sel = sel[0] + sel = LODrawPage(sel.Parent)[sel.Name] + return sel + + if sel.ImplementationName == OBJ_GRAPHIC: + sel = self.dp[sel.Name] + else: + debug(sel.ImplementationName) + + return sel + + @property + def dp(self): + return self.draw_page + @property + def draw_page(self): + return LODrawPage(self.obj.DrawPage) + + @property + def view_cursor(self): + return self._cc.ViewCursor + + @property + def cursor(self): + return self.obj.Text.createTextCursor() + + @property + def page_styles(self): + ps = self.obj.StyleFamilies['PageStyles'] + return LOWriterPageStyles(ps) + + +class LOShape(LOBaseObject): + + def __init__(self, obj, index): + self._index = index + super().__init__(obj) + + @property + def type(self): + return 'shape' + + @property + def name(self): + return self.obj.Name or f'shape{self.index}' + @name.setter + def name(self, value): + self.obj.Name = value + + @property + def index(self): + return self._index + + @property + def size(self): + s = self.obj.Size + a = dict(Width=s.Width, Height=s.Height) + return a + + @property + def string(self): + return self.obj.String + @string.setter + def string(self, value): + self.obj.String = value + + @property + def description(self): + return self.obj.Description + @description.setter + def description(self, value): + self.obj.Description = value + + @property + def cell(self): + return self.anchor + + @property + def anchor(self): + obj = self.obj.Anchor + if obj.ImplementationName == OBJ_CELL: + obj = LOCalcRange(obj) + elif obj.ImplementationName == OBJ_TEXT: + obj = LOWriterTextRange(obj, LODocs().active) + else: + debug('Anchor', obj.ImplementationName) + return obj + @anchor.setter + def anchor(self, value): + if hasattr(value, 'obj'): + value = value.obj + self.obj.Anchor = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value + + def remove(self): + self.obj.Parent.remove(self.obj) + return + + +class LODrawPage(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + shape = LOShape(self.obj[index], index) + else: + for i, o in enumerate(self.obj): + shape = self.obj[i] + name = shape.Name or f'shape{i}' + if name == index: + shape = LOShape(shape, i) + break + return shape + + @property + def name(self): + return self.obj.Name + + @property + def doc(self): + return self.obj.Forms.Parent + + @property + def width(self): + return self.obj.Width + + @property + def height(self): + return self.obj.Height + + @property + def count(self): + return self.obj.Count + + @property + def last(self): + return self[self.count - 1] + + def create_instance(self, name): + return self.doc.createInstance(name) + + def add(self, type_shape, args={}): + """Insert a shape in page, type shapes: + Line + Rectangle + Ellipse + Text + """ + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + + service = f'com.sun.star.drawing.{type_shape}Shape' + shape = self.create_instance(service) + shape.Size = Size(w, h) + shape.Position = Point(x, y) + index = self.count + shape.Name = f'{type_shape.lower()}{index}' + self.obj.add(shape) + return LOShape(self.obj[index], index) + + def remove(self, shape): + if hasattr(shape, 'obj'): + shape = shape.obj + return self.obj.remove(shape) + + def insert_image(self, path, args={}): + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + + image = self.create_instance('com.sun.star.drawing.GraphicObjectShape') + image.GraphicURL = _P.to_url(path) + image.Size = Size(w, h) + image.Position = Point(x, y) + index = self.count + image.Name = f'image{index}' + self.obj.add(image) + return LOShape(self.obj[index], index) + + +class LODrawImpress(LODocument): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, index): + if isinstance(index, int): + page = self.obj.DrawPages[index] + else: + page = self.obj.DrawPages.getByName(index) + return LODrawPage(page) + + @property + def selection(self): + sel = self.obj.CurrentSelection[0] + # ~ return _get_class_uno(sel) + return sel + + @property + def current_page(self): + return LODrawPage(self._cc.getCurrentPage()) + + def paste(self): + call_dispatch(self.frame, '.uno:Paste') + return self.selection + + def add(self, type_shape, args={}): + return self.current_page.add(type_shape, args) + + def insert_image(self, path, args={}): + self.current_page.insert_image(path, args) + return + + # ~ def export(self, path, mimetype='png'): + # ~ args = dict( + # ~ URL = _P.to_url(path), + # ~ MimeType = MIME_TYPE[mimetype], + # ~ ) + # ~ result = _export_image(self.obj, args) + # ~ return result + + +class LODraw(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + self._type = DRAW + + +class LOImpress(LODrawImpress): + + def __init__(self, obj): + super().__init__(obj) + self._type = IMPRESS + + +class BaseDateField(DateField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class BaseTimeField(TimeField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class BaseDateTimeField(DateTimeField): + + def db_value(self, value): + return _date_to_struct(value) + + def python_value(self, value): + return _struct_to_date(value) + + +class FirebirdDatabase(Database): + field_types = {'BOOL': 'BOOLEAN', 'DATETIME': 'TIMESTAMP'} + + def __init__(self, database, **kwargs): + super().__init__(database, **kwargs) + self._db = database + + def _connect(self): + return self._db + + def create_tables(self, models, **options): + options['safe'] = False + tables = self._db.tables + models = [m for m in models if not m.__name__.lower() in tables] + super().create_tables(models, **options) + + def execute_sql(self, sql, params=None, commit=True): + with __exception_wrapper__: + cursor = self._db.execute(sql, params) + return cursor + + def last_insert_id(self, cursor, query_type=None): + # ~ debug('LAST_ID', cursor) + return 0 + + def rows_affected(self, cursor): + return self._db.rows_affected + + @property + def path(self): + return self._db.path + + +class BaseRow: + pass + + +class BaseQuery(object): + PY_TYPES = { + 'SQL_LONG': 'getLong', + 'SQL_VARYING': 'getString', + 'SQL_FLOAT': 'getFloat', + 'SQL_BOOLEAN': 'getBoolean', + 'SQL_TYPE_DATE': 'getDate', + 'SQL_TYPE_TIME': 'getTime', + 'SQL_TIMESTAMP': 'getTimestamp', + } + TYPES_DATE = ('SQL_TYPE_DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP') + + def __init__(self, query): + self._query = query + self._meta = query.MetaData + self._cols = self._meta.ColumnCount + self._names = query.Columns.ElementNames + self._data = self._get_data() + + def __getitem__(self, index): + return self._data[index] + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + try: + row = self._data[self._index] + except IndexError: + raise StopIteration + self._index += 1 + return row + + def _to_python(self, index): + type_field = self._meta.getColumnTypeName(index) + value = getattr(self._query, self.PY_TYPES[type_field])(index) + if type_field in self.TYPES_DATE: + value = _struct_to_date(value) + return value + + def _get_row(self): + row = BaseRow() + for i in range(1, self._cols + 1): + column_name = self._meta.getColumnName(i) + value = self._to_python(i) + setattr(row, column_name, value) + return row + + def _get_data(self): + data = [] + while self._query.next(): + row = self._get_row() + data.append(row) + return data + + @property + def tuples(self): + data = [tuple(r.__dict__.values()) for r in self._data] + return tuple(data) + + @property + def dicts(self): + data = [r.__dict__ for r in self._data] + return tuple(data) + class LOBase(object): - TYPES = { + DB_TYPES = { str: 'setString', int: 'setInt', float: 'setFloat', @@ -1540,40 +2602,29 @@ class LOBase(object): # ~ setObjectWithInfo # ~ setPropertyValue # ~ setRef - def __init__(self, name, path='', **kwargs): - self._name = name - self._path = path + + def __init__(self, obj, args={}): + self._obj = obj + self._type = BASE self._dbc = create_instance('com.sun.star.sdb.DatabaseContext') - if path: - path_url = _path_url(path) + self._rows_affected = 0 + path = args.get('path', '') + self._path = _P(path) + self._name = self._path.name + if _P.exists(path): + if not self.is_registered: + self.register() + db = self._dbc.getByName(self.name) + else: db = self._dbc.createInstance() db.URL = 'sdbc:embedded:firebird' - db.DatabaseDocument.storeAsURL(path_url, ()) - if not self.exists: - self._dbc.registerDatabaseLocation(name, path_url) - else: - if name.startswith('odbc:'): - self._con = self._odbc(name, kwargs) - else: - db = self._dbc.getByName(name) - self.path = _path_system(self._dbc.getDatabaseLocation(name)) - self._con = db.getConnection('', '') + db.DatabaseDocument.storeAsURL(self._path.url, ()) + self.register() + self._obj = db + self._con = db.getConnection('', '') - if self._con is None: - msg = 'Not connected to: {}'.format(name) - else: - msg = 'Connected to: {}'.format(name) - debug(msg) - - def _odbc(self, name, kwargs): - dm = create_instance('com.sun.star.sdbc.DriverManager') - args = dict_to_property(kwargs) - try: - con = dm.getConnectionWithInfo('sdbc:{}'.format(name), args) - return con - except Exception as e: - error(str(e)) - return None + def __contains__(self, item): + return item in self.tables @property def obj(self): @@ -1583,25 +2634,26 @@ class LOBase(object): def name(self): return self._name - @property - def connection(self): - return self._con - @property def path(self): - return self._path - @path.setter - def path(self, value): - self._path = value + return str(self._path) @property - def exists(self): + def is_registered(self): return self._dbc.hasRegisteredDatabase(self.name) - @classmethod - def register(self, path, name): - if not self._dbc.hasRegisteredDatabase(name): - self._dbc.registerDatabaseLocation(name, _path_url(path)) + @property + def tables(self): + tables = [t.Name.lower() for t in self._con.getTables()] + return tables + + @property + def rows_affected(self): + return self._rows_affected + + def register(self): + if not self.is_registered: + self._dbc.registerDatabaseLocation(self.name, self._path.url) return def revoke(self, name): @@ -1609,10 +2661,7 @@ class LOBase(object): return True def save(self): - # ~ self._db.connection.commit() - # ~ self._db.connection.getTables().refresh() - # ~ oDisp.executeDispatch(oFrame,".uno:DBRefreshTables", "", 0, Array()) - self._obj.DatabaseDocument.store() + self.obj.DatabaseDocument.store() self.refresh() return @@ -1624,508 +2673,209 @@ class LOBase(object): self._con.getTables().refresh() return - def get_tables(self): - tables = self._con.getTables() - tables = [tables.getByIndex(i) for i in range(tables.Count)] - return tables + def initialize(self, database_proxy, tables): + db = FirebirdDatabase(self) + database_proxy.initialize(db) + db.create_tables(tables) + return + + def _validate_sql(self, sql, params): + limit = ' LIMIT ' + for p in params: + sql = sql.replace('?', f"'{p}'", 1) + if limit in sql: + sql = sql.split(limit)[0] + sql = sql.replace('SELECT', f'SELECT FIRST {params[-1]}') + return sql def cursor(self, sql, params): + if sql.startswith('SELECT'): + sql = self._validate_sql(sql, params) + cursor = self._con.prepareStatement(sql) + return cursor + + if not params: + cursor = self._con.createStatement() + return cursor + cursor = self._con.prepareStatement(sql) for i, v in enumerate(params, 1): - if not type(v) in self.TYPES: + t = type(v) + if not t in self.DB_TYPES: error('Type not support') - debug((i, type(v), v, self.TYPES[type(v)])) - getattr(cursor, self.TYPES[type(v)])(i, v) + debug((i, t, v, self.DB_TYPES[t])) + getattr(cursor, self.DB_TYPES[t])(i, v) return cursor def execute(self, sql, params): - debug(sql) - if params: - cursor = self.cursor(sql, params) - cursor.execute() + debug(sql, params) + cursor = self.cursor(sql, params) + + if sql.startswith('SELECT'): + result = cursor.executeQuery() + elif params: + result = cursor.executeUpdate() + self._rows_affected = result + self.save() else: - cursor = self._con.createStatement() - cursor.execute(sql) - # ~ resulset = cursor.executeQuery(sql) - # ~ rows = cursor.executeUpdate(sql) - self.save() - return cursor + result = cursor.execute(sql) + self.save() + return result -class LODrawImpress(LODocument): + def select(self, sql): + debug('SELECT', sql) + if not sql.startswith('SELECT'): + return () - def __init__(self, obj): - super().__init__(obj) + cursor = self._con.prepareStatement(sql) + query = cursor.executeQuery() + return BaseQuery(query) - @property - def draw_page(self): - return self._cc.getCurrentPage() - - def insert_image(self, path, **kwargs): - w = kwargs.get('width', 3000) - h = kwargs.get('Height', 3000) - 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) + def get_query(self, query): + sql, args = query.sql() + sql = self._validate_sql(sql, args) + return self.select(sql) class LOMath(LODocument): def __init__(self, obj): super().__init__(obj) + self._type = MATH -class LOBasicIde(LODocument): +class LOBasic(LODocument): def __init__(self, obj): super().__init__(obj) - - @property - def selection(self): - sel = self._cc.getSelection() - return sel + self._type = BASIC -class LOCellRange(object): +class LODocs(object): + _desktop = None - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - self._init_values() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - pass + def __init__(self): + self._desktop = get_desktop() + LODocs._desktop = self._desktop def __getitem__(self, index): - return LOCellRange(self.obj[index], self.doc) + doc = None + for i, doc in enumerate(self._desktop.Components): + if isinstance(index, int) and i == index: + doc = _get_class_doc(doc) + break + elif isinstance(index, str) and doc.Title == index: + doc = _get_class_doc(doc) + break + return doc def __contains__(self, item): - return item.in_range(self) + doc = self[item] + return not doc is None - def _init_values(self): - self._type_obj = self.obj.ImplementationName - self._type_content = EMPTY + def __iter__(self): + self._i = 0 + return self - if self._type_obj == OBJ_CELL: - self._type_content = self.obj.getType() - return + def __next__(self): + doc = self[self._i] + if doc is None: + raise StopIteration + self._i += 1 + return doc + + def __len__(self): + for i, _ in enumerate(self._desktop.Components): + pass + return i + 1 @property - def obj(self): - return self._obj + def active(self): + return _get_class_doc(self._desktop.getCurrentComponent()) - @property - def doc(self): - return self._doc + @classmethod + def new(cls, type_doc=CALC, args={}): + if type_doc == BASE: + return LOBase(None, args) - @property - def type(self): - return self._type_obj + path = f'private:factory/s{type_doc}' + opt = dict_to_property(args) + doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) + return _get_class_doc(doc) - @property - def type_content(self): - return self._type_content + @classmethod + def open(cls, path, args={}): + """ Open document in path + Usually options: + Hidden: True or False + AsTemplate: True or False + ReadOnly: True or False + Password: super_secret + MacroExecutionMode: 4 = Activate macros + Preview: True or False - @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 + 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 = _P.to_url(path) + opt = dict_to_property(args) + doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) + if doc is None: + return - @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, bool)): - self.obj.setValue(data) - elif isinstance(data, datetime.datetime): - d = data.toordinal() - t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY - self.obj.setValue(d - DATE_OFFSET + t) - elif isinstance(data, datetime.date): - d = data.toordinal() - self.obj.setValue(d - DATE_OFFSET) - elif isinstance(data, datetime.time): - d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY - self.obj.setValue(d) + return _get_class_doc(doc) - @property - def data(self): - return self.obj.getDataArray() - @data.setter - def data(self, values): - self.obj.setDataArray(values) + def connect(self, path): + return LOBase(None, {'path': path}) - @property - def formula(self): - return self.obj.getFormulaArray() - @formula.setter - def formula(self, values): - self.obj.setFormulaArray(values) - @property - def column(self): - a = self.address - if hasattr(a, 'Column'): - c = a.Column - else: - c = a.StartColumn - return c +def _add_listeners(events, control, name=''): + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + 'addFocusListener': EventsFocus, + # ~ 'addItemListener': EventsItem, + # ~ 'addKeyListener': EventsKey, + # ~ 'addTabListener': EventsTab, + } + if hasattr(control, 'obj'): + control = control.obj + # ~ debug(control.ImplementationName) + is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' + is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' + is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' - @property - def columns(self): - return self._obj.Columns.Count + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue - @property - def row(self): - a = self.address - if hasattr(a, 'Row'): - r = a.Row - else: - r = a.StartRow - return r + getattr(control, key)(listeners[key](events, name)) - @property - def rows(self): - return self._obj.Rows.Count + # ~ if is_grid: + # ~ controllers = EventsGrid(events, name) + # ~ control.addSelectionListener(controllers) + # ~ control.Model.GridDataModel.addGridDataListener(controllers) + return - def to_size(self, rows, cols): - cursor = self.sheet.get_cursor(self.obj[0,0]) - cursor.collapseToSize(cols, rows) - return LOCellRange(self.sheet[cursor.AbsoluteName].obj, self.doc) - def copy_from(self, rango, formula=False): - data = rango - if isinstance(rango, LOCellRange): - if formula: - data = rango.formula - else: - data = rango.data - rows = len(data) - cols = len(data[0]) - if formula: - self.to_size(rows, cols).formula = data - else: - self.to_size(rows, cols).data = data - return - - def copy_to(self, cell, formula=False): - rango = cell.to_size(self.rows, self.columns) - if formula: - rango.formula = self.data - else: - rango.data = self.data - return - - def copy(self, source): - self.sheet.obj.copyRange(self.address, source.range_address) - return - - def transpose(self, formula=False): - data = self.data - if formula: - data = self.formula - data = tuple(zip(*data)) - self.clear(1023) - self[0,0].copy_from(data, formula=formula) - return - - def transpose2(self): - # ~ 'Flags': 'A', - # ~ 'FormulaCommand': 0, - # ~ 'SkipEmptyCells': False, - # ~ 'AsLink': False, - # ~ 'MoveMode': 4, - args = { - 'Transpose': True, - } - args = dict_to_property(args) - self.select() - copy() - self.clear(1023) - self[0,0].select() - call_dispatch('.uno:InsertContents', args) - set_clipboard('') - return - - def offset(self, row=1, col=0): - ra = self.obj.getRangeAddress() - col = ra.EndColumn + col - row = ra.EndRow + row - return LOCellRange(self.sheet[row, col].obj, self.doc) - - @property - def next_cell(self): - a = self.current_region.address - if hasattr(a, 'StartColumn'): - col = a.StartColumn - else: - col = a.Column - if hasattr(a, 'EndRow'): - row = a.EndRow + 1 - else: - row = a.Row + 1 - - return LOCellRange(self.sheet[row, col].obj, self.doc) - - @property - def sheet(self): - return LOCalcSheet(self.obj.Spreadsheet, self.doc) - - @property - def charts(self): - return self.obj.Spreadsheet.Charts - - @property - def ps(self): - ps = Rectangle() - s = self.obj.Size - p = self.obj.Position - ps.X = p.X - ps.Y = p.Y - ps.Width = s.Width - ps.Height = s.Height - return ps - - @property - def draw_page(self): - return self.sheet.obj.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 range_address(self): - return self.obj.getRangeAddress() - - @property - def current_region(self): - cursor = self.sheet.get_cursor(self.obj[0,0]) - cursor.collapseToCurrentRegion() - return LOCellRange(self.sheet[cursor.AbsoluteName].obj, self.doc) - - @property - def visible(self): - cursor = self.sheet.get_cursor(self.obj) - rangos = [LOCellRange(self.sheet[r.AbsoluteName].obj, self.doc) - for r in cursor.queryVisibleCells()] - return tuple(rangos) - - @property - def empty(self): - cursor = self.sheet.get_cursor(self.obj) - rangos = [LOCellRange(self.sheet[r.AbsoluteName].obj, self.doc) - for r in cursor.queryEmptyCells()] - return tuple(rangos) - - @property - def back_color(self): - return self._obj.CellBackColor - @back_color.setter - def back_color(self, value): - self._obj.CellBackColor = get_color(value) - - @property - def cell_style(self): - return self.obj.CellStyle - @cell_style.setter - def cell_style(self, value): - self.obj.CellStyle = value - - @property - def auto_format(self): - return self.obj.CellStyle - @auto_format.setter - def auto_format(self, value): - self.obj.autoFormat(value) - - def auto_width(self): - self.obj.Columns.OptimalWidth = True - return - - 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 insert_shape(self, tipo, **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.{}Shape'.format(tipo)) - set_properties(img, kwargs) - 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 - - def in_range(self, rango): - if isinstance(rango, LOCellRange): - address = rango.address - else: - address = rango.getRangeAddress() - cursor = self.sheet.get_cursor(self.obj) - result = cursor.queryIntersection(address) - return bool(result.Count) - - def fill(self, source=1): - self.obj.fillAuto(0, source) - return - - def clear(self, what=31): - # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html - self.obj.clearContents(what) - return - - @property - def rows_visible(self): - return self._obj.getRows().IsVisible - @rows_visible.setter - def rows_visible(self, value): - self._obj.getRows().IsVisible = value - - @property - def columns_visible(self): - return self._obj.getColumns().IsVisible - @columns_visible.setter - def columns_visible(self, value): - self._obj.getColumns().IsVisible = value - - def get_column(self, index=0, first=False): - ca = self.address - ra = self.current_region.address - if hasattr(ca, 'Column'): - col = ca.Column - else: - col = ca.StartColumn + index - start = 1 - if first: - start = 0 - if hasattr(ra, 'Row'): - row_start = ra.Row + start - row_end = ra.Row + 1 - else: - row_start = ra.StartRow + start - row_end = ra.EndRow + 1 - return LOCellRange(self.sheet[row_start:row_end, col:col+1].obj, self.doc) - - def import_csv(self, path, **kwargs): - data = import_csv(path, **kwargs) - self.copy_from(data) - return - - def export_csv(self, path, **kwargs): - data = self.current_region.data - export_csv(path, data, **kwargs) - return - - def create_chart(self, tipo): - chart = LOChart(None, tipo) - chart.cell = self - return chart - - def search(self, options): - descriptor = self.obj.Spreadsheet.createSearchDescriptor() - descriptor.setSearchString(options.get('Search', '')) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - - if options.get('First', False): - found = self.obj.findFirst(descriptor) - else: - found = self.obj.findAll(descriptor) - - return found - - def replace(self, options): - descriptor = self.obj.Spreadsheet.createReplaceDescriptor() - descriptor.setSearchString(options['Search']) - descriptor.setReplaceString(options['Replace']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - found = self.obj.replaceAll(descriptor) - return found - - @property - def validation(self): - return self.obj.Validation - @validation.setter - def validation(self, values): - is_list = False - current = self.validation - for k, v in values.items(): - if k == 'Type' and v == 6: - is_list = True - if k == 'Formula1' and is_list: - if isinstance(v, (tuple, list)): - v = ';'.join(['"{}"'.format(i) for i in v]) - setattr(current, k, v) - self.obj.Validation = current +def _set_properties(model, properties): + if 'X' in properties: + properties['PositionX'] = properties.pop('X') + if 'Y' in properties: + properties['PositionY'] = properties.pop('Y') + keys = tuple(properties.keys()) + values = tuple(properties.values()) + model.setPropertyValues(keys, values) + return class EventsListenerBase(unohelper.Base, XEventListener): @@ -2145,18 +2895,6 @@ class EventsListenerBase(unohelper.Base, XEventListener): self._window.setMenuBar(None) -class EventsButton(EventsListenerBase, XActionListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def actionPerformed(self, event): - event_name = '{}_action'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): def __init__(self, controller, name): @@ -2189,281 +2927,74 @@ class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): class EventsMouseLink(EventsMouse): + def __init__(self, controller, name): + super().__init__(controller, name) + self._text_color = 0 + def mouseEntered(self, event): - obj = event.Source.Model - obj.TextColor = get_color('blue') + model = event.Source.Model + self._text_color = model.TextColor or 0 + model.TextColor = get_color('blue') return def mouseExited(self, event): - obj = event.Source.Model - obj.TextColor = 0 + model = event.Source.Model + model.TextColor = self._text_color return -class EventsMouseGrid(EventsMouse): - selected = False - - def mousePressed(self, event): - super().mousePressed(event) - # ~ obj = event.Source - # ~ col = obj.getColumnAtPoint(event.X, event.Y) - # ~ row = obj.getRowAtPoint(event.X, event.Y) - # ~ print(col, row) - # ~ if col == -1 and row == -1: - # ~ if self.selected: - # ~ obj.deselectAllRows() - # ~ else: - # ~ obj.selectAllRows() - # ~ self.selected = not self.selected - return - - def mouseReleased(self, event): - # ~ obj = event.Source - # ~ col = obj.getColumnAtPoint(event.X, event.Y) - # ~ row = obj.getRowAtPoint(event.X, event.Y) - # ~ if row == -1 and col > -1: - # ~ gdm = obj.Model.GridDataModel - # ~ for i in range(gdm.RowCount): - # ~ gdm.updateRowHeading(i, i + 1) - return - - -class EventsModify(EventsListenerBase, XModifyListener): - - def __init__(self, controller): - super().__init__(controller) - - def modified(self, event): - event_name = '{}_modified'.format(event.Source.Name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsItem(EventsListenerBase, XItemListener): +class EventsButton(EventsListenerBase, XActionListener): def __init__(self, controller, name): super().__init__(controller, name) - def disposing(self, event): - pass - - def itemStateChanged(self, event): - event_name = '{}_item_changed'.format(self.name) + def actionPerformed(self, event): + event_name = f'{self.name}_action' if hasattr(self._controller, event_name): getattr(self._controller, event_name)(event) return -class EventsItemRoadmap(EventsItem): - - def itemStateChanged(self, event): - dialog = event.Source.Context.Model - dialog.Step = event.ItemId + 1 - return - - class EventsFocus(EventsListenerBase, XFocusListener): + CONTROLS = ( + 'stardiv.Toolkit.UnoControlEditModel', + ) def __init__(self, controller, name): super().__init__(controller, name) def focusGained(self, event): service = event.Source.Model.ImplementationName - if service == 'stardiv.Toolkit.UnoControlListBoxModel': - return - obj = event.Source.Model - obj.BackgroundColor = COLOR_ON_FOCUS + # ~ print('Focus enter', service) + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + return def focusLost(self, event): - obj = event.Source.Model - obj.BackgroundColor = -1 - - -class EventsKey(EventsListenerBase, XKeyListener): - """ - event.KeyChar - event.KeyCode - event.KeyFunc - event.Modifiers - """ - - def __init__(self, controller, name): - super().__init__(controller, name) - - def keyPressed(self, event): - pass - - def keyReleased(self, event): - event_name = '{}_key_released'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsTab(EventsListenerBase, XTabListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def activated(self, id): - event_name = '{}_activated'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(id) - return - - -class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): - - def __init__(self, controller, name): - super().__init__(controller, name) - - def dataChanged(self, event): - event_name = '{}_data_changed'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def rowHeadingChanged(self, event): - pass - - def rowsInserted(self, event): - pass - - def rowsRemoved(self, evemt): - pass - - def selectionChanged(self, event): - event_name = '{}_selection_changed'.format(self.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - -class EventsKeyWindow(EventsListenerBase, XKeyListener): - """ - event.KeyChar - event.KeyCode - event.KeyFunc - event.Modifiers - """ - - def __init__(self, cls): - super().__init__(cls.events, cls.name) - self._cls = cls - - def keyPressed(self, event): - pass - - def keyReleased(self, event): - event_name = '{}_key_released'.format(self._cls.name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - else: - if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): - self._cls.close() - return - - -class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): - - def __init__(self, cls): - self._cls = cls - super().__init__(cls.events, cls.name, cls._window) - - def windowOpened(self, event): - event_name = '{}_opened'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def windowActivated(self, event): - control_name = '{}_activated'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - def windowDeactivated(self, event): - control_name = '{}_deactivated'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - def windowMinimized(self, event): - pass - - def windowNormalized(self, event): - pass - - def windowClosing(self, event): - if self._window: - control_name = 'window_closing' - else: - control_name = '{}_closing'.format(event.Source.Model.Name) - - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - # ~ else: - # ~ if not self._modal and not self._block: - # ~ event.Source.Visible = False - return - - def windowClosed(self, event): - control_name = '{}_closed'.format(event.Source.Model.Name) - if hasattr(self._controller, control_name): - getattr(self._controller, control_name)(event) - return - - # ~ XWindowListener - def windowResized(self, event): - sb = self._cls._subcont - sb.setPosSize(0, 0, event.Width, event.Height, SIZE) - event_name = '{}_resized'.format(self._name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def windowMoved(self, event): - pass - - def windowShown(self, event): - pass - - def windowHidden(self, event): - pass - - -class EventsMenu(EventsListenerBase, XMenuListener): - - def __init__(self, controller): - super().__init__(controller, '') - - def itemHighlighted(self, event): - pass - - def itemSelected(self, event): - name = event.Source.getCommand(event.MenuId) - if name.startswith('menu'): - event_name = '{}_selected'.format(name) - else: - event_name = 'menu_{}_selected'.format(name) - if hasattr(self._controller, event_name): - getattr(self._controller, event_name)(event) - return - - def itemActivated(self, event): - return - - def itemDeactivated(self, event): + service = event.Source.Model.ImplementationName + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = -1 return +# ~ BorderColor = ? +# ~ FontStyleName = ? +# ~ HelpURL = ? class UnoBaseObject(object): - def __init__(self, obj): + def __init__(self, obj, path=''): self._obj = obj - self._model = self.obj.Model - self._rules = {} + self._model = obj.Model + # ~ self._path = path + + def __setattr__(self, name, value): + exists = hasattr(self, name) + if not exists and not name in ('_obj', '_model'): + setattr(self._model, name, value) + else: + super().__setattr__(name, value) @property def obj(self): @@ -2472,6 +3003,16 @@ class UnoBaseObject(object): @property def model(self): return self._model + @property + def m(self): + return self._model + + @property + def properties(self): + return {} + @properties.setter + def properties(self, values): + _set_properties(self.model, values) @property def name(self): @@ -2479,8 +3020,127 @@ class UnoBaseObject(object): @property def parent(self): - ps = self.obj.getContext().PosSize - return self.obj.getContext() + return self.obj.Context + + @property + def tag(self): + return self.model.Tag + @tag.setter + def tag(self, value): + self.model.Tag = value + + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.setVisible(value) + + @property + def enabled(self): + return self.model.Enabled + @enabled.setter + def enabled(self, value): + self.model.Enabled = value + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + + @property + def align(self): + return self.model.Align + @align.setter + def align(self, value): + self.model.Align = value + + @property + def valign(self): + return self.model.VerticalAlign + @valign.setter + def valign(self, value): + self.model.VerticalAlign = value + + @property + def font_weight(self): + return self.model.FontWeight + @font_weight.setter + def font_weight(self, value): + self.model.FontWeight = value + + @property + def font_height(self): + return self.model.FontHeight + @font_height.setter + def font_height(self, value): + self.model.FontHeight = value + + @property + def font_name(self): + return self.model.FontName + @font_name.setter + def font_name(self, value): + self.model.FontName = value + + @property + def font_underline(self): + return self.model.FontUnderline + @font_underline.setter + def font_underline(self, value): + self.model.FontUnderline = value + + @property + def text_color(self): + return self.model.TextColor + @text_color.setter + def text_color(self, value): + self.model.TextColor = value + + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + + @property + def multi_line(self): + return self.model.MultiLine + @multi_line.setter + def multi_line(self, value): + self.model.MultiLine = value + + @property + def help_text(self): + return self.model.HelpText + @help_text.setter + def help_text(self, value): + self.model.HelpText = value + + @property + def border(self): + return self.model.Border + @border.setter + def border(self, value): + # ~ Bug for report + self.model.Border = value + + @property + def width(self): + return self._model.Width + @width.setter + def width(self, value): + self.model.Width = value + + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value def _get_possize(self, name): ps = self.obj.getPosSize() @@ -2517,90 +3177,35 @@ class UnoBaseObject(object): self._set_possize('Y', value) @property - def width(self): - return self._model.Width - @width.setter - def width(self, value): - self.model.Width = value + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value @property - def ps_width(self): - return self._get_possize('Width') - @ps_width.setter - def ps_width(self, value): - self._set_possize('Width', value) + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value @property - def height(self): - return self.model.Height - @height.setter - def height(self, value): - self.model.Height = value - - @property - def ps_height(self): - return self._get_possize('Height') - @ps_height.setter - def ps_height(self, value): - self._set_possize('Height', value) - - @property - def size(self): + def ps(self): ps = self.obj.getPosSize() - return (ps.Width, ps.Height) - @size.setter - def size(self, value): - ps = self.obj.getPosSize() - ps.Width = value[0] - ps.Height = value[1] - self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, SIZE) - - @property - def tag(self): - return self.model.Tag - @tag.setter - def tag(self, value): - self.model.Tag = value - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.setVisible(value) - - @property - def enabled(self): - return self.model.Enabled - @enabled.setter - def enabled(self, value): - self.model.Enabled = value - - @property - def step(self): - return self.model.Step - @step.setter - def step(self, value): - self.model.Step = value - - @property - def back_color(self): - return self.model.BackgroundColor - @back_color.setter - def back_color(self, value): - self.model.BackgroundColor = value - - @property - def rules(self): - return self._rules - @rules.setter - def rules(self, value): - self._rules = value + return ps + @ps.setter + def ps(self, ps): + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) def set_focus(self): self.obj.setFocus() return + def ps_from(self, source): + self.ps = source.ps + return + def center(self, horizontal=True, vertical=False): p = self.parent.Model w = p.Width @@ -2613,7 +3218,7 @@ class UnoBaseObject(object): self.y = y return - def move(self, origin, x=0, y=5): + def move(self, origin, x=0, y=5, center=False): if x: self.x = origin.x + origin.width + x else: @@ -2622,13 +3227,9 @@ class UnoBaseObject(object): self.y = origin.y + origin.height + y else: self.y = origin.y - return - def possize(self, origin): - self.x = origin.x - self.y = origin.y - self.width = origin.width - self.height = origin.height + if center: + self.center() return @@ -2676,6 +3277,55 @@ class UnoButton(UnoBaseObject): self.model.Label = value +class UnoRadio(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'radio' + + @property + def value(self): + return self.model.Label + @value.setter + def value(self, value): + self.model.Label = value + + +class UnoCheck(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'check' + + @property + def value(self): + return self.model.State + @value.setter + def value(self, value): + self.model.State = value + + @property + def label(self): + return self.model.Label + @label.setter + def label(self, value): + self.model.Label = value + + @property + def tri_state(self): + return self.model.TriState + @tri_state.setter + def tri_state(self, value): + self.model.TriState = value + + +# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html class UnoText(UnoBaseObject): def __init__(self, obj): @@ -2692,15 +3342,43 @@ class UnoText(UnoBaseObject): def value(self, value): self.model.Text = value - def validate(self): - return +class UnoImage(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'image' + + @property + def value(self): + return self.url + @value.setter + def value(self, value): + self.url = value + + @property + def url(self): + return self.m.ImageURL + @url.setter + def url(self, value): + self.m.ImageURL = None + self.m.ImageURL = _P.to_url(value) class UnoListBox(UnoBaseObject): def __init__(self, obj): super().__init__(obj) + self._path = '' + + def __setattr__(self, name, value): + if name in ('_path',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) @property def type(self): @@ -2720,7 +3398,13 @@ class UnoListBox(UnoBaseObject): @data.setter def data(self, values): self.model.StringItemList = list(sorted(values)) - return + + @property + def path(self): + return self._path + @path.setter + def path(self, value): + self._path = value def unselect(self): self.obj.selectItem(self.value, False) @@ -2738,15 +3422,11 @@ class UnoListBox(UnoBaseObject): return def _set_image_url(self, image): - if exists_path(image): - return _path_url(image) + if _P.exists(image): + return _P.to_url(image) - if not ID_EXTENSION: - return '' - - path = get_path_extension(ID_EXTENSION) - path = join(path, DIR['images'], image) - return _path_url(path) + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) def insert(self, value, path='', pos=-1, show=True): if pos < 0: @@ -2760,924 +3440,79 @@ class UnoListBox(UnoBaseObject): return -class UnoGrid(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._gdm = self._model.GridDataModel - # ~ self._data = [] - self._columns = {} - # ~ self._format_columns = () - - def __getitem__(self, index): - value = self._gdm.getCellData(index[0], index[1]) - return value - - @property - def type(self): - return 'grid' - - def _format_cols(self): - rows = tuple(tuple( - self._format_columns[i].format(r) for i, r in enumerate(row)) for row in self._data - ) - return rows - - # ~ @property - # ~ def format_columns(self): - # ~ return self._format_columns - # ~ @format_columns.setter - # ~ def format_columns(self, value): - # ~ self._format_columns = value - - @property - def value(self): - return self[self.column, self.row] - - @property - def data(self): - return self._data - @data.setter - def data(self, values): - # ~ self._data = values - self.clear() - headings = tuple(range(1, len(values) + 1)) - self._gdm.addRows(headings, values) - # ~ rows = range(grid_dm.RowCount) - # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows] - # ~ grid.Model.RowBackgroundColors = tuple(colors) - return - - @property - def row(self): - return self.obj.CurrentRow - - @property - def rows(self): - return self._gdm.RowCount - - @property - def column(self): - return self.obj.CurrentColumn - - @property - def columns(self): - return self._gdm.ColumnCount - - def set_cell_tooltip(self, col, row, value): - self._gdm.updateCellToolTip(col, row, value) - return - - def get_cell_tooltip(self, col, row): - value = self._gdm.getCellToolTip(col, row) - return value - - def _validate_column(self, data): - row = [] - for i, d in enumerate(data): - if i in self._columns: - if 'image' in self._columns[i]: - row.append(self._columns[i]['image']) - else: - row.append(d) - return tuple(row) - - def clear(self): - self._gdm.removeAllRows() - return - - def add_row(self, data): - # ~ self._data.append(data) - data = self._validate_column(data) - self._gdm.addRow(self.rows + 1, data) - return - - def remove_row(self, row): - self._gdm.removeRow(row) - # ~ del self._data[row] - self.update_row_heading() - return - - def update_row_heading(self): - for i in range(self.rows): - self._gdm.updateRowHeading(i, i + 1) - return - - def sort(self, column, asc=True): - self._gdm.sortByColumn(column, asc) - self.update_row_heading() - return - - def set_column_image(self, column, path): - gp = create_instance('com.sun.star.graphic.GraphicProvider') - data = dict_to_property({'URL': _path_url(path)}) - image = gp.queryGraphic(data) - if not column in self._columns: - self._columns[column] = {} - self._columns[column]['image'] = image - return - - -class UnoRoadmap(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._options = () - - @property - def options(self): - return self._options - @options.setter - def options(self, values): - self._options = values - for i, v in enumerate(values): - opt = self.model.createInstance() - opt.ID = i - opt.Label = v - self.model.insertByIndex(i, opt) - return - - @property - def enabled(self): - return True - @enabled.setter - def enabled(self, value): - for m in self.model: - m.Enabled = value - return - - def set_enabled(self, index, value): - self.model.getByIndex(index).Enabled = value - return - - -class UnoTree(UnoBaseObject): - - def __init__(self, obj, ): - super().__init__(obj) - self._tdm = None - self._data = [] - - @property - def selection(self): - return self.obj.Selection - - @property - def root(self): - if self._tdm is None: - return '' - return self._tdm.Root.DisplayValue - - @root.setter - def root(self, value): - self._add_data_model(value) - - def _add_data_model(self, name): - tdm = create_instance('com.sun.star.awt.tree.MutableTreeDataModel') - root = tdm.createNode(name, True) - root.DataValue = 0 - tdm.setRoot(root) - self.model.DataModel = tdm - self._tdm = self.model.DataModel - self._add_data() - return - - @property - def data(self): - return self._data - @data.setter - def data(self, values): - self._data = list(values) - self._add_data() - - def _add_data(self): - if not self.data: - return - - parents = {} - for node in self.data: - parent = parents.get(node[1], self._tdm.Root) - child = self._tdm.createNode(node[2], False) - child.DataValue = node[0] - parent.appendChild(child) - parents[node[0]] = child - self.obj.expandNode(self._tdm.Root) - return - - -class UnoTab(UnoBaseObject): - - def __init__(self, obj): - super().__init__(obj) - self._events = None - - def __getitem__(self, index): - return self.get_sheet(index) - - @property - def current(self): - return self.obj.getActiveTabID() - @property - def active(self): - return self.current - - def get_sheet(self, id): - if isinstance(id, int): - sheet = self.obj.Controls[id-1] - else: - sheet = self.obj.getControl(id.lower()) - return sheet - - @property - def sheets(self): - return self._sheets - @sheets.setter - def sheets(self, values): - i = len(self.obj.Controls) - for title in values: - i += 1 - sheet = self.model.createInstance('com.sun.star.awt.UnoPageModel') - sheet.Title = title - self.model.insertByName('sheet{}'.format(i), sheet) - return - - def insert(self, title): - id = len(self.obj.Controls) + 1 - sheet = self.model.createInstance('com.sun.star.awt.UnoPageModel') - sheet.Title = title - self.model.insertByName('sheet{}'.format(id), sheet) - return id - - def remove(self, id): - sheet = self.get_sheet(id) - for control in sheet.getControls(): - sheet.Model.removeByName(control.Model.Name) - sheet.removeControl(control) - # ~ self._model.removeByName('page_{}'.format(ID)) - - self.obj.removeTab(id) - return - - def activate(self, id): - self.obj.activateTab(id) - return - - @property - def events(self): - return self._events - @events.setter - def events(self, controllers): - self._events = controllers - - def _special_properties(self, tipo, properties): - columns = properties.pop('Columns', ()) - if tipo == 'grid': - properties['ColumnModel'] = _set_column_model(columns) - if not 'Width' in properties: - properties['Width'] = self.width - if not 'Height' in properties: - properties['Height'] = self.height - elif tipo == 'button' and 'ImageURL' in properties: - properties['ImageURL'] = self._set_image_url(properties['ImageURL']) - elif tipo == 'roadmap': - if not 'Height' in properties: - properties['Height'] = self.height - if 'Title' in properties: - properties['Text'] = properties.pop('Title') - elif tipo == 'pages': - if not 'Width' in properties: - properties['Width'] = self.width - if not 'Height' in properties: - properties['Height'] = self.height - - return properties - - def add_control(self, id, properties): - tipo = properties.pop('Type').lower() - root = properties.pop('Root', '') - sheets = properties.pop('Sheets', ()) - properties = self._special_properties(tipo, properties) - - sheet = self.get_sheet(id) - sheet_model = sheet.getModel() - model = sheet_model.createInstance(get_control_model(tipo)) - set_properties(model, properties) - name = properties['Name'] - sheet_model.insertByName(name, model) - - control = sheet.getControl(name) - add_listeners(self.events, control, name) - control = get_custom_class(tipo, control) - - if tipo == 'tree' and root: - control.root = root - elif tipo == 'pages' and sheets: - control.sheets = sheets - - setattr(self, name, control) - return - - -def get_custom_class(tipo, obj): - classes = { - 'label': UnoLabel, - 'button': UnoButton, - 'text': UnoText, - 'listbox': UnoListBox, - 'grid': UnoGrid, - 'link': UnoLabelLink, - 'roadmap': UnoRoadmap, - 'tree': UnoTree, - 'tab': UnoTab, - # ~ 'image': UnoImage, - # ~ 'radio': UnoRadio, - # ~ 'groupbox': UnoGroupBox, - 'formbutton': FormButton, - } - return classes[tipo](obj) - - -def get_control_model(control): - services = { - 'label': 'com.sun.star.awt.UnoControlFixedTextModel', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', - 'text': 'com.sun.star.awt.UnoControlEditModel', - 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', - 'button': 'com.sun.star.awt.UnoControlButtonModel', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', - 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', - 'tree': 'com.sun.star.awt.tree.TreeControlModel', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - 'image': 'com.sun.star.awt.UnoControlImageControlModel', - 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', - 'tab': 'com.sun.star.awt.UnoMultiPageModel', - } - return services[control] - - -def add_listeners(events, control, name=''): - listeners = { - 'addActionListener': EventsButton, - 'addMouseListener': EventsMouse, - 'addItemListener': EventsItem, - 'addFocusListener': EventsFocus, - 'addKeyListener': EventsKey, - 'addTabListener': EventsTab, - } - if hasattr(control, 'obj'): - control = contro.obj - # ~ debug(control.ImplementationName) - is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' - is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' - is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' - - for key, value in listeners.items(): - if hasattr(control, key): - if is_grid and key == 'addMouseListener': - control.addMouseListener(EventsMouseGrid(events, name)) - continue - if is_link and key == 'addMouseListener': - control.addMouseListener(EventsMouseLink(events, name)) - continue - if is_roadmap and key == 'addItemListener': - control.addItemListener(EventsItemRoadmap(events, name)) - continue - - getattr(control, key)(listeners[key](events, name)) - - if is_grid: - controllers = EventsGrid(events, name) - control.addSelectionListener(controllers) - control.Model.GridDataModel.addGridDataListener(controllers) - return - - -class WriterTable(ObjectBase): - - def __init__(self, obj): - super().__init__(obj) - - def __getitem__(self, key): - obj = super().__getitem__(key) - return WriterTableRange(obj, key, self.name) - - @property - def name(self): - return self.obj.Name - @name.setter - def name(self, value): - self.obj.Name = value - - -class WriterTableRange(ObjectBase): - - def __init__(self, obj, index, table_name): - self._index = index - self._table_name = table_name - super().__init__(obj) - self._is_cell = hasattr(self.obj, 'CellName') - - def __getitem__(self, key): - obj = super().__getitem__(key) - return WriterTableRange(obj, key, self._table_name) - - @property - def value(self): - return self.obj.String - @value.setter - def value(self, value): - self.obj.String = value - - @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) - - @property - def rows(self): - return len(self.data) - - @property - def columns(self): - return len(self.data[0]) - - @property - def name(self): - if self._is_cell: - name = '{}.{}'.format(self._table_name, self.obj.CellName) - elif isinstance(self._index, str): - name = '{}.{}'.format(self._table_name, self._index) - else: - c1 = self.obj[0,0].CellName - c2 = self.obj[self.rows-1,self.columns-1].CellName - name = '{}.{}:{}'.format(self._table_name, c1, c2) - return name - - def get_cell(self, *index): - return self[index] - - def get_column(self, index=0, start=1): - return self[start:self.rows,index:index+1] - - def get_series(self): - class Serie(): - pass - series = [] - for i in range(self.columns): - serie = Serie() - serie.label = self.get_cell(0,i).name - serie.data = self.get_column(i).data - serie.values = self.get_column(i).name - series.append(serie) - return series - - -class ChartFormat(object): - - def __call__(self, obj): - for k, v in self.__dict__.items(): - if hasattr(obj, k): - setattr(obj, k, v) - - -class LOChart(object): - BASE = 'com.sun.star.chart.{}Diagram' - - def __init__(self, obj, tipo=''): - self._obj = obj - self._type = tipo - self._name = '' - self._table = None - self._data = () - self._data_series = () - self._cell = None - self._cursor = None - self._doc = None - self._title = ChartFormat() - self._subtitle = ChartFormat() - self._legend = ChartFormat() - self._xaxistitle = ChartFormat() - self._yaxistitle = ChartFormat() - self._xaxis = ChartFormat() - self._yaxis = ChartFormat() - self._xmaingrid = ChartFormat() - self._ymaingrid = ChartFormat() - self._xhelpgrid = ChartFormat() - self._yhelpgrid = ChartFormat() - self._area = ChartFormat() - self._wall = ChartFormat() - self._dim3d = False - self._series = () - self._labels = () - return - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.insert() - - @property - def obj(self): - return self._obj - @obj.setter - def obj(self, value): - self._obj = value - - @property - def name(self): - return self._name - @name.setter - def name(self, value): - self._name = value - - @property - def type(self): - return self._type - @type.setter - def type(self, value): - self._type = value - - @property - def table(self): - return self._table - @table.setter - def table(self, value): - self._table = value - - @property - def data(self): - return self._data - @data.setter - def data(self, value): - self._data = value - - @property - def cell(self): - return self._cell - @cell.setter - def cell(self, value): - self._cell = value - self.doc = value.doc - - @property - def cursor(self): - return self._cursor - @cursor.setter - def cursor(self, value): - self._cursor = value - - @property - def doc(self): - return self._doc - @doc.setter - def doc(self, value): - self._doc = value - - @property - def width(self): - return self._width - @width.setter - def width(self, value): - self._width = value - - @property - def height(self): - return self._height - @height.setter - def height(self, value): - self._height = value - - @property - def title(self): - return self._title - - @property - def subtitle(self): - return self._subtitle - - @property - def legend(self): - return self._legend - - @property - def xaxistitle(self): - return self._xaxistitle - - @property - def yaxistitle(self): - return self._yaxistitle - - @property - def xaxis(self): - return self._xaxis - - @property - def yaxis(self): - return self._yaxis - - @property - def xmaingrid(self): - return self._xmaingrid - - @property - def ymaingrid(self): - return self._ymaingrid - - @property - def xhelpgrid(self): - return self._xhelpgrid - - @property - def yhelpgrid(self): - return self._yhelpgrid - - @property - def area(self): - return self._area - - @property - def wall(self): - return self._wall - - @property - def dim3d(self): - return self._dim3d - @dim3d.setter - def dim3d(self, value): - self._dim3d = value - - @property - def series(self): - return self._series - @series.setter - def series(self, value): - self._series = value - - @property - def data_series(self): - return self._series - @data_series.setter - def data_series(self, value): - self._data_series = value - - @property - def labels(self): - return self._labels - @labels.setter - def labels(self, value): - self._labels = value - - def _add_series_writer(self, chart): - dp = self.doc.create_instance('com.sun.star.chart2.data.DataProvider') - chart.attachDataProvider(dp) - chart_type = chart.getFirstDiagram().getCoordinateSystems()[0].getChartTypes()[0] - self._data_series = self.table[self.data].get_series() - series = [self._create_serie(dp, s) for s in self._data_series[1:]] - chart_type.setDataSeries(tuple(series)) - chart_data = chart.getData() - chart_data.ComplexRowDescriptions = self._data_series[0].data - return - - def _get_series(self): - rango = self._data_series - class Serie(): - pass - series = [] - for i in range(0, rango.columns, 2): - serie = Serie() - serie.label = rango[0, i+1].name - serie.xvalues = rango.get_column(i).name - serie.values = rango.get_column(i+1).name - series.append(serie) - return series - - def _add_series_calc(self, chart): - dp = self.doc.create_instance('com.sun.star.chart2.data.DataProvider') - chart.attachDataProvider(dp) - chart_type = chart.getFirstDiagram().getCoordinateSystems()[0].getChartTypes()[0] - series = self._get_series() - series = [self._create_serie(dp, s) for s in series] - chart_type.setDataSeries(tuple(series)) - return - - def _create_serie(self, dp, data): - serie = create_instance('com.sun.star.chart2.DataSeries') - rango = data.values - is_x = hasattr(data, 'xvalues') - if is_x: - xrango = data.xvalues - rango_label = data.label - - lds = create_instance('com.sun.star.chart2.data.LabeledDataSequence') - values = self._create_data(dp, rango, 'values-y') - lds.setValues(values) - if data.label: - label = self._create_data(dp, rango_label, '') - lds.setLabel(label) - - xlds = () - if is_x: - xlds = create_instance('com.sun.star.chart2.data.LabeledDataSequence') - values = self._create_data(dp, xrango, 'values-x') - xlds.setValues(values) - - if is_x: - serie.setData((lds, xlds)) - else: - serie.setData((lds,)) - - return serie - - def _create_data(self, dp, rango, role): - data = dp.createDataSequenceByRangeRepresentation(rango) - if not data is None: - data.Role = role - return data - - def _from_calc(self): - ps = self.cell.ps - ps.Width = self.width - ps.Height = self.height - charts = self.cell.charts - data = () - if self.data: - data = (self.data.address,) - charts.addNewByName(self.name, ps, data, True, True) - self.obj = charts.getByName(self.name) - chart = self.obj.getEmbeddedObject() - chart.setDiagram(chart.createInstance(self.BASE.format(self.type))) - if not self.data: - self._add_series_calc(chart) - return chart - - def _from_writer(self): - obj = self.doc.create_instance('com.sun.star.text.TextEmbeddedObject') - obj.setPropertyValue('CLSID', '12DCAE26-281F-416F-a234-c3086127382e') - obj.Name = self.name - obj.setSize(Size(self.width, self.height)) - self.doc.insert_content(self.cursor, obj) - self.obj = obj - chart = obj.getEmbeddedObject() - tipo = self.type - if self.type == 'Column': - tipo = 'Bar' - chart.Diagram.Vertical = True - chart.setDiagram(chart.createInstance(self.BASE.format(tipo))) - chart.DataSourceLabelsInFirstColumn = True - if isinstance(self.data, str): - self._add_series_writer(chart) - else: - chart_data = chart.getData() - labels = [r[0] for r in self.data] - data = [(r[1],) for r in self.data] - chart_data.setData(data) - chart_data.RowDescriptions = labels - - # ~ Bug - if tipo == 'Pie': - chart.setDiagram(chart.createInstance(self.BASE.format('Bar'))) - chart.setDiagram(chart.createInstance(self.BASE.format('Pie'))) - - return chart - - def insert(self): - if not self.cell is None: - chart = self._from_calc() - elif not self.cursor is None: - chart = self._from_writer() - - diagram = chart.Diagram - - if self.type == 'Bar': - diagram.Vertical = True - - if hasattr(self.title, 'String'): - chart.HasMainTitle = True - self.title(chart.Title) - - if hasattr(self.subtitle, 'String'): - chart.HasSubTitle = True - self.subtitle(chart.SubTitle) - - if self.legend.__dict__: - chart.HasLegend = True - self.legend(chart.Legend) - - if self.xaxistitle.__dict__: - diagram.HasXAxisTitle = True - self.xaxistitle(diagram.XAxisTitle) - - if self.yaxistitle.__dict__: - diagram.HasYAxisTitle = True - self.yaxistitle(diagram.YAxisTitle) - - if self.dim3d: - diagram.Dim3D = True - - if self.series: - data_series = chart.getFirstDiagram( - ).getCoordinateSystems( - )[0].getChartTypes()[0].DataSeries - for i, serie in enumerate(data_series): - for k, v in self.series[i].items(): - if hasattr(serie, k): - setattr(serie, k, v) - return self - - -def _set_column_model(columns): - #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html - column_model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) - for column in columns: - grid_column = create_instance('com.sun.star.awt.grid.GridColumn', True) - for k, v in column.items(): - setattr(grid_column, k, v) - column_model.addColumn(grid_column) - return column_model - - -def _set_image_url(image, id_extension=''): - if exists_path(image): - return _path_url(image) - - if not id_extension: - return '' - - path = get_path_extension(id_extension) - path = join(path, DIR['images'], image) - return _path_url(path) +UNO_CLASSES = { + 'label': UnoLabel, + 'link': UnoLabelLink, + 'button': UnoButton, + 'radio': UnoRadio, + 'check': UnoCheck, + 'text': UnoText, + 'image': UnoImage, + 'listbox': UnoListBox, +} class LODialog(object): + SEPARATION = 5 + MODELS = { + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'button': 'com.sun.star.awt.UnoControlButtonModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'check': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + # ~ 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + # ~ 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + # ~ 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + # ~ 'tree': 'com.sun.star.awt.tree.TreeControlModel', + # ~ 'pages': 'com.sun.star.awt.UnoMultiPageModel', + } - def __init__(self, **properties): - self._obj = self._create(properties) - self._init_values() - - def _init_values(self): - self._model = self._obj.Model - self._init_controls() + def __init__(self, args): + self._obj = self._create(args) + self._model = self.obj.Model self._events = None - self._color_on_focus = -1 - self._id_extension = '' - self._images = 'images' - return + self._modal = True + self._controls = {} + self._color_on_focus = COLOR_ON_FOCUS + self._id = '' + self._path = '' - def _create(self, properties): - path = properties.pop('Path', '') + def _create(self, args): + service = 'com.sun.star.awt.DialogProvider' + path = args.pop('Path', '') if path: - dp = create_instance('com.sun.star.awt.DialogProvider', True) - return dp.createDialog(_path_url(path)) + dp = create_instance(service, True) + dlg = dp.createDialog(_P.to_url(path)) + return dlg - if 'Location' in properties: - location = properties.get('Location', 'application') - library = properties.get('Library', 'Standard') + if 'Location' in args: + name = args['Name'] + library = args.get('Library', 'Standard') + location = args.get('Location', 'application') if location == 'user': location = 'application' - dp = create_instance('com.sun.star.awt.DialogProvider', True) - path = 'vnd.sun.star.script:{}.{}?location={}'.format( - library, properties['Name'], location) + url = f'vnd.sun.star.script:{library}.{name}?location={location}' if location == 'document': - uid = get_document().uid - path = 'vnd.sun.star.tdoc:/{}/Dialogs/{}/{}.xml'.format( - uid, library, properties['Name']) - return dp.createDialog(path) + dp = create_instance(service, args=docs.active.obj) + else: + dp = create_instance(service, True) + # ~ uid = docs.active.uid + # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml' + dlg = dp.createDialog(url) + return dlg dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) model = create_instance('com.sun.star.awt.UnoControlDialogModel', True) toolkit = create_instance('com.sun.star.awt.Toolkit', True) - set_properties(model, properties) + _set_properties(model, args) dlg.setModel(model) dlg.setVisible(False) dlg.createPeer(toolkit, None) - return dlg - def _get_type_control(self, name): - types = { - 'stardiv.Toolkit.UnoFixedTextControl': 'label', - 'stardiv.Toolkit.UnoFixedHyperlinkControl': 'link', - 'stardiv.Toolkit.UnoEditControl': 'text', - 'stardiv.Toolkit.UnoButtonControl': 'button', - 'stardiv.Toolkit.UnoListBoxControl': 'listbox', - 'stardiv.Toolkit.UnoRoadmapControl': 'roadmap', - 'stardiv.Toolkit.UnoMultiPageControl': 'pages', - } - return types[name] - - def _init_controls(self): - for control in self.obj.getControls(): - tipo = self._get_type_control(control.ImplementationName) - name = control.Model.Name - control = get_custom_class(tipo, control) - setattr(self, name, control) - return - @property def obj(self): return self._obj @@ -3687,20 +3522,19 @@ class LODialog(object): return self._model @property - def id_extension(self): - return self._id_extension - @id_extension.setter - def id_extension(self, value): - global ID_EXTENSION - ID_EXTENSION = value - self._id_extension = value + def controls(self): + return self._controls @property - def images(self): - return self._images - @images.setter - def images(self, value): - self._images = value + def path(self): + return self._path + @property + def id(self): + return self._id + @id.setter + def id(self, value): + self._id = value + self._path = _P.from_id(value) @property def height(self): @@ -3717,13 +3551,11 @@ class LODialog(object): self.model.Width = value @property - def color_on_focus(self): - return self._color_on_focus - @color_on_focus.setter - def color_on_focus(self, value): - global COLOR_ON_FOCUS - COLOR_ON_FOCUS = get_color(value) - self._color_on_focus = COLOR_ON_FOCUS + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value @property def step(self): @@ -3737,90 +3569,68 @@ class LODialog(object): return self._events @events.setter def events(self, controllers): - self._events = controllers + self._events = controllers(self) self._connect_listeners() + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + self._color_on_focus = get_color(value) + def _connect_listeners(self): - for control in self.obj.getControls(): - add_listeners(self._events, control, control.Model.Name) + for control in self.obj.Controls: + _add_listeners(self.events, control, control.Model.Name) 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', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', - 'text': 'com.sun.star.awt.UnoControlEditModel', - 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', - 'button': 'com.sun.star.awt.UnoControlButtonModel', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', - 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', - 'tree': 'com.sun.star.awt.tree.TreeControlModel', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', - 'image': 'com.sun.star.awt.UnoControlImageControlModel', - 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', - 'pages': 'com.sun.star.awt.UnoMultiPageModel', - } - return services[control] - - def _set_column_model(self, columns): - #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html - column_model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) - for column in columns: - grid_column = create_instance('com.sun.star.awt.grid.GridColumn', True) - for k, v in column.items(): - setattr(grid_column, k, v) - column_model.addColumn(grid_column) - return column_model - def _set_image_url(self, image): - if exists_path(image): - return _path_url(image) + if _P.exists(image): + return _P.to_url(image) - if not self.id_extension: - return '' + path = _P.join(self._path, DIR['images'], image) + return _P.to_url(path) - path = get_path_extension(self.id_extension) - path = join(path, self.images, image) - return _path_url(path) + def _special_properties(self, tipo, args): + columns = args.pop('Columns', ()) - def _special_properties(self, tipo, properties): - columns = properties.pop('Columns', ()) - if tipo == 'grid': - properties['ColumnModel'] = self._set_column_model(columns) - elif tipo == 'button' and 'ImageURL' in properties: - properties['ImageURL'] = self._set_image_url(properties['ImageURL']) + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + elif tipo == 'grid': + args['ColumnModel'] = self._set_column_model(columns) + elif tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + if not 'FocusOnClick' in args: + args['FocusOnClick'] = False elif tipo == 'roadmap': - if not 'Height' in properties: - properties['Height'] = self.height - if 'Title' in properties: - properties['Text'] = properties.pop('Title') + if not 'Height' in args: + args['Height'] = self.height + if 'Title' in args: + args['Text'] = args.pop('Title') elif tipo == 'tab': - if not 'Width' in properties: - properties['Width'] = self.width - if not 'Height' in properties: - properties['Height'] = self.height + if not 'Width' in args: + args['Width'] = self.width + if not 'Height' in args: + args['Height'] = self.height - return properties + return args - def add_control(self, properties): - tipo = properties.pop('Type').lower() - root = properties.pop('Root', '') - sheets = properties.pop('Sheets', ()) + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) - properties = self._special_properties(tipo, properties) - model = self.model.createInstance(self._get_control_model(tipo)) - set_properties(model, properties) - name = properties['Name'] + args = self._special_properties(tipo, args) + model = self.model.createInstance(self.MODELS[tipo]) + _set_properties(model, args) + name = args['Name'] self.model.insertByName(name, model) control = self.obj.getControl(name) - add_listeners(self.events, control, name) - control = get_custom_class(tipo, control) + _add_listeners(self.events, control, name) + control = UNO_CLASSES[tipo](control) + if tipo in ('listbox',): + control.path = self.path if tipo == 'tree' and root: control.root = root @@ -3829,20 +3639,21 @@ class LODialog(object): control.events = self.events setattr(self, name, control) - return + self._controls[name] = control + return control def center(self, control, x=0, y=0): w = self.width h = self.height if isinstance(control, tuple): - wt = SEPARATION * -1 + wt = self.SEPARATION * -1 for c in control: - wt += c.width + SEPARATION + wt += c.width + self.SEPARATION x = w / 2 - wt / 2 for c in control: c.x = x - x = c.x + c.width + SEPARATION + x = c.x + c.width + self.SEPARATION return if x < 0: @@ -3857,488 +3668,691 @@ class LODialog(object): control.y = y return + def open(self, modal=True): + self._modal = modal + if modal: + return self.obj.execute() + else: + self.visible = True + return -class LOWindow(object): - EMPTY = b""" - -""" + def close(self, value=0): + if self._modal: + value = self.obj.endDialog(value) + else: + self.visible = False + self.obj.dispose() + return value - def __init__(self, **kwargs): - self._events = None + +class LOSheets(object): + + def __getitem__(self, index): + return LODocs().active[index] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +class LOCells(object): + + def __getitem__(self, index): + return LODocs().active.active[index] + + +class LOShortCut(object): +# ~ getKeyEventsByCommand + + def __init__(self, app): + self._app = app + self._scm = None + self._init_values() + + def _init_values(self): + name = 'com.sun.star.ui.GlobalAcceleratorConfiguration' + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + uicm = manager.getUIConfigurationManager(service) + self._scm = uicm.ShortCutManager + return + + def __contains__(self, item): + cmd = self._get_command(item) + return bool(cmd) + + def _get_key_event(self, command): + events = self._scm.AllKeyEvents + for event in events: + cmd = self._scm.getCommandByKeyEvent(event) + if cmd == command: + break + return event + + def _to_key_event(self, shortcut): + key_event = KeyEvent() + keys = shortcut.split('+') + for v in keys[:-1]: + key_event.Modifiers += MODIFIERS[v.lower()] + key_event.KeyCode = getattr(Key, keys[-1].upper()) + return key_event + + def _get_command(self, shortcut): + command = '' + key_event = self._to_key_event(shortcut) + try: + command = self._scm.getCommandByKeyEvent(key_event) + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return command + + def add(self, shortcut, command): + if isinstance(command, dict): + command = _get_url_script(command) + key_event = self._to_key_event(shortcut) + self._scm.setKeyEvent(key_event, command) + self._scm.store() + return + + def reset(self): + self._scm.reset() + self._scm.store() + return + + def remove(self, shortcut): + key_event = self._to_key_event(shortcut) + try: + self._scm.removeKeyEvent(key_event) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return + + def remove_by_command(self, command): + if isinstance(command, dict): + command = _get_url_script(command) + try: + self._scm.removeCommandFromAllKeyEvents(command) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {command}') + return + + +class LOShortCuts(object): + + def __getitem__(self, index): + return LOShortCut(index) + + +class LOMenu(object): + + def __init__(self, app): + self._app = app + self._ui = None + self._pymenus = None self._menu = None - self._container = None - self._id_extension = '' - self._obj = self._create(kwargs) + self._menus = self._get_menus() + + def __getitem__(self, index): + if isinstance(index, int): + self._menu = self._menus[index] + else: + for menu in self._menus: + cmd = menu.get('CommandURL', '') + if MENUS[index.lower()] == cmd: + self._menu = menu + break + line = self._menu.get('CommandURL', '') + line += self._get_submenus(self._menu['ItemDescriptorContainer']) + return line + + def _get_menus(self): + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + self._ui = manager.getUIConfigurationManager(service) + self._pymenus = self._ui.getSettings(NODE_MENUBAR, True) + data = [] + for menu in self._pymenus: + data.append(data_to_dict(menu)) + return data + + def _get_info(self, menu): + line = menu.get('CommandURL', '') + line += self._get_submenus(menu['ItemDescriptorContainer']) + return line + + def _get_submenus(self, menu, level=1): + line = '' + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '----------') + line += f'\n{" " * level}├─ ({i}) {cmd}' + submenu = data.get('ItemDescriptorContainer', None) + if not submenu is None: + line += self._get_submenus(submenu, level + 1) + return line + + def __str__(self): + info = '\n'.join([self._get_info(m) for m in self._menus]) + return info + + def _get_index_menu(self, menu, command): + index = -1 + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '') + if cmd == command: + index = i + break + return index + + def insert(self, name, args): + idc = None + replace = False + command = args['CommandURL'] + label = args['Label'] + + self[name] + menu = self._menu['ItemDescriptorContainer'] + submenu = args.get('Submenu', False) + if submenu: + idc = self._ui.createSettings() + + index = self._get_index_menu(menu, command) + if index == -1: + if 'Index' in args: + index = args['Index'] + else: + index = self._get_index_menu(menu, args['After']) + 1 + else: + replace = True + + data = dict ( + CommandURL = command, + Label = label, + Style = 0, + Type = 0, + ItemDescriptorContainer = idc, + ) + self._save(menu, data, index, replace) + self._insert_submenu(idc, submenu) + return + + def _get_command(self, args): + shortcut = args.get('ShortCut', '') + cmd = args['CommandURL'] + if isinstance(cmd, dict): + cmd = _get_url_script(cmd) + if shortcut: + LOShortCut(self._app).add(shortcut, cmd) + return cmd + + def _insert_submenu(self, parent, menus): + for i, v in enumerate(menus): + submenu = v.pop('Submenu', False) + if submenu: + idc = self._ui.createSettings() + v['ItemDescriptorContainer'] = idc + v['Type'] = 0 + if v['Label'] == '-': + v['Type'] = 1 + else: + v['CommandURL'] = self._get_command(v) + self._save(parent, v, i) + if submenu: + self._insert_submenu(idc, submenu) + return + + def remove(self, name, command): + self[name] + menu = self._menu['ItemDescriptorContainer'] + index = self._get_index_menu(menu, command) + if index > -1: + uno.invoke(menu, 'removeByIndex', (index,)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + def _save(self, menu, properties, index, replace=False): + properties = dict_to_property(properties, True) + if replace: + uno.invoke(menu, 'replaceByIndex', (index, properties)) + else: + uno.invoke(menu, 'insertByIndex', (index, properties)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + +class LOMenus(object): + + def __getitem__(self, index): + return LOMenu(index) + + +class classproperty: + def __init__(self, method=None): + self.fget = method + + def __get__(self, instance, cls=None): + return self.fget(cls) + + def getter(self, method): + self.fget = method + return self + + +class ClipBoard(object): + SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard' + CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' + + class TextTransferable(unohelper.Base, XTransferable): + + def __init__(self, text): + df = DataFlavor() + df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT + df.HumanPresentableName = "encoded text utf-16" + self.flavors = (df,) + self._data = text + + def getTransferData(self, flavor): + return self._data + + def getTransferDataFlavors(self): + return self.flavors + + + @classmethod + def set(cls, value): + ts = cls.TextTransferable(value) + sc = create_instance(cls.SERVICE) + sc.setContents(ts, None) + return + + @classproperty + def contents(cls): + df = None + text = '' + sc = create_instance(cls.SERVICE) + transferable = sc.getContents() + data = transferable.getTransferDataFlavors() + for df in data: + if df.MimeType == cls.CLIPBOARD_FORMAT_TEXT: + break + if df: + text = transferable.getTransferData(df) + return text + + +class Paths(object): + FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' + + def __init__(self, path=''): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + self._path = Path(path) @property - def id_extension(self): - return self._id_extension - @id_extension.setter - def id_extension(self, value): - global ID_EXTENSION - ID_EXTENSION = value - self._id_extension = value + def path(self): + return str(self._path.parent) - def _create(self, properties): - ps = ( - properties.get('X', 0), - properties.get('Y', 0), - properties.get('Width', 500), - properties.get('Height', 500), - ) - self._title = properties.get('Title', TITLE) - self._create_frame(ps) - self._create_container(ps) - self._create_subcontainer(ps) - # ~ self._create_splitter(ps) - return - - def _create_frame(self, ps): - service = 'com.sun.star.frame.TaskCreator' - tc = create_instance(service, True) - self._frame = tc.createInstanceWithArguments(( - NamedValue('FrameName', 'EasyMacroWin'), - NamedValue('PosSize', Rectangle(*ps)), - )) - self._window = self._frame.getContainerWindow() - self._toolkit = self._window.getToolkit() - desktop = get_desktop() - self._frame.setCreator(desktop) - desktop.getFrames().append(self._frame) - self._frame.Title = self._title - return - - def _create_container(self, ps): - # ~ toolkit = self._window.getToolkit() - service = 'com.sun.star.awt.UnoControlContainer' - self._container = create_instance(service, True) - service = 'com.sun.star.awt.UnoControlContainerModel' - model = create_instance(service, True) - model.BackgroundColor = get_color(225, 225, 225) - self._container.setModel(model) - self._container.createPeer(self._toolkit, self._window) - self._container.setPosSize(*ps, POSSIZE) - self._frame.setComponent(self._container, None) - return - - def _create_subcontainer(self, ps): - service = 'com.sun.star.awt.ContainerWindowProvider' - cwp = create_instance(service, True) - with get_temp_file() as f: - f.write(self.EMPTY) - f.flush() - subcont = cwp.createContainerWindow( - _path_url(f.name), '', self._container.getPeer(), None) - - # ~ service = 'com.sun.star.awt.UnoControlDialog' - # ~ subcont2 = create_instance(service, True) - # ~ service = 'com.sun.star.awt.UnoControlDialogModel' - # ~ model = create_instance(service, True) - # ~ service = 'com.sun.star.awt.UnoControlContainer' - # ~ context = create_instance(service, True) - # ~ subcont2.setModel(model) - # ~ subcont2.setContext(context) - # ~ subcont2.createPeer(self._toolkit, self._container.getPeer()) - - subcont.setPosSize(0, 0, 500, 500, POSSIZE) - subcont.setVisible(True) - self._container.addControl('subcont', subcont) - self._subcont = subcont - return - - def _get_base_control(self, tipo): - services = { - 'label': 'com.sun.star.awt.UnoControlFixedText', - 'button': 'com.sun.star.awt.UnoControlButton', - 'text': 'com.sun.star.awt.UnoControlEdit', - 'listbox': 'com.sun.star.awt.UnoControlListBox', - 'link': 'com.sun.star.awt.UnoControlFixedHyperlink', - 'roadmap': 'com.sun.star.awt.UnoControlRoadmap', - 'image': 'com.sun.star.awt.UnoControlImageControl', - 'groupbox': 'com.sun.star.awt.UnoControlGroupBox', - 'radio': 'com.sun.star.awt.UnoControlRadioButton', - 'tree': 'com.sun.star.awt.tree.TreeControl', - 'grid': 'com.sun.star.awt.grid.UnoControlGrid', - 'tab': 'com.sun.star.awt.tab.UnoControlTabPage', - } - return services[tipo] - - def _special_properties(self, tipo, properties): - columns = properties.pop('Columns', ()) - if tipo == 'grid': - properties['ColumnModel'] = self._set_column_model(columns) - elif tipo == 'button' and 'ImageURL' in properties: - properties['ImageURL'] = _set_image_url( - properties['ImageURL'], self.id_extension) - elif tipo == 'roadmap': - if not 'Height' in properties: - properties['Height'] = self.height - if 'Title' in properties: - properties['Text'] = properties.pop('Title') - elif tipo == 'tab': - if not 'Width' in properties: - properties['Width'] = self.width - 20 - if not 'Height' in properties: - properties['Height'] = self.height - 20 - - return properties - - def add_control(self, properties): - tipo = properties.pop('Type').lower() - root = properties.pop('Root', '') - sheets = properties.pop('Sheets', ()) - - properties = self._special_properties(tipo, properties) - model = self._subcont.Model.createInstance(get_control_model(tipo)) - set_properties(model, properties) - name = properties['Name'] - self._subcont.Model.insertByName(name, model) - control = self._subcont.getControl(name) - add_listeners(self.events, control, name) - control = get_custom_class(tipo, control) - - if tipo == 'tree' and root: - control.root = root - elif tipo == 'tab' and sheets: - control.sheets = sheets - control.events = self.events - - setattr(self, name, control) - return - - def _create_popupmenu(self, menus): - menu = create_instance('com.sun.star.awt.PopupMenu', True) - for i, m in enumerate(menus): - label = m['label'] - cmd = m.get('event', '') - if not cmd: - cmd = label.lower().replace(' ', '_') - if label == '-': - menu.insertSeparator(i) - else: - menu.insertItem(i, label, m.get('style', 0), i) - menu.setCommand(i, cmd) - # ~ menu.setItemImage(i, path?, True) - menu.addMenuListener(EventsMenu(self.events)) - return menu - - def _create_menu(self, menus): - #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMenu.html - #~ nItemId specifies the ID of the menu item to be inserted. - #~ aText specifies the label of the menu item. - #~ nItemStyle 0 = Standard, CHECKABLE = 1, RADIOCHECK = 2, AUTOCHECK = 4 - #~ nItemPos specifies the position where the menu item will be inserted. - self._menu = create_instance('com.sun.star.awt.MenuBar', True) - for i, m in enumerate(menus): - self._menu.insertItem(i, m['label'], m.get('style', 0), i) - cmd = m['label'].lower().replace(' ', '_') - self._menu.setCommand(i, cmd) - submenu = self._create_popupmenu(m['submenu']) - self._menu.setPopupMenu(i, submenu) - - self._window.setMenuBar(self._menu) - return - - def add_menu(self, menus): - self._create_menu(menus) - return - - def _add_listeners(self, control=None): - if self.events is None: - return - controller = EventsWindow(self) - self._window.addTopWindowListener(controller) - self._window.addWindowListener(controller) - self._container.addKeyListener(EventsKeyWindow(self)) - return + @property + def file_name(self): + return self._path.name @property def name(self): - return self._title.lower().replace(' ', '_') + return self._path.stem @property - def events(self): - return self._events - @events.setter - def events(self, value): - self._events = value - self._add_listeners() + def ext(self): + return self._path.suffix[1:] @property - def width(self): - return self._container.Size.Width + def info(self): + return self.path, self.file_name, self.name, self.ext @property - def height(self): - return self._container.Size.Height + def url(self): + return self._path.as_uri() - def open(self): - self._window.setVisible(True) - return + @property + def size(self): + return self._path.stat().st_size - def close(self): - self._window.setMenuBar(None) - self._window.dispose() - self._frame.close(True) - return + @classproperty + def home(self): + return str(Path.home()) + @classproperty + def documents(self): + return self.config() -# ~ Python >= 3.7 -# ~ def __getattr__(name): + @classproperty + def temp_dir(self): + return tempfile.gettempdir() + @classproperty + def python(self): + return sys.executable -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) + @classmethod + def dir_tmp(self, only_name=False): + dt = tempfile.TemporaryDirectory() + if only_name: + dt = dt.name + return dt + @classmethod + def tmp(cls, ext=''): + tmp = tempfile.NamedTemporaryFile(suffix=ext) + return tmp.name -# ~ Export ok -def get_document(title=''): - doc = None - desktop = get_desktop() - if not title: - doc = _get_class_doc(desktop.getCurrentComponent()) - return doc + @classmethod + def config(cls, name='Work'): + """ + Return de path name in config + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html + """ + path = create_instance('com.sun.star.util.PathSettings') + return cls.to_system(getattr(path, name)) - for d in desktop.getComponents(): - if hasattr(d, 'Title') and d.Title == title: - doc = d - break + @classmethod + def get(cls, init_dir='', filters=()): + """ + Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html + filters: Example + ( + ('XML', '*.xml'), + ('TXT', '*.txt'), + ) + """ + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) + file_picker = create_instance(cls.FILE_PICKER) + file_picker.setTitle(_('Select path')) + file_picker.setDisplayDirectory(init_dir) + file_picker.initialize((2,)) + if filters: + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) - if doc is None: - return + path = '' + if file_picker.execute(): + path = cls.to_system(file_picker.getSelectedFiles()[0]) + return path - return _get_class_doc(doc) + @classmethod + def get_dir(cls, init_dir=''): + folder_picker = create_instance(cls.FILE_PICKER) + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) + folder_picker.setTitle(_('Select directory')) + folder_picker.setDisplayDirectory(init_dir) + path = '' + if folder_picker.execute(): + path = cls.to_system(folder_picker.getDisplayDirectory()) + return path -def get_documents(custom=True): - docs = [] - desktop = get_desktop() - for doc in desktop.getComponents(): - if custom: - docs.append(_get_class_doc(doc)) + @classmethod + def get_file(cls, init_dir='', filters=(), multiple=False): + """ + init_folder: folder default open + multiple: True for multiple selected + filters: Example + ( + ('XML', '*.xml'), + ('TXT', '*.txt'), + ) + """ + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) + + file_picker = create_instance(cls.FILE_PICKER) + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.setMultiSelectionMode(multiple) + + if filters: + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + path = '' + if file_picker.execute(): + files = file_picker.getSelectedFiles() + path = [cls.to_system(f) for f in files] + if not multiple: + path = path[0] + return path + + @classmethod + def replace_ext(cls, path, new_ext): + p = Paths(path) + name = f'{p.name}.{new_ext}' + path = cls.join(p.path, name) + return path + + @classmethod + def exists(cls, path): + result = False + if path: + path = cls.to_system(path) + result = Path(path).exists() + return result + + @classmethod + def exists_app(cls, name_app): + return bool(shutil.which(name_app)) + + @classmethod + def open(cls, path): + if IS_WIN: + os.startfile(path) else: - docs.append(doc) - return docs + pid = subprocess.Popen(['xdg-open', path]).pid + return + @classmethod + def is_dir(cls, path): + return Path(path).is_dir() -def get_selection(): - return get_document().selection + @classmethod + def is_file(cls, path): + return Path(path).is_file() + @classmethod + def join(cls, *paths): + return str(Path(paths[0]).joinpath(*paths[1:])) -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 + @classmethod + def save(cls, path, data, encoding='utf-8'): + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + @classmethod + def save_bin(cls, path, data): + result = bool(Path(path).write_bytes(data)) + return result -def active_cell(): - return get_cell() + @classmethod + def read(cls, path, encoding='utf-8'): + data = Path(path).read_text(encoding=encoding) + return data + @classmethod + def read_bin(cls, path): + data = Path(path).read_bytes() + return data -def active_sheet(): - return get_document().active - - -def create_dialog(properties): - return LODialog(**properties) - - -def create_window(kwargs): - return LOWindow(**kwargs) - - -# ~ Export ok -def get_config_path(name='Work'): - """ - Return de path name in config - http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html - """ - path = create_instance('com.sun.star.util.PathSettings') - return _path_system(getattr(path, name)) - - -def get_path_python(): - path = get_config_path('Module') - if IS_MAC: - path = join(path, '..', 'Resources', PYTHON) - else: - path = join(path, PYTHON) - - cmd = '"{}" -V'.format(path) - if run(cmd, True): + @classmethod + def to_url(cls, path): + if not path.startswith('file://'): + path = Path(path).as_uri() return path - path = PYTHON - cmd = '"{}" -V'.format(path) - result = run(cmd, True) - - if 'Python 3' in result: + @classmethod + def to_system(cls, path): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) return path - path = PYTHON + '3' - cmd = '"{}" -V'.format(path) - result = run(cmd, True) + @classmethod + def kill(cls, path): + result = True + p = Path(path) - if 'Python 3' in result: + try: + if p.is_file(): + p.unlink() + elif p.is_dir(): + shutil.rmtree(path) + except OSError as e: + log.error(e) + result = False + + return result + + @classmethod + def walk(cls, path, filters=''): + paths = [] + if filters in ('*', '*.*'): + filters = '' + for folder, _, files in os.walk(path): + if filters: + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [cls.join(folder, f) for f in files if pattern.search(f)] + else: + paths += [cls.join(folder, f) for f in files] + return paths + + @classmethod + def from_id(cls, id_ext): + pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') + path = _P.to_system(pip.getPackageLocation(id_ext)) return path - return '' + @classmethod + def from_json(cls, path): + data = json.loads(cls.read(path)) + return data + @classmethod + def to_json(cls, path, data): + data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) + return cls.save(path, data) -# ~ Export ok -def get_file(init_dir='', multiple=False, filters=()): - """ - init_folder: folder default open - multiple: True for multiple selected - filters: Example - ( - ('XML', '*.xml'), - ('TXT', '*.txt'), - ) - """ - if not init_dir: - init_dir = get_config_path() - init_dir = _path_url(init_dir) - file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker') - file_picker.setTitle(_('Select file')) - file_picker.setDisplayDirectory(init_dir) - file_picker.setMultiSelectionMode(multiple) + @classmethod + def from_csv(cls, path, args={}): + # ~ See https://docs.python.org/3.7/library/csv.html#csv.reader + with open(path) as f: + rows = tuple(csv.reader(f, **args)) + return rows - path = '' - if filters: - file_picker.setCurrentFilter(filters[0][0]) - for f in filters: - file_picker.appendFilter(f[0], f[1]) + @classmethod + def to_csv(cls, path, data, args={}): + with open(path, 'w') as f: + writer = csv.writer(f, **args) + writer.writerows(data) + return - if file_picker.execute(): - path = _path_system(file_picker.getSelectedFiles()[0]) - if multiple: - path = [_path_system(f) for f in file_picker.getSelectedFiles()] + @classmethod + def zip(cls, source, target='', pwd=''): + path_zip = target + if not isinstance(source, (tuple, list)): + path, _, name, _ = _P(source).info + start = len(path) + 1 + if not target: + path_zip = f'{path}/{name}.zip' - return path - - -# ~ Export ok -def get_path(init_dir='', filters=()): - """ - Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html - filters: Example - ( - ('XML', '*.xml'), - ('TXT', '*.txt'), - ) - """ - if not init_dir: - init_dir = get_config_path() - init_dir = _path_url(init_dir) - file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker') - file_picker.setTitle(_('Select file')) - file_picker.setDisplayDirectory(init_dir) - file_picker.initialize((2,)) - if filters: - file_picker.setCurrentFilter(filters[0][0]) - for f in filters: - file_picker.appendFilter(f[0], f[1]) - - path = '' - if file_picker.execute(): - path = _path_system(file_picker.getSelectedFiles()[0]) - return path - - -# ~ Export ok -def get_dir(init_dir=''): - folder_picker = create_instance('com.sun.star.ui.dialogs.FolderPicker') - if not init_dir: - init_dir = get_config_path() - init_dir = _path_url(init_dir) - folder_picker.setDisplayDirectory(init_dir) - - path = '' - if folder_picker.execute(): - path = _path_system(folder_picker.getDirectory()) - return path - - -# ~ Export ok -def get_info_path(path): - path, filename = os.path.split(path) - name, extension = os.path.splitext(filename) - return (path, filename, name, extension) - - -# ~ Export ok -def read_file(path, mode='r', array=False): - data = '' - with open(path, mode) as f: - if array: - data = tuple(f.read().splitlines()) + if isinstance(source, (tuple, list)): + files = [(f, f[len(_P(f).path)+1:]) for f in source] + elif _P.is_file(source): + files = ((source, source[start:]),) else: - data = f.read() - return data + files = [(f, f[start:]) for f in _P.walk(source)] + + compression = zipfile.ZIP_DEFLATED + with zipfile.ZipFile(path_zip, 'w', compression=compression) as z: + for f in files: + z.write(f[0], f[1]) + return + + @classmethod + def zip_content(cls, path): + with zipfile.ZipFile(path) as z: + names = z.namelist() + return names + + @classmethod + def unzip(cls, source, target='', members=None, pwd=None): + path = target + if not target: + path = _P(source).path + 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 + + @classmethod + def copy(cls, source, target='', name=''): + p, f, n, e = _P(source).info + if target: + p = target + if name: + e = '' + n = name + path_new = cls.join(p, f'{n}{e}') + shutil.copy(source, path_new) + return path_new +_P = Paths -# ~ Export ok -def save_file(path, mode='w', data=None): - with open(path, mode) as f: - f.write(data) - return +def __getattr__(name): + if name == 'active': + return LODocs().active + if name == 'active_sheet': + return LODocs().active.active + if name == 'selection': + return LODocs().active.selection + if name == 'current_region': + return LODocs().active.selection.current_region + if name in ('rectangle', 'pos_size'): + return Rectangle() + if name == 'paths': + return Paths + if name == 'docs': + return LODocs() + if name == 'sheets': + return LOSheets() + if name == 'cells': + return LOCells() + if name == 'menus': + return LOMenus() + if name == 'shortcuts': + return LOShortCuts() + if name == 'clipboard': + return ClipBoard + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") -# ~ Export ok -def to_json(path, data): - with open(path, 'w') as f: - f.write(json.dumps(data, indent=4, sort_keys=True)) - return +def create_dialog(args): + return LODialog(args) -# ~ Export ok -def from_json(path): - with open(path) as f: - data = json.loads(f.read()) - return data - - -# ~ Export ok -def json_dumps(data): - return json.dumps(data, indent=4, sort_keys=True) - - -# ~ Export ok -def json_loads(data): - return json.loads(data) - - -def get_path_extension(id): - path = '' - pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') - try: - path = _path_system(pip.getPackageLocation(id)) - except Exception as e: - error(e) - return path - - -def get_home(): - return Path.home() - - -# ~ Export ok def inputbox(message, default='', title=TITLE, echochar=''): class ControllersInput(object): @@ -4355,8 +4369,8 @@ def inputbox(message, default='', title=TITLE, echochar=''): 'Width': 200, 'Height': 80, } - dlg = LODialog(**args) - dlg.events = ControllersInput(dlg) + dlg = LODialog(args) + dlg.events = ControllersInput args = { 'Type': 'Label', @@ -4412,540 +4426,56 @@ def inputbox(message, default='', title=TITLE, echochar=''): return '' -# ~ Export ok -def new_doc(type_doc=CALC, **kwargs): - path = 'private:factory/s{}'.format(type_doc) - opt = dict_to_property(kwargs) - doc = get_desktop().loadComponentFromURL(path, '_default', 0, opt) - return _get_class_doc(doc) +def get_fonts(): + toolkit = create_instance('com.sun.star.awt.Toolkit') + device = toolkit.createScreenCompatibleDevice(0, 0) + return device.FontDescriptors -# ~ Export ok -def new_db(path, name=''): - p, fn, n, e = get_info_path(path) - if not name: - name = n - return LOBase(name, path) +# ~ From request +# ~ https://github.com/psf/requests/blob/master/requests/structures.py#L15 +class CaseInsensitiveDict(MutableMapping): + def __init__(self, data=None, **kwargs): + self._store = OrderedDict() + if data is None: + data = {} + self.update(data, **kwargs) -# ~ Todo -def exists_db(name): - dbc = create_instance('com.sun.star.sdb.DatabaseContext') - return dbc.hasRegisteredDatabase(name) + def __setitem__(self, key, value): + # Use the lowercased key for lookups, but store the actual + # key alongside the value. + self._store[key.lower()] = (key, value) + def __getitem__(self, key): + return self._store[key.lower()][1] -# ~ Todo -def register_db(name, path): - dbc = create_instance('com.sun.star.sdb.DatabaseContext') - dbc.registerDatabaseLocation(name, _path_url(path)) - return + def __delitem__(self, key): + del self._store[key.lower()] + def __iter__(self): + return (casedkey for casedkey, mappedvalue in self._store.values()) -# ~ Todo -def get_db(name): - return LOBase(name) + def __len__(self): + return len(self._store) + def lower_items(self): + """Like iteritems(), but with all lowercase keys.""" + values = ( + (lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items() + ) + return values -# ~ Export ok -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 + # Copy is required + def copy(self): + return CaseInsensitiveDict(self._store.values()) - http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1frame_1_1XComponentLoader.html - http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html - """ - path = _path_url(path) - opt = dict_to_property(kwargs) - doc = get_desktop().loadComponentFromURL(path, '_default', 0, opt) - if doc is None: - return + def __repr__(self): + return str(dict(self.items())) - return _get_class_doc(doc) - -# ~ Export ok -def open_file(path): - if IS_WIN: - os.startfile(path) - else: - pid = subprocess.Popen(['xdg-open', path]).pid - return - - -# ~ Export ok -def join(*paths): - return os.path.join(*paths) - - -# ~ Export ok -def is_dir(path): - return Path(path).is_dir() - - -# ~ Export ok -def is_file(path): - return Path(path).is_file() - - -# ~ Export ok -def get_file_size(path): - return Path(path).stat().st_size - - -# ~ Export ok -def is_created(path): - return is_file(path) and bool(get_file_size(path)) - - -# ~ Export ok -def replace_ext(path, extension): - path, _, name, _ = get_info_path(path) - return '{}/{}.{}'.format(path, name, extension) - - -# ~ Export ok -def zip_content(path): - with zipfile.ZipFile(path) as z: - names = z.namelist() - return names - - -def popen(command, stdin=None): - try: - proc = subprocess.Popen(shlex.split(command), shell=IS_WIN, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - for line in proc.stdout: - yield line.decode().rstrip() - except Exception as e: - error(e) - yield (e.errno, e.strerror) - - -def url_open(url, options={}, verify=True, json=False): - data = '' - err = '' - req = Request(url) - try: - if verify: - response = urlopen(req) - else: - context = ssl._create_unverified_context() - response = urlopen(req, context=context) - except HTTPError as e: - error(e) - err = str(e) - except URLError as e: - error(e.reason) - err = str(e.reason) - else: - if json: - data = json_loads(response.read()) - else: - data = response.read() - - return data, err - - -def run(command, wait=False): - try: - if 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 = ("%s\nrun [ERROR]: output = %s, error code = %s\n" - % (command, 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) - - -# ~ Export ok -def zip_files(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) - - -# ~ Export ok -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 - - -# ~ Export ok -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 - - -# ~ Export ok -def kill(path): - p = Path(path) - try: - if p.is_file(): - p.unlink() - elif p.is_dir(): - shutil.rmtree(path) - except OSError as e: - log.error(e) - 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 - - -# ~ Export ok -def set_clipboard(value): - ts = TextTransferable(value) - sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') - sc.setContents(ts, None) - return - - -# ~ Export ok -def copy(): - call_dispatch('.uno:Copy') - return - - -# ~ Export ok -def get_epoch(): - n = now() - return int(time.mktime(n.timetuple())) - - -# ~ Export ok -def file_copy(source, target='', name=''): - p, f, n, e = get_info_path(source) - if target: - p = target - if name: - e = '' - n = name - path_new = join(p, '{}{}'.format(n, e)) - shutil.copy(source, path_new) - return path_new - - -def get_path_content(path, filters=''): - paths = [] - if filters in ('*', '*.*'): - filters = '' - for folder, _, files in os.walk(path): - if filters: - pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) - paths += [join(folder, f) for f in files if pattern.search(f)] - else: - paths += files - return paths - - -def _get_menu(type_doc, name_menu): - instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' - service = TYPE_DOC[type_doc] - manager = create_instance(instance, True) - ui = manager.getUIConfigurationManager(service) - menus = ui.getSettings(NODE_MENUBAR, True) - command = MENUS_APP[type_doc][name_menu] - for menu in menus: - data = property_to_dict(menu) - if data.get('CommandURL', '') == command: - idc = data.get('ItemDescriptorContainer', None) - return ui, menus, idc - return None, None, None - - -def _get_index_menu(menu, command): - for i, m in enumerate(menu): - data = property_to_dict(m) - cmd = data.get('CommandURL', '') - if cmd == command: - return i - # ~ submenu = data.get('ItemDescriptorContainer', None) - # ~ if not submenu is None: - # ~ get_index_menu(submenu, command, count + 1) - return 0 - - -def _store_menu(ui, menus, menu, index, data=(), remove=False): - if remove: - uno.invoke(menu, 'removeByIndex', (index,)) - else: - properties = dict_to_property(data, True) - uno.invoke(menu, 'insertByIndex', (index + 1, properties)) - ui.replaceSettings(NODE_MENUBAR, menus) - ui.store() - return - - -def insert_menu(type_doc, name_menu, **kwargs): - ui, menus, menu = _get_menu(type_doc, name_menu.lower()) - if menu is None: - return 0 - - label = kwargs.get('Label', '-') - separator = False - if label == '-': - separator = True - command = kwargs.get('CommandURL', '') - index = kwargs.get('Index', 0) - if not index: - index = _get_index_menu(menu, kwargs['After']) - if separator: - data = {'Type': 1} - _store_menu(ui, menus, menu, index, data) - return index + 1 - - index_menu = _get_index_menu(menu, command) - if index_menu: - msg = 'Exists: %s' % command - debug(msg) - return 0 - - sub_menu = kwargs.get('Submenu', ()) - idc = None - if sub_menu: - idc = ui.createSettings() - - data = { - 'CommandURL': command, - 'Label': label, - 'Style': 0, - 'Type': 0, - 'ItemDescriptorContainer': idc - } - _store_menu(ui, menus, menu, index, data) - if sub_menu: - _add_sub_menus(ui, menus, idc, sub_menu) - return True - - -def _add_sub_menus(ui, menus, menu, sub_menu): - for i, sm in enumerate(sub_menu): - submenu = sm.pop('Submenu', ()) - sm['Type'] = 0 - if submenu: - idc = ui.createSettings() - sm['ItemDescriptorContainer'] = idc - if sm['Label'] == '-': - sm = {'Type': 1} - _store_menu(ui, menus, menu, i - 1, sm) - if submenu: - _add_sub_menus(ui, menus, idc, submenu) - return - - -def remove_menu(type_doc, name_menu, command): - ui, menus, menu = _get_menu(type_doc, name_menu.lower()) - if menu is None: - return False - - index = _get_index_menu(menu, command) - if not index: - debug('Not exists: %s' % command) - return False - - _store_menu(ui, menus, menu, index, remove=True) - return True - - -def _get_app_submenus(menus, count=0): - for i, menu in enumerate(menus): - data = property_to_dict(menu) - cmd = data.get('CommandURL', '') - msg = ' ' * count + '├─' + cmd - debug(msg) - submenu = data.get('ItemDescriptorContainer', None) - if not submenu is None: - _get_app_submenus(submenu, count + 1) - return - - -def get_app_menus(name_app, index=-1): - instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' - service = TYPE_DOC[name_app] - manager = create_instance(instance, True) - ui = manager.getUIConfigurationManager(service) - menus = ui.getSettings(NODE_MENUBAR, True) - if index == -1: - for menu in menus: - data = property_to_dict(menu) - debug(data.get('CommandURL', '')) - else: - menus = property_to_dict(menus[index])['ItemDescriptorContainer'] - _get_app_submenus(menus) - return menus - - -# ~ Export ok -def start(): - global _start - _start = now() - log.info(_start) - return - - -# ~ Export ok -def end(): - global _start - e = now() - return str(e - _start).split('.')[0] - - -# ~ Export ok # ~ https://en.wikipedia.org/wiki/Web_colors -def get_color(*value): - if len(value) == 1 and isinstance(value[0], int): - return value[0] - if len(value) == 1 and isinstance(value[0], tuple): - value = value[0] - +def get_color(value): COLORS = { 'aliceblue': 15792383, 'antiquewhite': 16444375, @@ -5096,10 +4626,9 @@ def get_color(*value): 'yellowgreen': 10145074, } - if len(value) == 3: + if isinstance(value, tuple): color = (value[0] << 16) + (value[1] << 8) + value[2] else: - value = value[0] if value[0] == '#': r, g, b = bytes.fromhex(value[1:]) color = (r << 16) + (g << 8) + b @@ -5111,359 +4640,15 @@ def get_color(*value): COLOR_ON_FOCUS = get_color('LightYellow') -# ~ Export ok -def render(template, data): - s = Template(template) - return s.safe_substitute(**data) - - -def _to_date(value): - new_value = value - if isinstance(value, Time): - new_value = datetime.time(value.Hours, value.Minutes, value.Seconds) - elif isinstance(value, Date): - new_value = datetime.date(value.Year, value.Month, value.Day) - elif isinstance(value, DateTime): - new_value = datetime.datetime( - value.Year, value.Month, value.Day, - value.Hours, value.Minutes, value.Seconds) - return new_value - - -def date_to_struct(value): - # ~ print(type(value)) - if isinstance(value, datetime.datetime): - d = DateTime() - d.Seconds = value.second - d.Minutes = value.minute - d.Hours = value.hour - d.Day = value.day - d.Month = value.month - d.Year = value.year - elif isinstance(value, datetime.date): - d = Date() - d.Day = value.day - d.Month = value.month - d.Year = value.year - return d - - -# ~ Export ok -def format(template, data): - """ - https://pyformat.info/ - """ - if isinstance(data, (str, int, float)): - # ~ print(template.format(data)) - return template.format(data) - - if isinstance(data, (Time, Date, DateTime)): - return template.format(_to_date(data)) - - if isinstance(data, tuple) and isinstance(data[0], tuple): - data = {r[0]: _to_date(r[1]) for r in data} - return template.format(**data) - - data = [_to_date(v) for v in data] - result = template.format(*data) - return result - - -def _get_url_script(macro): - macro['language'] = macro.get('language', 'Python') - macro['location'] = macro.get('location', 'user') - data = macro.copy() - if data['language'] == 'Python': - data['module'] = '.py$' - elif data['language'] == 'Basic': - data['module'] = '.{}.'.format(macro['module']) - if macro['location'] == 'user': - data['location'] = 'application' - else: - data['module'] = '.' - - url = 'vnd.sun.star.script:{library}{module}{name}?language={language}&location={location}' - path = url.format(**data) - return path - - -def _call_macro(macro): - #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification - name = 'com.sun.star.script.provider.MasterScriptProviderFactory' - factory = create_instance(name, False) - - macro['language'] = macro.get('language', 'Python') - macro['location'] = macro.get('location', 'user') - data = macro.copy() - if data['language'] == 'Python': - data['module'] = '.py$' - elif data['language'] == 'Basic': - data['module'] = '.{}.'.format(macro['module']) - if macro['location'] == 'user': - data['location'] = 'application' - else: - data['module'] = '.' - - args = macro.get('args', ()) - url = 'vnd.sun.star.script:{library}{module}{name}?language={language}&location={location}' - path = url.format(**data) - - script = factory.createScriptProvider('').getScript(path) - return script.invoke(args, None, None)[0] - - -# ~ Export ok -def call_macro(macro): - in_thread = macro.pop('thread') - if in_thread: - t = threading.Thread(target=_call_macro, args=(macro,)) - t.start() - return - - return _call_macro(macro) - - -class TimerThread(threading.Thread): - - def __init__(self, event, seconds, macro): - threading.Thread.__init__(self) - self.stopped = event - self.seconds = seconds - self.macro = macro - - def run(self): - info('Timer started... {}'.format(self.macro['name'])) - while not self.stopped.wait(self.seconds): - _call_macro(self.macro) - info('Timer stopped... {}'.format(self.macro['name'])) - return - - -# ~ Export ok -def timer(name, seconds, macro): - global _stop_thread - _stop_thread[name] = threading.Event() - thread = TimerThread(_stop_thread[name], seconds, macro) - thread.start() - return - - -# ~ Export ok -def stop_timer(name): - global _stop_thread - _stop_thread[name].set() - del _stop_thread[name] - return - - -def _get_key(password): - digest = hashlib.sha256(password.encode()).digest() - key = base64.urlsafe_b64encode(digest) - return key - - -# ~ Export ok -def encrypt(data, password): - f = Fernet(_get_key(password)) - token = f.encrypt(data).decode() - return token - - -# ~ Export ok -def decrypt(token, password): - data = '' - f = Fernet(_get_key(password)) - try: - data = f.decrypt(token.encode()).decode() - except InvalidToken as e: - error('Invalid Token') - return data - - -class SmtpServer(object): - - def __init__(self, config): - self._server = None - self._error = '' - self._sender = '' - self._is_connect = self._login(config) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @property - def is_connect(self): - return self._is_connect - - @property - def error(self): - return self._error - - def _login(self, config): - name = config['server'] - port = config['port'] - is_ssl = config['ssl'] - self._sender = config['user'] - hosts = ('gmail' in name or 'outlook' in name) - try: - if is_ssl and hosts: - self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) - self._server.ehlo() - self._server.starttls() - self._server.ehlo() - elif is_ssl: - self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) - self._server.ehlo() - else: - self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) - - self._server.login(self._sender, config['pass']) - msg = 'Connect to: {}'.format(name) - debug(msg) - return True - except smtplib.SMTPAuthenticationError as e: - if '535' in str(e): - self._error = _('Incorrect user or password') - return False - if '534' in str(e) and 'gmail' in name: - self._error = _('Allow less secure apps in GMail') - return False - except smtplib.SMTPException as e: - self._error = str(e) - return False - except Exception as e: - self._error = str(e) - return False - return False - - def _body(self, msg): - body = msg.replace('\\n', '
') - return body - - def send(self, message): - file_name = 'attachment; filename={}' - email = MIMEMultipart() - email['From'] = self._sender - email['To'] = message['to'] - email['Cc'] = message.get('cc', '') - email['Subject'] = message['subject'] - email['Date'] = formatdate(localtime=True) - if message.get('confirm', False): - email['Disposition-Notification-To'] = email['From'] - email.attach(MIMEText(self._body(message['body']), 'html')) - - for path in message.get('files', ()): - _, fn, _, _ = get_info_path(path) - part = MIMEBase('application', 'octet-stream') - part.set_payload(read_file(path, 'rb')) - encoders.encode_base64(part) - part.add_header('Content-Disposition', file_name.format(fn)) - email.attach(part) - - receivers = ( - email['To'].split(',') + - email['CC'].split(',') + - message.get('bcc', '').split(',')) - try: - self._server.sendmail(self._sender, receivers, email.as_string()) - msg = 'Email sent...' - debug(msg) - if message.get('path', ''): - self.save_message(email, message['path']) - return True - except Exception as e: - self._error = str(e) - return False - return False - - def save_message(self, email, path): - mbox = mailbox.mbox(path, create=True) - mbox.lock() - try: - msg = mailbox.mboxMessage(email) - mbox.add(msg) - mbox.flush() - finally: - mbox.unlock() - return - - def close(self): - try: - self._server.quit() - msg = 'Close connection...' - debug(msg) - except: - pass - return - - -def _send_email(server, messages): - with SmtpServer(server) as server: - if server.is_connect: - for msg in messages: - server.send(msg) - else: - error(server.error) - return server.error - - -def send_email(server, message): - messages = message - if isinstance(message, dict): - messages = (message,) - t = threading.Thread(target=_send_email, args=(server, messages)) - t.start() - return - - -def server_smtp_test(config): - with SmtpServer(config) as server: - if server.error: - error(server.error) - return server.error - - -def import_csv(path, **kwargs): - """ - See https://docs.python.org/3.5/library/csv.html#csv.reader - """ - with open(path) as f: - rows = tuple(csv.reader(f, **kwargs)) - return rows - - -def export_csv(path, data, **kwargs): - with open(path, 'w') as f: - writer = csv.writer(f, **kwargs) - writer.writerows(data) - return - - -def install_locales(path, domain='base', dir_locales=DIR['locales']): - p, *_ = get_info_path(path) - path_locales = join(p, dir_locales) - try: - lang = gettext.translation(domain, path_locales, languages=[LANG]) - lang.install() - _ = lang.gettext - except Exception as e: - from gettext import gettext as _ - error(e) - return _ - - -class LIBOServer(object): +class LOServer(object): HOST = 'localhost' PORT = '8100' - ARG = 'socket,host={},port={};urp;StarOffice.ComponentContext'.format(HOST, PORT) + ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext' CMD = ['soffice', '-env:SingleAppInstance=false', - '-env:UserInstallation=file:///tmp/LIBO_Process8100', + '-env:UserInstallation=file:///tmp/LO_Process8100', '--headless', '--norestore', '--invisible', - '--accept={}'.format(ARG)] + f'--accept={ARG}'] def __init__(self): self._server = None @@ -5524,23 +4709,3 @@ class LIBOServer(object): else: instance = self._sm.createInstance(name) return instance - - -# ~ controls = { - # ~ 'CheckBox': 'com.sun.star.awt.UnoControlCheckBoxModel', - # ~ 'ComboBox': 'com.sun.star.awt.UnoControlComboBoxModel', - # ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', - # ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', - # ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', - # ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', - # ~ 'GroupBox': 'com.sun.star.awt.UnoControlGroupBoxModel', - # ~ 'ImageControl': 'com.sun.star.awt.UnoControlImageControlModel', - # ~ 'NumericField': 'com.sun.star.awt.UnoControlNumericFieldModel', - # ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', - # ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', - # ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', - # ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', - # ~ 'SpinButton': 'com.sun.star.awt.UnoControlSpinButtonModel', - # ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', - # ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', -# ~ } diff --git a/source/pythonpath/main.py b/source/pythonpath/main.py new file mode 100644 index 0000000..8b290b8 --- /dev/null +++ b/source/pythonpath/main.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python3 + +import easymacro as app + + +ID_EXTENSION = '' + + +TITLE = 'ZAZ-PIP' +URL_PIP = 'https://bootstrap.pypa.io/get-pip.py' +PIP = 'pip' +URL_GIT = 'https://git.elmau.net/elmau' + + +PACKAGES = { + 'cffi': 'ok.png', + 'cryptography': 'ok.png', + 'httpx': 'ok.png', + 'lxml': 'ok.png', + 'numpy': 'ok.png', + 'pandas': 'ok.png', + 'psycopg2-binary': 'ok.png', + 'peewee': 'ok.png', + 'pillow': 'ok.png', + 'pytesseract': 'ok.png', + 'sounddevice': 'ok.png', +} + + +def open_dialog_pip(): + dialog = _create_dialog() + dialog.open() + return + + +def run(args, path_locales): + app.install_locales(path_locales) + globals()[args]() + return + + +@app.catch_exception +class Controllers(object): + OK1 = 'Successfully installed' + OK2 = 'Requirement already' + OK3 = 'Successfully uninstalled' + + def __init__(self, dialog): + self.d = dialog + self.path_python = app.paths.python + self._states = { + 'list': False, + 'install': False, + 'search': False, + 'versions': False, + } + + def _set_state(self, state): + for k in self._states.keys(): + self._states[k] = False + self._states[state] = True + return + + def cmd_install_pip_action(self, event): + msg = _('Do you want install PIP?') + if not app.question(msg, 'ZAZ-Pip'): + return + + self._install_pip() + return + + @app.run_in_thread + def _install_pip(self): + self.d.link_proyect.visible = False + self.d.lst_log.visible = True + path_pip = app.get_temp_file(True) + + self.d.lst_log.insert('Download PIP...') + data, err = app.url_open(URL_PIP, verify=False) + if err: + msg = _('Do you have internet connection?') + app.errorbox('{}\n\n{}'.format(msg, err)) + return + + app.save_file(path_pip, 'wb', data) + if not app.is_created(path_pip): + msg = _('File PIP not save') + app.errorbox(msg) + return + self.d.lst_log.insert(_('PIP save correctly...')) + + try: + + self.d.lst_log.insert(_('Start installing PIP...')) + cmd = '"{}" "{}" --user'.format(self.path_python, path_pip) + for line in app.popen(cmd): + if isinstance(line, tuple): + app.errorbox(line) + break + self.d.lst_log.insert(line) + + cmd = self._cmd_pip('-V') + label = app.run(cmd, True) + if label: + self.d.lbl_pip.value = label + self.d.cmd_install_pip.visible = False + self.d.cmd_admin_pip.visible = True + msg = _('PIP installed sucesfully') + app.msgbox(msg) + else: + msg = _('PIP not installed, see log') + app.warning(msg) + except Exception as e: + app.errorbox(e) + + return + + def _cmd_pip(self, args): + cmd = '"{}" -m pip {}'.format(self.path_python, args) + return cmd + + @app.catch_exception + def cmd_admin_pip_action(self, event): + self.d.lst_log.ps_from(self.d.lst_package) + self.d.lst_log.step = 1 + self.d.step = 1 + self.cmd_home_action(None) + return + + def cmd_close_action(self, event): + self.d.close() + return + + def cmd_home_action(self, event): + self.d.txt_search.value = '' + self._list() + return + + def txt_search_key_released(self, event): + if event.KeyCode == app.KEY['enter']: + self.cmd_search_action(None) + return + + if not self.d.txt_search.value.strip(): + self.cmd_home_action(None) + return + + @app.run_in_thread + def _list(self): + self._set_state('list') + self.d.lst_log.visible = False + self.d.lst_package.visible = True + + cmd = self._cmd_pip(' list --format=json') + self.d.lst_package.clear() + result = app.run(cmd, True) + packages = app.json_loads(result) + + for p in packages: + t = '{} - ({})'.format(p['name'], p['version']) + self.d.lst_package.insert(t, 'ok.png') + self.d.lst_package.select() + self.d.txt_search.set_focus() + return + + @app.run_in_thread + def _search(self, value): + self._set_state('search') + line = '' + cmd = self._cmd_pip(' search {}'.format(value)) + self.d.lst_package.clear() + for line in app.popen(cmd): + parts = line.split(')') + name = parts[0].strip() + ')' + description = parts[-1].strip() + parts = name.split('(') + name_verify = parts[0].strip() + package = '{} {}'.format(name, description) + image = PACKAGES.get(name_verify, 'question.png') + self.d.lst_package.insert(package, image) + + if line: + self.d.lst_package.select() + else: + self.d.lst_package.insert(_('Not found...'), 'error.png', show=False) + return + + def cmd_search_action(self, event): + search = self.d.txt_search.value.strip() + if not search: + self.d.txt_search.set_focus() + return + + self._search(search) + return + + @app.run_in_thread + def _install(self, value): + self._set_state('install') + self.d.lst_package.visible = False + self.d.lst_log.visible = True + + line = '' + name = value.split(' ')[0].strip() + cmd = self._cmd_pip(' install --upgrade --user {}'.format(name)) + self.d.lst_log.clear() + for line in app.popen(cmd): + if self.OK1 in line or self.OK2 in line: + self.d.lst_log.insert(line, 'ok.png') + else: + self.d.lst_log.insert(line) + return + + @app.catch_exception + def lst_package_double_click(self, event): + opt = 'install' + if self._states['list']: + opt = 'upgrade' + + name = self.d.lst_package.value + msg = _('Do you want {}:\n\n{} ?').format(opt, name) + if not app.question(msg, TITLE): + return + + self._install(name) + return + + @app.run_in_thread + def _uninstall(self, value): + self._set_state('install') + self.d.lst_package.visible = False + self.d.lst_log.visible = True + + line = '' + name = value.split(' ')[0].strip() + cmd = self._cmd_pip(' uninstall -y {}'.format(name)) + self.d.lst_log.clear() + for line in app.popen(cmd): + if self.OK3 in line: + self.d.lst_log.insert(line, 'ok.png') + else: + self.d.lst_log.insert(line) + return + + def cmd_uninstall_action(self, event): + if not self._states['list']: + msg = _('Select installed package') + app.warning(msg) + return + + name = self.d.lst_package.value + msg = _('Do you want uninstall:\n\n{} ?').format(name) + if not app.question(msg): + return + + self._uninstall(name) + return + + @app.catch_exception + def cmd_shell_action(self, name): + if app.IS_WIN: + cmd = '"{}"'.format(self.path_python) + app.open_file(cmd) + else: + cmd = 'exec "{}"' + if app.IS_MAC: + cmd = 'open "{}"' + elif app.DESKTOP == 'gnome': + cmd = 'gnome-terminal -- {}' + + cmd = cmd.format(self.path_python) + app.run(cmd) + return + + +@app.catch_exception +def _create_dialog(): + args= { + 'Name': 'dialog', + 'Title': 'Zaz-Pip', + 'Width': 200, + 'Height': 220, + } + dialog = app.create_dialog(args) + dialog.id = ID_EXTENSION + dialog.events = Controllers + + lbl_title = '{} {} - {}'.format(app.NAME, app.VERSION, app.OS) + args = { + 'Type': 'Label', + 'Name': 'lbl_title', + 'Label': lbl_title, + 'Width': 100, + 'Height': 15, + 'Border': 1, + 'Align': 1, + 'VerticalAlign': 1, + 'Step': 10, + 'FontHeight': 12, + } + dialog.add_control(args) + dialog.center(dialog.lbl_title, y=5) + + path_python = app.paths.python + cmd = '"{}" -V'.format(path_python) + label = app.run(cmd, True) + + args = { + 'Type': 'Label', + 'Name': 'lbl_python', + 'Label': str(label), + 'Width': 100, + 'Height': 15, + 'Border': 1, + 'Align': 1, + 'VerticalAlign': 1, + 'Step': 10, + 'FontHeight': 11, + } + dialog.add_control(args) + dialog.center(dialog.lbl_python, y=25) + + cmd = '"{}" -m pip -V'.format(path_python) + label = app.run(cmd, True) + exists_pip = True + if not label: + exists_pip = False + label = _('PIP not installed') + args = { + 'Type': 'Label', + 'Name': 'lbl_pip', + 'Label': label, + 'Width': 160, + 'Height': 30, + 'Border': 1, + 'Align': 1, + 'VerticalAlign': 1, + 'Step': 10, + 'MultiLine': True, + } + dialog.add_control(args) + dialog.center(dialog.lbl_pip, y=45) + + args = { + 'Type': 'Button', + 'Name': 'cmd_admin_pip', + 'Label': _('Admin PIP'), + 'Width': 60, + 'Height': 18, + 'Step': 10, + 'ImageURL': 'python.png', + 'ImagePosition': 1, + } + dialog.add_control(args) + dialog.center(dialog.cmd_admin_pip, y=80) + + args = { + 'Type': 'Button', + 'Name': 'cmd_install_pip', + 'Label': _('Install PIP'), + 'Width': 60, + 'Height': 18, + 'Step': 10, + 'ImageURL': 'install.png', + 'ImagePosition': 1, + } + dialog.add_control(args) + dialog.center(dialog.cmd_install_pip, y=80) + + args = { + 'Type': 'Link', + 'Name': 'link_proyect', + 'URL': URL_GIT, + 'Label': URL_GIT, + 'Border': 1, + 'Width': 130, + 'Height': 15, + 'Align': 1, + 'VerticalAlign': 1, + 'Step': 10, + } + dialog.add_control(args) + dialog.center(dialog.link_proyect, y=-5) + + args = { + 'Type': 'Listbox', + 'Name': 'lst_log', + 'Width': 160, + 'Height': 105, + 'Step': 10, + } + dialog.add_control(args) + dialog.center(dialog.lst_log, y=105) + + args = { + 'Type': 'Label', + 'Name': 'lbl_package', + 'Label': _('Packages'), + 'Width': 100, + 'Height': 15, + 'Border': 1, + 'Align': 1, + 'VerticalAlign': 1, + 'Step': 1, + } + lbl = dialog.add_control(args) + + args = { + 'Type': 'Text', + 'Name': 'txt_search', + 'Width': 180, + 'Height': 12, + 'Step': 1, + 'Border': 0, + } + dialog.add_control(args) + + args = { + 'Type': 'Listbox', + 'Name': 'lst_package', + 'Width': 180, + 'Height': 128, + 'Step': 1, + } + dialog.add_control(args) + + args = { + 'Type': 'Button', + 'Name': 'cmd_close', + 'Label': _('~Close'), + 'Width': 60, + 'Height': 18, + 'Step': 1, + 'ImageURL': 'close.png', + 'ImagePosition': 1, + # ~ 'PushButtonType': 2, + } + dialog.add_control(args) + dialog.center(dialog.cmd_close, y=-5) + + args = { + 'Type': 'Button', + 'Name': 'cmd_home', + 'Width': 18, + 'Height': 18, + 'Step': 1, + 'ImageURL': 'home.png', + 'FocusOnClick': False, + 'Y': 2, + } + dialog.add_control(args) + + args = { + 'Type': 'Button', + 'Name': 'cmd_search', + 'Width': 18, + 'Height': 18, + 'Step': 1, + 'ImageURL': 'search.png', + 'FocusOnClick': False, + 'Y': 2, + } + dialog.add_control(args) + + args = { + 'Type': 'Button', + 'Name': 'cmd_uninstall', + 'Width': 18, + 'Height': 18, + 'Step': 1, + 'ImageURL': 'uninstall.png', + 'FocusOnClick': False, + 'Y': 2, + } + dialog.add_control(args) + + args = { + 'Type': 'Button', + 'Name': 'cmd_install', + 'Width': 18, + 'Height': 18, + 'Step': 1, + 'ImageURL': 'install.png', + 'FocusOnClick': False, + 'Y': 2, + } + dialog.add_control(args) + + args = { + 'Type': 'Button', + 'Name': 'cmd_shell', + 'Width': 18, + 'Height': 18, + 'Step': 1, + 'ImageURL': 'shell.png', + 'FocusOnClick': False, + 'Y': 2, + } + dialog.add_control(args) + + controls = (dialog.cmd_home, dialog.cmd_search, + dialog.cmd_install, dialog.cmd_uninstall, dialog.cmd_shell) + dialog.lbl_package.move(dialog.cmd_home) + dialog.txt_search.move(dialog.lbl_package) + dialog.lst_package.move(dialog.txt_search) + dialog.lbl_package.center() + dialog.lst_package.center() + dialog.txt_search.center() + dialog.center(controls) + + dialog.step = 10 + + dialog.cmd_install_pip.visible = not exists_pip + dialog.cmd_admin_pip.visible = exists_pip + dialog.lst_log.visible = False + + return dialog