#!/usr/bin/env python3 # == Rapid Develop Macros in LibreOffice == # ~ This file is part of 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 getpass import logging import os import platform import sys import tempfile import threading import time from functools import wraps import uno import unohelper from com.sun.star.beans import PropertyValue from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.awt.PosSize import POSSIZE, SIZE from com.sun.star.awt import Size, Point from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA from com.sun.star.text.TextContentAnchorType import AS_CHARACTER from com.sun.star.lang import XEventListener from com.sun.star.awt import XActionListener from com.sun.star.awt import XMouseListener FILE_NAME_DEBUG = 'debug.log' MSG_LANG = { 'es': { 'OK': 'Aceptar', 'Cancel': 'Cancelar', } } FORMAT = '%(asctime)s - %(levelname)s - %(message)s' DATE = '%d/%m/%Y %H:%M:%S' LEVEL_ERROR = logging.getLevelName(logging.ERROR) LEVEL_DEBUG = logging.getLevelName(logging.DEBUG) LEVEL_INFO = logging.getLevelName(logging.INFO) logging.addLevelName(logging.ERROR, f'\033[1;41m{LEVEL_ERROR}\033[1;0m') logging.addLevelName(logging.DEBUG, f'\x1b[33m{LEVEL_DEBUG}\033[1;0m') logging.addLevelName(logging.INFO, f'\x1b[32m{LEVEL_INFO}\033[1;0m') logging.basicConfig(level=logging.DEBUG, format=FORMAT, datefmt=DATE) log = logging.getLogger(__name__) OBJ_CELL = 'ScCellObj' OBJ_RANGE = 'ScCellRangeObj' OBJ_RANGES = 'ScCellRangesObj' OBJ_TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_RANGES) CTX = uno.getComponentContext() SM = CTX.getServiceManager() def create_instance(name, with_context=False): if with_context: instance = SM.createInstanceWithContext(name, CTX) else: instance = SM.createInstance(name) return instance def _get_config(key, node_name): 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 (ca.hasByName(key)): data = ca.getPropertyValue(key) return data except Exception as e: log.error(e) return '' OS = sys.platform USER = getpass.getuser() PC = platform.node() LANGUAGE = _get_config('ooLocale', 'org.openoffice.Setup/L10N/') NAME = TITLE = _get_config('ooName', 'org.openoffice.Setup/Product') VERSION = _get_config('ooSetupVersion', 'org.openoffice.Setup/Product') # ~ DESKTOP = create_instance('com.sun.star.frame.Desktop', True) INFO_DEBUG = '{}\n\n{}\n\n{}'.format( sys.version, platform.platform(), '\n'.join(sys.path)) 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 debug(info): log.debug(info) return def error(info): log.error(info) return def catch_exception(f): @wraps(f) def func(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: log.error(f.__name__, exc_info=True) return func def run_in_thread(fn): def run(*k, **kw): t = threading.Thread(target=fn, args=k, kwargs=kw) t.start() return t return run def _(msg): L = LANGUAGE.split('-')[0] if L == 'en': return msg if not L in MSG_LANG: return msg return MSG_LANG[L][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() mb = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message)) return mb.execute() def question(message, title=TITLE): res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') return res == 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_desktop(): return create_instance('com.sun.star.frame.Desktop', True) def get_temp_file(): return tempfile.NamedTemporaryFile() 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 get_type_doc(obj): services = { '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.OfficeDatabaseDocument', 'math': 'com.sun.star.formula.FormulaProperties', 'basic': 'com.sun.star.script.BasicIDE', } for k, v in services.items(): if obj.supportsService(v): return k return '' # ~ Custom classes class LODocument(object): def __init__(self, obj): self._obj = obj self._init_values() def _init_values(self): self._type_doc = get_type_doc(self.obj) self._cc = self.obj.getCurrentController() return @property def obj(self): return self._obj @property def type(self): return self._type_doc @property def title(self): return self.obj.getTitle() @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.getURL()) @property def visible(self): w = self._cc.getFrame().getContainerWindow() return w.Visible @visible.setter def visible(self, value): w = self._cc.getFrame().getContainerWindow() w.setVisible(value) @property def zoom(self): return self._cc.ZoomValue @zoom.setter def zoom(self, value): self._cc.ZoomValue = value def create_instance(self, name): obj = self.obj.createInstance(name) return obj class LOCalc(LODocument): def __init__(self, obj): super().__init__(obj) @property def obj(self): return self._obj @property def active(self): return LOCalcSheet(self._cc.getActiveSheet(), self) @property def selection(self): sel = self.obj.getCurrentSelection() if sel.ImplementationName in OBJ_TYPE_RANGES: sel = LOCellRange(sel, self) return sel 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 create_instance(self, name): # ~ obj = self.obj.createInstance(name) # ~ return obj class LOCalcSheet(object): def __init__(self, obj, doc): self._obj = obj self._doc = doc self._init_values() def __getitem__(self, index): return LOCellRange(self.obj[index], self.doc) def _init_values(self): return @property def obj(self): return self._obj @property def doc(self): return self._doc class LOWriter(LODocument): def __init__(self, obj): super().__init__(obj) @property def obj(self): return self._obj @property def string(self): return self._obj.getText().String @property def text(self): return self._obj.getText() @property def cursor(self): return self.text.createTextCursor() @property def selection(self): sel = self._cc.getSelection() return LOTextRange(sel[0]) def insert_content(self, cursor, data, replace=False): self.text.insertTextContent(cursor, data, replace) return # ~ tt = doc.createInstance('com.sun.star.text.TextTable') # ~ tt.initialize(5, 2) # ~ f = doc.createInstance('com.sun.star.text.TextFrame') # ~ f.setSize(Size(10000, 500)) def insert_image(self, path, **kwargs): cursor = kwargs.get('cursor', self.selection.cursor.getEnd()) w = kwargs.get('width', 1000) h = kwargs.get('Height', 1000) image = self.create_instance('com.sun.star.text.GraphicObject') image.GraphicURL = _path_url(path) image.AnchorType = AS_CHARACTER image.Width = w image.Height = h self.insert_content(cursor, image) return class LOTextRange(object): def __init__(self, obj): self._obj = obj @property def obj(self): return self._obj @property def string(self): return self.obj.String @property def text(self): return self.obj.getText() @property def cursor(self): return self.text.createTextCursorByRange(self.obj) class LOBase(LODocument): def __init__(self, obj): super().__init__(obj) class LODrawImpress(LODocument): def __init__(self, obj): super().__init__(obj) @property def draw_page(self): return self._cc.getCurrentPage() @catch_exception def insert_image(self, path, **kwargs): w = kwargs.get('width', 3000) h = kwargs.get('Height', 1000) 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) class LOMath(LODocument): def __init__(self, obj): super().__init__(obj) class LOBasicIde(LODocument): def __init__(self, obj): super().__init__(obj) class LOCellRange(object): def __init__(self, obj, doc): self._obj = obj self._doc = doc self._init_values() def __enter__(self): return self def __exit__(self, *args): pass def __getitem__(self, index): return LOCellRange(self.obj[index], self.doc) def _init_values(self): self._type_obj = self.obj.ImplementationName self._type_content = EMPTY 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) else: obj = LOCellRange(self.obj[0,0], self.doc) return obj @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)): self.obj.setValue(data) @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) def offset(self, col=1, row=0): a = self.address col = a.Column + col row = a.Row + row return LOCellRange(self.sheet[row,col], self.doc) @property def sheet(self): return self.obj.Spreadsheet @property def draw_page(self): return self.sheet.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 current_region(self): cursor = self.sheet.createCursorByRange(self.obj[0,0]) cursor.collapseToCurrentRegion() return LOCellRange(self.sheet[cursor.AbsoluteName], self.doc) 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 class EventsListenerBase(unohelper.Base, XEventListener): def __init__(self, controller, window=None): self._controller = controller self._window = window def disposing(self, event): self._controller = None if not self._window is None: self._window.setMenuBar(None) class EventsButton(EventsListenerBase, XActionListener): def __init__(self, controller): super().__init__(controller) def actionPerformed(self, event): name = event.Source.Model.Name event_name = '{}_action'.format(name) if hasattr(self._controller, event_name): getattr(self._controller, event_name)(event) return class EventsMouse(EventsListenerBase, XMouseListener): def __init__(self, controller): super().__init__(controller) def mousePressed(self, event): name = event.Source.Model.Name event_name = '{}_click'.format(name) if event.ClickCount == 2: event_name = '{}_double_click'.format(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 class UnoBaseObject(object): def __init__(self, obj): self._obj = obj self._model = self.obj.Model self._rules = {} @property def obj(self): return self._obj @property def model(self): return self._model @property def name(self): return self.model.Name @property def parent(self): return self.obj.getContext() @property def x(self): return self.model.PositionX @x.setter def x(self, value): self.model.PositionX = value @property def y(self): return self.model.PositionY @y.setter def y(self, value): self.model.PositionY = 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 @property def tag(self): return self.model.Tag @tag.setter def tag(self, value): self.model.Tag = value @property def step(self): return self.model.Step @step.setter def step(self, value): self.model.Step = value @property def rules(self): return self._rules @rules.setter def rules(self, value): self._rules = value def set_focus(self): self.obj.setFocus() return 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): w = 0 h = 0 if x: w = origin.width if y: h = origin.height x = origin.x + x + w y = origin.y + y + h self.x = x self.y = y return class UnoLabel(UnoBaseObject): def __init__(self, obj): super().__init__(obj) @property def value(self): return self.model.Label @value.setter def value(self, value): self.model.Label = value class UnoButton(UnoBaseObject): def __init__(self, obj): super().__init__(obj) # ~ self._set_icon() def _set_icon(self): icon_name = self.tag.strip() if icon_name: path_icon = _file_url('{}/img/{}'.format(CURRENT_PATH, icon_name)) self._model.ImageURL = path_icon if self.value: self._model.ImageAlign = 0 return @property def value(self): return self.model.Label @value.setter def value(self, value): self.model.Label = value class UnoText(UnoBaseObject): def __init__(self, obj): super().__init__(obj) @property def value(self): return self.model.Text @value.setter def value(self, value): self.model.Text = value def validate(self): return class UnoListBox(UnoBaseObject): def __init__(self, obj): super().__init__(obj) self._data = [] @property def value(self): return self.obj.SelectedItem @property def data(self): return self._data @data.setter def data(self, values): self._data = list(sorted(values)) self.model.StringItemList = self.data return class LODialog(object): def __init__(self, properties): self._obj = self._create(properties) self._init_values() def _init_values(self): self._model = self._obj.Model self._init_controls() self._events = None # ~ self._response = None return def _create(self, properties): path = properties.pop('Path', '') if path: dp = create_instance('com.sun.star.awt.DialogProvider2', True) return dp.createDialog(_path_url(path)) if 'Library' in properties: location = properties['Location'] if location == 'user': location = 'application' dp = create_instance('com.sun.star.awt.DialogProvider2', True) path = 'vnd.sun.star.script:{}.{}?location={}'.format( properties['Library'], properties['Name'], location) return dp.createDialog(path) 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) dlg.setModel(model) dlg.setVisible(False) dlg.createPeer(toolkit, None) return dlg def _init_controls(self): return @property def obj(self): return self._obj @property def model(self): return self._model @property def events(self): return self._events @events.setter def events(self, controllers): self._events = controllers self._connect_listeners() def _connect_listeners(self): return def _add_listeners(self, control): if self.events is None: return listeners = { 'addActionListener': EventsButton, 'addMouseListener': EventsMouse, } for key, value in listeners.items(): if hasattr(control.obj, key): getattr(control.obj, key)(listeners[key](self.events)) 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', 'button': 'com.sun.star.awt.UnoControlButtonModel', 'text': 'com.sun.star.awt.UnoControlEditModel', 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', 'image': 'com.sun.star.awt.UnoControlImageControlModel', 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', 'tree': 'com.sun.star.awt.tree.TreeControlModel', 'grid': 'com.sun.star.awt.grid.UnoControlGridModel', } return services[control] def _get_custom_class(self, tipo, obj): classes = { 'label': UnoLabel, 'button': UnoButton, 'text': UnoText, 'listbox': UnoListBox, # ~ 'link': UnoLink, # ~ 'tab': UnoTab, # ~ 'roadmap': UnoRoadmap, # ~ 'image': UnoImage, # ~ 'radio': UnoRadio, # ~ 'groupbox': UnoGroupBox, # ~ 'tree': UnoTree, # ~ 'grid': UnoGrid, } return classes[tipo](obj) def add_control(self, properties): tipo = properties.pop('Type').lower() model = self.model.createInstance(self._get_control_model(tipo)) set_properties(model, properties) name = properties['Name'] self.model.insertByName(name, model) control = self._get_custom_class(tipo, self.obj.getControl(name)) self._add_listeners(control) setattr(self, name, control) return # ~ Python >= 3.7 # ~ def __getattr__(name): 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) def get_document(): doc = None desktop = get_desktop() try: doc = _get_class_doc(desktop.getCurrentComponent()) except Exception as e: log.error(e) return doc def get_selection(): return get_document().selection 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 def active_cell(): return get_cell() def create_dialog(properties): return LODialog(properties) 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