#!/usr/bin/env python3 # == Rapid Develop Macros in LibreOffice == # ~ https://git.cuates.net/elmau/easymacro # ~ easymacro 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. # ~ easymacro 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 easymacro. If not, see . import io import logging import os import platform import re import socket import ssl from socket import timeout from urllib import parse from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError from com.sun.star.awt import Rectangle, Size, Point from com.sun.star.awt import Key, KeyEvent, KeyModifier from com.sun.star.datatransfer import XTransferable, DataFlavor from com.sun.star.io import IOException, XOutputStream from com.sun.star.sheet import XRangeSelectionListener from com.sun.star.container import NoSuchElementException SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc' DIRS = {} class ClipBoard(object): SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard' CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' class TextTransferable(unohelper.Base, XTransferable): def __init__(self, text): df = DataFlavor() df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT df.HumanPresentableName = 'encoded text utf-16' self.flavors = (df,) self._data = text def getTransferData(self, flavor): return self._data def getTransferDataFlavors(self): return self.flavors @classmethod def set(cls, value): ts = cls.TextTransferable(value) sc = create_instance(cls.SERVICE) sc.setContents(ts, None) return @classproperty def contents(cls): df = None text = '' sc = create_instance(cls.SERVICE) transferable = sc.getContents() data = transferable.getTransferDataFlavors() for df in data: if df.MimeType == cls.CLIPBOARD_FORMAT_TEXT: break if df: text = transferable.getTransferData(df) return text class IOStream(object): """Classe for input/output stream""" class OutputStream(unohelper.Base, XOutputStream): def __init__(self): self._buffer = b'' self.closed = 0 @property def buffer(self): return self._buffer def closeOutput(self): self.closed = 1 def writeBytes(self, seq): if seq.value: self._buffer = seq.value def flush(self): pass @classmethod def buffer(cls): return io.BytesIO() @classmethod def input(cls, buffer): service = 'com.sun.star.io.SequenceInputStream' stream = create_instance(service, True) stream.initialize((uno.ByteSequence(buffer.getvalue()),)) return stream @classmethod def output(cls): return cls.OutputStream() class EventsRangeSelectionListener(EventsListenerBase, XRangeSelectionListener): def __init__(self, controller): super().__init__(controller, '') def done(self, event): range_selection = event.RangeDescriptor event_name = 'range_selection_done' if hasattr(self._controller, event_name): getattr(self._controller, event_name)(range_selection) return def aborted(self, event): range_selection = event.RangeDescriptor event_name = 'range_selection_aborted' if hasattr(self._controller, event_name): getattr(self._controller, event_name)() return class LOShapes(object): _type = 'ShapeCollection' def __init__(self, obj): self._obj = obj def __len__(self): return self.obj.Count def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __iter__(self): self._index = 0 return self def __next__(self): try: s = self.obj[self._index] shape = LOShape(s) except IndexError: raise StopIteration self._index += 1 return shape def __str__(self): return 'Shapes' @property def obj(self): return self._obj class LOShape(object): IMAGE = 'com.sun.star.drawing.GraphicObjectShape' def __init__(self, obj): self._obj = obj def __str__(self): return f'Shape: {self.name}' @property def obj(self): return self._obj @property def properties(self): # ~ properties = self.obj.PropertySetInfo.Properties # ~ data = {p.Name: getattr(self.obj, p.Name) for p in properties} data = self.obj.PropertySetInfo.Properties keys = [p.Name for p in data] values = self.obj.getPropertyValues(keys) data = dict(zip(keys, values)) return data @properties.setter def properties(self, values): _set_properties(self.obj, values) @property def shape_type(self): return self.obj.ShapeType @property def name(self): return self.obj.Name @name.setter def name(self, value): self.obj.Name = value @property def is_image(self): return self.shape_type == self.IMAGE @property def is_shape(self): return self.shape_type != self.IMAGE @property def size(self): s = self.obj.Size return s @size.setter def size(self, value): self.obj.Size = value @property def width(self): s = self.obj.Size return s.Width @width.setter def width(self, value): s = self.size s.Width = value self.size = s @property def height(self): s = self.obj.Size return s.Height @height.setter def height(self, value): s = self.size s.Height = value self.size = s @property def position(self): return self.obj.Position @property def x(self): return self.position.X @property def y(self): return self.position.Y @property def string(self): return self.obj.String @string.setter def string(self, value): self.obj.String = value @property def title(self): return self.obj.Title @title.setter def title(self, value): self.obj.Title = value @property def description(self): return self.obj.Description @description.setter def description(self, value): self.obj.Description = value class LOShortCuts(object): """Classe for manager shortcuts""" KEYS = {getattr(Key, k): k for k in dir(Key)} MODIFIERS = { 'shift': KeyModifier.SHIFT, 'ctrl': KeyModifier.MOD1, 'alt': KeyModifier.MOD2, 'ctrlmac': KeyModifier.MOD3, } COMBINATIONS = { 0: '', 1: 'shift', 2: 'ctrl', 4: 'alt', 8: 'ctrlmac', 3: 'shift+ctrl', 5: 'shift+alt', 9: 'shift+ctrlmac', 6: 'ctrl+alt', 10: 'ctrl+ctrlmac', 12: 'alt+ctrlmac', 7: 'shift+ctrl+alt', 11: 'shift+ctrl+ctrlmac', 13: 'shift+alt+ctrlmac', 14: 'ctrl+alt+ctrlmac', 15: 'shift+ctrl+alt+ctrlmac', } def __init__(self, app: str=''): self._app = app service = 'com.sun.star.ui.GlobalAcceleratorConfiguration' if app: service = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' type_app = LODocuments.TYPES[app] manager = create_instance(service, True) uicm = manager.getUIConfigurationManager(type_app) self._config = uicm.ShortCutManager else: self._config = create_instance(service) def __getitem__(self, index): return LOShortCuts(index) def __contains__(self, item): cmd = self.get_by_shortcut(item) return bool(cmd) def __iter__(self): self._i = -1 return self def __next__(self): self._i += 1 try: event = self._config.AllKeyEvents[self._i] event = self._get_info(event) except IndexError: raise StopIteration return event @classmethod def to_key_event(cls, shortcut: str): """Convert from string shortcut (Shift+Ctrl+Alt+LETTER) to KeyEvent""" key_event = KeyEvent() keys = shortcut.split('+') try: for m in keys[:-1]: key_event.Modifiers += cls.MODIFIERS[m.lower()] key_event.KeyCode = getattr(Key, keys[-1].upper()) except Exception as e: error(e) key_event = None return key_event @classmethod def get_url_script(cls, command: Union[str, dict]) -> str: """Get uno command or url for macro""" url = command if isinstance(url, str) and not url.startswith('.uno:'): url = f'.uno:{command}' elif isinstance(url, dict): url = Macro.get_url_script(command) return url def _get_shortcut(self, k): """Get shortcut for key event""" # ~ print(k.KeyCode, str(k.KeyChar), k.KeyFunc, k.Modifiers) shortcut = f'{self.COMBINATIONS[k.Modifiers]}+{self.KEYS[k.KeyCode]}' return shortcut def _get_info(self, key): """Get shortcut and command""" cmd = self._config.getCommandByKeyEvent(key) shortcut = self._get_shortcut(key) return shortcut, cmd def get_all(self): """Get all events key""" events = [(self._get_info(k)) for k in self._config.AllKeyEvents] return events def get_by_command(self, command: Union[str, dict]): """Get shortcuts by command""" url = LOShortCuts.get_url_script(command) key_events = self._config.getKeyEventsByCommand(url) shortcuts = [self._get_shortcut(k) for k in key_events] return shortcuts def get_by_shortcut(self, shortcut: str): """Get command by shortcut""" command = '' key_event = LOShortCuts.to_key_event(shortcut) if key_event: command = self._config.getCommandByKeyEvent(key_event) return command def set(self, shortcut: str, command: Union[str, dict]) -> bool: """Set shortcut to command :param shortcut: Shortcut like Shift+Ctrl+Alt+LETTER :type shortcut: str :param command: Command tu assign, 'UNOCOMMAND' or dict with macro info :type command: str or dict :return: True if set sucesfully :rtype: bool """ result = True url = LOShortCuts.get_url_script(command) key_event = LOShortCuts.to_key_event(shortcut) try: self._config.setKeyEvent(key_event, url) self._config.store() except Exception as e: error(e) result = False return result def remove_by_shortcut(self, shortcut: str): """Remove by shortcut""" key_event = LOShortCuts.to_key_event(shortcut) try: self._config.removeKeyEvent(key_event) result = True except NoSuchElementException: debug(f'No exists: {shortcut}') result = False return result def remove_by_command(self, command: Union[str, dict]): """Remove by shortcut""" url = LOShortCuts.get_url_script(command) self._config.removeCommandFromAllKeyEvents(url) return def reset(self): """Reset configuration""" self._config.reset() self._config.store() return class LOMenuDebug(): """Classe for debug info menu""" @classmethod def _get_info(cls, menu, index): """Get every option menu""" line = f"({index}) {menu.get('CommandURL', '----------')}" submenu = menu.get('ItemDescriptorContainer', None) if not submenu is None: line += cls._get_submenus(submenu) return line @classmethod def _get_submenus(cls, menu, level=1): """Get submenus""" 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 += cls._get_submenus(submenu, level + 1) return line def __call__(cls, menu): for i, m in enumerate(menu): data = data_to_dict(m) print(cls._get_info(data, i)) return class LOMenuBase(): """Classe base for menus""" NODE = 'private:resource/menubar/menubar' config = None menus = None app = '' @classmethod def _get_index(cls, parent: Any, name: Union[int, str]=''): """Get index menu from name :param parent: Menu parent :type parent: pyUno :param name: Menu name for search if is str :type name: int or str :return: Index of menu :rtype: int """ index = None if isinstance(name, str) and name: for i, m in enumerate(parent): menu = data_to_dict(m) if menu.get('CommandURL', '') == name: index = i break elif isinstance(name, str): index = len(parent) - 1 elif isinstance(name, int): index = name return index @classmethod def _get_command_url(cls, menu: dict): """Get url from command and set shortcut :param menu: Menu data :type menu: dict :return: URL command :rtype: str """ shortcut = menu.pop('ShortCut', '') command = menu['CommandURL'] url = LOShortCuts.get_url_script(command) if shortcut: LOShortCuts(cls.app).set(shortcut, command) return url @classmethod def _save(cls, parent: Any, menu: dict, index: int): """Insert menu :param parent: Menu parent :type parent: pyUno :param menu: New menu data :type menu: dict :param index: Position to insert :type index: int """ # ~ Some day # ~ self._menus.insertByIndex(index, new_menu) properties = dict_to_property(menu, True) uno.invoke(parent, 'insertByIndex', (index, properties)) cls.config.replaceSettings(cls.NODE, cls.menus) return @classmethod def _insert_submenu(cls, parent: Any, menus: list): """Insert submenus recursively :param parent: Menu parent :type parent: pyUno :param menus: List of menus :type menus: list """ for i, menu in enumerate(menus): submenu = menu.pop('Submenu', False) if submenu: idc = cls.config.createSettings() menu['ItemDescriptorContainer'] = idc menu['Type'] = 0 if menu['Label'][0] == '-': menu['Type'] = 1 else: menu['CommandURL'] = cls._get_command_url(menu) cls._save(parent, menu, i) if submenu: cls._insert_submenu(idc, submenu) return @classmethod def _get_first_command(cls, command): url = command if isinstance(command, dict): url = Macro.get_url_script(command) return url @classmethod def insert(cls, parent: Any, menu: dict, after: Union[int, str]=''): """Insert new menu :param parent: Menu parent :type parent: pyUno :param menu: New menu data :type menu: dict :param after: After menu insert :type after: int or str """ index = cls._get_index(parent, after) + 1 submenu = menu.pop('Submenu', False) menu['Type'] = 0 idc = cls.config.createSettings() menu['ItemDescriptorContainer'] = idc menu['CommandURL'] = cls._get_first_command(menu['CommandURL']) cls._save(parent, menu, index) if submenu: cls._insert_submenu(idc, submenu) return @classmethod def remove(cls, parent: Any, name: Union[str, dict]): """Remove name in parent :param parent: Menu parent :type parent: pyUno :param menu: Menu name :type menu: str """ if isinstance(name, dict): name = Macro.get_url_script(name) index = cls._get_index(parent, name) if index is None: debug(f'Not found: {name}') return uno.invoke(parent, 'removeByIndex', (index,)) cls.config.replaceSettings(cls.NODE, cls.menus) cls.config.store() return class LOMenu(object): """Classe for individual menu""" def __init__(self, config: Any, menus: Any, app: str, menu: Any): """ :param config: Configuration Mananer :type config: pyUno :param menus: Menu bar main :type menus: pyUno :param app: Name LibreOffice module :type app: str :para menu: Particular menu :type menu: pyUno """ self._config = config self._menus = menus self._app = app self._parent = menu def __contains__(self, name): """If exists name in menu""" exists = False for m in self._parent: menu = data_to_dict(m) cmd = menu.get('CommandURL', '') if name == cmd: exists = True break return exists def __getitem__(self, index): """Index access""" if isinstance(index, int): menu = data_to_dict(self._parent[index]) else: for m in self._parent: menu = data_to_dict(m) cmd = menu.get('CommandURL', '') if cmd == index: break obj = LOMenu(self._config, self._menus, self._app, menu['ItemDescriptorContainer']) return obj def debug(self): """Debug menu""" LOMenuDebug()(self._parent) return def insert(self, menu: dict, after: Union[int, str]='', save: bool=True): """Insert new menu :param menu: New menu data :type menu: dict :param after: Insert in after menu :type after: int or str :param save: For persistente save :type save: bool """ LOMenuBase.config = self._config LOMenuBase.menus = self._menus LOMenuBase.app = self._app LOMenuBase.insert(self._parent, menu, after) if save: self._config.store() return def remove(self, menu: str): """Remove menu :param menu: Menu name :type menu: str """ LOMenuBase.config = self._config LOMenuBase.menus = self._menus LOMenuBase.remove(self._parent, menu) return class LOMenuApp(object): """Classe for manager menu by LibreOffice module""" NODE = 'private:resource/menubar/menubar' MENUS = { 'file': '.uno:PickList', 'picklist': '.uno:PickList', 'tools': '.uno:ToolsMenu', 'help': '.uno:HelpMenu', 'window': '.uno:WindowList', 'edit': '.uno:EditMenu', 'view': '.uno:ViewMenu', 'insert': '.uno:InsertMenu', 'format': '.uno:FormatMenu', 'styles': '.uno:FormatStylesMenu', 'formatstyles': '.uno:FormatStylesMenu', 'sheet': '.uno:SheetMenu', 'data': '.uno:DataMenu', 'table': '.uno:TableMenu', 'formatform': '.uno:FormatFormMenu', 'page': '.uno:PageMenu', 'shape': '.uno:ShapeMenu', 'slide': '.uno:SlideMenu', 'slideshow': '.uno:SlideShowMenu', } def __init__(self, app: str): """ :param app: LibreOffice Module: calc, writer, draw, impress, math, main :type app: str """ self._app = app self._config = self._get_config() self._menus = self._config.getSettings(self.NODE, True) def _get_config(self): """Get config manager""" service = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' type_app = LODocuments.TYPES[self._app] manager = create_instance(service, True) config = manager.getUIConfigurationManager(type_app) return config def debug(self): """Debug menu""" LOMenuDebug()(self._menus) return def __contains__(self, name): """If exists name in menu""" exists = False for m in self._menus: menu = data_to_dict(m) cmd = menu.get('CommandURL', '') if name == cmd: exists = True break return exists def __getitem__(self, index): """Index access""" if isinstance(index, int): menu = data_to_dict(self._menus[index]) else: for m in self._menus: menu = data_to_dict(m) cmd = menu.get('CommandURL', '') if cmd == index or cmd == self.MENUS[index.lower()]: break obj = LOMenu(self._config, self._menus, self._app, menu['ItemDescriptorContainer']) return obj def insert(self, menu: dict, after: Union[int, str]='', save: bool=True): """Insert new menu :param menu: New menu data :type menu: dict :param after: Insert in after menu :type after: int or str :param save: For persistente save :type save: bool """ LOMenuBase.config = self._config LOMenuBase.menus = self._menus LOMenuBase.app = self._app LOMenuBase.insert(self._menus, menu, after) if save: self._config.store() return def remove(self, menu: str): """Remove menu :param menu: Menu name :type menu: str """ LOMenuBase.config = self._config LOMenuBase.menus = self._menus LOMenuBase.remove(self._menus, menu) return class LOMenus(object): """Classe for manager menus""" def __getitem__(self, index): """Index access""" return LOMenuApp(index) class LOEvents(): def __init__(self, obj): self._obj = obj def __contains__(self, item): return self.obj.hasByName(item) def __getitem__(self, index): """Index access""" return self.obj.getByName(index) def __setitem__(self, name: str, macro: dict): """Set macro to event :param name: Event name :type name: str :param macro: Macro execute in event :type name: dict """ pv = '[]com.sun.star.beans.PropertyValue' args = () if macro: url = Macro.get_url_script(macro) args = dict_to_property(dict(EventType='Script', Script=url)) uno.invoke(self.obj, 'replaceByName', (name, uno.Any(pv, args))) @property def obj(self): return self._obj @property def names(self): return self.obj.ElementNames def remove(self, name): pv = '[]com.sun.star.beans.PropertyValue' uno.invoke(self.obj, 'replaceByName', (name, uno.Any(pv, ()))) return class LOMain(): """Classe for LibreOffice""" class commands(): """Class for disable and enable commands `See DispatchCommands `_ """ @classmethod def _set_app_command(cls, command: str, disable: bool): """Disable or enabled UNO command :param command: UNO command to disable or enabled :type command: str :param disable: True if disable, False if active :type disable: bool :return: True if correctly update, False if not. :rtype: bool """ NEW_NODE_NAME = f'zaz_disable_command_{command.lower()}' name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationUpdateAccess' node_name = '/org.openoffice.Office.Commands/Execute/Disabled' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) update = cp.createInstanceWithArguments(service, (node,)) result = True try: if disable: new_node = update.createInstanceWithArguments(()) new_node.setPropertyValue('Command', command) update.insertByName(NEW_NODE_NAME, new_node) else: update.removeByName(NEW_NODE_NAME) update.commitChanges() except Exception as e: result = False return result @classmethod def disable(cls, command: str): """Disable UNO command :param command: UNO command to disable :type command: str :return: True if correctly disable, False if not. :rtype: bool """ return cls._set_app_command(command, True) @classmethod def enabled(cls, command): """Enabled UNO command :param command: UNO command to enabled :type command: str :return: True if correctly disable, False if not. :rtype: bool """ return cls._set_app_command(command, False) @classproperty def cmd(cls): """Disable or enable commands""" return cls.commands @classproperty def desktop(cls): """Create desktop instance :return: Desktop instance :rtype: pyUno """ obj = create_instance('com.sun.star.frame.Desktop', True) return obj class LODocument(): def __init__(self, obj): self._obj = obj self._cc = 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 original pyUno object""" return self._obj @property def type(self): """Get type document""" return self._type @property def title(self): """Get title document""" return self.obj.getTitle() @title.setter def title(self, value): self.obj.setTitle(value) @property def uid(self): """Get Runtime UID""" return self.obj.RuntimeUID @property def is_saved(self): """Get is saved""" return self.obj.hasLocation() @property def is_modified(self): """Get is modified""" return self.obj.isModified() @property def is_read_only(self): """Get is read only""" return self.obj.isReadonly() @property def path(self): """Get path in system files""" return Paths.to_system(self.obj.URL) @property def dir(self): """Get directory from path""" return Paths(self.path).path @property def file_name(self): """Get only file name""" return Paths(self.path).file_name @property def name(self): """Get name without extension""" return Paths(self.path).name @property def visible(self): """Get windows visible""" w = self.frame.ContainerWindow return w.isVisible() @visible.setter def visible(self, value): w = self.frame.ContainerWindow w.setVisible(value) @property def zoom(self): """Get current zoom value""" return self._cc.ZoomValue @zoom.setter def zoom(self, value): self._cc.ZoomValue = value @property def status_bar(self): """Get status bar""" bar = self._cc.getStatusIndicator() return bar @property def selection(self): """Get current selecction""" sel = self.obj.CurrentSelection return sel @property def table_auto_formats(self): taf = create_instance('com.sun.star.sheet.TableAutoFormats') return taf.ElementNames def save(self, path: str='', args: dict={}) -> bool: """Save document :param path: Path to save document :type path: str :param args: Optional: Extra argument for save :type args: dict :return: True if save correctly, False if not :rtype: bool """ if not path: self.obj.store() return True path_save = Paths.to_url(path) opt = dict_to_property(args) try: self.obj.storeAsURL(path_save, opt) except Exception as e: error(e) return False return True def close(self): """Close document""" self.obj.close(True) return def to_pdf(self, path: str='', args: dict={}): """Export to PDF :param path: Path to export document :type path: str :param args: Optional: Extra argument for export :type args: dict :return: None if path or stream in memory :rtype: bytes or None `See PDF Export `_ """ stream = None path_pdf = 'private:stream' filter_name = f'{self.type}_pdf_Export' filter_data = dict_to_property(args, True) filters = { 'FilterName': filter_name, 'FilterData': filter_data, } if path: path_pdf = Paths.to_url(path) else: stream = IOStream.output() filters['OutputStream'] = stream opt = dict_to_property(filters) try: self.obj.storeToURL(path_pdf, opt) except Exception as e: error(e) if not stream is None: stream = stream.buffer return stream def export(self, path: str='', filter_name: str='', args: dict={}): """Export to others formats :param path: Path to export document :type path: str :param filter_name: Filter name to export :type filter_name: str :param args: Optional: Extra argument for export :type args: dict :return: None if path or stream in memory :rtype: bytes or None """ FILTERS = { 'xlsx': 'Calc MS Excel 2007 XML', 'xls': 'MS Excel 97', 'docx': 'MS Word 2007 XML', 'doc': 'MS Word 97', 'rtf': 'Rich Text Format', } stream = None path_target = 'private:stream' filter_name = FILTERS.get(filter_name, filter_name) filter_data = dict_to_property(args, True) filters = { 'FilterName': filter_name, 'FilterData': filter_data, } if path: path_target = Paths.to_url(path) else: stream = IOStream.output() filters['OutputStream'] = stream opt = dict_to_property(filters) try: self.obj.storeToURL(path_target, opt) except Exception as e: error(e) if not stream is None: stream = stream.buffer return stream def set_focus(self): """Send focus to windows""" w = self.frame.ComponentWindow w.setFocus() return def copy(self): """Copy current selection""" LOMain.dispatch(self.frame, 'Copy') return def paste(self): """Paste current content in clipboard""" sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') transferable = sc.getContents() self._cc.insertTransferable(transferable) return def paste_special(self): """Insert contents, show dialog box Paste Special""" LOMain.dispatch(self.frame, 'InsertContents') return def paste_values(self): """Paste only values""" args = { 'Flags': 'SVDT', # ~ 'FormulaCommand': 0, # ~ 'SkipEmptyCells': False, # ~ 'Transpose': False, # ~ 'AsLink': False, # ~ 'MoveMode': 4, } LOMain.dispatch(self.frame, 'InsertContents', args) return def clear_undo(self): """Clear history undo""" self.obj.getUndoManager().clear() return class LODocMain(): """Classe for start module""" _type = 'main' def __init__(self, obj): self._obj = obj @property def obj(self): return self._obj @property def type(self): return self._type class LOCellStyle(): def __init__(self, obj): self._obj = obj def __str__(self): return f'CellStyle: {self.name}' @property def obj(self): return self._obj @property def name(self): return self.obj.Name @property def properties(self): properties = self.obj.PropertySetInfo.Properties data = {p.Name: getattr(self.obj, p.Name) for p in properties} return data @properties.setter def properties(self, values): _set_properties(self.obj, values) class LOCellStyles(): def __init__(self, obj, doc): self._obj = obj self._doc = doc def __len__(self): return len(self.obj) def __getitem__(self, index): return LOCellStyle(self.obj[index]) def __setitem__(self, key, value): self.obj[key] = value def __delitem__(self, key): if not isinstance(key, str): key = key.Name del self.obj[key] def __contains__(self, item): return item in self.obj @property def obj(self): return self._obj @property def names(self): return self.obj.ElementNames def new(self, name: str): obj = self._doc.create_instance('com.sun.star.style.CellStyle') self.obj[name] = obj return LOCellStyle(obj) class LODocCalc(LODocument): """Classe for Calc module""" TYPE_RANGES = ('ScCellObj', 'ScCellRangeObj') RANGES = 'ScCellRangesObj' SHAPE = 'com.sun.star.drawing.SvxShapeCollection' _type = 'calc' def __init__(self, obj): super().__init__(obj) self._sheets = obj.Sheets self._listener_range_selection = None def __getitem__(self, index): """Index access""" return LOCalcSheet(self._sheets[index]) def __setitem__(self, key: str, value: Any): """Insert new sheet""" self._sheets[key] = value def __len__(self): return self._sheets.Count def __contains__(self, item): return item in self._sheets def __iter__(self): self._i = 0 return self def __next__(self): try: sheet = LOCalcSheet(self._sheets[self._i]) except Exception as e: raise StopIteration self._i += 1 return sheet def __str__(self): return f'Calc: {self.title}' @property def selection(self): sel = self.obj.CurrentSelection type_obj = sel.ImplementationName if type_obj in self.TYPE_RANGES: sel = LOCalcRange(sel) elif type_obj == self.RANGES: sel = LOCalcRanges(sel) elif type_obj == self.SHAPE: if len(sel) == 1: sel = LOShape(sel[0]) else: sel = LOShapes(sel) else: debug(type_obj) return sel @property def headers(self): """Get true if is visible columns/rows headers""" return self._cc.ColumnRowHeaders @headers.setter def headers(self, value): """Set visible columns/rows headers""" self._cc.ColumnRowHeaders = value @property def tabs(self): """Get true if is visible tab sheets""" return self._cc.SheetTabs @tabs.setter def tabs(self, value): """Set visible tab sheets""" self._cc.SheetTabs = value @property def names(self): """Get all sheet names""" names = self.obj.Sheets.ElementNames return names @property def active(self): """Get active sheet""" return LOCalcSheet(self._cc.ActiveSheet) @property def new_sheet(self): sheet = self._create_instance('com.sun.star.sheet.Spreadsheet') return sheet @property def events(self): return LOEvents(self.obj.Events) @property def cs(self): return self.cell_styles @property def cell_styles(self): obj = self.obj.StyleFamilies['CellStyles'] return LOCellStyles(obj, self) def activate(self, sheet: Any): """Activate sheet :param sheet: Sheet to activate :type sheet: str, pyUno or LOCalcSheet """ obj = sheet if isinstance(sheet, LOCalcSheet): obj = sheet.obj elif isinstance(sheet, str): obj = self._sheets[sheet] self._cc.setActiveSheet(obj) return def remove(self, name: str): """Remove sheet by name :param name: Name sheet will remove :type name: str """ if isinstance(name, LOCalcSheet): name = name.name self._sheets.removeByName(name) return def move(self, name:str, pos: int=-1): """Move sheet name to position :param name: Name sheet to move :type name: str :param pos: New position, if pos=-1 move to end :type pos: int """ index = pos if pos < 0: index = len(self) if isinstance(name, LOCalcSheet): name = name.name self._sheets.moveByName(name, index) return def _get_new_name_sheet(self, name): i = 1 new_name = f'{name}_{i}' while new_name in self: i += 1 new_name = f'{name}_{i}' return new_name def copy_sheet(self, name: Any, new_name: str='', pos: int=-1): """Copy sheet by name """ if isinstance(name, LOCalcSheet): name = name.name index = pos if pos < 0: index = len(self) if not new_name: new_name = self._get_new_name_sheet(name) self._sheets.copyByName(name, new_name, index) return LOCalcSheet(self._sheets[new_name]) def copy_from(self, doc: Any, source: Any=None, target: Any=None, pos: int=-1): """Copy sheet from document """ index = pos if pos < 0: index = len(self) names = source if not source: names = doc.names elif isinstance(source, str): names = (source,) elif isinstance(source, LOCalcSheet): names = (source.name,) new_names = target if not target: new_names = names elif isinstance(target, str): new_names = (target,) for i, name in enumerate(names): self._sheets.importSheet(doc.obj, name, index + i) self[index + i].name = new_names[i] return LOCalcSheet(self._sheets[index]) def sort(self, reverse=False): """Sort sheets by name :param reverse: For order in reverse :type reverse: bool """ names = sorted(self.names, reverse=reverse) for i, n in enumerate(names): self.move(n, i) return @run_in_thread def start_range_selection(self, controllers: Any, args: dict={}): """Start select range selection by user `See Api RangeSelectionArguments `_ """ if args: args['CloseOnMouseRelease'] = args.get('CloseOnMouseRelease', True) else: args = dict( Title = 'Please select a range', CloseOnMouseRelease = True) properties = dict_to_property(args) self._listener_range_selection = EventsRangeSelectionListener(controllers(self)) self._cc.addRangeSelectionListener(self._listener_range_selection) self._cc.startRangeSelection(properties) return def remove_range_selection_listener(self): if not self._listener_range_selection is None: self._cc.removeRangeSelectionListener(self._listener_range_selection) return def select(self, rango: Any): obj = rango if hasattr(rango, 'obj'): obj = rango.obj self._cc.select(obj) return @property def ranges(self): obj = self._create_instance('com.sun.star.sheet.SheetCellRanges') return LOCalcRanges(obj) def get_ranges(self, address: str): ranges = self.ranges ranges.add([sheet[address] for sheet in self]) return ranges class LOCalcRanges(object): def __init__(self, obj): self._obj = obj def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __len__(self): return self._obj.Count def __iter__(self): self._index = 0 return self def __next__(self): try: r = self.obj[self._index] rango = LOCalcRange(r) except IndexError: raise StopIteration self._index += 1 return rango def __contains__(self, item): return self._obj.hasByName(item.name) def __getitem__(self, index): r = self.obj[index] rango = LOCalcRange(r) return rango def __str__(self): s = f'Ranges: {self.names}' return s @property def obj(self): return self._obj @property def names(self): return self.obj.ElementNames @property def data(self): rows = [r.data for r in self] return rows @data.setter def data(self, values): for i, data in enumerate(values): self[i].data = data @property def style(self): return '' @style.setter def style(self, value): for r in self: r.style = value def add(self, rangos: Any): if isinstance(rangos, LOCalcRange): rangos = (rangos,) for r in rangos: self.obj.addRangeAddress(r.range_address, False) return def remove(self, rangos: Any): if isinstance(rangos, LOCalcRange): rangos = (rangos,) for r in rangos: self.obj.removeRangeAddress(r.range_address) return class LOWriterTextRange(object): def __init__(self, obj, doc): self._obj = obj self._doc = doc @property def obj(self): return self._obj @property def text(self): return self.obj.Text @property def cursor(self): return self.text.createTextCursorByRange(self.obj) def insert_comment(self, content: str, author: str='', dt: Any=None): # ~ range.Text.insertTextContent(cursor, comment, False) comment = self._doc._create_instance('com.sun.star.text.textfield.Annotation') comment.Content = content comment.Author = author comment.attach(self.cursor.End) return class LODocWriter(LODocument): _type = 'writer' TEXT_RANGES = 'SwXTextRanges' def __init__(self, obj): super().__init__(obj) self._view_settings = self._cc.ViewSettings @property def selection(self): sel = self.obj.CurrentSelection type_obj = sel.ImplementationName if type_obj == self.TEXT_RANGES: if len(sel) == 1: sel = LOWriterTextRange(sel[0], self) return sel @property def zoom(self): return self._view_settings.ZoomValue @zoom.setter def zoom(self, value): self._view_settings.ZoomValue = value class LODocDrawImpress(LODocument): def __init__(self, obj): super().__init__(obj) class LODocDraw(LODocDrawImpress): _type = 'draw' def __init__(self, obj): super().__init__(obj) class LODocImpress(LODocDrawImpress): _type = 'impress' def __init__(self, obj): super().__init__(obj) class LODocMath(LODocDrawImpress): _type = 'math' def __init__(self, obj): super().__init__(obj) class LODocBase(LODocument): _type = 'base' def __init__(self, obj): super().__init__(obj) class LODocIDE(LODocument): _type = 'basicide' def __init__(self, obj): super().__init__(obj) def __getattr__(name): classes = { 'io': IOStream, 'clipboard': ClipBoard, 'shortcuts': LOShortCuts(), 'menus': LOMenus(), 'lo': LOMain, 'command': LOMain.cmd, 'docs': LODocuments(), } if name in classes: return classes[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") class LOServer(object): """Started LibeOffice like server """ 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): """Stop server """ 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