diff --git a/docs/source/application.rst b/docs/source/application.rst new file mode 100644 index 0000000..cee96d6 --- /dev/null +++ b/docs/source/application.rst @@ -0,0 +1,104 @@ + +Application +=========== + +Remember, always import library. + +.. code-block:: python + + import easymacro as app + + +Create instances +---------------- + +* Instances without context + +.. code-block:: python + + toolkit = app.create_instance('com.sun.star.awt.Toolkit') + +* Instances with context + +.. code-block:: python + + service = 'com.sun.star.awt.DialogProvider2' + dialog = app.create_instance(service, True) + + +* Get desktop + +.. code-block:: python + + desktop = app.create_instance('com.sun.star.frame.Desktop', True) + +Or + +.. code-block:: python + + desktop = app.lo.desktop + + +Fonst +----- + +* Get all fonts + +.. code-block:: python + + fonts = app.lo.fonts() + for f in fonts: + print(f'Name: {f.Name} - StyleName: {f.StyleName}') + + +Filters +------- + +* Get all `support filters`_ + +.. code-block:: python + + filters = app.lo.filters() + ds = [] + for f in filters: + data = f"UI Name: {f['UIName']} - Name: {f['Name']} - Type: {f['Type']}" + app.debug(data) + + +Call dispatch +------------- + +You can call any `dispatch command`_ used only if property or method no exists in original object or in `easymacro.py` + +.. code-block:: python + + doc = app.docs.active + command = 'Gallery' + app.lo.dispatch(doc, command) + +Method automatically add `.uno:` + + +Disable or enabled commands +--------------------------- + +You can disable any command. + +.. code-block:: python + + cmd = 'OpenFromCalc' + result = app.command.disable(cmd) + app.debug(result) + +And enabled. + +.. code-block:: python + + result = app.command.enabled(cmd) + app.debug(result) + +`OpenFromCalc` is options for open documents in calc, disable or enabled menu entry and icon toolbar. + + +.. _dispatch command: https://wiki.documentfoundation.org/Development/DispatchCommands +.. _support filters: https://help.libreoffice.org/latest/en-US/text/shared/guide/convertfilters.html diff --git a/docs/source/documents.rst b/docs/source/documents.rst new file mode 100644 index 0000000..559e774 --- /dev/null +++ b/docs/source/documents.rst @@ -0,0 +1,486 @@ + +Documents +========= + +Remember, always import library. + +.. code-block:: python + + import easymacro as app + + +Current doc +----------- + +.. code-block:: python + + doc = app.docs.active + app.msgbox(doc.title) + + +Iter docs +--------- + +.. code-block:: python + + for doc in app.docs: + app.debug(doc.type, doc.title) + + +Count +----- + +.. code-block:: python + + count = len(app.docs) + app.debug(count) + + +Get by index +------------ + +.. code-block:: python + + doc = app.docs[1] + app.debug(doc.type, doc.title) + + +Get by name +----------- + +.. code-block:: python + + name = 'MyDoc.ods' + if name in app.docs: + doc = app.docs[name] + app.debug(doc.type, doc.title) + + +If contain +---------- + +.. code-block:: python + + result = 'myfile.ods' in app.docs + app.debug(result) + + +New +--- + +For default create new Calc document. + +.. code-block:: python + + doc = app.docs.new() + app.debug(doc.type) + + +For new Writer document. + +.. code-block:: python + + doc = app.docs.new('writer') + app.debug(doc.type) + + +With arguments. + +.. code-block:: python + + args= {'Hidden': True} + doc = app.docs.new('writer', args) + msg = f'{doc.type} - {doc.title}' + app.msgbox(msg) + doc.visible = True + + +Other documents. + +.. code-block:: python + + doc = app.docs.new('draw') + app.debug(doc.type) + + doc = app.docs.new('impress') + app.debug(doc.type) + + doc = app.docs.new('math') + app.debug(doc.type) + + +Open +---- + +.. code-block:: python + + path = '/home/mau/ask_example.ods' + doc = app.docs.open(path) + + +While LibreOffice support format, you can open arbitrary file. + +.. code-block:: python + + path = '/home/mau/example.xlsx' + doc = app.docs.open(path) + + +With arguments. + +.. code-block:: python + + path = '/home/mau/example.ods' + args = {'Password': 'letmein'} + doc = app.docs.open(path, args) + + +Save +---- + +* Save new documents + +.. code-block:: python + + path = '/home/mau/myfile.ods' + doc = app.docs.new() + doc.save(path) + +* If previously open and modify then. + +.. code-block:: python + + doc.save() + +* Open exists file and save with other name. + +.. code-block:: python + + path = '/home/mau/myfile.ods' + doc = app.docs.open(path) + new_path = '/home/mau/other_name.ods' + doc.save(new_path) + + +Close +----- + +.. code-block:: python + + doc = app.docs.new() + app.msgbox(doc.title) + doc.close() + + +To PDF +------ + +* Save in path + +.. code-block:: python + + doc = app.active + path = '/home/mau/test.pdf' + doc.to_pdf(path) + +* Save in memory + +.. code-block:: python + + doc = app.active + pdf = doc.to_pdf() + + +Export +------ + +* Export common formats + +.. code-block:: python + + doc = app.docs.new() + path = '/home/mau/myfile.xlsx' + filter_name = 'xlsx' + doc.export(path, filter_name) + + path = '/home/mau/myfile.xls' + filter_name = 'xls' + doc.export(path, filter_name) + + doc = app.docs.new('writer') + path = '/home/mau/myfile.docx' + filter_name = 'docx' + doc.export(path, filter_name) + + path = '/home/mau/myfile.doc' + filter_name = 'doc' + doc.export(path, filter_name) + + path = '/home/mau/myfile.rtf' + filter_name = 'rtf' + doc.export(path, filter_name) + + +* Export in memory. + +.. code-block:: python + + doc = app.docs.new() + filter_name = 'xlsx' + excel_doc = doc.export(filter_name=filter_name) + + +Properties +---------- + +Common properties for documents + +obj +^^^ + +* Get original object pyUNO (read only) + +.. code-block:: python + + doc = app.active + app.debug(type(doc)) + app.debug(type(doc.obj)) + + +title +^^^^^ + +.. code-block:: python + + doc = app.active + app.debug(doc.title) + doc.title = 'New title' + app.debug(doc.title) + + +type +^^^^ + +* Get type document: calc, writer, etc. (read only) + +.. code-block:: python + + doc = app.active + app.debug(doc.type) + + +uid +^^^ + +* Get internal RuntimeUID form document. (read only) + +.. code-block:: python + + doc = app.active + app.debug(doc.uid) + + +is_saved +^^^^^^^^ + +* If document is saved or not (read only) + +.. code-block:: python + + doc = app.active + app.debug(doc.is_saved) + + +is_modified +^^^^^^^^^^^ + +* If document has been modified (read only) + +.. code-block:: python + + doc = app.active + app.debug(doc.is_modified) + + +is_read_only +^^^^^^^^^^^^ + +.. code-block:: python + + doc = app.active + app.debug(doc.is_read_only) + + +path +^^^^ + +* Get path of document. (read only) + +.. code-block:: python + + doc = app.active + app.debug(doc.path) + + +dir +^^^ + +* Get only directory from path saved (read only) + +.. code-block:: python + + doc = app.active + app.debug(doc.dir) + + +file_name +^^^^^^^^^ + +* Get only file name from path saved (read only) + +.. code-block:: python + + doc = app.active + app.debug(doc.file_name) + + +name +^^^^ + +* Get only name without extension (read only) + +.. code-block:: python + + doc = app.active + app.debug(doc.name) + + +visible +^^^^^^^ + +* Hide or show document. + +.. code-block:: python + + doc = app.active + doc.visible = False + app.msgbox(doc.visible) + doc.visible = True + + +zoom +^^^^ + +* Get or set zoom value. + +.. code-block:: python + + doc = app.active + zoom = doc.zoom + app.msgbox(zoom) + doc.zoom = zoom * 2 + app.msgbox(doc.zoom) + doc.zoom = zoom + + +status_bar +^^^^^^^^^^ + +* Get status bar, always control in other thread. + +.. code-block:: python + + @app.run_in_thread + def update_status_bar(sb, text, limit): + sb.start(text, limit) + for i in range(limit): + sb.setValue(i) + app.sleep(1) + # ~ Is important free status bar + sb.end() + return + + def main(): + doc = app.active + update_status_bar(doc.status_bar, 'Line', 10) + return + + +selection +^^^^^^^^^ + +* **CAUTION**: Selection can be many things. + +.. code-block:: python + + doc = app.active + selection = doc.selection + app.debug(selection) + + +Methods +------- + +set_focus +^^^^^^^^^ + +.. code-block:: python + + for doc in app.docs: + app.debug(doc.title) + doc.set_focus() + app.sleep(1) + +copy +^^^^ + +* Copy current selection + +.. code-block:: python + + doc = app.active + doc.copy() + + +paste +^^^^^ + +* Paste any content in clipboard + +.. code-block:: python + + doc = app.active + doc.paste() + + +paste_special +^^^^^^^^^^^^^ + +* Show dialog box Paste Special + +.. code-block:: python + + doc = app.active + doc.paste_special() + +paste_values +^^^^^^^^^^^^ + +* Paste only values + +.. code-block:: python + + doc = app.active + doc.paste_values() + + +clear_undo +^^^^^^^^^^ + +* Clear history undo + +.. code-block:: python + + doc = app.active + doc.clear_undo() + diff --git a/docs/source/easymacro.rst b/docs/source/easymacro.rst index 9085ce0..a1bd07e 100644 --- a/docs/source/easymacro.rst +++ b/docs/source/easymacro.rst @@ -1,5 +1,5 @@ -easymacro module -================ +API easymacro +============= .. automodule:: easymacro :members: diff --git a/docs/source/generated/easymacro.rst b/docs/source/generated/easymacro.rst index 71166a2..40429ed 100644 --- a/docs/source/generated/easymacro.rst +++ b/docs/source/generated/easymacro.rst @@ -45,7 +45,20 @@ Dates Email Hash + IOStream Json + LODocBase + LODocCalc + LODocDraw + LODocDrawImpress + LODocIDE + LODocImpress + LODocMain + LODocMath + LODocWriter + LODocument + LODocuments + LOMain LOServer MBT Macro @@ -54,7 +67,7 @@ Shell Timer Url - commands + classproperty diff --git a/docs/source/index.rst b/docs/source/index.rst index 5d6b626..8d28024 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -26,7 +26,9 @@ You can used **easymacro** with any extension or directly in your macros. tools paths email - api + application + documents + easymacro Indices and tables diff --git a/source/easymacro.py b/source/easymacro.py index 37921bf..d02ca05 100644 --- a/source/easymacro.py +++ b/source/easymacro.py @@ -22,6 +22,7 @@ import csv import datetime import getpass import hashlib +import io import json import logging import os @@ -59,9 +60,11 @@ from email.utils import formatdate from email import encoders import uno +import unohelper from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.beans import PropertyValue, NamedValue +from com.sun.star.io import IOException, XOutputStream from com.sun.star.ui.dialogs import TemplateDescription @@ -303,7 +306,7 @@ def mri(obj: Any) -> None: `See MRI `_ """ - mri = create_instance('mytools.Mri') + m = create_instance('mytools.Mri') if m is None: msg = 'Extension MRI not found' error(msg) @@ -311,7 +314,7 @@ def mri(obj: Any) -> None: if hasattr(obj, 'obj'): obj = obj.obj - mri.inspect(obj) + m.inspect(obj) return @@ -467,8 +470,9 @@ def render(template, data): # Classes +# ~ https://github.com/django/django/blob/main/django/utils/functional.py#L61 +class classproperty: -class _classproperty: def __init__(self, method=None): self.fget = method @@ -485,7 +489,7 @@ class Dates(object): """ _start = None - @_classproperty + @classproperty def now(cls): """Current local date and time @@ -494,7 +498,7 @@ class Dates(object): """ return datetime.datetime.now().replace(microsecond=0) - @_classproperty + @classproperty def today(cls): """Current local date @@ -503,7 +507,7 @@ class Dates(object): """ return datetime.date.today() - @_classproperty + @classproperty def time(cls): """Current local time @@ -513,7 +517,7 @@ class Dates(object): t = cls.now.time().replace(microsecond=0) return t - @_classproperty + @classproperty def epoch(cls): """Get unix time @@ -927,30 +931,30 @@ class Paths(object): } return data - @_classproperty + @classproperty def home(self): """Get user home""" return str(Path.home()) - @_classproperty + @classproperty def documents(self): """Get user save documents""" return self.config() - @_classproperty + @classproperty def user_profile(self): """Get path user profile""" path = self.config('UserConfig') path = str(Path(path).parent) return path - @_classproperty + @classproperty def user_config(self): """Get path config in user profile""" path = self.config('UserConfig') return path - @_classproperty + @classproperty def python(self): """Get path executable python""" if IS_WIN: @@ -1918,9 +1922,48 @@ class Color(object): COLOR_ON_FOCUS = Color()('LightYellow') -class LO(object): - """Class for LibreOffice - """ +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 LOMain(): + """Classe for LibreOffice""" + class _commands(): """Class for disable and enable commands @@ -1971,7 +2014,7 @@ class LO(object): """ return cls._set_app_command(command, True) - @classmethod + @classmethod def enabled(cls, command): """Enabled UNO command @@ -1982,12 +2025,13 @@ class LO(object): """ return cls._set_app_command(command, False) - @_classproperty + # ~ @classproperty def cmd(cls): + """Disable or enable commands""" return cls._commands - @_classproperty - def desktop(cls) -> Any: + # ~ @classproperty + def desktop(cls): """Create desktop instance :return: Desktop instance @@ -2046,6 +2090,469 @@ class LO(object): return rows +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 frame(self): + """Get frame document""" + return self._cc.getFrame() + + @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 _create_instance(self, name): + obj = self.obj.createInstance(name) + return obj + + 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(): + _type = 'main' + + def __init__(self, obj): + self._obj = obj + + @property + def obj(self): + return self._obj + + @property + def type(self): + return self._type + + +class LODocCalc(LODocument): + _type = 'calc' + + def __init__(self, obj): + super().__init__(obj) + + +class LODocWriter(LODocument): + _type = 'writer' + + def __init__(self, obj): + super().__init__(obj) + self._view_settings = self._cc.ViewSettings + + @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) + + +class LODocuments(): + """Classe for documents + """ + _classes = { + 'com.sun.star.sheet.SpreadsheetDocument': LODocCalc, + 'com.sun.star.text.TextDocument': LODocWriter, + 'com.sun.star.drawing.DrawingDocument': LODocDraw, + 'com.sun.star.presentation.PresentationDocument': LODocImpress, + 'com.sun.star.formula.FormulaProperties': LODocMath, + 'com.sun.star.script.BasicIDE': LODocIDE, + 'com.sun.star.sdb.OfficeDatabaseDocument': LODocBase, + 'com.sun.star.frame.StartModule': LODocMain + } + # ~ BASE: 'com.sun.star.sdb.DocumentDataSource', + + _desktop = LOMain.desktop + + def __len__(self): + # ~ len(self._desktop.Components) + for i, _ in enumerate(self._desktop.Components): + pass + return i + 1 + + def __getitem__(self, index): + # ~ self._desktop.Components[index] + obj = None + + for i, doc in enumerate(self._desktop.Components): + if isinstance(index, int) and i == index: + obj = self._get_class_doc(doc) + break + elif isinstance(index, str) and doc.Title == index: + obj = self._get_class_doc(doc) + break + + return obj + + def __contains__(self, item): + doc = self[item] + return not doc is None + + def __iter__(self): + self._i = -1 + return self + + def __next__(self): + self._i += 1 + doc = self[self._i] + if doc is None: + raise StopIteration + else: + return doc + + def _get_class_doc(self, doc): + """Identify type doc""" + main = 'com.sun.star.frame.StartModule' + if doc.supportsService(main): + return self._classes[main](doc) + + mm = create_instance('com.sun.star.frame.ModuleManager') + type_module = mm.identify(doc) + return self._classes[type_module](doc) + + @property + def active(self): + """Get active doc""" + doc = self._desktop.getCurrentComponent() + obj = self._get_class_doc(doc) + return obj + + def new(self, type_doc: str='calc', args: dict={}): + """Create new document + + :param type_doc: The type doc to create, default is Calc + :type type_doc: str + :param args: Extra argument + :type args: dict + :return: New document + :rtype: Custom classe + """ + url = f'private:factory/s{type_doc}' + opt = dict_to_property(args) + doc = self._desktop.loadComponentFromURL(url, '_default', 0, opt) + obj = self._get_class_doc(doc) + return obj + + def open(self, path: str, args: dict={}): + """ Open document from path + + :param path: Path to document + :type path: str + :param args: Extra argument + 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 + :type args: dict + + `See API XComponentLoader `_ + `See API MediaDescriptor `_ + """ + url = Paths.to_url(path) + opt = dict_to_property(args) + doc = self._desktop.loadComponentFromURL(url, '_default', 0, opt) + if doc is None: + return + + obj = self._get_class_doc(doc) + return obj + + def __getattr__(name): classes = { 'dates': Dates, @@ -2059,8 +2566,11 @@ def __getattr__(name): 'url': Url, 'email': Email, 'color': Color(), - 'lo': LO, - 'desktop': LO.desktop, + 'io': IOStream, + 'lo': LOMain, + 'command': LOMain.cmd, + 'docs': LODocuments(), + 'active': LODocuments().active, } if name in classes: return classes[name]