#!/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 ctypes import datetime import errno import getpass import json import logging import os import platform import re import shlex import shutil import subprocess import sys import tempfile import threading import time import zipfile from collections import OrderedDict from collections.abc import MutableMapping from datetime import datetime from functools import wraps from operator import itemgetter from pathlib import Path, PurePath from pprint import pprint from subprocess import PIPE 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.datatransfer import XTransferable, DataFlavor 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 MSG_LANG = { 'es': { 'OK': 'Aceptar', 'Cancel': 'Cancelar', 'Select file': 'Seleccionar archivo', } } 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' CALC = 'calc' WRITER = 'writer' OBJ_CELL = 'ScCellObj' OBJ_RANGE = 'ScCellRangeObj' OBJ_RANGES = 'ScCellRangesObj' OBJ_TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_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.OfficeDatabaseDocument', 'math': 'com.sun.star.formula.FormulaProperties', 'basic': 'com.sun.star.script.BasicIDE', } NODE_MENUBAR = 'private:resource/menubar/menubar' 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_APP = { 'calc': MENUS_CALC, } FILE_NAME_DEBUG = 'zaz-debug.log' 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') 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__) 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_app_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 '' LANGUAGE = _get_app_config('ooLocale', 'org.openoffice.Setup/L10N/') LANG = LANGUAGE.split('-')[0] NAME = TITLE = _get_app_config('ooName', 'org.openoffice.Setup/Product') VERSION = _get_app_config('ooSetupVersion', 'org.openoffice.Setup/Product') 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 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 class LogWin(object): 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), 0) return def info(data): log.info(data) return def debug(info): if IS_WIN: # ~ app = LOApp(self.ctx, self.sm, self.desktop, self.toolkit) # ~ doc = app.getDoc(FILE_NAME_DEBUG) # ~ if not doc: # ~ doc = app.newDoc(WRITER) # ~ out = OutputDoc(doc) # ~ sys.stdout = out # ~ pprint(info) doc = LogWin(new_doc('writer').obj) doc.write(info) return log.debug(str(info)) return def error(info): log.error(info) return def save_log(path, data): with open(path, 'a') as out: out.write('{} -{}- '.format(str(datetime.now())[:19], LOG_NAME)) pprint(data, stream=out) return 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 get_config(key='', prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) values = {} path = join(get_config_path('UserConfig'), path_json) if not exists_path(path): return values with open(path, 'r', encoding='utf-8') as fh: data = fh.read() if data: values = json.loads(data) if key: return values.get(key, None) 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(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': 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_dispatch(): return create_instance('com.sun.star.frame.DispatchHelper') def get_temp_file(): delete = True if IS_WIN: delete = False return tempfile.NamedTemporaryFile(delete=delete) 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 # ~ Delete # ~ def exists(path): # ~ return Path(path).exists() def exists_path(path): return Path(path).exists() 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 TYPE_DOC.items(): if obj.supportsService(v): return k return '' # ~ def _properties(values): # ~ p = [PropertyValue(Name=n, Value=v) for n, v in values.items()] # ~ return tuple(p) def dict_to_property(values, uno_any=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 property_to_dict(values): d = {i.Name: i.Value for i in values} return d # ~ Third classes # ~ https://github.com/psf/requests/blob/v2.22.0/requests/structures.py class CaseInsensitiveDict(MutableMapping): """A case-insensitive ``dict``-like object. Implements all methods and operations of ``MutableMapping`` as well as dict's ``copy``. Also provides ``lower_items``. All keys are expected to be strings. The structure remembers the case of the last key to be set, and ``iter(instance)``, ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` will contain case-sensitive keys. However, querying and contains testing is case insensitive:: cid = CaseInsensitiveDict() cid['Accept'] = 'application/json' cid['aCCEPT'] == 'application/json' # True list(cid) == ['Accept'] # True For example, ``headers['content-encoding']`` will return the value of a ``'Content-Encoding'`` response header, regardless of how the header name was originally stored. If the constructor, ``.update``, or equality comparison operations are given keys that have equal ``.lower()``s, the behavior is undefined. """ 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.""" return ( (lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items() ) def __eq__(self, other): if isinstance(other, Mapping): other = CaseInsensitiveDict(other) else: return NotImplemented # Compare insensitively return dict(self.lower_items()) == dict(other.lower_items()) # Copy is required def copy(self): return CaseInsensitiveDict(self._store.values()) def __repr__(self): return str(dict(self.items())) # ~ 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 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.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 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) return def focus(self): w = self._cc.getFrame().getComponentWindow() w.setFocus() return def paste(self): sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') transferable = sc.getContents() self._cc.insertTransferable(transferable) return self.obj.getCurrentSelection() 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 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 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) @property def selection(self): sel = self._cc.getSelection() return sel 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 def select(self): self.doc._cc.select(self.obj) 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 EventsMouseGrid(EventsMouse): selected = False def mousePressed(self, event): super().mousePressed(event) # ~ obj = event.Source # ~ col = obj.getColumnAtPoint(event.X, event.Y) # ~ row = obj.getRowAtPoint(event.X, event.Y) # ~ print(col, row) # ~ if col == -1 and row == -1: # ~ if self.selected: # ~ obj.deselectAllRows() # ~ else: # ~ obj.selectAllRows() # ~ self.selected = not self.selected return def mouseReleased(self, event): obj = event.Source col = obj.getColumnAtPoint(event.X, event.Y) row = obj.getRowAtPoint(event.X, event.Y) if row == -1 and col > -1: gdm = obj.Model.GridDataModel for i in range(gdm.RowCount): gdm.updateRowHeading(i, i + 1) return class 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 type(self): return 'label' @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) @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 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 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 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 data(self): return self._data @data.setter def data(self, values): # ~ self._data = values self._gdm.removeAllRows() 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 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._data.sort(key=itemgetter(column), reverse=not 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 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): if control.type == 'grid' and key == 'addMouseListener': control.obj.addMouseListener(EventsMouseGrid(self.events)) continue 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, 'grid': UnoGrid, # ~ 'link': UnoLink, # ~ 'tab': UnoTab, # ~ 'roadmap': UnoRoadmap, # ~ 'image': UnoImage, # ~ 'radio': UnoRadio, # ~ 'groupbox': UnoGroupBox, # ~ 'tree': UnoTree, } return classes[tipo](obj) 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) # ~ column_model.setDefaultColumns(len(columns)) 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) # ~ mri(grid_column) return column_model def _set_image_url(self, path): if exists_path(path): return _path_url(path) return '' @catch_exception def add_control(self, properties): tipo = properties.pop('Type').lower() columns = properties.pop('Columns', ()) if tipo == 'grid': properties['ColumnModel'] = self._set_column_model(columns) if tipo == 'button' and 'ImageURL' in properties: properties['ImageURL'] = self._set_image_url(properties['ImageURL']) 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 def get_config_path(name='Work'): """ Return de path name in config http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html """ path = create_instance('com.sun.star.util.PathSettings') return _path_system(getattr(path, name)) def get_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) if filters: file_picker.setCurrentFilter(filters[0][0]) for f in filters: file_picker.appendFilter(f[0], f[1]) if file_picker.execute(): if multiple: return [_path_system(f) for f in file_picker.getSelectedFiles()] return _path_system(file_picker.getSelectedFiles()[0]) return '' def get_info_path(path): path, filename = os.path.split(path) name, extension = os.path.splitext(filename) return (path, filename, name, extension) def get_path_extension(id): pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') path = _path_system(pip.getPackageLocation(id)) return path def inputbox(message, default='', title=TITLE): 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(dlg) 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, } 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 new_doc(type_doc=CALC): path = 'private:factory/s{}'.format(type_doc) doc = get_desktop().loadComponentFromURL(path, '_default', 0, ()) return _get_class_doc(doc) 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 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 = _properties(kwargs) opt = dict_to_property(kwargs) doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt) if doc is None: return return _get_class_doc(doc) def open_file(path): if IS_WIN: os.startfile(path) else: subprocess.Popen(['xdg-open', path]) return def join(*paths): return os.path.join(*paths) def is_dir(path): return Path(path).is_dir() def is_file(path): return Path(path).is_file() def get_file_size(path): return Path(path).stat().st_size def is_created(path): return is_file(path) and bool(get_file_size(path)) def replace_ext(path, extension): path, _, name, _ = get_info_path(path) return '{}/{}.{}'.format(path, name, extension) def zip_names(path): with zipfile.ZipFile(path) as z: names = z.namelist() return names def run(command, wait=False): # ~ debug(command) # ~ debug(shlex.split(command)) try: if wait: # ~ p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) # ~ p.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) def zip(source, target='', mode='w', pwd=''): if pwd: return _zippwd(source, target, pwd) if isinstance(source, (tuple, list)): if not target: return False with zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) as z: for path in source: _, name, _, _ = get_info_path(path) z.write(path, name) return is_created(target) if is_file(source): if not target: target = replace_ext(source, 'zip') z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) _, name, _, _ = get_info_path(source) z.write(source, name) z.close() return is_created(target) if not target: target = join( PurePath(source).parent, '{}.zip'.format(PurePath(source).name)) z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) root_len = len(os.path.abspath(source)) for root, dirs, files in os.walk(source): relative = os.path.abspath(root)[root_len:] for f in files: fullpath = join(root, f) file_name = join(relative, f) z.write(fullpath, file_name) z.close() return is_created(target) 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 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 def kill(path): p = Path(path) if p.is_file(): try: p.unlink() except: pass elif p.is_dir(): p.rmdir() 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 def set_clipboard(value): ts = TextTransferable(value) sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') sc.setContents(ts, None) return def copy(doc=None): if doc is None: doc = get_document() if hasattr(doc, 'frame'): frame = doc.frame else: frame = doc.getCurrentController().getFrame() dispatch = get_dispatch() dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ()) return def get_epoch(): now = datetime.datetime.now() return int(time.mktime(now.timetuple())) def file_copy(source, target='', name=''): p, f, n, e = get_info_path(source) if target: p = target if name: n = name path_new = join(p, '{}{}'.format(n, e)) shutil.copy(source, path_new) return def get_files(path, ext='*'): docs = [] for folder, _, files in os.walk(path): pattern = re.compile(r'\.{}'.format(ext), re.IGNORECASE) docs += [join(folder, f) for f in files if pattern.search(f)] return docs 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 # ~ name = 'com.sun.star.configuration.ConfigurationProvider' # ~ cp = create_instance(name, True) # ~ node = PropertyValue(Name='nodepath', Value=NODE_SETTING) # ~ try: # ~ cua = cp.createInstanceWithArguments( # ~ 'com.sun.star.configuration.ConfigurationUpdateAccess', (node,)) # ~ cua.setPropertyValue(key, json.dumps(value)) # ~ cua.commitChanges() # ~ except Exception as e: # ~ log.error(e, exc_info=True) # ~ return False