From f410bf3d19a0f426c3c512582bb8e96c166a6c55 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 20 Dec 2020 22:00:38 -0600 Subject: [PATCH] Separate main --- conf.py | 2 +- files/ZAZBarCode_v0.7.0.oxt | Bin 297705 -> 301379 bytes source/Addons.xcu | 2 +- source/Office/Accelerators.xcu | 8 +- source/ZAZBarCode.py | 19 +- source/pythonpath/easymacro.py | 8580 ++++++++++++++++++-------------- source/pythonpath/main.py | 31 + 7 files changed, 4894 insertions(+), 3748 deletions(-) create mode 100644 source/pythonpath/main.py diff --git a/conf.py b/conf.py index ff2f0fd..95d1c69 100644 --- a/conf.py +++ b/conf.py @@ -111,7 +111,7 @@ MENU_MAIN = { MENUS = ( { 'title': {'en': 'Insert BarCode', 'es': 'Insertar Código'}, - 'argument': 'ask', + 'argument': 'used_dialog', 'context': 'calc,writer,impress,draw', 'icon': 'barcode', 'toolbar': False, diff --git a/files/ZAZBarCode_v0.7.0.oxt b/files/ZAZBarCode_v0.7.0.oxt index 36d10f2b97ccdb4e4cdde2fab2a7dc752e840f5f..450b679e575412df828c88076b5a4c11ca72abe9 100644 GIT binary patch delta 43888 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
h4UM391d)6AcI`@j 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 a=com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument,com.sun.star.presentation.PresentationDocument,com.sun.star.drawing.DrawingDocument - service:net.elmau.zaz.BarCode?ask + service:net.elmau.zaz.BarCode?used_dialog _self diff --git a/source/Office/Accelerators.xcu b/source/Office/Accelerators.xcu index 3db9b03..c803283 100644 --- a/source/Office/Accelerators.xcu +++ b/source/Office/Accelerators.xcu @@ -5,28 +5,28 @@ - service:net.elmau.zaz.BarCode?ask + service:net.elmau.zaz.BarCode?used_dialog - service:net.elmau.zaz.BarCode?ask + service:net.elmau.zaz.BarCode?used_dialog - service:net.elmau.zaz.BarCode?ask + service:net.elmau.zaz.BarCode?used_dialog - service:net.elmau.zaz.BarCode?ask + service:net.elmau.zaz.BarCode?used_dialog diff --git a/source/ZAZBarCode.py b/source/ZAZBarCode.py index bb514eb..80f3e65 100644 --- a/source/ZAZBarCode.py +++ b/source/ZAZBarCode.py @@ -2,6 +2,9 @@ import gettext import uno import unohelper from com.sun.star.task import XJobExecutor, XJob +import main + + import easymacro as app import qrcode @@ -15,8 +18,6 @@ TITLE = 'ZAZ BarCode' QR = 'qrcode' -_ = app.install_locales(__file__) - class Controllers(object): @@ -72,13 +73,15 @@ class ZAZBarCode(unohelper.Base, XJob, XJobExecutor): return self._path def trigger(self, args): - self._type = args - if args == 'ask' and not self._get_values(): - return + main.ID_EXTENSION = ID_EXTENSION + main.run(args, __file__) + # ~ self._type = args + # ~ if args == 'ask' and not self._get_values(): + # ~ return - doc = app.get_document() - getattr(self, '_insert_in_{}'.format(doc.type))(doc) - app.kill(self._path) + # ~ doc = app.get_document() + # ~ getattr(self, '_insert_in_{}'.format(doc.type))(doc) + # ~ app.kill(self._path) return def _create_code(self, path=''): diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py index 5a75695..d91c6af 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 self._is_paragraph: + 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,499 +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 transpose(self, formula=False): - data = self.data - if formula: - data = self.formula - data = tuple(zip(*data)) - self.clear(1023) - self[0,0].copy_from(data, formula=formula) - return + getattr(control, key)(listeners[key](events, name)) - def transpose2(self): - # ~ 'Flags': 'A', - # ~ 'FormulaCommand': 0, - # ~ 'SkipEmptyCells': False, - # ~ 'AsLink': False, - # ~ 'MoveMode': 4, - args = { - 'Transpose': True, - } - args = dict_to_property(args) - self.select() - copy() - self.clear(1023) - self[0,0].select() - call_dispatch('.uno:InsertContents', args) - set_clipboard('') - return + if is_grid: + controllers = EventsGrid(events, name) + control.addSelectionListener(controllers) + control.Model.GridDataModel.addGridDataListener(controllers) + return - def offset(self, row=1, col=0): - ra = self.obj.getRangeAddress() - col = ra.EndColumn + col - row = ra.EndRow + row - return LOCellRange(self.sheet[row, col].obj, self.doc) - @property - def next_cell(self): - a = self.current_region.address - if hasattr(a, 'StartColumn'): - col = a.StartColumn - else: - col = a.Column - if hasattr(a, 'EndRow'): - row = a.EndRow + 1 - else: - row = a.Row + 1 - - return LOCellRange(self.sheet[row, col].obj, self.doc) - - @property - def sheet(self): - return LOCalcSheet(self.obj.Spreadsheet, self.doc) - - @property - def charts(self): - return self.obj.Spreadsheet.Charts - - @property - def ps(self): - ps = Rectangle() - s = self.obj.Size - p = self.obj.Position - ps.X = p.X - ps.Y = p.Y - ps.Width = s.Width - ps.Height = s.Height - return ps - - @property - def draw_page(self): - return self.sheet.obj.getDrawPage() - - @property - def name(self): - return self.obj.AbsoluteName - - @property - def address(self): - if self._type_obj == OBJ_CELL: - a = self.obj.getCellAddress() - elif self._type_obj == OBJ_RANGE: - a = self.obj.getRangeAddress() - else: - a = self.obj.getRangeAddressesAsString() - return a - - @property - def range_address(self): - return self.obj.getRangeAddress() - - @property - def current_region(self): - cursor = self.sheet.get_cursor(self.obj[0,0]) - cursor.collapseToCurrentRegion() - return LOCellRange(self.sheet[cursor.AbsoluteName].obj, self.doc) - - @property - def visible(self): - cursor = self.sheet.get_cursor(self.obj) - rangos = [LOCellRange(self.sheet[r.AbsoluteName].obj, self.doc) - for r in cursor.queryVisibleCells()] - return tuple(rangos) - - @property - def empty(self): - cursor = self.sheet.get_cursor(self.obj) - rangos = [LOCellRange(self.sheet[r.AbsoluteName].obj, self.doc) - for r in cursor.queryEmptyCells()] - return tuple(rangos) - - @property - def back_color(self): - return self._obj.CellBackColor - @back_color.setter - def back_color(self, value): - self._obj.CellBackColor = get_color(value) - - @property - def cell_style(self): - return self.obj.CellStyle - @cell_style.setter - def cell_style(self, value): - self.obj.CellStyle = value - - @property - def auto_format(self): - return self.obj.CellStyle - @auto_format.setter - def auto_format(self, value): - self.obj.autoFormat(value) - - def auto_width(self): - self.obj.Columns.OptimalWidth = True - return - - def insert_image(self, path, **kwargs): - s = self.obj.Size - w = kwargs.get('width', s.Width) - h = kwargs.get('Height', s.Height) - img = self.doc.create_instance('com.sun.star.drawing.GraphicObjectShape') - img.GraphicURL = _path_url(path) - self.draw_page.add(img) - img.Anchor = self.obj - img.setSize(Size(w, h)) - return - - def insert_shape(self, tipo, **kwargs): - s = self.obj.Size - w = kwargs.get('width', s.Width) - h = kwargs.get('Height', s.Height) - img = self.doc.create_instance('com.sun.star.drawing.{}Shape'.format(tipo)) - set_properties(img, kwargs) - self.draw_page.add(img) - img.Anchor = self.obj - img.setSize(Size(w, h)) - return - - def select(self): - self.doc._cc.select(self.obj) - return - - def in_range(self, rango): - if isinstance(rango, LOCellRange): - address = rango.address - else: - address = rango.getRangeAddress() - cursor = self.sheet.get_cursor(self.obj) - result = cursor.queryIntersection(address) - return bool(result.Count) - - def fill(self, source=1): - self.obj.fillAuto(0, source) - return - - def clear(self, what=31): - # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html - self.obj.clearContents(what) - return - - @property - def rows_visible(self): - return self._obj.getRows().IsVisible - @rows_visible.setter - def rows_visible(self, value): - self._obj.getRows().IsVisible = value - - @property - def columns_visible(self): - return self._obj.getColumns().IsVisible - @columns_visible.setter - def columns_visible(self, value): - self._obj.getColumns().IsVisible = value - - def get_column(self, index=0, first=False): - ca = self.address - ra = self.current_region.address - if hasattr(ca, 'Column'): - col = ca.Column - else: - col = ca.StartColumn + index - start = 1 - if first: - start = 0 - if hasattr(ra, 'Row'): - row_start = ra.Row + start - row_end = ra.Row + 1 - else: - row_start = ra.StartRow + start - row_end = ra.EndRow + 1 - return LOCellRange(self.sheet[row_start:row_end, col:col+1].obj, self.doc) - - def import_csv(self, path, **kwargs): - data = import_csv(path, **kwargs) - self.copy_from(data) - return - - def export_csv(self, path, **kwargs): - data = self.current_region.data - export_csv(path, data, **kwargs) - return - - def create_chart(self, tipo): - chart = LOChart(None, tipo) - chart.cell = self - return chart - - def search(self, options): - descriptor = self.obj.Spreadsheet.createSearchDescriptor() - descriptor.setSearchString(options.get('Search', '')) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - - if options.get('First', False): - found = self.obj.findFirst(descriptor) - else: - found = self.obj.findAll(descriptor) - - return found - - def replace(self, options): - descriptor = self.obj.Spreadsheet.createReplaceDescriptor() - descriptor.setSearchString(options['Search']) - descriptor.setReplaceString(options['Replace']) - descriptor.SearchCaseSensitive = options.get('CaseSensitive', False) - descriptor.SearchWords = options.get('Words', False) - if hasattr(descriptor, 'SearchRegularExpression'): - descriptor.SearchRegularExpression = options.get('RegularExpression', False) - if hasattr(descriptor, 'SearchType') and 'Type' in options: - descriptor.SearchType = options['Type'] - found = self.obj.replaceAll(descriptor) - return found - - @property - def validation(self): - return self.obj.Validation - @validation.setter - def validation(self, values): - is_list = False - current = self.validation - for k, v in values.items(): - if k == 'Type' and v == 6: - is_list = True - if k == 'Formula1' and is_list: - if isinstance(v, (tuple, list)): - v = ';'.join(['"{}"'.format(i) for i in v]) - setattr(current, k, v) - self.obj.Validation = current +def _set_properties(model, properties): + if 'X' in properties: + properties['PositionX'] = properties.pop('X') + if 'Y' in properties: + properties['PositionY'] = properties.pop('Y') + keys = tuple(properties.keys()) + values = tuple(properties.values()) + model.setPropertyValues(keys, values) + return class EventsListenerBase(unohelper.Base, XEventListener): @@ -2130,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): @@ -2174,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 @@ -2213,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): @@ -2298,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 @@ -2418,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): @@ -2457,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): @@ -2464,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() @@ -2502,90 +4280,35 @@ class UnoBaseObject(object): self._set_possize('Y', value) @property - def width(self): - return self._model.Width - @width.setter - def width(self, value): - self.model.Width = value + def tab_index(self): + return self._model.TabIndex + @tab_index.setter + def tab_index(self, value): + self.model.TabIndex = value @property - def ps_width(self): - return self._get_possize('Width') - @ps_width.setter - def ps_width(self, value): - self._set_possize('Width', value) + def tab_stop(self): + return self._model.Tabstop + @tab_stop.setter + def tab_stop(self, value): + self.model.Tabstop = value @property - def height(self): - return self.model.Height - @height.setter - def height(self, value): - self.model.Height = value - - @property - def ps_height(self): - return self._get_possize('Height') - @ps_height.setter - def ps_height(self, value): - self._set_possize('Height', value) - - @property - def size(self): + def ps(self): ps = self.obj.getPosSize() - return (ps.Width, ps.Height) - @size.setter - def size(self, value): - ps = self.obj.getPosSize() - ps.Width = value[0] - ps.Height = value[1] - self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, SIZE) - - @property - def tag(self): - return self.model.Tag - @tag.setter - def tag(self, value): - self.model.Tag = value - - @property - def visible(self): - return self.obj.Visible - @visible.setter - def visible(self, value): - self.obj.setVisible(value) - - @property - def enabled(self): - return self.model.Enabled - @enabled.setter - def enabled(self, value): - self.model.Enabled = value - - @property - def step(self): - return self.model.Step - @step.setter - def step(self, value): - self.model.Step = value - - @property - def back_color(self): - return self.model.BackgroundColor - @back_color.setter - def back_color(self, value): - self.model.BackgroundColor = value - - @property - def rules(self): - return self._rules - @rules.setter - def rules(self, value): - self._rules = value + return ps + @ps.setter + def ps(self, ps): + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) def set_focus(self): self.obj.setFocus() return + def ps_from(self, source): + self.ps = source.ps + return + def center(self, horizontal=True, vertical=False): p = self.parent.Model w = p.Width @@ -2598,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: @@ -2607,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 @@ -2661,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): @@ -2678,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): @@ -2705,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) @@ -2723,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: @@ -2745,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 @@ -2903,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) @@ -2924,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 @@ -2950,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 @@ -3014,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] @@ -3659,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 @@ -3672,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): @@ -3702,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): @@ -3722,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: @@ -3842,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 = ( @@ -3894,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) @@ -3909,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): @@ -4024,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 @@ -4057,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 @@ -4068,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): @@ -4313,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', @@ -4370,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_files(source, target='', mode='w', pwd=''): - if pwd: - return _zippwd(source, target, pwd) - - if isinstance(source, (tuple, list)): - if not target: - return False - - with zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) as z: - for path in source: - _, name, _, _ = get_info_path(path) - z.write(path, name) - - return is_created(target) - - if is_file(source): - if not target: - target = replace_ext(source, 'zip') - z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) - _, name, _, _ = get_info_path(source) - z.write(source, name) - z.close() - return is_created(target) - - if not target: - target = join( - PurePath(source).parent, - '{}.zip'.format(PurePath(source).name)) - z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) - root_len = len(os.path.abspath(source)) - for root, dirs, files in os.walk(source): - relative = os.path.abspath(root)[root_len:] - for f in files: - fullpath = join(root, f) - file_name = join(relative, f) - z.write(fullpath, file_name) - z.close() - - return is_created(target) - - -# ~ Export ok -def unzip(source, path='', members=None, pwd=None): - if not path: - path, _, _, _ = get_info_path(source) - with zipfile.ZipFile(source) as z: - if not pwd is None: - pwd = pwd.encode() - if isinstance(members, str): - members = (members,) - z.extractall(path, members=members, pwd=pwd) - return True - - -# ~ Export ok -def merge_zip(target, zips): - try: - with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: - for path in zips: - with zipfile.ZipFile(path, compression=zipfile.ZIP_DEFLATED) as s: - for name in s.namelist(): - t.writestr(name, s.open(name).read()) - except Exception as e: - error(e) - return False - - return True - - -# ~ Export ok -def kill(path): - p = Path(path) - try: - if p.is_file(): - p.unlink() - elif p.is_dir(): - shutil.rmtree(path) - except OSError as e: - log.error(e) - return - - -def get_size_screen(): - if IS_WIN: - user32 = ctypes.windll.user32 - res = '{}x{}'.format(user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)) - else: - args = 'xrandr | grep "*" | cut -d " " -f4' - res = run(args, True) - return res.strip() - - -def get_clipboard(): - df = None - text = '' - sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') - transferable = sc.getContents() - data = transferable.getTransferDataFlavors() - for df in data: - if df.MimeType == CLIPBOARD_FORMAT_TEXT: - break - if df: - text = transferable.getTransferData(df) - return text - - -class TextTransferable(unohelper.Base, XTransferable): - """Keep clipboard data and provide them.""" - - def __init__(self, text): - df = DataFlavor() - df.MimeType = CLIPBOARD_FORMAT_TEXT - df.HumanPresentableName = "encoded text utf-16" - self.flavors = [df] - self.data = [text] - - def getTransferData(self, flavor): - if not flavor: - return - for i, f in enumerate(self.flavors): - if flavor.MimeType == f.MimeType: - return self.data[i] - return - - def getTransferDataFlavors(self): - return tuple(self.flavors) - - def isDataFlavorSupported(self, flavor): - if not flavor: - return False - mtype = flavor.MimeType - for f in self.flavors: - if mtype == f.MimeType: - return True - return False - - -# ~ Export ok -def set_clipboard(value): - ts = TextTransferable(value) - sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') - sc.setContents(ts, None) - return - - -# ~ Export ok -def copy(): - call_dispatch('.uno:Copy') - return - - -# ~ Export ok -def get_epoch(): - n = now() - return int(time.mktime(n.timetuple())) - - -# ~ Export ok -def file_copy(source, target='', name=''): - p, f, n, e = get_info_path(source) - if target: - p = target - if name: - e = '' - n = name - path_new = join(p, '{}{}'.format(n, e)) - shutil.copy(source, path_new) - return path_new - - -def get_path_content(path, filters=''): - paths = [] - if filters in ('*', '*.*'): - filters = '' - for folder, _, files in os.walk(path): - if filters: - pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) - paths += [join(folder, f) for f in files if pattern.search(f)] - else: - paths += files - return paths - - -def _get_menu(type_doc, name_menu): - instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' - service = TYPE_DOC[type_doc] - manager = create_instance(instance, True) - ui = manager.getUIConfigurationManager(service) - menus = ui.getSettings(NODE_MENUBAR, True) - command = MENUS_APP[type_doc][name_menu] - for menu in menus: - data = property_to_dict(menu) - if data.get('CommandURL', '') == command: - idc = data.get('ItemDescriptorContainer', None) - return ui, menus, idc - return None, None, None - - -def _get_index_menu(menu, command): - for i, m in enumerate(menu): - data = property_to_dict(m) - cmd = data.get('CommandURL', '') - if cmd == command: - return i - # ~ submenu = data.get('ItemDescriptorContainer', None) - # ~ if not submenu is None: - # ~ get_index_menu(submenu, command, count + 1) - return 0 - - -def _store_menu(ui, menus, menu, index, data=(), remove=False): - if remove: - uno.invoke(menu, 'removeByIndex', (index,)) - else: - properties = dict_to_property(data, True) - uno.invoke(menu, 'insertByIndex', (index + 1, properties)) - ui.replaceSettings(NODE_MENUBAR, menus) - ui.store() - return - - -def insert_menu(type_doc, name_menu, **kwargs): - ui, menus, menu = _get_menu(type_doc, name_menu.lower()) - if menu is None: - return 0 - - label = kwargs.get('Label', '-') - separator = False - if label == '-': - separator = True - command = kwargs.get('CommandURL', '') - index = kwargs.get('Index', 0) - if not index: - index = _get_index_menu(menu, kwargs['After']) - if separator: - data = {'Type': 1} - _store_menu(ui, menus, menu, index, data) - return index + 1 - - index_menu = _get_index_menu(menu, command) - if index_menu: - msg = 'Exists: %s' % command - debug(msg) - return 0 - - sub_menu = kwargs.get('Submenu', ()) - idc = None - if sub_menu: - idc = ui.createSettings() - - data = { - 'CommandURL': command, - 'Label': label, - 'Style': 0, - 'Type': 0, - 'ItemDescriptorContainer': idc - } - _store_menu(ui, menus, menu, index, data) - if sub_menu: - _add_sub_menus(ui, menus, idc, sub_menu) - return True - - -def _add_sub_menus(ui, menus, menu, sub_menu): - for i, sm in enumerate(sub_menu): - submenu = sm.pop('Submenu', ()) - sm['Type'] = 0 - if submenu: - idc = ui.createSettings() - sm['ItemDescriptorContainer'] = idc - if sm['Label'] == '-': - sm = {'Type': 1} - _store_menu(ui, menus, menu, i - 1, sm) - if submenu: - _add_sub_menus(ui, menus, idc, submenu) - return - - -def remove_menu(type_doc, name_menu, command): - ui, menus, menu = _get_menu(type_doc, name_menu.lower()) - if menu is None: - return False - - index = _get_index_menu(menu, command) - if not index: - debug('Not exists: %s' % command) - return False - - _store_menu(ui, menus, menu, index, remove=True) - return True - - -def _get_app_submenus(menus, count=0): - for i, menu in enumerate(menus): - data = property_to_dict(menu) - cmd = data.get('CommandURL', '') - msg = ' ' * count + '├─' + cmd - debug(msg) - submenu = data.get('ItemDescriptorContainer', None) - if not submenu is None: - _get_app_submenus(submenu, count + 1) - return - - -def get_app_menus(name_app, index=-1): - instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' - service = TYPE_DOC[name_app] - manager = create_instance(instance, True) - ui = manager.getUIConfigurationManager(service) - menus = ui.getSettings(NODE_MENUBAR, True) - if index == -1: - for menu in menus: - data = property_to_dict(menu) - debug(data.get('CommandURL', '')) - else: - menus = property_to_dict(menus[index])['ItemDescriptorContainer'] - _get_app_submenus(menus) - return menus - - -# ~ Export ok -def start(): - global _start - _start = now() - log.info(_start) - return - - -# ~ Export ok -def end(): - global _start - e = now() - return str(e - _start).split('.')[0] - - -# ~ Export ok # ~ https://en.wikipedia.org/wiki/Web_colors -def get_color(*value): - if len(value) == 1 and isinstance(value[0], int): - return value[0] - if len(value) == 1 and isinstance(value[0], tuple): - value = value[0] - +def get_color(value): COLORS = { 'aliceblue': 15792383, 'antiquewhite': 16444375, @@ -5054,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 @@ -5069,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 @@ -5482,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/source/pythonpath/main.py b/source/pythonpath/main.py new file mode 100644 index 0000000..f2fa222 --- /dev/null +++ b/source/pythonpath/main.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import easymacro as app + + +ID_EXTENSION = '' +_ = None + + +def _use_dialog(): + print('dialog') + return + + +def _insert_code(type_code): + print(type_code) + return + + +@app.catch_exception +def run(args, path_locales): + global _ + + _ = app.install_locales(path_locales) + + if args == 'used_dialog': + _use_dialog() + else: + _insert_code(args) + + return