#!/usr/bin/env python3 # == Rapid Develop Macros in LibreOffice == # ~ This file is part of ZAZ. # ~ https://gitlab.com/mauriciobaeza/zaz # ~ ZAZ is free software: you can redistribute it and/or modify # ~ it under the terms of the GNU General Public License as published by # ~ the Free Software Foundation, either version 3 of the License, or # ~ (at your option) any later version. # ~ ZAZ is distributed in the hope that it will be useful, # ~ but WITHOUT ANY WARRANTY; without even the implied warranty of # ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # ~ GNU General Public License for more details. # ~ You should have received a copy of the GNU General Public License # ~ along with ZAZ. If not, see . import datetime import getpass import gettext import hashlib import logging import os import platform import re import shlex import shutil import socket import subprocess import sys import threading import time 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 from typing import Any import uno import unohelper from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.awt import Rectangle, Size, Point from com.sun.star.awt import Key, KeyModifier, KeyEvent from com.sun.star.container import NoSuchElementException from com.sun.star.beans import PropertyValue, NamedValue from com.sun.star.sheet import TableFilterField from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA from com.sun.star.util import Time, Date, DateTime from com.sun.star.text.TextContentAnchorType import AS_CHARACTER from com.sun.star.awt import XActionListener from com.sun.star.lang import XEventListener from com.sun.star.awt import XMouseListener from com.sun.star.awt import XMouseMotionListener from com.sun.star.awt import XFocusListener # ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1FontUnderline.html from com.sun.star.awt import FontUnderline from com.sun.star.style.VerticalAlignment import TOP, MIDDLE, BOTTOM try: from peewee import Database, DateTimeField, DateField, TimeField, \ __exception_wrapper__ except ImportError as e: Database = DateField = TimeField = DateTimeField = object print('Install peewee') 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') logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) log = logging.getLogger(__name__) 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) # ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL class FilterOperator(IntEnum): EMPTY = 0 NO_EMPTY = 1 EQUAL = 2 NOT_EQUAL = 3 # ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4 class Border(IntEnum): NO_BORDER = 0 BORDER = 1 SIMPLE = 2 OS = platform.system() IS_WIN = OS == 'Windows' IS_MAC = OS == 'Darwin' USER = getpass.getuser() PC = platform.node() DESKTOP = os.environ.get('DESKTOP_SESSION', '') INFO_DEBUG = f"{sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) _MACROS = {} SECONDS_DAY = 60 * 60 * 24 DIR = { 'images': 'images', 'locales': 'locales', } DEFAULT_MIME_TYPE = 'png' MODIFIERS = { 'shift': KeyModifier.SHIFT, 'ctrl': KeyModifier.MOD1, 'alt': KeyModifier.MOD2, 'ctrlmac': KeyModifier.MOD3, } # ~ Menus NODE_MENUBAR = 'private:resource/menubar/menubar' MENUS = { 'file': '.uno:PickList', 'tools': '.uno:ToolsMenu', 'help': '.uno:HelpMenu', 'windows': '.uno:WindowList', 'edit': '.uno:EditMenu', 'view': '.uno:ViewMenu', 'insert': '.uno:InsertMenu', 'format': '.uno:FormatMenu', 'styles': '.uno:FormatStylesMenu', 'sheet': '.uno:SheetMenu', 'data': '.uno:DataMenu', 'table': '.uno:TableMenu', 'form': '.uno:FormatFormMenu', 'page': '.uno:PageMenu', 'shape': '.uno:ShapeMenu', 'slide': '.uno:SlideMenu', 'show': '.uno:SlideShowMenu', } MIME_TYPE = { 'png': 'image/png', 'jpg': 'image/jpeg', } MESSAGES = { 'es': { 'OK': 'Aceptar', 'Cancel': 'Cancelar', 'Select 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: 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 def get_app_config(node_name, key=''): name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationAccess' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) try: ca = cp.createInstanceWithArguments(service, (node,)) if ca and not key: return ca if ca and ca.hasByName(key): return ca.getPropertyValue(key) except Exception as e: error(e) return '' 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') 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 error(info): log.error(info) return 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 catch_exception(f): @wraps(f) def func(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: name = f.__name__ if IS_WIN: debug(traceback.format_exc()) log.error(name, exc_info=True) return func def inspect(obj: Any) -> None: zaz = create_instance('net.elmau.zaz.inspect') if hasattr(obj, 'obj'): obj = obj.obj zaz.inspect(obj) return def mri(obj): m = create_instance('mytools.Mri') if m is None: msg = 'Extension MRI not found' error(msg) return m.inspect(obj) return def now(only_time=False): now = datetime.datetime.now() if only_time: now = now.time() return now def today(): return datetime.date.today() def _(msg): if LANG == 'en': return msg if not LANG in MESSAGES: return msg return MESSAGES[LANG][msg] def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): """ Create message box type_msg: infobox, warningbox, errorbox, querybox, messbox http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMessageBoxFactory.html """ toolkit = create_instance('com.sun.star.awt.Toolkit') parent = toolkit.getDesktopWindow() box = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message)) return box.execute() def question(message, title=TITLE): result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') return result == YES def warning(message, title=TITLE): return msgbox(message, title, type_msg='warningbox') def errorbox(message, title=TITLE): return msgbox(message, title, type_msg='errorbox') def get_type_doc(obj: Any) -> str: for k, v in TYPE_DOC.items(): if obj.supportsService(v): return k return '' 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 _get_class_uno(obj: Any) -> Any: classes = dict( SwXTextGraphicObject = LOImage, SvxShapeText = LOImage, ) name = obj.ImplementationName print(f'ImplementationName = {name}') instance = obj if name in classes: instance = classes[name](obj) return instance def dict_to_property(values: dict, uno_any: bool=False): ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) if uno_any: ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps) return ps def _array_to_dict(values): d = {v[0]: v[1] for v in values} return d def _property_to_dict(values): d = {v.Name: v.Value for v in values} return d 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 _path_url(path: str) -> str: if path.startswith('file://'): return path return uno.systemPathToFileUrl(path) def _path_system(path: str) -> str: if path.startswith('file://'): return str(Path(uno.fileUrlToSystemPath(path)).resolve()) return path def _get_dispatch() -> Any: return create_instance('com.sun.star.frame.DispatchHelper') def call_dispatch(frame: Any, url: str, args: dict={}) -> None: dispatch = _get_dispatch() opt = dict_to_property(args) dispatch.executeDispatch(frame, url, '', 0, opt) return def 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 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): cmd = shlex.split(command) result = subprocess.run(cmd, capture_output=capture, text=True) if capture: result = result.stdout else: result = result.returncode return result # ~ def popen(command, stdin=None): # ~ try: # ~ proc = subprocess.Popen(shlex.split(command), shell=IS_WIN, # ~ stdout=subprocess.PIPE, stderr=subprocess.STDOUT) # ~ for line in proc.stdout: # ~ yield line.decode().rstrip() # ~ except Exception as e: # ~ error(e) # ~ yield (e.errno, e.strerror) def sleep(seconds): time.sleep(seconds) return class TimerThread(threading.Thread): def __init__(self, event, seconds, macro): threading.Thread.__init__(self) self.stopped = event self.seconds = seconds self.macro = macro def run(self): info('Timer started... {}'.format(self.macro['name'])) while not self.stopped.wait(self.seconds): _call_macro(self.macro) info('Timer stopped... {}'.format(self.macro['name'])) return def start_timer(name, seconds, macro): global _MACROS _MACROS[name] = threading.Event() thread = TimerThread(_MACROS[name], seconds, macro) thread.start() return def stop_timer(name): global _MACROS _MACROS[name].set() del _MACROS[name] return def install_locales(path, domain='base', dir_locales=DIR['locales']): path_locales = _P.join(_P(path).path, dir_locales) try: lang = gettext.translation(domain, path_locales, languages=[LANG]) lang.install() _ = lang.gettext except Exception as e: from gettext import gettext as _ error(e) return _ def _export_image(obj, args): name = 'com.sun.star.drawing.GraphicExportFilter' exporter = create_instance(name) path = _P.to_system(args['URL']) args = dict_to_property(args) exporter.setSourceDocument(obj) exporter.filter(args) return _P.exists(path) def sha256(data): result = hashlib.sha256(data.encode()).hexdigest() return result def sha512(data): result = hashlib.sha512(data.encode()).hexdigest() return result # ~ Classes class LOBaseObject(object): def __init__(self, obj): self._obj = obj def __setattr__(self, name, value): exists = hasattr(self, name) if not exists and not name in ('_obj', '_index'): setattr(self._obj, name, value) else: super().__setattr__(name, value) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass @property def obj(self): return self._obj class LOImage(object): TYPE = { 'png': 'image/png', 'jpg': 'image/jpeg', } def __init__(self, obj): self._obj = obj @property def obj(self): return self._obj @property def name(self): return self.obj.Name or 'img' @property def mimetype(self): return self.obj.Bitmap.MimeType def save(self, path, mimetype=DEFAULT_MIME_TYPE): p = _P(path) if _P.is_dir(path): name = self.name else: path = p.path name = p.name path = _P.join(path, f'{name}.{mimetype.lower()}') args = dict( URL = _P.to_url(path), MimeType = self.TYPE[mimetype], ) if not _export_image(self.obj, args): path = '' # ~ size = len(self.obj.Bitmap.DIB) # ~ data = self.obj.GraphicStream.readBytes((), size) # ~ data = data[-1].value # ~ data = self.obj.Bitmap.DIB.value # ~ data = self.obj.Graphic.DIB.value # ~ _P.save_bin(path, data) return path class LODocument(object): def __init__(self, obj): self._obj = obj self._cc = self.obj.getCurrentController() self._undo = True def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() @property def obj(self): return self._obj @property def title(self): return self.obj.getTitle() @title.setter def title(self, value): self.obj.setTitle(value) @property def type(self): return self._type @property def uid(self): return self.obj.RuntimeUID @property def frame(self): return self._cc.getFrame() @property def is_saved(self): return self.obj.hasLocation() @property def is_modified(self): return self.obj.isModified() @property def is_read_only(self): return self.obj.isReadOnly() @property def path(self): return _path_system(self.obj.URL) @property def status_bar(self): return self._cc.getStatusIndicator() @property def visible(self): w = self.frame.ContainerWindow return w.isVisible() @visible.setter def visible(self, value): w = self.frame.ContainerWindow w.setVisible(value) @property def zoom(self): return self._cc.ZoomValue @zoom.setter 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() @property def selection(self): sel = self.obj.CurrentSelection return _get_class_uno(sel) def create_instance(self, name): obj = self.obj.createInstance(name) return obj def set_focus(self): w = self.frame.ComponentWindow w.setFocus() return def copy(self): call_dispatch(self.frame, '.uno:Copy') return def paste(self): sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') transferable = sc.getContents() self._cc.insertTransferable(transferable) return def select(self, obj): self._cc.select(obj) return def to_pdf(self, path: str='', args: dict={}): path_pdf = path filter_name = '{}_pdf_Export'.format(self.type) 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_pdf = '' return _P.exists(path_pdf) def save(self, path: str='', args: dict={}) -> bool: result = True opt = dict_to_property(args) if path: try: self.obj.storeAsURL(_path_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 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 __len__(self): return self._sheets.Count @property def selection(self): sel = self.obj.CurrentSelection if sel.ImplementationName in TYPE_RANGES: sel = LOCalcRange(sel) return sel @property def active(self): return LOCalcSheet(self._cc.ActiveSheet) @property def db_ranges(self): # ~ return LOCalcDataBaseRanges(self.obj.DataBaseRanges) return self.obj.DatabaseRanges 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): def __init__(self, obj): self._obj = obj self._control = self.doc.CurrentController.getControl(self.obj) def __setattr__(self, name, value): if name == '_control': self.__dict__[name] = value else: super().__setattr__(name, value) @property def doc(self): return self.obj.Parent.Parent.Parent @property def name(self): return self.obj.Name @property def label(self): return self.obj.Label def set_focus(self): self._control.setFocus() return class LOForm(object): def __init__(self, obj): self._obj = obj def __getitem__(self, index): return LOFormControl(self.obj[index]) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __contains__(self, item): return item in self.obj def __len__(self): return len(self.obj) @property def obj(self): return self._obj class LOSheetForms(object): def __init__(self, obj): self._obj = obj def __getitem__(self, index): return LOForm(self.obj[index]) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __contains__(self, item): return item in self.obj def __len__(self): return len(self.obj) @property def obj(self): return self._obj class LOSheetRows(object): def __init__(self, sheet): self._sheet = sheet self._obj = sheet.obj.Rows def __getitem__(self, index): return LOSheetRows(self.obj[index]) @property def obj(self): return self._obj def insert(self, index, count): self.obj.insertByIndex(index, count) end = index + count return self._sheet[index:end,0:] class LOCalcSheet(object): def __init__(self, obj): self._obj = obj def __getitem__(self, index): return LOCalcRange(self.obj[index]) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass @property def obj(self): return self._obj @property def name(self): return self._obj.Name @name.setter def name(self, value): self._obj.Name = value @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 self.obj.DrawPage.Forms.Parent @property def charts(self): return LOSheetCharts(self.obj.Charts, self) @property def rows(self): return LOSheetRows(self) @property def forms(self): return LOSheetForms(self.obj.DrawPage.Forms) def get_cursor(self, cell=None): if cell is None: cursor = self.obj.createCursor() else: cursor = self.obj.createCursorByRange(cell) return cursor def render(self, data, rango=None, clean=True): if rango is None: rango = self.used_area return rango.render(data, clean) class LOCalcRange(object): def __init__(self, obj): self._obj = obj self._sd = None def __getitem__(self, index): return LOCalcRange(self.obj[index]) def __iter__(self): self._r = 0 self._c = 0 return self def __next__(self): try: rango = self[self._r, self._c] except Exception as e: raise StopIteration self._c += 1 if self._c == self.columns: self._c = 0 self._r +=1 return rango def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __str__(self): if self.is_none: s = 'Range: None' else: s = f'Range: {self.name}' return s @property def obj(self): return self._obj @property def is_none(self): return self.obj is None @property def 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 rows(self): return self.obj.Rows.Count @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): self.obj.setDataArray(values) @property def formula(self): return self.obj.getFormulaArray() @formula.setter def formula(self, values): self.obj.setFormulaArray(values) @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 def select(self): self.doc.select(self.obj) return def offset(self, rows=0, cols=1): ra = self.range_address col = ra.EndColumn + cols row = ra.EndRow + rows return LOCalcRange(self.sheet[row, col].obj) def to_size(self, rows, cols): cursor = self.cursor cursor.collapseToSize(cols, rows) return LOCalcRange(self.sheet[cursor.AbsoluteName].obj) def copy_to(self, cell, formula=False): rango = cell.to_size(self.rows, self.columns) if formula: rango.formula = self.data else: rango.data = self.data return def auto_width(self): self.obj.Columns.OptimalWidth = True return def render(self, data, clean=True): self._sd = self.sheet.obj.createSearchDescriptor() self._sd.SearchCaseSensitive = False for k, v in data.items(): cell = self._render_value(k, v) return cell def _render_value(self, key, value, parent=''): cell = None if isinstance(value, dict): for k, v in value.items(): cell = self._render_value(k, v, key) return cell elif isinstance(value, (list, tuple)): self._render_list(key, value) return search = f'{{{key}}}' if parent: search = f'{{{parent}.{key}}}' ranges = self.find_all(search) for cell in ranges or range(0): self._set_new_value(cell, search, value) return LOCalcRange(cell) def _set_new_value(self, cell, search, value): if not cell.ImplementationName == 'ScCellObj': return if isinstance(value, str): pattern = re.compile(search, re.IGNORECASE) new_value = pattern.sub(value, cell.String) cell.String = new_value else: LOCalcRange(cell).value = value return def _render_list(self, key, rows): for row in rows: for k, v in row.items(): self._render_value(k, v) return def find_all(self, search_string): if self._sd is None: self._sd = self.sheet.obj.createSearchDescriptor() self._sd.SearchCaseSensitive = False self._sd.setSearchString(search_string) ranges = self.obj.findAll(self._sd) return ranges def filter(self, args, with_headers=True): ff = TableFilterField() ff.Field = args['Field'] ff.Operator = args['Operator'] if isinstance(args['Value'], str): ff.IsNumeric = False ff.StringValue = args['Value'] else: ff.IsNumeric = True ff.NumericValue = args['Value'] fd = self.obj.createFilterDescriptor(True) fd.ContainsHeader = with_headers fd.FilterFields = ((ff,)) # ~ self.obj.AutoFilter = True self.obj.filter(fd) return def copy_format_from(self, rango): rango.select() self.doc.copy() self.select() args = { 'Flags': 'T', 'MoveMode': 4, } url = '.uno:InsertContents' call_dispatch(self.doc.frame, url, args) return def to_image(self): self.select() self.doc.copy() args = {'SelectedFormat': 141} url = '.uno:ClipboardFormatItems' call_dispatch(self.doc.frame, url, args) return self.sheet.shapes[-1] def insert_image(self, path, args={}): ps = self.possize args['Width'] = args.get('Width', ps['Width']) args['Height'] = args.get('Height', ps['Height']) args['X'] = args.get('X', ps['X']) args['Y'] = args.get('Y', ps['Y']) # ~ img.ResizeWithCell = True img = self.sheet.dp.insert_image(path, args) img.Anchor = self.obj args.clear() return img 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' @property def obj(self): return self._obj @property def string(self): return self.obj.String @string.setter def string(self, value): self.obj.String = value @property def value(self): return self.string @property def is_table(self): return self._is_table @property def text(self): return self.obj.getText() @property def cursor(self): return self.text.createTextCursorByRange(self.obj) def offset(self): cursor = self.cursor.getEnd() return LOWriterTextRange(cursor, self._doc) def insert_content(self, data, cursor=None, replace=False): if cursor is None: cursor = self.cursor self.text.insertTextContent(cursor, data, replace) return def insert_image(self, path, args={}): w = args.get('Width', 1000) h = args.get('Height', 1000) image = self._doc.create_instance('com.sun.star.text.GraphicObject') image.GraphicURL = _P.to_url(path) image.AnchorType = AS_CHARACTER image.Width = w image.Height = h self.insert_content(image) return class LOWriterTextRanges(object): def __init__(self, obj, doc): self._obj = obj self._doc = doc def __getitem__(self, index): return LOWriterTextRange(self.obj[index], self._doc) @property def obj(self): return self._obj class LOWriter(LODocument): def __init__(self, obj): super().__init__(obj) self._type = WRITER @property def selection(self): sel = self.obj.CurrentSelection # ~ print(sel.ImplementationName) if sel.ImplementationName == 'SwXTextRanges': sel = LOWriterTextRanges(sel, self) return sel @property def view_cursor(self): return self._cc.ViewCursor @property def cursor(self): return self.obj.Text.createTextCursor() @property def page_styles(self): ps = self.obj.StyleFamilies['PageStyles'] return LOWriterPageStyles(ps) class LOShape(LOBaseObject): def __init__(self, obj, index): self._index = index super().__init__(obj) @property def name(self): return self.obj.Name or f'shape{self.index}' @name.setter def name(self, value): self.obj.Name = value @property def index(self): return self._index @property def string(self): return self.obj.String @string.setter def string(self, value): self.obj.String = value @property def anchor(self): return self.obj.Anchor @anchor.setter def anchor(self, value): if hasattr(value, 'obj'): value = value.obj self.obj.Anchor = value def remove(self): self.obj.Parent.remove(self.obj) return class LODrawPage(LOBaseObject): def __init__(self, obj): super().__init__(obj) def __getitem__(self, index): if isinstance(index, int): shape = LOShape(self.obj[index], index) else: for i, o in enumerate(self.obj): shape = self.obj[i] name = shape.Name or f'shape{i}' if name == index: shape = LOShape(shape, i) break return shape @property def name(self): return self.obj.Name @property def doc(self): return self.obj.Forms.Parent @property def width(self): return self.obj.Width @property def height(self): return self.obj.Height @property def count(self): return self.obj.Count def create_instance(self, name): return self.doc.createInstance(name) def add(self, type_shape, args={}): """Insert a shape in page, type shapes: Line Rectangle Ellipse Text """ w = args.get('Width', 3000) h = args.get('Height', 3000) x = args.get('X', 1000) y = args.get('Y', 1000) service = f'com.sun.star.drawing.{type_shape}Shape' shape = self.create_instance(service) shape.Size = Size(w, h) shape.Position = Point(x, y) index = self.count shape.Name = f'{type_shape.lower()}{index}' self.obj.add(shape) return LOShape(self.obj[index], index) def insert_image(self, path, args={}): w = args.get('Width', 3000) h = args.get('Height', 3000) x = args.get('X', 1000) y = args.get('Y', 1000) image = self.create_instance('com.sun.star.drawing.GraphicObjectShape') image.GraphicURL = _P.to_url(path) image.Size = Size(w, h) image.Position = Point(x, y) index = self.count image.Name = f'image{index}' self.obj.add(image) return LOShape(self.obj[index], index) class LODrawImpress(LODocument): def __init__(self, obj): super().__init__(obj) def __getitem__(self, index): if isinstance(index, int): page = self.obj.DrawPages[index] else: page = self.obj.DrawPages.getByName(index) return LODrawPage(page) @property def selection(self): sel = self.obj.CurrentSelection[0] return _get_class_uno(sel) @property def current_page(self): return LODrawPage(self._cc.getCurrentPage()) def paste(self): call_dispatch(self.frame, '.uno:Paste') return self.selection def add(self, type_shape, args={}): return self.current_page.add(type_shape, args) def insert_image(self, path, args={}): self.current_page.insert_image(path, args) return # ~ def export(self, path, mimetype='png'): # ~ args = dict( # ~ URL = _P.to_url(path), # ~ MimeType = MIME_TYPE[mimetype], # ~ ) # ~ result = _export_image(self.obj, args) # ~ return result class LODraw(LODrawImpress): def __init__(self, obj): super().__init__(obj) self._type = DRAW class LOImpress(LODrawImpress): def __init__(self, obj): super().__init__(obj) self._type = IMPRESS class BaseDateField(DateField): def db_value(self, value): return _date_to_struct(value) def python_value(self, value): return _struct_to_date(value) class BaseTimeField(TimeField): def db_value(self, value): return _date_to_struct(value) def python_value(self, value): return _struct_to_date(value) class BaseDateTimeField(DateTimeField): def db_value(self, value): return _date_to_struct(value) def python_value(self, value): return _struct_to_date(value) class FirebirdDatabase(Database): field_types = {'BOOL': 'BOOLEAN', 'DATETIME': 'TIMESTAMP'} def __init__(self, database, **kwargs): super().__init__(database, **kwargs) self._db = database def _connect(self): return self._db def create_tables(self, models, **options): options['safe'] = False tables = self._db.tables models = [m for m in models if not m.__name__.lower() in tables] super().create_tables(models, **options) def execute_sql(self, sql, params=None, commit=True): with __exception_wrapper__: cursor = self._db.execute(sql, params) return cursor def last_insert_id(self, cursor, query_type=None): # ~ debug('LAST_ID', cursor) return 0 def rows_affected(self, cursor): return self._db.rows_affected @property def path(self): return self._db.path class BaseRow: pass class BaseQuery(object): PY_TYPES = { 'SQL_LONG': 'getLong', 'SQL_VARYING': 'getString', 'SQL_FLOAT': 'getFloat', 'SQL_BOOLEAN': 'getBoolean', 'SQL_TYPE_DATE': 'getDate', 'SQL_TYPE_TIME': 'getTime', 'SQL_TIMESTAMP': 'getTimestamp', } TYPES_DATE = ('SQL_TYPE_DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP') def __init__(self, query): self._query = query self._meta = query.MetaData self._cols = self._meta.ColumnCount self._names = query.Columns.ElementNames self._data = self._get_data() def __getitem__(self, index): return self._data[index] def __iter__(self): self._index = 0 return self def __next__(self): try: row = self._data[self._index] except IndexError: raise StopIteration self._index += 1 return row def _to_python(self, index): type_field = self._meta.getColumnTypeName(index) value = getattr(self._query, self.PY_TYPES[type_field])(index) if type_field in self.TYPES_DATE: value = _struct_to_date(value) return value def _get_row(self): row = BaseRow() for i in range(1, self._cols + 1): column_name = self._meta.getColumnName(i) value = self._to_python(i) setattr(row, column_name, value) return row def _get_data(self): data = [] while self._query.next(): row = self._get_row() data.append(row) return data @property def tuples(self): data = [tuple(r.__dict__.values()) for r in self._data] return tuple(data) @property def dicts(self): data = [r.__dict__ for r in self._data] return tuple(data) class LOBase(object): DB_TYPES = { str: 'setString', int: 'setInt', float: 'setFloat', bool: 'setBoolean', Date: 'setDate', Time: 'setTime', DateTime: 'setTimestamp', } # ~ setArray # ~ setBinaryStream # ~ setBlob # ~ setByte # ~ setBytes # ~ setCharacterStream # ~ setClob # ~ setNull # ~ setObject # ~ setObjectNull # ~ setObjectWithInfo # ~ setPropertyValue # ~ setRef def __init__(self, obj, args={}): self._obj = obj self._type = BASE self._dbc = create_instance('com.sun.star.sdb.DatabaseContext') 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(self._path.url, ()) self.register() self._obj = db self._con = db.getConnection('', '') def __contains__(self, item): return item in self.tables @property def obj(self): return self._obj @property def name(self): return self._name @property def path(self): return str(self._path) @property def is_registered(self): return self._dbc.hasRegisteredDatabase(self.name) @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): self._dbc.revokeDatabaseLocation(name) return True def save(self): self.obj.DatabaseDocument.store() self.refresh() return def close(self): self._con.close() return def refresh(self): self._con.getTables().refresh() return 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): t = type(v) if not t in self.DB_TYPES: error('Type not support') 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, 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: result = cursor.execute(sql) self.save() return result def select(self, sql): debug('SELECT', sql) if not sql.startswith('SELECT'): return () cursor = self._con.prepareStatement(sql) query = cursor.executeQuery() return BaseQuery(query) 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 LOBasic(LODocument): def __init__(self, obj): super().__init__(obj) self._type = BASIC class LODocs(object): _desktop = None def __init__(self): self._desktop = get_desktop() LODocs._desktop = self._desktop def __getitem__(self, index): doc = None for i, doc in enumerate(self._desktop.Components): if isinstance(index, int) and i == index: doc = _get_class_doc(doc) break elif isinstance(index, str) and doc.Title == index: doc = _get_class_doc(doc) break return doc def __contains__(self, item): doc = self[item] return not doc is None def __iter__(self): self._i = 0 return self def __next__(self): doc = self[self._i] if doc is None: raise StopIteration self._i += 1 return doc def __len__(self): for i, _ in enumerate(self._desktop.Components): pass return i + 1 @property def active(self): return _get_class_doc(self._desktop.getCurrentComponent()) @classmethod def new(cls, type_doc=CALC, args={}): if type_doc == BASE: return LOBase(None, args) 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) @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 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(args) doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt) if doc is None: return return _get_class_doc(doc) def connect(self, path): return LOBase(None, {'path': path}) 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' 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 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): def __init__(self, controller, name, window=None): self._controller = controller self._name = name self._window = window @property def name(self): return self._name def disposing(self, event): self._controller = None if not self._window is None: self._window.setMenuBar(None) class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): def __init__(self, controller, name): super().__init__(controller, name) def mousePressed(self, event): event_name = '{}_click'.format(self._name) if event.ClickCount == 2: event_name = '{}_double_click'.format(self._name) if hasattr(self._controller, event_name): getattr(self._controller, event_name)(event) return def mouseReleased(self, event): pass def mouseEntered(self, event): pass def mouseExited(self, event): pass # ~ XMouseMotionListener def mouseMoved(self, event): pass def mouseDragged(self, event): pass class EventsMouseLink(EventsMouse): def __init__(self, controller, name): super().__init__(controller, name) self._text_color = 0 def mouseEntered(self, event): model = event.Source.Model self._text_color = model.TextColor or 0 model.TextColor = get_color('blue') return def mouseExited(self, event): 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 # ~ BorderColor = ? # ~ FontStyleName = ? # ~ HelpURL = ? class UnoBaseObject(object): def __init__(self, obj): self._obj = obj 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) @property def obj(self): return self._obj @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): return self.model.Name @property def parent(self): 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() return getattr(ps, name) def _set_possize(self, name, value): ps = self.obj.getPosSize() setattr(ps, name, value) self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) return @property def x(self): if hasattr(self.model, 'PositionX'): return self.model.PositionX return self._get_possize('X') @x.setter def x(self, value): if hasattr(self.model, 'PositionX'): self.model.PositionX = value else: self._set_possize('X', value) @property def y(self): if hasattr(self.model, 'PositionY'): return self.model.PositionY return self._get_possize('Y') @y.setter def y(self, value): if hasattr(self.model, 'PositionY'): self.model.PositionY = value else: self._set_possize('Y', value) @property def tab_index(self): return self._model.TabIndex @tab_index.setter def tab_index(self, value): self.model.TabIndex = value @property def tab_stop(self): return self._model.Tabstop @tab_stop.setter def tab_stop(self, value): self.model.Tabstop = value def center(self, horizontal=True, vertical=False): p = self.parent.Model w = p.Width h = p.Height if horizontal: x = w / 2 - self.width / 2 self.x = x if vertical: y = h / 2 - self.height / 2 self.y = y return def move(self, origin, x=0, y=5, center=False): if x: self.x = origin.x + origin.width + x else: self.x = origin.x if y: self.y = origin.y + origin.height + y else: self.y = origin.y if center: self.center() return class UnoLabel(UnoBaseObject): def __init__(self, obj): super().__init__(obj) @property def type(self): return 'label' @property def value(self): return self.model.Label @value.setter def value(self, value): self.model.Label = value class UnoLabelLink(UnoLabel): def __init__(self, obj): super().__init__(obj) @property def type(self): return 'link' class UnoButton(UnoBaseObject): def __init__(self, obj): super().__init__(obj) @property def type(self): return 'button' @property def value(self): return self.model.Label @value.setter def value(self, value): self.model.Label = value class UnoRadio(UnoBaseObject): def __init__(self, obj): super().__init__(obj) @property def type(self): return 'radio' @property def value(self): return self.model.Label @value.setter def value(self, value): self.model.Label = value class UnoCheck(UnoBaseObject): def __init__(self, obj): super().__init__(obj) @property def type(self): return 'check' @property def value(self): return self.model.State @value.setter def value(self, value): self.model.State = value @property def label(self): return self.model.Label @label.setter def label(self, value): self.model.Label = value @property def tri_state(self): return self.model.TriState @tri_state.setter def tri_state(self, value): self.model.TriState = value # ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html class UnoText(UnoBaseObject): def __init__(self, obj): super().__init__(obj) @property def type(self): return 'text' @property def value(self): return self.model.Text @value.setter def value(self, value): self.model.Text = value UNO_CLASSES = { 'label': UnoLabel, 'link': UnoLabelLink, 'button': UnoButton, 'radio': UnoRadio, 'check': UnoCheck, 'text': UnoText, } class LODialog(object): MODELS = { 'label': 'com.sun.star.awt.UnoControlFixedTextModel', 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', 'button': 'com.sun.star.awt.UnoControlButtonModel', 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', 'check': 'com.sun.star.awt.UnoControlCheckBoxModel', 'text': 'com.sun.star.awt.UnoControlEditModel', # ~ 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', # ~ 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', # ~ 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', # ~ 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', # ~ 'tree': 'com.sun.star.awt.tree.TreeControlModel', # ~ 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', # ~ 'image': 'com.sun.star.awt.UnoControlImageControlModel', # ~ 'pages': 'com.sun.star.awt.UnoMultiPageModel', } def __init__(self, args): self._obj = self._create(args) self._model = self.obj.Model self._events = None self._modal = True self._controls = {} self._color_on_focus = COLOR_ON_FOCUS def _create(self, args): service = 'com.sun.star.awt.DialogProvider' path = args.pop('Path', '') if path: dp = create_instance(service, True) dlg = dp.createDialog(_path_url(path)) return dlg if 'Location' in args: name = args['Name'] library = args.get('Library', 'Standard') location = args.get('Location', 'application') if location == 'user': location = 'application' url = f'vnd.sun.star.script:{library}.{name}?location={location}' if location == 'document': 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, args) dlg.setModel(model) dlg.setVisible(False) dlg.createPeer(toolkit, None) return dlg @property def obj(self): return self._obj @property def model(self): return self._model @property def controls(self): return self._controls @property def visible(self): return self.obj.Visible @visible.setter def visible(self, value): self.obj.Visible = value @property def events(self): return self._events @events.setter def events(self, 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.Controls: _add_listeners(self.events, control, control.Model.Name) return def _special_properties(self, tipo, args): columns = args.pop('Columns', ()) if tipo == 'link' and not 'Label' in args: args['Label'] = args['URL'] elif tipo == 'grid': args['ColumnModel'] = self._set_column_model(columns) elif tipo == 'button': if 'ImageURL' in args: args['ImageURL'] = self._set_image_url(args['ImageURL']) if not 'FocusOnClick' in args: args['FocusOnClick'] = False elif tipo == 'roadmap': if not 'Height' in args: args['Height'] = self.height if 'Title' in args: args['Text'] = args.pop('Title') elif tipo == 'tab': if not 'Width' in args: args['Width'] = self.width if not 'Height' in args: args['Height'] = self.height return args def add_control(self, args): tipo = args.pop('Type').lower() root = args.pop('Root', '') sheets = args.pop('Sheets', ()) 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 = UNO_CLASSES[tipo](control) if tipo == 'tree' and root: control.root = root elif tipo == 'pages' and sheets: control.sheets = sheets control.events = self.events setattr(self, name, control) self._controls[name] = control return control 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 classproperty: def __init__(self, method=None): self.fget = method def __get__(self, instance, cls=None): return self.fget(cls) def getter(self, method): self.fget = method return self class Paths(object): 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 @property def info(self): return self.path, self.file_name, self.name, self.ext @property def url(self): return self._path.as_uri() @classproperty def home(self): return str(Path.home()) @classmethod def replace_ext(cls, path, new_ext): p = Paths(path) name = f'{p.name}.{new_ext}' path = cls.join(p.path, name) return path @classmethod def exists(cls, path): result = False if path: path = cls.to_system(path) result = Path(path).exists() return result @classmethod def exists_app(cls, name_app): return bool(shutil.which(name_app)) @classmethod def open(cls, path): if IS_WIN: os.startfile(path) else: pid = subprocess.Popen(['xdg-open', path]).pid return @classmethod def is_dir(cls, path): return Path(path).is_dir() @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 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 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 += files return paths @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 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 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() raise AttributeError(f"module '{__name__}' has no attribute '{name}'") def create_dialog(args): return LODialog(args) def inputbox(message, default='', title=TITLE, echochar=''): class ControllersInput(object): def __init__(self, dlg): self.d = dlg def cmd_ok_action(self, event): self.d.close(1) return args = { 'Title': title, 'Width': 200, 'Height': 80, } dlg = LODialog(args) dlg.events = ControllersInput args = { 'Type': 'Label', 'Name': 'lbl_msg', 'Label': message, 'Width': 140, 'Height': 50, 'X': 5, 'Y': 5, 'MultiLine': True, 'Border': 1, } dlg.add_control(args) args = { 'Type': 'Text', 'Name': 'txt_value', 'Text': default, 'Width': 190, 'Height': 15, } if echochar: args['EchoChar'] = ord(echochar[0]) dlg.add_control(args) dlg.txt_value.move(dlg.lbl_msg) args = { 'Type': 'button', 'Name': 'cmd_ok', 'Label': _('OK'), 'Width': 40, 'Height': 15, 'DefaultButton': True, 'PushButtonType': 1, } dlg.add_control(args) dlg.cmd_ok.move(dlg.lbl_msg, 10, 0) args = { 'Type': 'button', 'Name': 'cmd_cancel', 'Label': _('Cancel'), 'Width': 40, 'Height': 15, 'PushButtonType': 2, } dlg.add_control(args) dlg.cmd_cancel.move(dlg.cmd_ok) if dlg.open(): return dlg.txt_value.value return '' def get_fonts(): toolkit = create_instance('com.sun.star.awt.Toolkit') device = toolkit.createScreenCompatibleDevice(0, 0) return device.FontDescriptors # ~ 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) 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] def __delitem__(self, key): del self._store[key.lower()] def __iter__(self): return (casedkey for casedkey, mappedvalue in self._store.values()) 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 # Copy is required def copy(self): return CaseInsensitiveDict(self._store.values()) def __repr__(self): return str(dict(self.items())) # ~ https://en.wikipedia.org/wiki/Web_colors def get_color(value): COLORS = { 'aliceblue': 15792383, 'antiquewhite': 16444375, 'aqua': 65535, 'aquamarine': 8388564, 'azure': 15794175, 'beige': 16119260, 'bisque': 16770244, 'black': 0, 'blanchedalmond': 16772045, 'blue': 255, 'blueviolet': 9055202, 'brown': 10824234, 'burlywood': 14596231, 'cadetblue': 6266528, 'chartreuse': 8388352, 'chocolate': 13789470, 'coral': 16744272, 'cornflowerblue': 6591981, 'cornsilk': 16775388, 'crimson': 14423100, 'cyan': 65535, 'darkblue': 139, 'darkcyan': 35723, 'darkgoldenrod': 12092939, 'darkgray': 11119017, 'darkgreen': 25600, 'darkgrey': 11119017, 'darkkhaki': 12433259, 'darkmagenta': 9109643, 'darkolivegreen': 5597999, 'darkorange': 16747520, 'darkorchid': 10040012, 'darkred': 9109504, 'darksalmon': 15308410, 'darkseagreen': 9419919, 'darkslateblue': 4734347, 'darkslategray': 3100495, 'darkslategrey': 3100495, 'darkturquoise': 52945, 'darkviolet': 9699539, 'deeppink': 16716947, 'deepskyblue': 49151, 'dimgray': 6908265, 'dimgrey': 6908265, 'dodgerblue': 2003199, 'firebrick': 11674146, 'floralwhite': 16775920, 'forestgreen': 2263842, 'fuchsia': 16711935, 'gainsboro': 14474460, 'ghostwhite': 16316671, 'gold': 16766720, 'goldenrod': 14329120, 'gray': 8421504, 'grey': 8421504, 'green': 32768, 'greenyellow': 11403055, 'honeydew': 15794160, 'hotpink': 16738740, 'indianred': 13458524, 'indigo': 4915330, 'ivory': 16777200, 'khaki': 15787660, 'lavender': 15132410, 'lavenderblush': 16773365, 'lawngreen': 8190976, 'lemonchiffon': 16775885, 'lightblue': 11393254, 'lightcoral': 15761536, 'lightcyan': 14745599, 'lightgoldenrodyellow': 16448210, 'lightgray': 13882323, 'lightgreen': 9498256, 'lightgrey': 13882323, 'lightpink': 16758465, 'lightsalmon': 16752762, 'lightseagreen': 2142890, 'lightskyblue': 8900346, 'lightslategray': 7833753, 'lightslategrey': 7833753, 'lightsteelblue': 11584734, 'lightyellow': 16777184, 'lime': 65280, 'limegreen': 3329330, 'linen': 16445670, 'magenta': 16711935, 'maroon': 8388608, 'mediumaquamarine': 6737322, 'mediumblue': 205, 'mediumorchid': 12211667, 'mediumpurple': 9662683, 'mediumseagreen': 3978097, 'mediumslateblue': 8087790, 'mediumspringgreen': 64154, 'mediumturquoise': 4772300, 'mediumvioletred': 13047173, 'midnightblue': 1644912, 'mintcream': 16121850, 'mistyrose': 16770273, 'moccasin': 16770229, 'navajowhite': 16768685, 'navy': 128, 'oldlace': 16643558, 'olive': 8421376, 'olivedrab': 7048739, 'orange': 16753920, 'orangered': 16729344, 'orchid': 14315734, 'palegoldenrod': 15657130, 'palegreen': 10025880, 'paleturquoise': 11529966, 'palevioletred': 14381203, 'papayawhip': 16773077, 'peachpuff': 16767673, 'peru': 13468991, 'pink': 16761035, 'plum': 14524637, 'powderblue': 11591910, 'purple': 8388736, 'red': 16711680, 'rosybrown': 12357519, 'royalblue': 4286945, 'saddlebrown': 9127187, 'salmon': 16416882, 'sandybrown': 16032864, 'seagreen': 3050327, 'seashell': 16774638, 'sienna': 10506797, 'silver': 12632256, 'skyblue': 8900331, 'slateblue': 6970061, 'slategray': 7372944, 'slategrey': 7372944, 'snow': 16775930, 'springgreen': 65407, 'steelblue': 4620980, 'tan': 13808780, 'teal': 32896, 'thistle': 14204888, 'tomato': 16737095, 'turquoise': 4251856, 'violet': 15631086, 'wheat': 16113331, 'white': 16777215, 'whitesmoke': 16119285, 'yellow': 16776960, 'yellowgreen': 10145074, } if isinstance(value, tuple): color = (value[0] << 16) + (value[1] << 8) + value[2] else: if value[0] == '#': r, g, b = bytes.fromhex(value[1:]) color = (r << 16) + (g << 8) + b else: color = COLORS.get(value.lower(), -1) return color COLOR_ON_FOCUS = get_color('LightYellow') class LOServer(object): HOST = 'localhost' PORT = '8100' ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext' CMD = ['soffice', '-env:SingleAppInstance=false', '-env:UserInstallation=file:///tmp/LO_Process8100', '--headless', '--norestore', '--invisible', f'--accept={ARG}'] def __init__(self): self._server = None self._ctx = None self._sm = None self._start_server() self._init_values() def _init_values(self): global CTX global SM if not self.is_running: return ctx = uno.getComponentContext() service = 'com.sun.star.bridge.UnoUrlResolver' resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx) self._ctx = resolver.resolve('uno:{}'.format(self.ARG)) self._sm = self._ctx.getServiceManager() CTX = self._ctx SM = self._sm return @property def is_running(self): try: s = socket.create_connection((self.HOST, self.PORT), 5.0) s.close() debug('LibreOffice is running...') return True except ConnectionRefusedError: return False def _start_server(self): if self.is_running: return for i in range(3): self._server = subprocess.Popen(self.CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(3) if self.is_running: break return def stop(self): if self._server is None: print('Search pgrep soffice') else: self._server.terminate() debug('LibreOffice is stop...') return def create_instance(self, name, with_context=True): if with_context: instance = self._sm.createInstanceWithContext(name, self._ctx) else: instance = self._sm.createInstance(name) return instance