From 55867ef436437b772f60c8f4c59447b563abea91 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 24 Dec 2020 20:55:42 -0600 Subject: [PATCH] Start version 6 --- conf.py | 2 +- easymacro.py | 8521 ++++++++++++++++++-------------- files/ZAZFavorites_v0.5.0.oxt | Bin 71042 -> 0 bytes files/ZAZFavorites_v0.6.0.oxt | Bin 0 -> 76850 bytes source/description.xml | 2 +- source/pythonpath/easymacro.py | 8521 ++++++++++++++++++-------------- 6 files changed, 9698 insertions(+), 7348 deletions(-) delete mode 100644 files/ZAZFavorites_v0.5.0.oxt create mode 100644 files/ZAZFavorites_v0.6.0.oxt diff --git a/conf.py b/conf.py index 9f2b603..dca511e 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 = 'ZAZFavorites' diff --git a/easymacro.py b/easymacro.py index 8259aca..5682097 100644 --- a/easymacro.py +++ b/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,8 +33,8 @@ import re import shlex import shutil import socket -import subprocess import ssl +import subprocess import sys import tempfile import threading @@ -42,14 +42,19 @@ 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, Union from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError -from string import Template -from subprocess import PIPE +import imaplib import smtplib from smtplib import SMTPException, SMTPAuthenticationError from email.mime.multipart import MIMEMultipart @@ -61,147 +66,53 @@ 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 import Rectangle, Size, Point 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 Key, KeyModifier, KeyEvent +from com.sun.star.container import NoSuchElementException from com.sun.star.datatransfer import XTransferable, DataFlavor + +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.ControlCharacter import PARAGRAPH_BREAK 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 XMenuListener 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 XFocusListener 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 import XWindowListener +from com.sun.star.awt import XTopWindowListener from com.sun.star.awt.grid import XGridDataListener from com.sun.star.awt.grid import XGridSelectionListener +from com.sun.star.script import ScriptEventDescriptor +# ~ 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 + +from com.sun.star.view.SelectionType import SINGLE, MULTI, RANGE + +from com.sun.star.sdb.CommandType import TABLE, QUERY, COMMAND 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('You need install peewee, only if you will develop with Base') -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' -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') @@ -211,19 +122,169 @@ 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') +# ~ but, not modify this file, modify in import file +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 + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a +class ValidationType(): + from com.sun.star.sheet.ValidationType \ + import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM +VT = ValidationType + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7 +class ValidationAlertStyle(): + from com.sun.star.sheet.ValidationAlertStyle \ + import STOP, WARNING, INFO, MACRO +VAS = ValidationAlertStyle + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html +class ConditionOperator(): + from com.sun.star.sheet.ConditionOperator2 \ + import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \ + LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE +CO = ConditionOperator + + +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) + +PYTHON = 'python' +if IS_WIN: + PYTHON = 'python.exe' + +_MACROS = {} +_start = 0 + SECONDS_DAY = 60 * 60 * 24 +DIR = { + 'images': 'images', + 'locales': 'locales', +} + +KEY = { + 'enter': 1280, +} + +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', +} + +DEFAULT_MIME_TYPE = 'png' +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 @@ -245,33 +306,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 @@ -283,52 +352,27 @@ def catch_exception(f): except Exception as e: name = f.__name__ if IS_WIN: - debug(traceback.format_exc()) + msgbox(traceback.format_exc()) log.error(name, exc_info=True) 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 @@ -343,7 +387,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 @@ -351,64 +395,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'): @@ -418,13 +412,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): @@ -435,183 +429,612 @@ 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') + + +# ~ https://wiki.documentfoundation.org/Development/DispatchCommands +# ~ Used only if not exists in API +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, shell=IS_WIN) + if capture: + result = result.stdout + else: + result = result.returncode + return result + + +def popen(command): + 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 + + +def switch_design_mode(doc): + call_dispatch(doc.frame, '.uno:SwitchControlDesignMode') + return + + +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 + + +class ImapServer(object): + + def __init__(self, config): + self._server = None + self._error = '' + 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): + try: + # ~ hosts = 'gmail' in config['server'] + if config['ssl']: + self._server = imaplib.IMAP4_SSL(config['server'], config['port']) + else: + self._server = imaplib.IMAP4(config['server'], config['port']) + self._server.login(config['user'], config['password']) + self._server.select() + return True + except imaplib.IMAP4.error as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return False + + def get_folders(self, exclude=()): + folders = {} + result, subdir = self._server.list() + for s in subdir: + print(s.decode('utf-8')) + return folders + + def close(self): + try: + self._server.close() + self._server.logout() + msg = 'Close connection...' + debug(msg) + except: + pass + 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', '_view'): + 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] - - 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 - - @property - def obj(self): - return self._obj - @obj.setter - def obj(self, value): - self._obj = value - - -class LOObjectBase(object): - - def __init__(self, obj): - self.__dict__['_obj'] = obj - - 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): + FILTERS = { + 'doc': 'MS Word 97', + 'docx': 'MS Word 2007 XML', + } 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._undo = True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() @property def obj(self): @@ -625,12 +1048,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): @@ -650,19 +1073,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 @@ -672,6 +1107,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') @@ -681,56 +1141,386 @@ 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 insert_contents(self, args={}): + call_dispatch(self.frame, '.uno:InsertContents', args) 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) + + 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, + } + 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 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.store() + return result + + def close(self): + self.obj.close(True) + return -class FormControlBase(object): +class LOCellStyle(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def name(self): + return self.obj.Name + + @property + def properties(self): + properties = self.obj.PropertySetInfo.Properties + data = {p.Name: getattr(self.obj, p.Name) for p in properties} + return data + @properties.setter + def properties(self, values): + _set_properties(self.obj, values) + + +class LOCellStyles(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + 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 new(self, name: str=''): + obj = self._doc.create_instance('com.sun.star.style.CellStyle') + if name: + self.obj[name] = obj + obj = LOCellStyle(obj) + return obj + + +class LOCalc(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = CALC + self._sheets = obj.Sheets + + def __getitem__(self, index): + return LOCalcSheet(self._sheets[index]) + + def __setitem__(self, key, value): + self._sheets[key] = value + + def __len__(self): + return self._sheets.Count + + def __contains__(self, item): + 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): + return self._cc.ColumnRowHeaders + @headers.setter + def headers(self, value): + self._cc.ColumnRowHeaders = value + + @property + def tabs(self): + return self._cc.SheetTabs + @tabs.setter + def tabs(self, value): + self._cc.SheetTabs = value + + @property + def cs(self): + return self.cell_styles + @property + def cell_styles(self): + obj = self.obj.StyleFamilies['CellStyles'] + return LOCellStyles(obj, self) + + @property + 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._sheets[sheet] + self._cc.setActiveSheet(obj) + return + + def new_sheet(self): + s = self.create_instance('com.sun.star.sheet.Spreadsheet') + return s + + def insert(self, name): + names = name + if isinstance(name, str): + names = (name,) + for n in names: + self._sheets[n] = self.new_sheet() + return LOCalcSheet(self._sheets[n]) + + def move(self, name, pos=-1): + index = pos + if pos < 0: + index = len(self) + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.moveByName(name, index) + return + + def remove(self, name): + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.removeByName(name) + return + + 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 = len(self) + + names = source + if not source: + names = doc.names + elif isinstance(source, str): + names = (source,) + + new_names = target + if not target: + new_names = names + elif isinstance(target, str): + new_names = (target,) + + for i, name in enumerate(names): + self._sheets.importSheet(doc.obj, name, index + i) + self[index + i].name = new_names[i] + + return LOCalcSheet(self._sheets[index]) + + def sort(self, reverse=False): + names = sorted(self.names, reverse=reverse) + for i, n in enumerate(names): + self.move(n, i) + return + + def render(self, data, sheet=None, clean=True): + if sheet is None: + sheet = self.active + return sheet.render(data, clean=clean) + + +class LOChart(object): + + def __init__(self, name, obj, draw_page): + self._name = name + self._obj = obj + self._eobj = self._obj.EmbeddedObject + self._type = 'Column' + self._cell = None + self._shape = self._get_shape(draw_page) + self._pos = self._shape.Position + + def __getitem__(self, index): + return LOBaseObject(self.diagram.getDataRowProperties(index)) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def diagram(self): + return self._eobj.Diagram + + @property + def type(self): + return self._type + @type.setter + def type(self, value): + self._type = value + if value == 'Bar': + self.diagram.Vertical = True + return + type_chart = f'com.sun.star.chart.{value}Diagram' + self._eobj.setDiagram(self._eobj.createInstance(type_chart)) + + @property + def cell(self): + return self._cell + @cell.setter + def cell(self, value): + self._cell = value + self._shape.Anchor = value.obj + + @property + def position(self): + return self._pos + @position.setter + def position(self, value): + self._pos = value + self._shape.Position = value + + def _get_shape(self, draw_page): + for shape in draw_page: + if shape.PersistName == self.name: + break + return shape + + +class LOSheetCharts(object): + + def __init__(self, obj, sheet): + self._obj = obj + self._sheet = sheet + + def __getitem__(self, index): + return LOChart(index, self.obj[index], self._sheet.draw_page) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + def new(self, name, pos_size, data): + self.obj.addNewByName(name, pos_size, data, True, True) + return LOChart(name, self.obj[name], self._sheet.draw_page) + + +class LOFormControl(LOBaseObject): EVENTS = { 'action': 'actionPerformed', 'click': 'mousePressed', @@ -740,22 +1530,43 @@ class FormControlBase(object): 'mousePressed': 'XMouseListener', } - def __init__(self, obj): - self._obj = obj + def __init__(self, obj, view, form): + super().__init__(obj) + self._view = view + self._form = form + self._m = view.Model self._index = -1 - self._rules = {} - @property - def obj(self): - return self._obj + def __setattr__(self, name, value): + if name in ('_form', '_view', '_m', '_index'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) - @property - def name(self): - return self.obj.Name + def __str__(self): + return f'{self.name} ({self.type}) {[self.index]}' @property def form(self): - return self.obj.getParent() + return self._form + + @property + def doc(self): + return self.obj.Parent.Forms.Parent + + @property + def name(self): + return self._m.Name + @name.setter + def name(self, value): + self._m.Name = value + + @property + def tag(self): + return self._m.Tag + @tag.setter + def tag(self, value): + self._m.Tag = value @property def index(self): @@ -764,23 +1575,16 @@ class FormControlBase(object): def index(self, value): self._index = value + @property + def enabled(self): + return self._m.Enabled + @enabled.setter + def enabled(self, value): + self._m.Enabled = 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) @@ -802,83 +1606,227 @@ class FormControlBase(object): self.form.registerScriptEvent(self.index, event) return - -class FormButton(FormControlBase): - - def __init__(self, obj): - super().__init__(obj) + def set_focus(self): + self._view.setFocus() + return +class LOFormControlLabel(LOFormControl): -class LOForm(ObjectBase): + def __init__(self, obj, view, form): + super().__init__(obj, view, form) - def __init__(self, obj): - super().__init__(obj) + @property + def type(self): + return 'label' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Label = value + + +class LOFormControlText(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self._m.Text + @value.setter + def value(self, value): + self._m.Text = value + + +class LOFormControlButton(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Text = Label + + +FORM_CONTROL_CLASS = { + 'label': LOFormControlLabel, + 'text': LOFormControlText, + 'button': LOFormControlButton, +} + + +class LOForm(object): + MODELS = { + 'label': 'com.sun.star.form.component.FixedText', + 'text': 'com.sun.star.form.component.TextField', + 'button': 'com.sun.star.form.component.CommandButton', + } + + def __init__(self, obj, draw_page): + self._obj = obj + self._dp = draw_page + self._controls = {} self._init_controls() def __getitem__(self, index): - if isinstance(index, int): - return self._controls[index] - else: - return getattr(self, index) + control = self.obj[index] + return self._controls[control.Name] - 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', - } - return types[name] + 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) + + def __str__(self): + return f'Form: {self.name}' 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) + types = { + 'com.sun.star.form.OFixedTextModel': 'label', + 'com.sun.star.form.OEditModel': 'text', + 'com.sun.star.form.OButtonModel': 'button', + } + for i, control in enumerate(self.obj): + name = control.Name + tipo = types[control.ImplementationName] + view = self.doc.CurrentController.getControl(control) + control = FORM_CONTROL_CLASS[tipo](control, view) control.index = i - self._controls.append(control) - setattr(self, c.Name, control) + setattr(self, name, control) + self._controls[name] = control + return + + @property + def obj(self): + return self._obj @property def name(self): - return self._obj.getName() + return self.obj.Name @name.setter def name(self, value): - self._obj.setName(value) + self.obj.Name = value + @property + def source(self): + return self.obj.DataSourceName + @source.setter + def source(self, value): + self.obj.DataSourceName = value -class LOForms(ObjectBase): + @property + def type(self): + return self.obj.CommandType + @type.setter + def type(self, value): + self.obj.CommandType = value - def __init__(self, obj, doc): - self._doc = doc - super().__init__(obj) - - def __getitem__(self, index): - form = super().__getitem__(index) - return LOForm(form) + @property + def command(self): + return self.obj.Command + @command.setter + def command(self, value): + self.obj.Command = value @property def doc(self): - return self._doc + return self.obj.Parent.Parent + + def _special_properties(self, tipo, args): + if tipo == 'button': + # ~ if 'ImageURL' in args: + # ~ args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + return args + + def add(self, args): + name = args['Name'] + tipo = args.pop('Type').lower() + w = args.pop('Width') + h = args.pop('Height') + x = args.pop('X', 0) + y = args.pop('Y', 0) + control = self.doc.createInstance('com.sun.star.drawing.ControlShape') + control.setSize(Size(w, h)) + control.setPosition(Point(x, y)) + model = self.doc.createInstance(self.MODELS[tipo]) + args = self._special_properties(tipo, args) + _set_properties(model, args) + control.Control = model + index = len(self) + self.obj.insertByIndex(index, model) + self._dp.add(control) + view = self.doc.CurrentController.getControl(self.obj.getByName(name)) + control = FORM_CONTROL_CLASS[tipo](control, view, self.obj) + control.index = index + setattr(self, name, control) + self._controls[name] = control + return control + + +class LOSheetForms(object): + + def __init__(self, draw_page): + self._dp = draw_page + self._obj = draw_page.Forms + + def __getitem__(self, index): + return LOForm(self.obj[index], self._dp) + + 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 + + @property + def doc(self): + return self.obj.Parent @property def count(self): - return self.obj.getCount() + return len(self) @property def names(self): - return self.obj.getElementNames() - - def exists(self, name): - return name in self.names + return self.obj.ElementNames def insert(self, name): - form = self.doc.create_instance('com.sun.star.form.component.Form') + form = self.doc.createInstance('com.sun.star.form.component.Form') self.obj.insertByName(name, form) - return self[name] + return LOForm(form, self._dp) def remove(self, index): if isinstance(index, int): @@ -888,356 +1836,128 @@ class LOForms(ObjectBase): return -class LOCellStyle(LOObjectBase): +# ~ IsFiltered, +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetRows(object): - 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): + def __init__(self, sheet, obj): + self._sheet = sheet self._obj = obj + def __getitem__(self, index): + if isinstance(index, int): + rows = LOSheetRows(self._sheet, self.obj[index]) + else: + rango = self._sheet[index.start:index.stop,0:] + rows = LOSheetRows(self._sheet, rango.obj.Rows) + return rows + 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() - - 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) - - def __setitem__(self, key, value): - self._sheets[key] = value - - def __contains__(self, item): - return item in self.obj.Sheets - - @property - def headers(self): - return self._cc.ColumnRowHeaders - @headers.setter - def headers(self, value): - self._cc.ColumnRowHeaders = value - - @property - def tabs(self): - return self._cc.SheetTabs - @tabs.setter - def tabs(self, value): - self._cc.SheetTabs = value - - @property - def active(self): - return LOCalcSheet(self._cc.getActiveSheet(), self) - - def activate(self, sheet): - obj = sheet - if isinstance(sheet, LOCalcSheet): - obj = sheet.obj - elif isinstance(sheet, str): - obj = self[sheet].obj - 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 - - @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 - 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) - - 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 - - names = source - if not names: - names = self.names - elif isinstance(source, str): - names = (source,) - - 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_from(self, doc, source='', target='', pos=-1): - index = pos - if pos < 0: - index = self._sheets.Count + pos + 1 - - names = source - if not names: - names = doc.names - elif isinstance(source, str): - names = (source,) - - new_names = target - if not target: - new_names = names - 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] - - # ~ doc.getCurrentController().setActiveSheet(sheet) - # ~ For controls in sheet - # ~ doc.getCurrentController().setFormDesignMode(False) - - return LOCalcSheet(self._sheets[index], self) - - def sort(self, reverse=False): - names = sorted(self.names, reverse=reverse) - for i, n in enumerate(names): - self.sheets.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 - - -class LOCalcSheets(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - - 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 obj(self): + return self._obj - def copy(self, name, new_name, pos): - self.obj.copyByName(name, new_name, pos) + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def color(self): + return self.obj.CellBackColor + @color.setter + def color(self, value): + self.obj.CellBackColor = value + + @property + def is_transparent(self): + return self.obj.IsCellBackgroundTransparent + @is_transparent.setter + def is_transparent(self, value): + self.obj.IsCellBackgroundTransparent = value + + @property + def height(self): + return self.obj.Height + @height.setter + def height(self, value): + self.obj.Height = value + + def optimal(self): + self.obj.OptimalHeight = True 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) + def insert(self, index, count): + self.obj.insertByIndex(index, count) return - def remove(self, name): - sheet = self.obj[name] - self.obj.removeByName(sheet.Name) + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetColumns(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, (int, str)): + rows = LOSheetColumns(self._sheet, self.obj[index]) + else: + rango = self._sheet[0,index.start:index.stop] + rows = LOSheetColumns(self._sheet, rango.obj.Columns) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def width(self): + return self.obj.Width + @width.setter + def width(self, value): + self.obj.Width = value + + def optimal(self): + self.obj.OptimalWidth = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) return class LOCalcSheet(object): - def __init__(self, obj, doc): + def __init__(self, obj): self._obj = obj - self._doc = doc - self._init_values() def __getitem__(self, index): - return LOCellRange(self.obj[index], self.doc) + return LOCalcRange(self.obj[index]) def __enter__(self): return self @@ -1245,23 +1965,13 @@ 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} + def __str__(self): + return f'easymacro.LOCalcSheet: {self.name}' @property def obj(self): return self._obj - @property - def doc(self): - return self._doc - - @property - def images(self): - return self._images - @property def name(self): return self._obj.Name @@ -1276,27 +1986,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): @@ -1317,133 +2012,985 @@ 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, self.obj.Rows) + + @property + def columns(self): + return LOSheetColumns(self, self.obj.Columns) @property def forms(self): - return LOForms(self._dp.getForms(), self.doc) + return LOSheetForms(self.obj.DrawPage) @property def events(self): - return self._events + names = ('OnFocus', 'OnUnfocus', 'OnSelect', 'OnDoubleClick', + 'OnRightClick', 'OnChange', 'OnCalculate') + evs = self.obj.Events + events = {n: _property_to_dict(evs.getByName(n)) for n in names + if evs.getByName(n)} + return events @events.setter - def events(self, controllers): - self._events = controllers - self._connect_listeners() + def events(self, values): + pv = '[]com.sun.star.beans.PropertyValue' + ev = self.obj.Events + for name, v in values.items(): + url = _get_url_script(v) + args = dict_to_property(dict(EventType='Script', Script=url)) + # ~ e.replaceByName(k, args) + uno.invoke(ev, 'replaceByName', (name, uno.Any(pv, args))) - def _connect_listeners(self): - if self.events is None: + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + 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 + + 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) + + def find(self, search_string, rango=None): + if rango is None: + rango = self.used_area + return rango.find(search_string) + + +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 __contains__(self, item): + return item.in_range(self) + + 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 LOSheetRows(self.sheet, 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) + + @property + def merged_area(self): + cursor = self.cursor + cursor.collapseToMergedArea() + rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + return rango + + @property + def empty(self): + cursor = self.sheet.get_cursor(self.obj) + cursor = self.cursor + rangos = cursor.queryEmptyCells() + rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(rangos) + + @property + def merge(self): + return self.obj.IsMerged + @merge.setter + def merge(self, value): + self.obj.merge(value) + + @property + def style(self): + return self.obj.CellStyle + @style.setter + def style(self, value): + self.obj.CellStyle = value + + @property + def auto_format(self): + return '' + @auto_format.setter + def auto_format(self, value): + self.obj.autoFormat(value) + + @property + def validation(self): + return self.obj.Validation + @validation.setter + def validation(self, values): + current = self.validation + if not values: + current.Type = ValidationType.ANY + current.ShowInputMessage = False + else: + is_list = False + for k, v in values.items(): + if k == 'Type' and v == VT.LIST: + 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 select(self): + self.doc.select(self.obj) + return + + def search(self, options, find_all=True): + rangos = None + + descriptor = self.sheet.search_descriptor + descriptor.setSearchString(options['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 find_all: + found = self.obj.findAll(descriptor) + else: + found = self.obj.findFirst(descriptor) + + if found: + if found.ImplementationName == OBJ_CELL: + rangos = LOCalcRange(found) + else: + rangos = [LOCalcRange(f) for f in found] + + return rangos + + def replace(self, options): + descriptor = self.sheet.replace_descriptor + 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'] + count = self.obj.replaceAll(descriptor) + return count + + def in_range(self, rango): + if isinstance(rango, LOCalcRange): + address = rango.range_address + else: + address = rango.RangeAddress + result = self.cursor.queryIntersection(address) + return bool(result.Count) + + 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(self, source): + self.sheet.obj.copyRange(self.address, source.range_address) + 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_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 optimal_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 - listeners = { - 'addModifyListener': EventsModify, + 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(self, search_string): + if self._sd is None: + self._sd = self.sheet.obj.createSearchDescriptor() + self._sd.SearchCaseSensitive = False + + self._sd.setSearchString(search_string) + cell = self.obj.findFirst(self._sd) + if cell: + cell = LOCalcRange(cell) + return cell + + 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, } - for key, value in listeners.items(): - getattr(self.obj, key)(listeners[key](self.events)) - print('add_listener') + 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 insert_shape(self, tipo, 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']) + + shape = self.sheet.dp.add(tipo, args) + shape.anchor = self.obj + args.clear() + return + + 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 + + def transpose(self): + # ~ 'Flags': 'A', + # ~ 'FormulaCommand': 0, + # ~ 'SkipEmptyCells': False, + # ~ 'AsLink': False, + # ~ 'MoveMode': 4, + self.select() + self.doc.copy() + self.clear(1023) + self[0,0].select() + self.doc.insert_contents({'Transpose': True}) + _CB.set('') + return + + def transpose_data(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 merge_by_row(self): + for r in range(len(self.rows)): + self[r].merge = True + return + + def fill(self, source=1): + self.obj.fillAuto(0, source) return -class LOWriter(LODocument): +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' + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + for i, p in enumerate(self.obj): + if i == self._index: + obj = LOWriterTextRange(p, self._doc) + self._index += 1 + return obj + raise StopIteration + @property def obj(self): return self._obj @property def string(self): - return self._obj.getText().String + s = '' + if not self._is_table: + s = self.obj.String + return s + @string.setter + def string(self, value): + self.obj.String = value + + @property + def value(self): + return self.string + + @property + def is_table(self): + return self._is_table @property def text(self): - return self._obj.getText() + return self.obj.Text @property def cursor(self): - return self.text.createTextCursor() + return self.text.createTextCursorByRange(self.obj) @property - def paragraphs(self): - return [LOTextRange(p) for p in self.text] + def dp(self): + return self._doc.dp - @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 offset(self): + cursor = self.cursor.getEnd() + return LOWriterTextRange(cursor, self._doc) - 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): + def insert_content(self, data, cursor=None, replace=False): + if cursor is None: + cursor = self.cursor self.text.insertTextContent(cursor, data, replace) return - # ~ f = doc.createInstance('com.sun.star.text.TextFrame') - # ~ f.setSize(Size(10000, 500)) + def new_line(self, count=1): + cursor = self.cursor + for i in range(count): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + return self._doc.selection - 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) + def insert_table(self, data): + table = self._doc.create_instance('com.sun.star.text.TextTable') + rows = len(data) + cols = len(data[0]) + table.initialize(rows, cols) + self.insert_content(table) + table.DataArray = data + name = table.Name + table = LOWriterTextTable(self._doc.tables[name], self._doc) + return table + + 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(cursor, image) + 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): + for i, p in enumerate(self.obj): + if i == index: + obj = LOWriterTextRange(p, self._doc) + break + return obj + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + for i, p in enumerate(self.obj): + if i == self._index: + obj = LOWriterTextRange(p, self._doc) + self._index += 1 + return obj + raise StopIteration + + @property + def obj(self): + return self._obj + + +class LOWriterTextTable(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + + @property + def data(self): + return self._obj.DataArray + @data.setter + def data(self, values): + self._obj.DataArray = values + + +class LOWriterTextTables(object): + + def __init__(self, doc): + self._doc = doc + self._obj = doc.obj.TextTables + + def __getitem__(self, key): + return LOWriterTextTable(self._obj[key], self._doc) + + def __len__(self): + return self._obj.Count + + def insert(self, data, text_range=None): + if text_range is None: + text_range = self._doc.selection + text_range.insert_table(data) 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 +class LOWriter(LODocument): - def select(self, text): - self._cc.select(text) - return + def __init__(self, obj): + super().__init__(obj) + self._type = WRITER - def search(self, options): - descriptor = self.obj.createSearchDescriptor() + @property + def text(self): + return LOWriterTextRange(self.obj.Text, self) + + @property + def paragraphs(self): + return LOWriterTextRanges(self.obj.Text, self) + + @property + def tables(self): + return LOWriterTextTables(self) + + @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) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + def goto_start(self): + self.view_cursor.gotoStart(False) + return self.selection + + def goto_end(self): + self.view_cursor.gotoEnd(False) + return self.selection + + def search(self, options, find_all=True): + descriptor = self.search_descriptor descriptor.setSearchString(options.get('Search', '')) descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) descriptor.SearchWords = options.get('Words', False) @@ -1455,15 +3002,20 @@ class LOWriter(LODocument): if hasattr(descriptor, 'SearchType') and 'Type' in options: descriptor.SearchType = options['Type'] - if options.get('First', False): - found = self.obj.findFirst(descriptor) - else: + result = False + if find_all: found = self.obj.findAll(descriptor) + if len(found): + result = [LOWriterTextRange(f, self) for f in found] + else: + found = self.obj.findFirst(descriptor) + if found: + result = LOWriterTextRange(found, self) - return found + return result def replace(self, options): - descriptor = self.obj.createReplaceDescriptor() + descriptor = self.replace_descriptor descriptor.setSearchString(options['Search']) descriptor.setReplaceString(options['Replace']) descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) @@ -1478,41 +3030,446 @@ class LOWriter(LODocument): found = self.obj.replaceAll(descriptor) return found + def select(self, text): + if hasattr(text, 'obj'): + text = text.obj + self._cc.select(text) + return -class LOTextRange(object): - def __init__(self, obj): - self._obj = obj - self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph' - self._is_table = self.obj.ImplementationName == 'SwXTextTable' +class LOShape(LOBaseObject): + IMAGE = 'com.sun.star.drawing.GraphicObjectShape' + + def __init__(self, obj, index): + self._index = index + super().__init__(obj) @property - def obj(self): - return self._obj + def type(self): + t = self.shape_type[21:] + if self.is_image: + t = 'image' + return t @property - def is_paragraph(self): - return self._is_paragraph + def shape_type(self): + return self.obj.ShapeType @property - def is_table(self): - return self._is_table + def is_image(self): + return self.shape_type == self.IMAGE + + @property + def name(self): + return self.obj.Name or f'{self.type}{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 text(self): - return self.obj.getText() + def description(self): + return self.obj.Description + @description.setter + def description(self, value): + self.obj.Description = value @property - def cursor(self): - return self.text.createTextCursorByRange(self.obj) + 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 + + @property + def path(self): + return self.url + @property + def url(self): + url = '' + if self.is_image: + url = _P.to_system(self.obj.GraphicURL.OriginURL) + return url + + @property + def mimetype(self): + mt = '' + if self.is_image: + mt = self.obj.GraphicURL.MimeType + return mt + + @property + def linked(self): + l = False + if self.is_image: + l = self.obj.GraphicURL.Linked + return l + + def delete(self): + self.remove() + return + def remove(self): + self.obj.Parent.remove(self.obj) + return + + def save(self, path: str, mimetype=DEFAULT_MIME_TYPE): + if _P.is_dir(path): + name = self.name + ext = mimetype.lower() + else: + p = _P(path) + path = p.path + name = p.name + ext = p.ext.lower() + + path = _P.join(path, f'{name}.{ext}') + args = dict( + URL = _P.to_url(path), + MimeType = MIME_TYPE[ext], + ) + if not _export_image(self.obj, args): + path = '' + return path + + # ~ def save2(self, path: str): + # ~ size = len(self.obj.Bitmap.DIB) + # ~ data = self.obj.GraphicStream.readBytes((), size) + # ~ data = data[-1].value + # ~ path = _P.join(path, f'{self.name}.png') + # ~ _P.save_bin(path, b'') + # ~ 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 + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + if self._index == self.count: + raise StopIteration + shape = self[self._index] + self._index += 1 + 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 + """ + index = self.count + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + name = args.get('Name', f'{type_shape.lower()}{index}') + + service = f'com.sun.star.drawing.{type_shape}Shape' + shape = self.create_instance(service) + shape.Size = Size(w, h) + shape.Position = Point(x, y) + shape.Name = name + 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 remove_all(self): + while self.count: + self.obj.remove(self.obj[0]) + return + + def insert_image(self, path, args={}): + index = self.count + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + name = args.get('Name', f'image{index}') + + 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) + image.Name = name + 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.current_page[-1] + + 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', @@ -1534,40 +3491,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): @@ -1577,25 +3523,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): @@ -1603,10 +3550,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 @@ -1618,452 +3562,211 @@ 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) + document = None + for i, doc in enumerate(self._desktop.Components): + if isinstance(index, int) and i == index: + document = _get_class_doc(doc) + break + elif isinstance(index, str) and doc.Title == index: + document = _get_class_doc(doc) + break + return document 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 = -1 + return self - if self._type_obj == OBJ_CELL: - self._type_content = self.obj.getType() - return - - @property - def obj(self): - return self._obj - - @property - def doc(self): - return self._doc - - @property - def type(self): - return self._type_obj - - @property - def type_content(self): - return self._type_content - - @property - def first(self): - if self.type == OBJ_RANGES: - obj = LOCellRange(self.obj[0][0,0], self.doc) + def __next__(self): + self._i += 1 + doc = self[self._i] + if doc is None: + raise StopIteration else: - obj = LOCellRange(self.obj[0,0], self.doc) - return obj + return doc + + def __len__(self): + for i, _ in enumerate(self._desktop.Components): + pass + return i + 1 @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) + def active(self): + return _get_class_doc(self._desktop.getCurrentComponent()) - @property - def data(self): - return self.obj.getDataArray() - @data.setter - def data(self, values): - self.obj.setDataArray(values) + @classmethod + def new(cls, type_doc=CALC, args={}): + if type_doc == BASE: + return LOBase(None, args) - @property - def formula(self): - return self.obj.getFormulaArray() - @formula.setter - def formula(self, values): - self.obj.setFormulaArray(values) + 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 column(self): - a = self.address - if hasattr(a, 'Column'): - c = a.Column - else: - c = a.StartColumn - return c + @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 columns(self): - return self._obj.Columns.Count + 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 rows(self): - return self._obj.Rows.Count + return _get_class_doc(doc) - 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 connect(self, path): + return LOBase(None, {'path': path}) - 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 _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' + is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl' - def copy(self, source): - self.sheet.obj.copyRange(self.address, source.range_address) - return + 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 - 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) + getattr(control, key)(listeners[key](events, name)) - @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 + if is_grid: + controllers = EventsGrid(events, name) + control.addSelectionListener(controllers) + control.Model.GridDataModel.addGridDataListener(controllers) + return - 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 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 +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): @@ -2083,18 +3786,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): @@ -2127,14 +3818,129 @@ 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 EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def actionPerformed(self, event): + event_name = f'{self.name}_action' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + CONTROLS = ( + 'stardiv.Toolkit.UnoControlEditModel', + ) + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + service = event.Source.Model.ImplementationName + # ~ print('Focus enter', service) + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + return + + def focusLost(self, event): + service = event.Source.Model.ImplementationName + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = -1 + return + + +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) + # ~ else: + # ~ if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): + # ~ self._cls.close() + return + + +class EventsItem(EventsListenerBase, XItemListener): + + 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) + 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 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 @@ -2166,79 +3972,6 @@ class EventsMouseGrid(EventsMouse): 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): - - 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) - 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): - - 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 - - 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): @@ -2251,55 +3984,28 @@ class EventsTab(EventsListenerBase, XTabListener): return -class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): +class EventsMenu(EventsListenerBase, XMenuListener): - def __init__(self, controller, name): - super().__init__(controller, name) + def __init__(self, controller): + super().__init__(controller, '') - 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): + def itemHighlighted(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) + def itemSelected(self, event): + name = event.Source.getCommand(event.MenuId) + if name.startswith('menu'): + event_name = '{}_selected'.format(name) else: - if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): - self._cls.close() + 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): return @@ -2371,37 +4077,27 @@ class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): 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): - 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 + + 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) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass @property def obj(self): @@ -2410,6 +4106,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): @@ -2417,8 +4123,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() @@ -2455,74 +4280,35 @@ class UnoBaseObject(object): self._set_possize('Y', value) @property - def width(self): - return self._model.Width - @width.setter - def width(self, value): - if hasattr(self.model, 'Width'): - self.model.Width = value - elif hasattr(self.obj, 'PosSize'): - self._set_possize('Width', value) + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value @property - def height(self): - if hasattr(self.model, 'Height'): - return self.model.Height + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value + + @property + def ps(self): ps = self.obj.getPosSize() - return ps.Height - @height.setter - def height(self, value): - if hasattr(self.model, 'Height'): - self.model.Height = value - elif hasattr(self.obj, 'PosSize'): - self._set_possize('Height', value) - - @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 @@ -2535,7 +4321,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: @@ -2544,13 +4330,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 @@ -2598,6 +4380,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 UnoCheckBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'checkbox' + + @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): @@ -2615,14 +4446,45 @@ class UnoText(UnoBaseObject): 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): @@ -2642,7 +4504,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) @@ -2660,15 +4528,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: @@ -2682,130 +4546,18 @@ 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 = () + def __setattr__(self, name, value): + if name in ('_options',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + @property def options(self): return self._options @@ -2840,16 +4592,41 @@ class UnoTree(UnoBaseObject): self._tdm = None self._data = [] + def __setattr__(self, name, value): + if name in ('_tdm', '_data'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + @property def selection(self): - return self.obj.Selection + sel = self.obj.Selection + return sel.DataValue, sel.DisplayValue + + @property + def parent(self): + parent = self.obj.Selection.Parent + if parent is None: + return () + return parent.DataValue, parent.DisplayValue + + def _get_parents(self, node): + value = (node.DisplayValue,) + parent = node.Parent + if parent is None: + return value + return self._get_parents(parent) + value + + @property + def parents(self): + values = self._get_parents(self.obj.Selection) + return values @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) @@ -2861,9 +4638,15 @@ class UnoTree(UnoBaseObject): tdm.setRoot(root) self.model.DataModel = tdm self._tdm = self.model.DataModel - self._add_data() return + @property + def path(self): + return self.root + @path.setter + def path(self, value): + self.data = _P.walk_dir(value, True) + @property def data(self): return self._data @@ -2887,61 +4670,297 @@ class UnoTree(UnoBaseObject): return -class UnoTab(UnoBaseObject): +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html +class UnoGrid(UnoBaseObject): def __init__(self, obj): super().__init__(obj) + self._gdm = self.model.GridDataModel + self._columns = [] + self._data = [] + # ~ self._format_columns = () + + def __setattr__(self, name, value): + if name in ('_gdm', '_columns', '_data'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getitem__(self, key): + value = self._gdm.getCellData(key[0], key[1]) + return value + + def __setitem__(self, key, value): + self._gdm.updateCellData(key[0], key[1], value) + return + + @property + def type(self): + return 'grid' + + @property + def columns(self): + return self._columns + @columns.setter + def columns(self, values): + self._columns = values + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html + model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) + for properties in values: + column = create_instance('com.sun.star.awt.grid.GridColumn', True) + for k, v in properties.items(): + setattr(column, k, v) + model.addColumn(column) + self.model.ColumnModel = model + return + + @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 value(self): + if self.column == -1 or self.row == -1: + return '' + return self[self.column, self.row] + @value.setter + def value(self, value): + if self.column > -1 and self.row > -1: + self[self.column, self.row] = value + + @property + def row(self): + return self.obj.CurrentRow + + @property + def column(self): + return self.obj.CurrentColumn + + def clear(self): + self._gdm.removeAllRows() + return + + # UP + 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 rows(self): + # ~ return self._gdm.RowCount + + # ~ @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 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 UnoPage(object): + + def __init__(self, obj): + self._obj = obj self._events = None + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._obj.Model + + # ~ @property + # ~ def id(self): + # ~ return self.m.TabPageID + + @property + def parent(self): + return self.obj.Context + + 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 _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args + + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + args = self._special_properties(tipo, args) + model = self.model.createInstance(UNO_MODELS[tipo]) + _set_properties(model, args) + name = args['Name'] + self.model.insertByName(name, model) + control = self.obj.getControl(name) + _add_listeners(self._events, control, name) + control = UNO_CLASSES[tipo](control) + + if tipo in ('listbox',): + control.path = self.path + + if tipo == 'tree' and root: + control.root = root + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + return control + + +class UnoPages(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._sheets = [] + self._events = None + + def __setattr__(self, name, value): + if name in ('_sheets', '_events'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + def __getitem__(self, index): - return self.get_sheet(index) + name = index + if isinstance(index, int): + name = f'sheet{index}' + sheet = self.obj.getControl(name) + page = UnoPage(sheet) + page._events = self._events + return page + + @property + def type(self): + return 'pages' @property def current(self): - return self.obj.getActiveTabID() + return self.obj.ActiveTabID @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') + self._sheets = values + for i, title in enumerate(values): + sheet = self.m.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) + self.m.insertByName(f'sheet{i + 1}', sheet) return @property @@ -2951,644 +4970,144 @@ class UnoTab(UnoBaseObject): 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 + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value - return properties + def insert(self, title): + self._sheets.append(title) + id = len(self._sheets) + sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') + sheet.Title = title + self.m.insertByName(f'sheet{id}', sheet) + return self[id] - 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) + def remove(self, id): + self.obj.removeTab(id) + return - 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) + def activate(self, id): + self.obj.activateTab(id) 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, + 'checkbox': UnoCheckBox, + 'text': UnoText, + 'image': UnoImage, + 'listbox': UnoListBox, + 'roadmap': UnoRoadmap, + 'tree': UnoTree, + 'grid': UnoGrid, + 'pages': UnoPages, +} + +UNO_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', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + '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', +# ~ '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', 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', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + } - 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 = '' + self._init_controls() - 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').lower() 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): + name = name.split('.')[2] 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', + 'UnoFixedTextControl': 'label', + 'UnoEditControl': 'text', + 'UnoButtonControl': 'button', } return types[name] @@ -3596,7 +5115,7 @@ class LODialog(object): for control in self.obj.getControls(): tipo = self._get_type_control(control.ImplementationName) name = control.Model.Name - control = get_custom_class(tipo, control) + control = UNO_CLASSES[tipo](control) setattr(self, name, control) return @@ -3609,20 +5128,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): @@ -3639,13 +5157,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): @@ -3659,112 +5175,101 @@ 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): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args - 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']) - 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 - if not 'Height' in properties: - properties['Height'] = self.height + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args - return properties + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) - def add_control(self, properties): - tipo = properties.pop('Type').lower() - root = properties.pop('Root', '') - sheets = properties.pop('Sheets', ()) + return args - properties = self._special_properties(tipo, properties) - model = self.model.createInstance(self._get_control_model(tipo)) - set_properties(model, properties) - name = properties['Name'] + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + 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 + elif tipo == 'grid' and columns: + control.columns = columns elif tipo == 'pages' and sheets: control.sheets = sheets 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: @@ -3779,27 +5284,302 @@ 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 + + def close(self, value=0): + if self._modal: + value = self.obj.endDialog(value) + else: + self.visible = False + self.obj.dispose() + return value + + +class LOSheets(object): + + def __getitem__(self, index): + return LODocs().active[index] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +class LOCells(object): + + def __getitem__(self, index): + return LODocs().active.active[index] + + +class LOShortCut(object): +# ~ getKeyEventsByCommand + + def __init__(self, app): + self._app = app + self._scm = None + self._init_values() + + def _init_values(self): + name = 'com.sun.star.ui.GlobalAcceleratorConfiguration' + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + uicm = manager.getUIConfigurationManager(service) + self._scm = uicm.ShortCutManager + return + + def __contains__(self, item): + cmd = self._get_command(item) + return bool(cmd) + + def _get_key_event(self, command): + events = self._scm.AllKeyEvents + for event in events: + cmd = self._scm.getCommandByKeyEvent(event) + if cmd == command: + break + return event + + def _to_key_event(self, shortcut): + key_event = KeyEvent() + keys = shortcut.split('+') + for v in keys[:-1]: + key_event.Modifiers += MODIFIERS[v.lower()] + key_event.KeyCode = getattr(Key, keys[-1].upper()) + return key_event + + def _get_command(self, shortcut): + command = '' + key_event = self._to_key_event(shortcut) + try: + command = self._scm.getCommandByKeyEvent(key_event) + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return command + + def add(self, shortcut, command): + if isinstance(command, dict): + command = _get_url_script(command) + key_event = self._to_key_event(shortcut) + self._scm.setKeyEvent(key_event, command) + self._scm.store() + return + + def reset(self): + self._scm.reset() + self._scm.store() + return + + def remove(self, shortcut): + key_event = self._to_key_event(shortcut) + try: + self._scm.removeKeyEvent(key_event) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return + + def remove_by_command(self, command): + if isinstance(command, dict): + command = _get_url_script(command) + try: + self._scm.removeCommandFromAllKeyEvents(command) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {command}') + return + + +class LOShortCuts(object): + + def __getitem__(self, index): + return LOShortCut(index) + + +class LOMenu(object): + + def __init__(self, app): + self._app = app + self._ui = None + self._pymenus = None + self._menu = None + self._menus = self._get_menus() + + def __getitem__(self, index): + if isinstance(index, int): + self._menu = self._menus[index] + else: + for menu in self._menus: + cmd = menu.get('CommandURL', '') + if MENUS[index.lower()] == cmd: + self._menu = menu + break + line = self._menu.get('CommandURL', '') + line += self._get_submenus(self._menu['ItemDescriptorContainer']) + return line + + def _get_menus(self): + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + self._ui = manager.getUIConfigurationManager(service) + self._pymenus = self._ui.getSettings(NODE_MENUBAR, True) + data = [] + for menu in self._pymenus: + data.append(data_to_dict(menu)) + return data + + def _get_info(self, menu): + line = menu.get('CommandURL', '') + line += self._get_submenus(menu['ItemDescriptorContainer']) + return line + + def _get_submenus(self, menu, level=1): + line = '' + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '----------') + line += f'\n{" " * level}├─ ({i}) {cmd}' + submenu = data.get('ItemDescriptorContainer', None) + if not submenu is None: + line += self._get_submenus(submenu, level + 1) + return line + + def __str__(self): + info = '\n'.join([self._get_info(m) for m in self._menus]) + return info + + def _get_index_menu(self, menu, command): + index = -1 + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '') + if cmd == command: + index = i + break + return index + + def insert(self, name, args): + idc = None + replace = False + command = args['CommandURL'] + label = args['Label'] + + self[name] + menu = self._menu['ItemDescriptorContainer'] + submenu = args.get('Submenu', False) + if submenu: + idc = self._ui.createSettings() + + index = self._get_index_menu(menu, command) + if index == -1: + if 'Index' in args: + index = args['Index'] + else: + index = self._get_index_menu(menu, args['After']) + 1 + else: + replace = True + + data = dict ( + CommandURL = command, + Label = label, + Style = 0, + Type = 0, + ItemDescriptorContainer = idc, + ) + self._save(menu, data, index, replace) + self._insert_submenu(idc, submenu) + return + + def _get_command(self, args): + shortcut = args.get('ShortCut', '') + cmd = args['CommandURL'] + if isinstance(cmd, dict): + cmd = _get_url_script(cmd) + if shortcut: + LOShortCut(self._app).add(shortcut, cmd) + return cmd + + def _insert_submenu(self, parent, menus): + for i, v in enumerate(menus): + submenu = v.pop('Submenu', False) + if submenu: + idc = self._ui.createSettings() + v['ItemDescriptorContainer'] = idc + v['Type'] = 0 + if v['Label'] == '-': + v['Type'] = 1 + else: + v['CommandURL'] = self._get_command(v) + self._save(parent, v, i) + if submenu: + self._insert_submenu(idc, submenu) + return + + def remove(self, name, command): + self[name] + menu = self._menu['ItemDescriptorContainer'] + index = self._get_index_menu(menu, command) + if index > -1: + uno.invoke(menu, 'removeByIndex', (index,)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + def _save(self, menu, properties, index, replace=False): + properties = dict_to_property(properties, True) + if replace: + uno.invoke(menu, 'replaceByIndex', (index, properties)) + else: + uno.invoke(menu, 'insertByIndex', (index, properties)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + +class LOMenus(object): + + def __getitem__(self, index): + return LOMenu(index) + class LOWindow(object): - EMPTY = b""" + EMPTY = """ """ + 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', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + } - def __init__(self, **kwargs): + def __init__(self, args): self._events = None self._menu = None self._container = None - self._id_extension = '' - self._obj = self._create(kwargs) - - @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 + self._model = None + self._id = '' + self._path = '' + self._obj = self._create(args) def _create(self, properties): ps = ( @@ -3831,12 +5611,11 @@ class LOWindow(object): 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) + model.BackgroundColor = get_color((225, 225, 225)) self._container.setModel(model) self._container.createPeer(self._toolkit, self._window) self._container.setPosSize(*ps, POSSIZE) @@ -3846,86 +5625,17 @@ class LOWindow(object): 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()) + path_tmp = _P.save_tmp(self.EMPTY) + subcont = cwp.createContainerWindow( + _P.to_url(path_tmp), '', self._container.getPeer(), None) + _P.kill(path_tmp) 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) + self._model = subcont.Model return def _create_popupmenu(self, menus): @@ -3961,31 +5671,94 @@ class LOWindow(object): 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)) + # ~ self._container.addKeyListener(EventsKeyWindow(self)) return - @property - def name(self): - return self._title.lower().replace(' ', '_') + 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 _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args + + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + 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._subcont.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 + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + return control @property def events(self): return self._events @events.setter - def events(self, value): - self._events = value + def events(self, controllers): + self._events = controllers(self) self._add_listeners() + @property + def model(self): + return self._model + @property def width(self): return self._container.Size.Width @@ -3994,6 +5767,14 @@ class LOWindow(object): def height(self): return self._container.Size.Height + @property + def name(self): + return self._title.lower().replace(' ', '_') + + def add_menu(self, menus): + self._create_menu(menus) + return + def open(self): self._window.setVisible(True) return @@ -4005,235 +5786,478 @@ class LOWindow(object): return -# ~ Python >= 3.7 -# ~ def __getattr__(name): +def create_window(args): + return LOWindow(args) -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) +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 -# ~ Export ok -def get_document(title=''): - doc = None - desktop = get_desktop() - if not title: - doc = _get_class_doc(desktop.getCurrentComponent()) - return doc +class ClipBoard(object): + SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard' + CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' - for d in desktop.getComponents(): - if hasattr(d, 'Title') and d.Title == title: - doc = d - break + class TextTransferable(unohelper.Base, XTransferable): - if doc is None: + 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 - return _get_class_doc(doc) + @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 +_CB = ClipBoard -def get_documents(custom=True): - docs = [] - desktop = get_desktop() - for doc in desktop.getComponents(): - if custom: - docs.append(_get_class_doc(doc)) +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 path(self): + return str(self._path.parent) + + @property + def file_name(self): + return self._path.name + + @property + def name(self): + return self._path.stem + + @property + def ext(self): + return self._path.suffix[1:] + + @property + def info(self): + return self.path, self.file_name, self.name, self.ext + + @property + def url(self): + return self._path.as_uri() + + @property + def size(self): + return self._path.stat().st_size + + @classproperty + def home(self): + return str(Path.home()) + + @classproperty + def documents(self): + return self.config() + + @classproperty + def temp_dir(self): + return tempfile.gettempdir() + + @classproperty + def python(self): + if IS_WIN: + path = self.join(self.config('Module'), PYTHON) + elif IS_MAC: + path = self.join(self.config('Module'), '..', 'Resources', PYTHON) else: - docs.append(doc) - return docs + path = sys.executable + return path + @classmethod + def dir_tmp(self, only_name=False): + dt = tempfile.TemporaryDirectory() + if only_name: + dt = dt.name + return dt -def get_selection(): - return get_document().selection + @classmethod + def tmp(cls, ext=''): + tmp = tempfile.NamedTemporaryFile(suffix=ext) + return tmp.name + @classmethod + def save_tmp(cls, data): + path_tmp = cls.tmp() + cls.save(path_tmp, data) + return path_tmp -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 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)) + @classmethod + def get(cls, init_dir='', filters: str=''): + """ + Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html + filters: 'xml' or 'txt,xml' + """ + 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: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) -def active_cell(): - return get_cell() + path = '' + if file_picker.execute(): + path = cls.to_system(file_picker.getSelectedFiles()[0]) + return path + @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) -def create_dialog(properties): - return LODialog(**properties) + path = '' + if folder_picker.execute(): + path = cls.to_system(folder_picker.getDisplayDirectory()) + return path + @classmethod + def get_file(cls, init_dir: str='', filters: str='', multiple: bool=False): + """ + init_folder: folder default open + multiple: True for multiple selected + filters: 'xml' or 'xml,txt' + """ + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) -def create_window(kwargs): - return LOWindow(**kwargs) + file_picker = create_instance(cls.FILE_PICKER) + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.setMultiSelectionMode(multiple) + if filters: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) -# ~ 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)) + 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 -def get_path_python(): - path = get_config_path('Module') - return join(path, PYTHON) + @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)) -# ~ 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) - - path = '' - if filters: - file_picker.setCurrentFilter(filters[0][0]) - for f in filters: - file_picker.appendFilter(f[0], f[1]) - - if file_picker.execute(): - path = _path_system(file_picker.getSelectedFiles()[0]) - if multiple: - path = [_path_system(f) for f in file_picker.getSelectedFiles()] - - 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()) + @classmethod + def open(cls, path): + if IS_WIN: + os.startfile(path) else: - data = f.read() - return data + pid = subprocess.Popen(['xdg-open', path]).pid + return + + @classmethod + def is_dir(cls, path): + return Path(path).is_dir() + + @classmethod + def is_file(cls, path): + return Path(path).is_file() + + @classmethod + def join(cls, *paths): + return str(Path(paths[0]).joinpath(*paths[1:])) + + @classmethod + def save(cls, path, data, encoding='utf-8'): + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path, data): + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def 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 + + @classmethod + def to_url(cls, path): + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def to_system(cls, path): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def kill(cls, path): + result = True + p = Path(path) + + 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 files(cls, path, pattern='*'): + files = [str(p) for p in Path(path).glob(pattern) if p.is_file()] + return files + + @classmethod + def dirs(cls, path): + dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()] + return dirs + + @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 walk_dir(cls, path, tree=False): + folders = [] + if tree: + i = 0 + p = 0 + parents = {path: 0} + for root, dirs, _ in os.walk(path): + for name in dirs: + i += 1 + rn = cls.join(root, name) + if not rn in parents: + parents[rn] = i + folders.append((i, parents[root], name)) + else: + for root, dirs, _ in os.walk(path): + folders += [cls.join(root, name) for name in dirs] + return folders + + @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 + + @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) + + @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 + + @classmethod + def to_csv(cls, path, data, args={}): + with open(path, 'w') as f: + writer = csv.writer(f, **args) + writer.writerows(data) + return + + @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' + + 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: + 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 merge_zip(cls, 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 + + @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): @@ -4250,8 +6274,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', @@ -4307,540 +6331,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 = ("run [ERROR]: output = %s, error code = %s\n" - % (e.output, e.returncode)) - error(msg) - return False - - if result is None: - return True - - return result.decode() - - -def _zippwd(source, target, pwd): - if IS_WIN: - return False - if not exists_app('zip'): - return False - - cmd = 'zip' - opt = '-j ' - args = "{} --password {} ".format(cmd, pwd) - - if isinstance(source, (tuple, list)): - if not target: - return False - args += opt + target + ' ' + ' '.join(source) - else: - if is_file(source) and not target: - target = replace_ext(source, 'zip') - elif is_dir(source) and not target: - target = join(PurePath(source).parent, - '{}.zip'.format(PurePath(source).name)) - opt = '-r ' - args += opt + target + ' ' + source - - result = run(args, True) - if not result: - return False - - return is_created(target) - - -# ~ Export ok -def zip(source, target='', mode='w', pwd=''): - if pwd: - return _zippwd(source, target, pwd) - - if isinstance(source, (tuple, list)): - if not target: - return False - - with zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) as z: - for path in source: - _, name, _, _ = get_info_path(path) - z.write(path, name) - - return is_created(target) - - if is_file(source): - if not target: - target = replace_ext(source, 'zip') - z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) - _, name, _, _ = get_info_path(source) - z.write(source, name) - z.close() - return is_created(target) - - if not target: - target = join( - PurePath(source).parent, - '{}.zip'.format(PurePath(source).name)) - z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) - root_len = len(os.path.abspath(source)) - for root, dirs, files in os.walk(source): - relative = os.path.abspath(root)[root_len:] - for f in files: - fullpath = join(root, f) - file_name = join(relative, f) - z.write(fullpath, file_name) - z.close() - - return is_created(target) - - -# ~ 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, @@ -4991,10 +6531,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 @@ -5006,359 +6545,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 @@ -5419,23 +6614,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/files/ZAZFavorites_v0.5.0.oxt b/files/ZAZFavorites_v0.5.0.oxt deleted file mode 100644 index 62348e08dcf56197cb14141d05e471ac14010d36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71042 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 Mzyuy+1Bwp%7qM@q5dZ)H diff --git a/files/ZAZFavorites_v0.6.0.oxt b/files/ZAZFavorites_v0.6.0.oxt new file mode 100644 index 0000000000000000000000000000000000000000..9ea1d3ad2dcf492d57bcf95276963b9193bb0853 GIT binary patch literal 76850 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 literal 0 HcmV?d00001 diff --git a/source/description.xml b/source/description.xml index 1132090..e2d1235 100644 --- a/source/description.xml +++ b/source/description.xml @@ -1,7 +1,7 @@ - + Favorites files Archivos favoritos diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py index 8259aca..5682097 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,8 +33,8 @@ import re import shlex import shutil import socket -import subprocess import ssl +import subprocess import sys import tempfile import threading @@ -42,14 +42,19 @@ 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, Union from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError -from string import Template -from subprocess import PIPE +import imaplib import smtplib from smtplib import SMTPException, SMTPAuthenticationError from email.mime.multipart import MIMEMultipart @@ -61,147 +66,53 @@ 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 import Rectangle, Size, Point 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 Key, KeyModifier, KeyEvent +from com.sun.star.container import NoSuchElementException from com.sun.star.datatransfer import XTransferable, DataFlavor + +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.ControlCharacter import PARAGRAPH_BREAK 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 XMenuListener 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 XFocusListener 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 import XWindowListener +from com.sun.star.awt import XTopWindowListener from com.sun.star.awt.grid import XGridDataListener from com.sun.star.awt.grid import XGridSelectionListener +from com.sun.star.script import ScriptEventDescriptor +# ~ 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 + +from com.sun.star.view.SelectionType import SINGLE, MULTI, RANGE + +from com.sun.star.sdb.CommandType import TABLE, QUERY, COMMAND 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('You need install peewee, only if you will develop with Base') -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' -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') @@ -211,19 +122,169 @@ 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') +# ~ but, not modify this file, modify in import file +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 + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a +class ValidationType(): + from com.sun.star.sheet.ValidationType \ + import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM +VT = ValidationType + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7 +class ValidationAlertStyle(): + from com.sun.star.sheet.ValidationAlertStyle \ + import STOP, WARNING, INFO, MACRO +VAS = ValidationAlertStyle + + +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html +class ConditionOperator(): + from com.sun.star.sheet.ConditionOperator2 \ + import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \ + LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE +CO = ConditionOperator + + +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) + +PYTHON = 'python' +if IS_WIN: + PYTHON = 'python.exe' + +_MACROS = {} +_start = 0 + SECONDS_DAY = 60 * 60 * 24 +DIR = { + 'images': 'images', + 'locales': 'locales', +} + +KEY = { + 'enter': 1280, +} + +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', +} + +DEFAULT_MIME_TYPE = 'png' +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 @@ -245,33 +306,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 @@ -283,52 +352,27 @@ def catch_exception(f): except Exception as e: name = f.__name__ if IS_WIN: - debug(traceback.format_exc()) + msgbox(traceback.format_exc()) log.error(name, exc_info=True) 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 @@ -343,7 +387,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 @@ -351,64 +395,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'): @@ -418,13 +412,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): @@ -435,183 +429,612 @@ 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') + + +# ~ https://wiki.documentfoundation.org/Development/DispatchCommands +# ~ Used only if not exists in API +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, shell=IS_WIN) + if capture: + result = result.stdout + else: + result = result.returncode + return result + + +def popen(command): + 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 + + +def switch_design_mode(doc): + call_dispatch(doc.frame, '.uno:SwitchControlDesignMode') + return + + +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 + + +class ImapServer(object): + + def __init__(self, config): + self._server = None + self._error = '' + 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): + try: + # ~ hosts = 'gmail' in config['server'] + if config['ssl']: + self._server = imaplib.IMAP4_SSL(config['server'], config['port']) + else: + self._server = imaplib.IMAP4(config['server'], config['port']) + self._server.login(config['user'], config['password']) + self._server.select() + return True + except imaplib.IMAP4.error as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return False + + def get_folders(self, exclude=()): + folders = {} + result, subdir = self._server.list() + for s in subdir: + print(s.decode('utf-8')) + return folders + + def close(self): + try: + self._server.close() + self._server.logout() + msg = 'Close connection...' + debug(msg) + except: + pass + 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', '_view'): + 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] - - 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 - - @property - def obj(self): - return self._obj - @obj.setter - def obj(self, value): - self._obj = value - - -class LOObjectBase(object): - - def __init__(self, obj): - self.__dict__['_obj'] = obj - - 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): + FILTERS = { + 'doc': 'MS Word 97', + 'docx': 'MS Word 2007 XML', + } 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._undo = True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() @property def obj(self): @@ -625,12 +1048,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): @@ -650,19 +1073,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 @@ -672,6 +1107,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') @@ -681,56 +1141,386 @@ 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 insert_contents(self, args={}): + call_dispatch(self.frame, '.uno:InsertContents', args) 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) + + 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, + } + 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 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.store() + return result + + def close(self): + self.obj.close(True) + return -class FormControlBase(object): +class LOCellStyle(LOBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def name(self): + return self.obj.Name + + @property + def properties(self): + properties = self.obj.PropertySetInfo.Properties + data = {p.Name: getattr(self.obj, p.Name) for p in properties} + return data + @properties.setter + def properties(self, values): + _set_properties(self.obj, values) + + +class LOCellStyles(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + 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 new(self, name: str=''): + obj = self._doc.create_instance('com.sun.star.style.CellStyle') + if name: + self.obj[name] = obj + obj = LOCellStyle(obj) + return obj + + +class LOCalc(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._type = CALC + self._sheets = obj.Sheets + + def __getitem__(self, index): + return LOCalcSheet(self._sheets[index]) + + def __setitem__(self, key, value): + self._sheets[key] = value + + def __len__(self): + return self._sheets.Count + + def __contains__(self, item): + 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): + return self._cc.ColumnRowHeaders + @headers.setter + def headers(self, value): + self._cc.ColumnRowHeaders = value + + @property + def tabs(self): + return self._cc.SheetTabs + @tabs.setter + def tabs(self, value): + self._cc.SheetTabs = value + + @property + def cs(self): + return self.cell_styles + @property + def cell_styles(self): + obj = self.obj.StyleFamilies['CellStyles'] + return LOCellStyles(obj, self) + + @property + 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._sheets[sheet] + self._cc.setActiveSheet(obj) + return + + def new_sheet(self): + s = self.create_instance('com.sun.star.sheet.Spreadsheet') + return s + + def insert(self, name): + names = name + if isinstance(name, str): + names = (name,) + for n in names: + self._sheets[n] = self.new_sheet() + return LOCalcSheet(self._sheets[n]) + + def move(self, name, pos=-1): + index = pos + if pos < 0: + index = len(self) + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.moveByName(name, index) + return + + def remove(self, name): + if isinstance(name, LOCalcSheet): + name = name.name + self._sheets.removeByName(name) + return + + 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 = len(self) + + names = source + if not source: + names = doc.names + elif isinstance(source, str): + names = (source,) + + new_names = target + if not target: + new_names = names + elif isinstance(target, str): + new_names = (target,) + + for i, name in enumerate(names): + self._sheets.importSheet(doc.obj, name, index + i) + self[index + i].name = new_names[i] + + return LOCalcSheet(self._sheets[index]) + + def sort(self, reverse=False): + names = sorted(self.names, reverse=reverse) + for i, n in enumerate(names): + self.move(n, i) + return + + def render(self, data, sheet=None, clean=True): + if sheet is None: + sheet = self.active + return sheet.render(data, clean=clean) + + +class LOChart(object): + + def __init__(self, name, obj, draw_page): + self._name = name + self._obj = obj + self._eobj = self._obj.EmbeddedObject + self._type = 'Column' + self._cell = None + self._shape = self._get_shape(draw_page) + self._pos = self._shape.Position + + def __getitem__(self, index): + return LOBaseObject(self.diagram.getDataRowProperties(index)) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def diagram(self): + return self._eobj.Diagram + + @property + def type(self): + return self._type + @type.setter + def type(self, value): + self._type = value + if value == 'Bar': + self.diagram.Vertical = True + return + type_chart = f'com.sun.star.chart.{value}Diagram' + self._eobj.setDiagram(self._eobj.createInstance(type_chart)) + + @property + def cell(self): + return self._cell + @cell.setter + def cell(self, value): + self._cell = value + self._shape.Anchor = value.obj + + @property + def position(self): + return self._pos + @position.setter + def position(self, value): + self._pos = value + self._shape.Position = value + + def _get_shape(self, draw_page): + for shape in draw_page: + if shape.PersistName == self.name: + break + return shape + + +class LOSheetCharts(object): + + def __init__(self, obj, sheet): + self._obj = obj + self._sheet = sheet + + def __getitem__(self, index): + return LOChart(index, self.obj[index], self._sheet.draw_page) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __contains__(self, item): + return item in self.obj + + def __len__(self): + return len(self.obj) + + @property + def obj(self): + return self._obj + + def new(self, name, pos_size, data): + self.obj.addNewByName(name, pos_size, data, True, True) + return LOChart(name, self.obj[name], self._sheet.draw_page) + + +class LOFormControl(LOBaseObject): EVENTS = { 'action': 'actionPerformed', 'click': 'mousePressed', @@ -740,22 +1530,43 @@ class FormControlBase(object): 'mousePressed': 'XMouseListener', } - def __init__(self, obj): - self._obj = obj + def __init__(self, obj, view, form): + super().__init__(obj) + self._view = view + self._form = form + self._m = view.Model self._index = -1 - self._rules = {} - @property - def obj(self): - return self._obj + def __setattr__(self, name, value): + if name in ('_form', '_view', '_m', '_index'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) - @property - def name(self): - return self.obj.Name + def __str__(self): + return f'{self.name} ({self.type}) {[self.index]}' @property def form(self): - return self.obj.getParent() + return self._form + + @property + def doc(self): + return self.obj.Parent.Forms.Parent + + @property + def name(self): + return self._m.Name + @name.setter + def name(self, value): + self._m.Name = value + + @property + def tag(self): + return self._m.Tag + @tag.setter + def tag(self, value): + self._m.Tag = value @property def index(self): @@ -764,23 +1575,16 @@ class FormControlBase(object): def index(self, value): self._index = value + @property + def enabled(self): + return self._m.Enabled + @enabled.setter + def enabled(self, value): + self._m.Enabled = 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) @@ -802,83 +1606,227 @@ class FormControlBase(object): self.form.registerScriptEvent(self.index, event) return - -class FormButton(FormControlBase): - - def __init__(self, obj): - super().__init__(obj) + def set_focus(self): + self._view.setFocus() + return +class LOFormControlLabel(LOFormControl): -class LOForm(ObjectBase): + def __init__(self, obj, view, form): + super().__init__(obj, view, form) - def __init__(self, obj): - super().__init__(obj) + @property + def type(self): + return 'label' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Label = value + + +class LOFormControlText(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'text' + + @property + def value(self): + return self._m.Text + @value.setter + def value(self, value): + self._m.Text = value + + +class LOFormControlButton(LOFormControl): + + def __init__(self, obj, view, form): + super().__init__(obj, view, form) + + @property + def type(self): + return 'button' + + @property + def value(self): + return self._m.Label + @value.setter + def value(self, value): + self._m.Text = Label + + +FORM_CONTROL_CLASS = { + 'label': LOFormControlLabel, + 'text': LOFormControlText, + 'button': LOFormControlButton, +} + + +class LOForm(object): + MODELS = { + 'label': 'com.sun.star.form.component.FixedText', + 'text': 'com.sun.star.form.component.TextField', + 'button': 'com.sun.star.form.component.CommandButton', + } + + def __init__(self, obj, draw_page): + self._obj = obj + self._dp = draw_page + self._controls = {} self._init_controls() def __getitem__(self, index): - if isinstance(index, int): - return self._controls[index] - else: - return getattr(self, index) + control = self.obj[index] + return self._controls[control.Name] - 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', - } - return types[name] + 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) + + def __str__(self): + return f'Form: {self.name}' 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) + types = { + 'com.sun.star.form.OFixedTextModel': 'label', + 'com.sun.star.form.OEditModel': 'text', + 'com.sun.star.form.OButtonModel': 'button', + } + for i, control in enumerate(self.obj): + name = control.Name + tipo = types[control.ImplementationName] + view = self.doc.CurrentController.getControl(control) + control = FORM_CONTROL_CLASS[tipo](control, view) control.index = i - self._controls.append(control) - setattr(self, c.Name, control) + setattr(self, name, control) + self._controls[name] = control + return + + @property + def obj(self): + return self._obj @property def name(self): - return self._obj.getName() + return self.obj.Name @name.setter def name(self, value): - self._obj.setName(value) + self.obj.Name = value + @property + def source(self): + return self.obj.DataSourceName + @source.setter + def source(self, value): + self.obj.DataSourceName = value -class LOForms(ObjectBase): + @property + def type(self): + return self.obj.CommandType + @type.setter + def type(self, value): + self.obj.CommandType = value - def __init__(self, obj, doc): - self._doc = doc - super().__init__(obj) - - def __getitem__(self, index): - form = super().__getitem__(index) - return LOForm(form) + @property + def command(self): + return self.obj.Command + @command.setter + def command(self, value): + self.obj.Command = value @property def doc(self): - return self._doc + return self.obj.Parent.Parent + + def _special_properties(self, tipo, args): + if tipo == 'button': + # ~ if 'ImageURL' in args: + # ~ args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + return args + + def add(self, args): + name = args['Name'] + tipo = args.pop('Type').lower() + w = args.pop('Width') + h = args.pop('Height') + x = args.pop('X', 0) + y = args.pop('Y', 0) + control = self.doc.createInstance('com.sun.star.drawing.ControlShape') + control.setSize(Size(w, h)) + control.setPosition(Point(x, y)) + model = self.doc.createInstance(self.MODELS[tipo]) + args = self._special_properties(tipo, args) + _set_properties(model, args) + control.Control = model + index = len(self) + self.obj.insertByIndex(index, model) + self._dp.add(control) + view = self.doc.CurrentController.getControl(self.obj.getByName(name)) + control = FORM_CONTROL_CLASS[tipo](control, view, self.obj) + control.index = index + setattr(self, name, control) + self._controls[name] = control + return control + + +class LOSheetForms(object): + + def __init__(self, draw_page): + self._dp = draw_page + self._obj = draw_page.Forms + + def __getitem__(self, index): + return LOForm(self.obj[index], self._dp) + + 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 + + @property + def doc(self): + return self.obj.Parent @property def count(self): - return self.obj.getCount() + return len(self) @property def names(self): - return self.obj.getElementNames() - - def exists(self, name): - return name in self.names + return self.obj.ElementNames def insert(self, name): - form = self.doc.create_instance('com.sun.star.form.component.Form') + form = self.doc.createInstance('com.sun.star.form.component.Form') self.obj.insertByName(name, form) - return self[name] + return LOForm(form, self._dp) def remove(self, index): if isinstance(index, int): @@ -888,356 +1836,128 @@ class LOForms(ObjectBase): return -class LOCellStyle(LOObjectBase): +# ~ IsFiltered, +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetRows(object): - 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): + def __init__(self, sheet, obj): + self._sheet = sheet self._obj = obj + def __getitem__(self, index): + if isinstance(index, int): + rows = LOSheetRows(self._sheet, self.obj[index]) + else: + rango = self._sheet[index.start:index.stop,0:] + rows = LOSheetRows(self._sheet, rango.obj.Rows) + return rows + 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() - - 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) - - def __setitem__(self, key, value): - self._sheets[key] = value - - def __contains__(self, item): - return item in self.obj.Sheets - - @property - def headers(self): - return self._cc.ColumnRowHeaders - @headers.setter - def headers(self, value): - self._cc.ColumnRowHeaders = value - - @property - def tabs(self): - return self._cc.SheetTabs - @tabs.setter - def tabs(self, value): - self._cc.SheetTabs = value - - @property - def active(self): - return LOCalcSheet(self._cc.getActiveSheet(), self) - - def activate(self, sheet): - obj = sheet - if isinstance(sheet, LOCalcSheet): - obj = sheet.obj - elif isinstance(sheet, str): - obj = self[sheet].obj - 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 - - @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 - 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) - - 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 - - names = source - if not names: - names = self.names - elif isinstance(source, str): - names = (source,) - - 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_from(self, doc, source='', target='', pos=-1): - index = pos - if pos < 0: - index = self._sheets.Count + pos + 1 - - names = source - if not names: - names = doc.names - elif isinstance(source, str): - names = (source,) - - new_names = target - if not target: - new_names = names - 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] - - # ~ doc.getCurrentController().setActiveSheet(sheet) - # ~ For controls in sheet - # ~ doc.getCurrentController().setFormDesignMode(False) - - return LOCalcSheet(self._sheets[index], self) - - def sort(self, reverse=False): - names = sorted(self.names, reverse=reverse) - for i, n in enumerate(names): - self.sheets.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 - - -class LOCalcSheets(object): - - def __init__(self, obj, doc): - self._obj = obj - self._doc = doc - - 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 obj(self): + return self._obj - def copy(self, name, new_name, pos): - self.obj.copyByName(name, new_name, pos) + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def color(self): + return self.obj.CellBackColor + @color.setter + def color(self, value): + self.obj.CellBackColor = value + + @property + def is_transparent(self): + return self.obj.IsCellBackgroundTransparent + @is_transparent.setter + def is_transparent(self, value): + self.obj.IsCellBackgroundTransparent = value + + @property + def height(self): + return self.obj.Height + @height.setter + def height(self, value): + self.obj.Height = value + + def optimal(self): + self.obj.OptimalHeight = True 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) + def insert(self, index, count): + self.obj.insertByIndex(index, count) return - def remove(self, name): - sheet = self.obj[name] - self.obj.removeByName(sheet.Name) + def remove(self, index, count): + self.obj.removeByIndex(index, count) + return + + +# ~ IsManualPageBreak, +# ~ IsStartOfNewPage +class LOSheetColumns(object): + + def __init__(self, sheet, obj): + self._sheet = sheet + self._obj = obj + + def __getitem__(self, index): + if isinstance(index, (int, str)): + rows = LOSheetColumns(self._sheet, self.obj[index]) + else: + rango = self._sheet[0,index.start:index.stop] + rows = LOSheetColumns(self._sheet, rango.obj.Columns) + return rows + + def __len__(self): + return self.obj.Count + + @property + def obj(self): + return self._obj + + @property + def visible(self): + return self._obj.IsVisible + @visible.setter + def visible(self, value): + self._obj.IsVisible = value + + @property + def width(self): + return self.obj.Width + @width.setter + def width(self, value): + self.obj.Width = value + + def optimal(self): + self.obj.OptimalWidth = True + return + + def insert(self, index, count): + self.obj.insertByIndex(index, count) + return + + def remove(self, index, count): + self.obj.removeByIndex(index, count) return class LOCalcSheet(object): - def __init__(self, obj, doc): + def __init__(self, obj): self._obj = obj - self._doc = doc - self._init_values() def __getitem__(self, index): - return LOCellRange(self.obj[index], self.doc) + return LOCalcRange(self.obj[index]) def __enter__(self): return self @@ -1245,23 +1965,13 @@ 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} + def __str__(self): + return f'easymacro.LOCalcSheet: {self.name}' @property def obj(self): return self._obj - @property - def doc(self): - return self._doc - - @property - def images(self): - return self._images - @property def name(self): return self._obj.Name @@ -1276,27 +1986,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): @@ -1317,133 +2012,985 @@ 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, self.obj.Rows) + + @property + def columns(self): + return LOSheetColumns(self, self.obj.Columns) @property def forms(self): - return LOForms(self._dp.getForms(), self.doc) + return LOSheetForms(self.obj.DrawPage) @property def events(self): - return self._events + names = ('OnFocus', 'OnUnfocus', 'OnSelect', 'OnDoubleClick', + 'OnRightClick', 'OnChange', 'OnCalculate') + evs = self.obj.Events + events = {n: _property_to_dict(evs.getByName(n)) for n in names + if evs.getByName(n)} + return events @events.setter - def events(self, controllers): - self._events = controllers - self._connect_listeners() + def events(self, values): + pv = '[]com.sun.star.beans.PropertyValue' + ev = self.obj.Events + for name, v in values.items(): + url = _get_url_script(v) + args = dict_to_property(dict(EventType='Script', Script=url)) + # ~ e.replaceByName(k, args) + uno.invoke(ev, 'replaceByName', (name, uno.Any(pv, args))) - def _connect_listeners(self): - if self.events is None: + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + 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 + + 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) + + def find(self, search_string, rango=None): + if rango is None: + rango = self.used_area + return rango.find(search_string) + + +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 __contains__(self, item): + return item.in_range(self) + + 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 LOSheetRows(self.sheet, 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) + + @property + def merged_area(self): + cursor = self.cursor + cursor.collapseToMergedArea() + rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj) + return rango + + @property + def empty(self): + cursor = self.sheet.get_cursor(self.obj) + cursor = self.cursor + rangos = cursor.queryEmptyCells() + rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos] + return tuple(rangos) + + @property + def merge(self): + return self.obj.IsMerged + @merge.setter + def merge(self, value): + self.obj.merge(value) + + @property + def style(self): + return self.obj.CellStyle + @style.setter + def style(self, value): + self.obj.CellStyle = value + + @property + def auto_format(self): + return '' + @auto_format.setter + def auto_format(self, value): + self.obj.autoFormat(value) + + @property + def validation(self): + return self.obj.Validation + @validation.setter + def validation(self, values): + current = self.validation + if not values: + current.Type = ValidationType.ANY + current.ShowInputMessage = False + else: + is_list = False + for k, v in values.items(): + if k == 'Type' and v == VT.LIST: + 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 select(self): + self.doc.select(self.obj) + return + + def search(self, options, find_all=True): + rangos = None + + descriptor = self.sheet.search_descriptor + descriptor.setSearchString(options['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 find_all: + found = self.obj.findAll(descriptor) + else: + found = self.obj.findFirst(descriptor) + + if found: + if found.ImplementationName == OBJ_CELL: + rangos = LOCalcRange(found) + else: + rangos = [LOCalcRange(f) for f in found] + + return rangos + + def replace(self, options): + descriptor = self.sheet.replace_descriptor + 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'] + count = self.obj.replaceAll(descriptor) + return count + + def in_range(self, rango): + if isinstance(rango, LOCalcRange): + address = rango.range_address + else: + address = rango.RangeAddress + result = self.cursor.queryIntersection(address) + return bool(result.Count) + + 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(self, source): + self.sheet.obj.copyRange(self.address, source.range_address) + 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_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 optimal_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 - listeners = { - 'addModifyListener': EventsModify, + 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(self, search_string): + if self._sd is None: + self._sd = self.sheet.obj.createSearchDescriptor() + self._sd.SearchCaseSensitive = False + + self._sd.setSearchString(search_string) + cell = self.obj.findFirst(self._sd) + if cell: + cell = LOCalcRange(cell) + return cell + + 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, } - for key, value in listeners.items(): - getattr(self.obj, key)(listeners[key](self.events)) - print('add_listener') + 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 insert_shape(self, tipo, 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']) + + shape = self.sheet.dp.add(tipo, args) + shape.anchor = self.obj + args.clear() + return + + 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 + + def transpose(self): + # ~ 'Flags': 'A', + # ~ 'FormulaCommand': 0, + # ~ 'SkipEmptyCells': False, + # ~ 'AsLink': False, + # ~ 'MoveMode': 4, + self.select() + self.doc.copy() + self.clear(1023) + self[0,0].select() + self.doc.insert_contents({'Transpose': True}) + _CB.set('') + return + + def transpose_data(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 merge_by_row(self): + for r in range(len(self.rows)): + self[r].merge = True + return + + def fill(self, source=1): + self.obj.fillAuto(0, source) return -class LOWriter(LODocument): +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' + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + for i, p in enumerate(self.obj): + if i == self._index: + obj = LOWriterTextRange(p, self._doc) + self._index += 1 + return obj + raise StopIteration + @property def obj(self): return self._obj @property def string(self): - return self._obj.getText().String + s = '' + if not self._is_table: + s = self.obj.String + return s + @string.setter + def string(self, value): + self.obj.String = value + + @property + def value(self): + return self.string + + @property + def is_table(self): + return self._is_table @property def text(self): - return self._obj.getText() + return self.obj.Text @property def cursor(self): - return self.text.createTextCursor() + return self.text.createTextCursorByRange(self.obj) @property - def paragraphs(self): - return [LOTextRange(p) for p in self.text] + def dp(self): + return self._doc.dp - @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 offset(self): + cursor = self.cursor.getEnd() + return LOWriterTextRange(cursor, self._doc) - 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): + def insert_content(self, data, cursor=None, replace=False): + if cursor is None: + cursor = self.cursor self.text.insertTextContent(cursor, data, replace) return - # ~ f = doc.createInstance('com.sun.star.text.TextFrame') - # ~ f.setSize(Size(10000, 500)) + def new_line(self, count=1): + cursor = self.cursor + for i in range(count): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + return self._doc.selection - 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) + def insert_table(self, data): + table = self._doc.create_instance('com.sun.star.text.TextTable') + rows = len(data) + cols = len(data[0]) + table.initialize(rows, cols) + self.insert_content(table) + table.DataArray = data + name = table.Name + table = LOWriterTextTable(self._doc.tables[name], self._doc) + return table + + 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(cursor, image) + 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): + for i, p in enumerate(self.obj): + if i == index: + obj = LOWriterTextRange(p, self._doc) + break + return obj + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + for i, p in enumerate(self.obj): + if i == self._index: + obj = LOWriterTextRange(p, self._doc) + self._index += 1 + return obj + raise StopIteration + + @property + def obj(self): + return self._obj + + +class LOWriterTextTable(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._obj.Name + + @property + def data(self): + return self._obj.DataArray + @data.setter + def data(self, values): + self._obj.DataArray = values + + +class LOWriterTextTables(object): + + def __init__(self, doc): + self._doc = doc + self._obj = doc.obj.TextTables + + def __getitem__(self, key): + return LOWriterTextTable(self._obj[key], self._doc) + + def __len__(self): + return self._obj.Count + + def insert(self, data, text_range=None): + if text_range is None: + text_range = self._doc.selection + text_range.insert_table(data) 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 +class LOWriter(LODocument): - def select(self, text): - self._cc.select(text) - return + def __init__(self, obj): + super().__init__(obj) + self._type = WRITER - def search(self, options): - descriptor = self.obj.createSearchDescriptor() + @property + def text(self): + return LOWriterTextRange(self.obj.Text, self) + + @property + def paragraphs(self): + return LOWriterTextRanges(self.obj.Text, self) + + @property + def tables(self): + return LOWriterTextTables(self) + + @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) + + @property + def search_descriptor(self): + return self.obj.createSearchDescriptor() + + @property + def replace_descriptor(self): + return self.obj.createReplaceDescriptor() + + def goto_start(self): + self.view_cursor.gotoStart(False) + return self.selection + + def goto_end(self): + self.view_cursor.gotoEnd(False) + return self.selection + + def search(self, options, find_all=True): + descriptor = self.search_descriptor descriptor.setSearchString(options.get('Search', '')) descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) descriptor.SearchWords = options.get('Words', False) @@ -1455,15 +3002,20 @@ class LOWriter(LODocument): if hasattr(descriptor, 'SearchType') and 'Type' in options: descriptor.SearchType = options['Type'] - if options.get('First', False): - found = self.obj.findFirst(descriptor) - else: + result = False + if find_all: found = self.obj.findAll(descriptor) + if len(found): + result = [LOWriterTextRange(f, self) for f in found] + else: + found = self.obj.findFirst(descriptor) + if found: + result = LOWriterTextRange(found, self) - return found + return result def replace(self, options): - descriptor = self.obj.createReplaceDescriptor() + descriptor = self.replace_descriptor descriptor.setSearchString(options['Search']) descriptor.setReplaceString(options['Replace']) descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) @@ -1478,41 +3030,446 @@ class LOWriter(LODocument): found = self.obj.replaceAll(descriptor) return found + def select(self, text): + if hasattr(text, 'obj'): + text = text.obj + self._cc.select(text) + return -class LOTextRange(object): - def __init__(self, obj): - self._obj = obj - self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph' - self._is_table = self.obj.ImplementationName == 'SwXTextTable' +class LOShape(LOBaseObject): + IMAGE = 'com.sun.star.drawing.GraphicObjectShape' + + def __init__(self, obj, index): + self._index = index + super().__init__(obj) @property - def obj(self): - return self._obj + def type(self): + t = self.shape_type[21:] + if self.is_image: + t = 'image' + return t @property - def is_paragraph(self): - return self._is_paragraph + def shape_type(self): + return self.obj.ShapeType @property - def is_table(self): - return self._is_table + def is_image(self): + return self.shape_type == self.IMAGE + + @property + def name(self): + return self.obj.Name or f'{self.type}{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 text(self): - return self.obj.getText() + def description(self): + return self.obj.Description + @description.setter + def description(self, value): + self.obj.Description = value @property - def cursor(self): - return self.text.createTextCursorByRange(self.obj) + 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 + + @property + def path(self): + return self.url + @property + def url(self): + url = '' + if self.is_image: + url = _P.to_system(self.obj.GraphicURL.OriginURL) + return url + + @property + def mimetype(self): + mt = '' + if self.is_image: + mt = self.obj.GraphicURL.MimeType + return mt + + @property + def linked(self): + l = False + if self.is_image: + l = self.obj.GraphicURL.Linked + return l + + def delete(self): + self.remove() + return + def remove(self): + self.obj.Parent.remove(self.obj) + return + + def save(self, path: str, mimetype=DEFAULT_MIME_TYPE): + if _P.is_dir(path): + name = self.name + ext = mimetype.lower() + else: + p = _P(path) + path = p.path + name = p.name + ext = p.ext.lower() + + path = _P.join(path, f'{name}.{ext}') + args = dict( + URL = _P.to_url(path), + MimeType = MIME_TYPE[ext], + ) + if not _export_image(self.obj, args): + path = '' + return path + + # ~ def save2(self, path: str): + # ~ size = len(self.obj.Bitmap.DIB) + # ~ data = self.obj.GraphicStream.readBytes((), size) + # ~ data = data[-1].value + # ~ path = _P.join(path, f'{self.name}.png') + # ~ _P.save_bin(path, b'') + # ~ 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 + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + if self._index == self.count: + raise StopIteration + shape = self[self._index] + self._index += 1 + 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 + """ + index = self.count + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + name = args.get('Name', f'{type_shape.lower()}{index}') + + service = f'com.sun.star.drawing.{type_shape}Shape' + shape = self.create_instance(service) + shape.Size = Size(w, h) + shape.Position = Point(x, y) + shape.Name = name + 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 remove_all(self): + while self.count: + self.obj.remove(self.obj[0]) + return + + def insert_image(self, path, args={}): + index = self.count + w = args.get('Width', 3000) + h = args.get('Height', 3000) + x = args.get('X', 1000) + y = args.get('Y', 1000) + name = args.get('Name', f'image{index}') + + 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) + image.Name = name + 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.current_page[-1] + + 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', @@ -1534,40 +3491,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): @@ -1577,25 +3523,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): @@ -1603,10 +3550,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 @@ -1618,452 +3562,211 @@ 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) + document = None + for i, doc in enumerate(self._desktop.Components): + if isinstance(index, int) and i == index: + document = _get_class_doc(doc) + break + elif isinstance(index, str) and doc.Title == index: + document = _get_class_doc(doc) + break + return document 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 = -1 + return self - if self._type_obj == OBJ_CELL: - self._type_content = self.obj.getType() - return - - @property - def obj(self): - return self._obj - - @property - def doc(self): - return self._doc - - @property - def type(self): - return self._type_obj - - @property - def type_content(self): - return self._type_content - - @property - def first(self): - if self.type == OBJ_RANGES: - obj = LOCellRange(self.obj[0][0,0], self.doc) + def __next__(self): + self._i += 1 + doc = self[self._i] + if doc is None: + raise StopIteration else: - obj = LOCellRange(self.obj[0,0], self.doc) - return obj + return doc + + def __len__(self): + for i, _ in enumerate(self._desktop.Components): + pass + return i + 1 @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) + def active(self): + return _get_class_doc(self._desktop.getCurrentComponent()) - @property - def data(self): - return self.obj.getDataArray() - @data.setter - def data(self, values): - self.obj.setDataArray(values) + @classmethod + def new(cls, type_doc=CALC, args={}): + if type_doc == BASE: + return LOBase(None, args) - @property - def formula(self): - return self.obj.getFormulaArray() - @formula.setter - def formula(self, values): - self.obj.setFormulaArray(values) + 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 column(self): - a = self.address - if hasattr(a, 'Column'): - c = a.Column - else: - c = a.StartColumn - return c + @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 columns(self): - return self._obj.Columns.Count + 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 rows(self): - return self._obj.Rows.Count + return _get_class_doc(doc) - 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 connect(self, path): + return LOBase(None, {'path': path}) - 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 _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' + is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl' - def copy(self, source): - self.sheet.obj.copyRange(self.address, source.range_address) - return + 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 - 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) + getattr(control, key)(listeners[key](events, name)) - @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 + if is_grid: + controllers = EventsGrid(events, name) + control.addSelectionListener(controllers) + control.Model.GridDataModel.addGridDataListener(controllers) + return - 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 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 +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): @@ -2083,18 +3786,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): @@ -2127,14 +3818,129 @@ 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 EventsButton(EventsListenerBase, XActionListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def actionPerformed(self, event): + event_name = f'{self.name}_action' + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + CONTROLS = ( + 'stardiv.Toolkit.UnoControlEditModel', + ) + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + service = event.Source.Model.ImplementationName + # ~ print('Focus enter', service) + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + return + + def focusLost(self, event): + service = event.Source.Model.ImplementationName + if service in self.CONTROLS: + obj = event.Source.Model + obj.BackgroundColor = -1 + return + + +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) + # ~ else: + # ~ if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): + # ~ self._cls.close() + return + + +class EventsItem(EventsListenerBase, XItemListener): + + 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) + 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 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 @@ -2166,79 +3972,6 @@ class EventsMouseGrid(EventsMouse): 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): - - 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) - 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): - - 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 - - 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): @@ -2251,55 +3984,28 @@ class EventsTab(EventsListenerBase, XTabListener): return -class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener): +class EventsMenu(EventsListenerBase, XMenuListener): - def __init__(self, controller, name): - super().__init__(controller, name) + def __init__(self, controller): + super().__init__(controller, '') - 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): + def itemHighlighted(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) + def itemSelected(self, event): + name = event.Source.getCommand(event.MenuId) + if name.startswith('menu'): + event_name = '{}_selected'.format(name) else: - if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): - self._cls.close() + 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): return @@ -2371,37 +4077,27 @@ class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): 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): - 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 + + 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) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass @property def obj(self): @@ -2410,6 +4106,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): @@ -2417,8 +4123,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() @@ -2455,74 +4280,35 @@ class UnoBaseObject(object): self._set_possize('Y', value) @property - def width(self): - return self._model.Width - @width.setter - def width(self, value): - if hasattr(self.model, 'Width'): - self.model.Width = value - elif hasattr(self.obj, 'PosSize'): - self._set_possize('Width', value) + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value @property - def height(self): - if hasattr(self.model, 'Height'): - return self.model.Height + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value + + @property + def ps(self): ps = self.obj.getPosSize() - return ps.Height - @height.setter - def height(self, value): - if hasattr(self.model, 'Height'): - self.model.Height = value - elif hasattr(self.obj, 'PosSize'): - self._set_possize('Height', value) - - @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 @@ -2535,7 +4321,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: @@ -2544,13 +4330,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 @@ -2598,6 +4380,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 UnoCheckBox(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'checkbox' + + @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): @@ -2615,14 +4446,45 @@ class UnoText(UnoBaseObject): 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): @@ -2642,7 +4504,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) @@ -2660,15 +4528,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: @@ -2682,130 +4546,18 @@ 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 = () + def __setattr__(self, name, value): + if name in ('_options',): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + @property def options(self): return self._options @@ -2840,16 +4592,41 @@ class UnoTree(UnoBaseObject): self._tdm = None self._data = [] + def __setattr__(self, name, value): + if name in ('_tdm', '_data'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + @property def selection(self): - return self.obj.Selection + sel = self.obj.Selection + return sel.DataValue, sel.DisplayValue + + @property + def parent(self): + parent = self.obj.Selection.Parent + if parent is None: + return () + return parent.DataValue, parent.DisplayValue + + def _get_parents(self, node): + value = (node.DisplayValue,) + parent = node.Parent + if parent is None: + return value + return self._get_parents(parent) + value + + @property + def parents(self): + values = self._get_parents(self.obj.Selection) + return values @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) @@ -2861,9 +4638,15 @@ class UnoTree(UnoBaseObject): tdm.setRoot(root) self.model.DataModel = tdm self._tdm = self.model.DataModel - self._add_data() return + @property + def path(self): + return self.root + @path.setter + def path(self, value): + self.data = _P.walk_dir(value, True) + @property def data(self): return self._data @@ -2887,61 +4670,297 @@ class UnoTree(UnoBaseObject): return -class UnoTab(UnoBaseObject): +# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html +class UnoGrid(UnoBaseObject): def __init__(self, obj): super().__init__(obj) + self._gdm = self.model.GridDataModel + self._columns = [] + self._data = [] + # ~ self._format_columns = () + + def __setattr__(self, name, value): + if name in ('_gdm', '_columns', '_data'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getitem__(self, key): + value = self._gdm.getCellData(key[0], key[1]) + return value + + def __setitem__(self, key, value): + self._gdm.updateCellData(key[0], key[1], value) + return + + @property + def type(self): + return 'grid' + + @property + def columns(self): + return self._columns + @columns.setter + def columns(self, values): + self._columns = values + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html + model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) + for properties in values: + column = create_instance('com.sun.star.awt.grid.GridColumn', True) + for k, v in properties.items(): + setattr(column, k, v) + model.addColumn(column) + self.model.ColumnModel = model + return + + @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 value(self): + if self.column == -1 or self.row == -1: + return '' + return self[self.column, self.row] + @value.setter + def value(self, value): + if self.column > -1 and self.row > -1: + self[self.column, self.row] = value + + @property + def row(self): + return self.obj.CurrentRow + + @property + def column(self): + return self.obj.CurrentColumn + + def clear(self): + self._gdm.removeAllRows() + return + + # UP + 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 rows(self): + # ~ return self._gdm.RowCount + + # ~ @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 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 UnoPage(object): + + def __init__(self, obj): + self._obj = obj self._events = None + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def obj(self): + return self._obj + + @property + def model(self): + return self._obj.Model + + # ~ @property + # ~ def id(self): + # ~ return self.m.TabPageID + + @property + def parent(self): + return self.obj.Context + + 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 _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args + + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + args = self._special_properties(tipo, args) + model = self.model.createInstance(UNO_MODELS[tipo]) + _set_properties(model, args) + name = args['Name'] + self.model.insertByName(name, model) + control = self.obj.getControl(name) + _add_listeners(self._events, control, name) + control = UNO_CLASSES[tipo](control) + + if tipo in ('listbox',): + control.path = self.path + + if tipo == 'tree' and root: + control.root = root + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + return control + + +class UnoPages(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._sheets = [] + self._events = None + + def __setattr__(self, name, value): + if name in ('_sheets', '_events'): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + def __getitem__(self, index): - return self.get_sheet(index) + name = index + if isinstance(index, int): + name = f'sheet{index}' + sheet = self.obj.getControl(name) + page = UnoPage(sheet) + page._events = self._events + return page + + @property + def type(self): + return 'pages' @property def current(self): - return self.obj.getActiveTabID() + return self.obj.ActiveTabID @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') + self._sheets = values + for i, title in enumerate(values): + sheet = self.m.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) + self.m.insertByName(f'sheet{i + 1}', sheet) return @property @@ -2951,644 +4970,144 @@ class UnoTab(UnoBaseObject): 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 + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.Visible = value - return properties + def insert(self, title): + self._sheets.append(title) + id = len(self._sheets) + sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel') + sheet.Title = title + self.m.insertByName(f'sheet{id}', sheet) + return self[id] - 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) + def remove(self, id): + self.obj.removeTab(id) + return - 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) + def activate(self, id): + self.obj.activateTab(id) 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, + 'checkbox': UnoCheckBox, + 'text': UnoText, + 'image': UnoImage, + 'listbox': UnoListBox, + 'roadmap': UnoRoadmap, + 'tree': UnoTree, + 'grid': UnoGrid, + 'pages': UnoPages, +} + +UNO_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', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + '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', +# ~ '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', 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', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + } - 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 = '' + self._init_controls() - 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').lower() 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): + name = name.split('.')[2] 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', + 'UnoFixedTextControl': 'label', + 'UnoEditControl': 'text', + 'UnoButtonControl': 'button', } return types[name] @@ -3596,7 +5115,7 @@ class LODialog(object): for control in self.obj.getControls(): tipo = self._get_type_control(control.ImplementationName) name = control.Model.Name - control = get_custom_class(tipo, control) + control = UNO_CLASSES[tipo](control) setattr(self, name, control) return @@ -3609,20 +5128,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): @@ -3639,13 +5157,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): @@ -3659,112 +5175,101 @@ 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): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args - 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']) - 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 - if not 'Height' in properties: - properties['Height'] = self.height + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args - return properties + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) - def add_control(self, properties): - tipo = properties.pop('Type').lower() - root = properties.pop('Root', '') - sheets = properties.pop('Sheets', ()) + return args - properties = self._special_properties(tipo, properties) - model = self.model.createInstance(self._get_control_model(tipo)) - set_properties(model, properties) - name = properties['Name'] + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + 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 + elif tipo == 'grid' and columns: + control.columns = columns elif tipo == 'pages' and sheets: control.sheets = sheets 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: @@ -3779,27 +5284,302 @@ 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 + + def close(self, value=0): + if self._modal: + value = self.obj.endDialog(value) + else: + self.visible = False + self.obj.dispose() + return value + + +class LOSheets(object): + + def __getitem__(self, index): + return LODocs().active[index] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +class LOCells(object): + + def __getitem__(self, index): + return LODocs().active.active[index] + + +class LOShortCut(object): +# ~ getKeyEventsByCommand + + def __init__(self, app): + self._app = app + self._scm = None + self._init_values() + + def _init_values(self): + name = 'com.sun.star.ui.GlobalAcceleratorConfiguration' + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + uicm = manager.getUIConfigurationManager(service) + self._scm = uicm.ShortCutManager + return + + def __contains__(self, item): + cmd = self._get_command(item) + return bool(cmd) + + def _get_key_event(self, command): + events = self._scm.AllKeyEvents + for event in events: + cmd = self._scm.getCommandByKeyEvent(event) + if cmd == command: + break + return event + + def _to_key_event(self, shortcut): + key_event = KeyEvent() + keys = shortcut.split('+') + for v in keys[:-1]: + key_event.Modifiers += MODIFIERS[v.lower()] + key_event.KeyCode = getattr(Key, keys[-1].upper()) + return key_event + + def _get_command(self, shortcut): + command = '' + key_event = self._to_key_event(shortcut) + try: + command = self._scm.getCommandByKeyEvent(key_event) + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return command + + def add(self, shortcut, command): + if isinstance(command, dict): + command = _get_url_script(command) + key_event = self._to_key_event(shortcut) + self._scm.setKeyEvent(key_event, command) + self._scm.store() + return + + def reset(self): + self._scm.reset() + self._scm.store() + return + + def remove(self, shortcut): + key_event = self._to_key_event(shortcut) + try: + self._scm.removeKeyEvent(key_event) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {shortcut}') + return + + def remove_by_command(self, command): + if isinstance(command, dict): + command = _get_url_script(command) + try: + self._scm.removeCommandFromAllKeyEvents(command) + self._scm.store() + except NoSuchElementException: + debug(f'No exists: {command}') + return + + +class LOShortCuts(object): + + def __getitem__(self, index): + return LOShortCut(index) + + +class LOMenu(object): + + def __init__(self, app): + self._app = app + self._ui = None + self._pymenus = None + self._menu = None + self._menus = self._get_menus() + + def __getitem__(self, index): + if isinstance(index, int): + self._menu = self._menus[index] + else: + for menu in self._menus: + cmd = menu.get('CommandURL', '') + if MENUS[index.lower()] == cmd: + self._menu = menu + break + line = self._menu.get('CommandURL', '') + line += self._get_submenus(self._menu['ItemDescriptorContainer']) + return line + + def _get_menus(self): + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[self._app] + manager = create_instance(instance, True) + self._ui = manager.getUIConfigurationManager(service) + self._pymenus = self._ui.getSettings(NODE_MENUBAR, True) + data = [] + for menu in self._pymenus: + data.append(data_to_dict(menu)) + return data + + def _get_info(self, menu): + line = menu.get('CommandURL', '') + line += self._get_submenus(menu['ItemDescriptorContainer']) + return line + + def _get_submenus(self, menu, level=1): + line = '' + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '----------') + line += f'\n{" " * level}├─ ({i}) {cmd}' + submenu = data.get('ItemDescriptorContainer', None) + if not submenu is None: + line += self._get_submenus(submenu, level + 1) + return line + + def __str__(self): + info = '\n'.join([self._get_info(m) for m in self._menus]) + return info + + def _get_index_menu(self, menu, command): + index = -1 + for i, v in enumerate(menu): + data = data_to_dict(v) + cmd = data.get('CommandURL', '') + if cmd == command: + index = i + break + return index + + def insert(self, name, args): + idc = None + replace = False + command = args['CommandURL'] + label = args['Label'] + + self[name] + menu = self._menu['ItemDescriptorContainer'] + submenu = args.get('Submenu', False) + if submenu: + idc = self._ui.createSettings() + + index = self._get_index_menu(menu, command) + if index == -1: + if 'Index' in args: + index = args['Index'] + else: + index = self._get_index_menu(menu, args['After']) + 1 + else: + replace = True + + data = dict ( + CommandURL = command, + Label = label, + Style = 0, + Type = 0, + ItemDescriptorContainer = idc, + ) + self._save(menu, data, index, replace) + self._insert_submenu(idc, submenu) + return + + def _get_command(self, args): + shortcut = args.get('ShortCut', '') + cmd = args['CommandURL'] + if isinstance(cmd, dict): + cmd = _get_url_script(cmd) + if shortcut: + LOShortCut(self._app).add(shortcut, cmd) + return cmd + + def _insert_submenu(self, parent, menus): + for i, v in enumerate(menus): + submenu = v.pop('Submenu', False) + if submenu: + idc = self._ui.createSettings() + v['ItemDescriptorContainer'] = idc + v['Type'] = 0 + if v['Label'] == '-': + v['Type'] = 1 + else: + v['CommandURL'] = self._get_command(v) + self._save(parent, v, i) + if submenu: + self._insert_submenu(idc, submenu) + return + + def remove(self, name, command): + self[name] + menu = self._menu['ItemDescriptorContainer'] + index = self._get_index_menu(menu, command) + if index > -1: + uno.invoke(menu, 'removeByIndex', (index,)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + def _save(self, menu, properties, index, replace=False): + properties = dict_to_property(properties, True) + if replace: + uno.invoke(menu, 'replaceByIndex', (index, properties)) + else: + uno.invoke(menu, 'insertByIndex', (index, properties)) + self._ui.replaceSettings(NODE_MENUBAR, self._pymenus) + self._ui.store() + return + + +class LOMenus(object): + + def __getitem__(self, index): + return LOMenu(index) + class LOWindow(object): - EMPTY = b""" + EMPTY = """ """ + 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', + 'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', + 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', + 'pages': 'com.sun.star.awt.UnoMultiPageModel', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'combobox': 'com.sun.star.awt.UnoControlComboBoxModel', + } - def __init__(self, **kwargs): + def __init__(self, args): self._events = None self._menu = None self._container = None - self._id_extension = '' - self._obj = self._create(kwargs) - - @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 + self._model = None + self._id = '' + self._path = '' + self._obj = self._create(args) def _create(self, properties): ps = ( @@ -3831,12 +5611,11 @@ class LOWindow(object): 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) + model.BackgroundColor = get_color((225, 225, 225)) self._container.setModel(model) self._container.createPeer(self._toolkit, self._window) self._container.setPosSize(*ps, POSSIZE) @@ -3846,86 +5625,17 @@ class LOWindow(object): 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()) + path_tmp = _P.save_tmp(self.EMPTY) + subcont = cwp.createContainerWindow( + _P.to_url(path_tmp), '', self._container.getPeer(), None) + _P.kill(path_tmp) 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) + self._model = subcont.Model return def _create_popupmenu(self, menus): @@ -3961,31 +5671,94 @@ class LOWindow(object): 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)) + # ~ self._container.addKeyListener(EventsKeyWindow(self)) return - @property - def name(self): - return self._title.lower().replace(' ', '_') + 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 _special_properties(self, tipo, args): + if tipo == 'link' and not 'Label' in args: + args['Label'] = args['URL'] + return args + + if tipo == 'button': + if 'ImageURL' in args: + args['ImageURL'] = self._set_image_url(args['ImageURL']) + args['FocusOnClick'] = args.get('FocusOnClick', False) + return args + + if tipo == 'roadmap': + args['Height'] = args.get('Height', self.height) + if 'Title' in args: + args['Text'] = args.pop('Title') + return args + + if tipo == 'tree': + args['SelectionType'] = args.get('SelectionType', SINGLE) + return args + + if tipo == 'grid': + args['ShowRowHeader'] = args.get('ShowRowHeader', True) + return args + + if tipo == 'pages': + args['Width'] = args.get('Width', self.width) + args['Height'] = args.get('Height', self.height) + + return args + + def add_control(self, args): + tipo = args.pop('Type').lower() + root = args.pop('Root', '') + sheets = args.pop('Sheets', ()) + columns = args.pop('Columns', ()) + + 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._subcont.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 + elif tipo == 'grid' and columns: + control.columns = columns + elif tipo == 'pages' and sheets: + control.sheets = sheets + control.events = self.events + + setattr(self, name, control) + return control @property def events(self): return self._events @events.setter - def events(self, value): - self._events = value + def events(self, controllers): + self._events = controllers(self) self._add_listeners() + @property + def model(self): + return self._model + @property def width(self): return self._container.Size.Width @@ -3994,6 +5767,14 @@ class LOWindow(object): def height(self): return self._container.Size.Height + @property + def name(self): + return self._title.lower().replace(' ', '_') + + def add_menu(self, menus): + self._create_menu(menus) + return + def open(self): self._window.setVisible(True) return @@ -4005,235 +5786,478 @@ class LOWindow(object): return -# ~ Python >= 3.7 -# ~ def __getattr__(name): +def create_window(args): + return LOWindow(args) -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) +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 -# ~ Export ok -def get_document(title=''): - doc = None - desktop = get_desktop() - if not title: - doc = _get_class_doc(desktop.getCurrentComponent()) - return doc +class ClipBoard(object): + SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard' + CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' - for d in desktop.getComponents(): - if hasattr(d, 'Title') and d.Title == title: - doc = d - break + class TextTransferable(unohelper.Base, XTransferable): - if doc is None: + 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 - return _get_class_doc(doc) + @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 +_CB = ClipBoard -def get_documents(custom=True): - docs = [] - desktop = get_desktop() - for doc in desktop.getComponents(): - if custom: - docs.append(_get_class_doc(doc)) +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 path(self): + return str(self._path.parent) + + @property + def file_name(self): + return self._path.name + + @property + def name(self): + return self._path.stem + + @property + def ext(self): + return self._path.suffix[1:] + + @property + def info(self): + return self.path, self.file_name, self.name, self.ext + + @property + def url(self): + return self._path.as_uri() + + @property + def size(self): + return self._path.stat().st_size + + @classproperty + def home(self): + return str(Path.home()) + + @classproperty + def documents(self): + return self.config() + + @classproperty + def temp_dir(self): + return tempfile.gettempdir() + + @classproperty + def python(self): + if IS_WIN: + path = self.join(self.config('Module'), PYTHON) + elif IS_MAC: + path = self.join(self.config('Module'), '..', 'Resources', PYTHON) else: - docs.append(doc) - return docs + path = sys.executable + return path + @classmethod + def dir_tmp(self, only_name=False): + dt = tempfile.TemporaryDirectory() + if only_name: + dt = dt.name + return dt -def get_selection(): - return get_document().selection + @classmethod + def tmp(cls, ext=''): + tmp = tempfile.NamedTemporaryFile(suffix=ext) + return tmp.name + @classmethod + def save_tmp(cls, data): + path_tmp = cls.tmp() + cls.save(path_tmp, data) + return path_tmp -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 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)) + @classmethod + def get(cls, init_dir='', filters: str=''): + """ + Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html + filters: 'xml' or 'txt,xml' + """ + 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: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) -def active_cell(): - return get_cell() + path = '' + if file_picker.execute(): + path = cls.to_system(file_picker.getSelectedFiles()[0]) + return path + @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) -def create_dialog(properties): - return LODialog(**properties) + path = '' + if folder_picker.execute(): + path = cls.to_system(folder_picker.getDisplayDirectory()) + return path + @classmethod + def get_file(cls, init_dir: str='', filters: str='', multiple: bool=False): + """ + init_folder: folder default open + multiple: True for multiple selected + filters: 'xml' or 'xml,txt' + """ + if not init_dir: + init_dir = cls.documents + init_dir = cls.to_url(init_dir) -def create_window(kwargs): - return LOWindow(**kwargs) + file_picker = create_instance(cls.FILE_PICKER) + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.setMultiSelectionMode(multiple) + if filters: + filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')] + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) -# ~ 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)) + 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 -def get_path_python(): - path = get_config_path('Module') - return join(path, PYTHON) + @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)) -# ~ 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) - - path = '' - if filters: - file_picker.setCurrentFilter(filters[0][0]) - for f in filters: - file_picker.appendFilter(f[0], f[1]) - - if file_picker.execute(): - path = _path_system(file_picker.getSelectedFiles()[0]) - if multiple: - path = [_path_system(f) for f in file_picker.getSelectedFiles()] - - 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()) + @classmethod + def open(cls, path): + if IS_WIN: + os.startfile(path) else: - data = f.read() - return data + pid = subprocess.Popen(['xdg-open', path]).pid + return + + @classmethod + def is_dir(cls, path): + return Path(path).is_dir() + + @classmethod + def is_file(cls, path): + return Path(path).is_file() + + @classmethod + def join(cls, *paths): + return str(Path(paths[0]).joinpath(*paths[1:])) + + @classmethod + def save(cls, path, data, encoding='utf-8'): + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path, data): + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def 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 + + @classmethod + def to_url(cls, path): + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def to_system(cls, path): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def kill(cls, path): + result = True + p = Path(path) + + 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 files(cls, path, pattern='*'): + files = [str(p) for p in Path(path).glob(pattern) if p.is_file()] + return files + + @classmethod + def dirs(cls, path): + dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()] + return dirs + + @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 walk_dir(cls, path, tree=False): + folders = [] + if tree: + i = 0 + p = 0 + parents = {path: 0} + for root, dirs, _ in os.walk(path): + for name in dirs: + i += 1 + rn = cls.join(root, name) + if not rn in parents: + parents[rn] = i + folders.append((i, parents[root], name)) + else: + for root, dirs, _ in os.walk(path): + folders += [cls.join(root, name) for name in dirs] + return folders + + @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 + + @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) + + @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 + + @classmethod + def to_csv(cls, path, data, args={}): + with open(path, 'w') as f: + writer = csv.writer(f, **args) + writer.writerows(data) + return + + @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' + + 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: + 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 merge_zip(cls, 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 + + @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): @@ -4250,8 +6274,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', @@ -4307,540 +6331,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 = ("run [ERROR]: output = %s, error code = %s\n" - % (e.output, e.returncode)) - error(msg) - return False - - if result is None: - return True - - return result.decode() - - -def _zippwd(source, target, pwd): - if IS_WIN: - return False - if not exists_app('zip'): - return False - - cmd = 'zip' - opt = '-j ' - args = "{} --password {} ".format(cmd, pwd) - - if isinstance(source, (tuple, list)): - if not target: - return False - args += opt + target + ' ' + ' '.join(source) - else: - if is_file(source) and not target: - target = replace_ext(source, 'zip') - elif is_dir(source) and not target: - target = join(PurePath(source).parent, - '{}.zip'.format(PurePath(source).name)) - opt = '-r ' - args += opt + target + ' ' + source - - result = run(args, True) - if not result: - return False - - return is_created(target) - - -# ~ Export ok -def zip(source, target='', mode='w', pwd=''): - if pwd: - return _zippwd(source, target, pwd) - - if isinstance(source, (tuple, list)): - if not target: - return False - - with zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) as z: - for path in source: - _, name, _, _ = get_info_path(path) - z.write(path, name) - - return is_created(target) - - if is_file(source): - if not target: - target = replace_ext(source, 'zip') - z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) - _, name, _, _ = get_info_path(source) - z.write(source, name) - z.close() - return is_created(target) - - if not target: - target = join( - PurePath(source).parent, - '{}.zip'.format(PurePath(source).name)) - z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) - root_len = len(os.path.abspath(source)) - for root, dirs, files in os.walk(source): - relative = os.path.abspath(root)[root_len:] - for f in files: - fullpath = join(root, f) - file_name = join(relative, f) - z.write(fullpath, file_name) - z.close() - - return is_created(target) - - -# ~ 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, @@ -4991,10 +6531,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 @@ -5006,359 +6545,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 @@ -5419,23 +6614,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', -# ~ }