From 06dd86b77e853b25f02cff74acd44cabfb7a1733 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 15 Oct 2019 17:23:35 -0500 Subject: [PATCH] Add support for show in main application --- CHANGELOG | 6 + VERSION | 2 +- conf.py | 28 +- easymacro.py | 2319 +++++++++++++++++++++++++++++--- files/ZAZFavorites_v0.3.0.oxt | Bin 0 -> 65455 bytes source/Jobs.xcu | 28 - source/META-INF/manifest.xml | 11 +- source/ZAZFavorites.py | 4 +- source/description.xml | 2 +- source/pythonpath/easymacro.py | 2319 +++++++++++++++++++++++++++++--- zaz.py | 185 ++- 11 files changed, 4473 insertions(+), 431 deletions(-) create mode 100644 files/ZAZFavorites_v0.3.0.oxt delete mode 100755 source/Jobs.xcu diff --git a/CHANGELOG b/CHANGELOG index 3b183ba..1d1843c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +v 0.3.0 [15-oct-2019] +--------------------- + - Add menus in start application. + - Update easymacro.py + + v 0.2.0 [27-sep-2019] --------------------- - Update easymacro.py diff --git a/VERSION b/VERSION index 5faa42c..69367fd 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -0.2.0 +0.3.0 diff --git a/conf.py b/conf.py index 0543de4..839b08a 100644 --- a/conf.py +++ b/conf.py @@ -26,7 +26,7 @@ import logging TYPE_EXTENSION = 1 # ~ https://semver.org/ -VERSION = '0.2.0' +VERSION = '0.3.0' # ~ Your great extension name, not used spaces NAME = 'ZAZFavorites' @@ -184,7 +184,6 @@ FILES = { 'update': f'{NAME.lower()}.update.xml', 'addin': 'CalcAddIn.xcu', 'shortcut': 'Accelerators.xcu', - 'jobs': 'Jobs.xcu', 'easymacro': True, } @@ -206,6 +205,7 @@ PATHS = { 'regmerge': '/usr/lib/libreoffice/program/regmerge', 'soffice': ('soffice', PROGRAM, FILE_TEST), 'install': ('unopkg', 'add', '-v', '-f', '-s'), + 'profile': '/home/mau/.config/libreoffice/4/user', } @@ -428,21 +428,6 @@ FILE_ADDONS = f""" """ -NODE_ADDONS = '\n ' -if TYPE_EXTENSION > 1: - NODE_ADDONS = f'\n ' -if TYPE_EXTENSION == 3: - NODE_ADDONS += '\n ' - -FILE_MANIFEST = f""" - - - - {NODE_ADDONS} - -""" - - FILE_UPDATE = '' if URL_XML_UPDATE: FILE_UPDATE = f""" @@ -627,9 +612,16 @@ FILE_SHORTCUTS = f""" """ +DATA_MANIFEST = [FILES['py'], f"Office/{FILES['shortcut']}", 'Addons.xcu'] +if TYPE_EXTENSION > 1: + DATA_MANIFEST.append(FILES['rdb']) +if TYPE_EXTENSION == 3: + DATA_MANIFEST.append('CalcAddIn.xcu') + + DATA = { 'py': FILE_PY, - 'manifest': FILE_MANIFEST, + 'manifest': DATA_MANIFEST, 'description': FILE_DESCRIPTION, 'addons': FILE_ADDONS, 'update': FILE_UPDATE, diff --git a/easymacro.py b/easymacro.py index 3bf53b9..bd5f1aa 100644 --- a/easymacro.py +++ b/easymacro.py @@ -18,6 +18,7 @@ # ~ along with ZAZ. If not, see . import base64 +import csv import ctypes import datetime import errno @@ -30,6 +31,7 @@ import platform import re import shlex import shutil +import socket import subprocess import sys import tempfile @@ -59,25 +61,37 @@ import mailbox import uno import unohelper from com.sun.star.util import Time, Date, DateTime -from com.sun.star.beans import PropertyValue +from com.sun.star.beans import PropertyValue, NamedValue 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.awt import Rectangle +from com.sun.star.awt import KeyEvent +from com.sun.star.awt.KeyFunction import QUIT from com.sun.star.datatransfer import XTransferable, DataFlavor from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK 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 +from com.sun.star.awt import XMouseMotionListener +from com.sun.star.util import XModifyListener +from com.sun.star.awt import XTopWindowListener +from com.sun.star.awt import XWindowListener +from com.sun.star.awt import XMenuListener +from com.sun.star.awt import XKeyListener +from com.sun.star.awt import XItemListener +from com.sun.star.awt import XFocusListener + try: from fernet import Fernet, InvalidToken - CRYPTO = True except ImportError: - CRYPTO = False + pass MSG_LANG = { @@ -119,9 +133,15 @@ TYPE_DOC = { 'base': 'com.sun.star.sdb.DocumentDataSource', 'math': 'com.sun.star.formula.FormulaProperties', 'basic': 'com.sun.star.script.BasicIDE', + 'main': 'com.sun.star.frame.StartModule', } NODE_MENUBAR = 'private:resource/menubar/menubar' +MENUS_MAIN = { + 'file': '.uno:PickList', + 'tools': '.uno:ToolsMenu', + 'help': '.uno:HelpMenu', +} MENUS_CALC = { 'file': '.uno:PickList', 'edit': '.uno:EditMenu', @@ -150,6 +170,7 @@ MENUS_WRITER = { } MENUS_APP = { + 'main': MENUS_MAIN, 'calc': MENUS_CALC, 'writer': MENUS_WRITER, } @@ -174,13 +195,13 @@ log = logging.getLogger(__name__) _start = 0 _stop_thread = {} TIMEOUT = 10 +SECONDS_DAY = 60 * 60 * 24 CTX = uno.getComponentContext() SM = CTX.getServiceManager() -# ~ Export ok def create_instance(name, with_context=False): if with_context: instance = SM.createInstanceWithContext(name, CTX) @@ -189,28 +210,35 @@ def create_instance(name, with_context=False): return instance -def _get_app_config(key, node_name): +def get_app_config(node_name, key=''): name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationAccess' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) try: ca = cp.createInstanceWithArguments(service, (node,)) - if ca and (ca.hasByName(key)): - data = ca.getPropertyValue(key) - return data + if ca and not key: + return ca + if ca and ca.hasByName(key): + return ca.getPropertyValue(key) except Exception as e: - log.error(e) + error(e) return '' -LANGUAGE = _get_app_config('ooLocale', 'org.openoffice.Setup/L10N/') +# ~ FILTER_PDF = '/org.openoffice.Office.Common/Filter/PDF/Export/' +LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') LANG = LANGUAGE.split('-')[0] -NAME = TITLE = _get_app_config('ooName', 'org.openoffice.Setup/Product') -VERSION = _get_app_config('ooSetupVersion', 'org.openoffice.Setup/Product') +NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') +VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') + +nd = '/org.openoffice.Office.Calc/Calculate/Other/Date' +d = get_app_config(nd, 'DD') +m = get_app_config(nd, 'MM') +y = get_app_config(nd, 'YY') +DATE_OFFSET = datetime.date(y, m, d).toordinal() -# ~ Export ok def mri(obj): m = create_instance('mytools.Mri') if m is None: @@ -239,7 +267,6 @@ class LogWin(object): def __init__(self, doc): self.doc = doc - self.doc.Title = FILE_NAME_DEBUG def write(self, info): text = self.doc.Text @@ -249,18 +276,15 @@ class LogWin(object): return -# ~ Export ok def info(data): log.info(data) return -# ~ Export ok def debug(info): if IS_WIN: doc = get_document(FILE_NAME_DEBUG) if doc is None: - # ~ doc = new_doc('writer') return doc = LogWin(doc.obj) doc.write(info) @@ -270,13 +294,11 @@ def debug(info): return -# ~ Export ok def error(info): log.error(info) return -# ~ Export ok def save_log(path, data): with open(path, 'a') as out: out.write('{} -{}- '.format(str(now())[:19], LOG_NAME)) @@ -296,7 +318,31 @@ def now(): return datetime.datetime.now() -# ~ Export ok +def today(): + return datetime.date.today() + + +def time(): + return datetime.datetime.now().time() + + +def get_date(year, month, day, hour=-1, minute=-1, second=-1): + if hour > -1 or minute > -1 or second > -1: + h = hour + m = minute + s = second + if h == -1: + h = 0 + if m == -1: + m = 0 + if s == -1: + s = 0 + d = datetime.datetime(year, month, day, h, m, s) + else: + d = datetime.date(year, month, day) + return d + + def get_config(key='', default=None, prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) values = None @@ -314,7 +360,6 @@ def get_config(key='', default=None, prefix='config'): return values -# ~ Export ok def set_config(key, value, prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) path = join(get_config_path('UserConfig'), path_json) @@ -322,10 +367,9 @@ def set_config(key, value, prefix='config'): 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 + return True -# ~ Export ok def sleep(seconds): time.sleep(seconds) return @@ -342,7 +386,6 @@ def _(msg): return MSG_LANG[L][msg] -# ~ Export ok def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): """ Create message box type_msg: infobox, warningbox, errorbox, querybox, messbox @@ -354,18 +397,15 @@ def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infob return mb.execute() -# ~ Export ok def question(message, title=TITLE): res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') return res == YES -# ~ Export ok def warning(message, title=TITLE): return msgbox(message, title, type_msg='warningbox') -# ~ Export ok def errorbox(message, title=TITLE): return msgbox(message, title, type_msg='errorbox') @@ -374,12 +414,10 @@ def get_desktop(): return create_instance('com.sun.star.frame.Desktop', True) -# ~ Export ok def get_dispatch(): return create_instance('com.sun.star.frame.DispatchHelper') -# ~ Export ok def call_dispatch(url, args=()): frame = get_document().frame dispatch = get_dispatch() @@ -387,7 +425,6 @@ def call_dispatch(url, args=()): return -# ~ Export ok def get_temp_file(): delete = True if IS_WIN: @@ -407,7 +444,6 @@ def _path_system(path): return path -# ~ Export ok def exists_app(name): try: dn = subprocess.DEVNULL @@ -418,12 +454,10 @@ def exists_app(name): return True -# ~ Export ok def exists_path(path): return Path(path).exists() -# ~ Export ok def get_type_doc(obj): for k, v in TYPE_DOC.items(): if obj.supportsService(v): @@ -443,12 +477,99 @@ def property_to_dict(values): return d +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 array_to_dict(values): d = {r[0]: r[1] for r in values} return d # ~ Custom classes +class ObjectBase(object): + + def __init__(self, obj): + self._obj = obj + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __getitem__(self, index): + return self.obj[index] + + def __getattr__(self, name): + a = None + if name == 'obj': + a = super().__getattr__(name) + else: + if hasattr(self.obj, name): + a = getattr(self.obj, name) + return a + + @property + def obj(self): + return self._obj + @obj.setter + def obj(self, value): + self._obj = value + + +class LOObjectBase(object): + + def __init__(self, obj): + self.__dict__['_obj'] = obj + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + return True + + def __setattr__(self, name, value): + print('BASE__setattr__', name) + if name == '_obj': + super().__setattr__(name, value) + else: + self.obj.setPropertyValue(name, value) + + # ~ def _try_for_method(self, name): + # ~ a = None + # ~ m = 'get{}'.format(name) + # ~ if hasattr(self.obj, m): + # ~ a = getattr(self.obj, m)() + # ~ else: + # ~ a = getattr(self.obj, name) + # ~ return a + + def __getattr__(self, name): + print('BASE__getattr__', name) + if name == 'obj': + a = super().__getattr__(name) + else: + a = self.obj.getPropertyValue(name) + # ~ Bug + if a is None: + msg = 'Error get: {} - {}'.format(self.obj.ImplementationName, name) + error(msg) + raise Exception(msg) + return a + + @property + def obj(self): + return self._obj + + class LODocument(object): def __init__(self, obj): @@ -457,23 +578,30 @@ class LODocument(object): def _init_values(self): self._type_doc = get_type_doc(self.obj) - if self._type_doc == 'base': - self._cc = self.obj.DatabaseDocument.getCurrentController() - else: - self._cc = self.obj.getCurrentController() + # ~ if self._type_doc == 'base': + # ~ self._cc = self.obj.DatabaseDocument.getCurrentController() + # ~ else: + 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() + @title.setter + def title(self, value): + self.obj.setTitle(value) + + @property + def uid(self): + return self.obj.RuntimeUID + + @property + def type(self): + return self._type_doc @property def frame(self): @@ -502,7 +630,7 @@ class LODocument(object): @property def visible(self): w = self._cc.getFrame().getContainerWindow() - return w.Visible + return w.isVisible() @visible.setter def visible(self, value): w = self._cc.getFrame().getContainerWindow() @@ -515,6 +643,11 @@ class LODocument(object): def zoom(self, value): self._cc.ZoomValue = value + @property + def table_auto_formats(self): + taf = create_instance('com.sun.star.sheet.TableAutoFormats') + return taf.ElementNames + def create_instance(self, name): obj = self.obj.createInstance(name) return obj @@ -568,19 +701,150 @@ class LODocument(object): return path_pdf -class LOCalc(LODocument): +class LOForm(ObjectBase): def __init__(self, obj): super().__init__(obj) + @property + def name(self): + return self._obj.getName() + @name.setter + def name(self, value): + self._obj.setName(value) + + +class LOForms(ObjectBase): + + def __init__(self, obj, doc): + self._doc = doc + super().__init__(obj) + + def __getitem__(self, index): + form = super().__getitem__(index) + return LOForm(form) + + @property + def doc(self): + return self._doc + + @property + def count(self): + return self.obj.getCount() + + @property + def names(self): + return self.obj.getElementNames() + + def exists(self, name): + return name in self.names + + def insert(self, name): + form = self.doc.create_instance('com.sun.star.form.component.Form') + self.obj.insertByName(name, form) + return self[name] + + def remove(self, index): + if isinstance(index, int): + self.obj.removeByIndex(index) + else: + self.obj.removeByName(index) + return + + +class LOCellStyle(LOObjectBase): + + def __init__(self, obj): + super().__init__(obj) + + @property + def name(self): + return self.obj.Name + + def apply(self, properties): + set_properties(self.obj, properties) + return + + +class LOCellStyles(object): + + def __init__(self, obj): + self._obj = obj + + 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 apply(self, style, properties): + set_properties(style, properties) + return + + +class LOCalc(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._sheets = obj.getSheets() + + def __getitem__(self, index): + if isinstance(index, str): + index = [s.Name for s in self._sheets if s.CodeName == index][0] or index + return LOCalcSheet(self._sheets[index], self) + + def __setitem__(self, key, value): + self._sheets[key] = value + + def __contains__(self, item): + return item in self.obj.Sheets + + @property + def headers(self): + return self._cc.ColumnRowHeaders + @headers.setter + def headers(self, value): + self._cc.ColumnRowHeaders = value + + @property + def tabs(self): + return self._cc.SheetTabs + @tabs.setter + def tabs(self, value): + self._cc.SheetTabs = value + @property def active(self): return LOCalcSheet(self._cc.getActiveSheet(), self) + def activate(self, sheet): + obj = sheet + if isinstance(sheet, LOCalcSheet): + obj = sheet.obj + elif isinstance(sheet, str): + obj = self[sheet].obj + self._cc.setActiveSheet(obj) + return + @property def selection(self): sel = self.obj.getCurrentSelection() @@ -588,6 +852,97 @@ class LOCalc(LODocument): sel = LOCellRange(sel, self) return sel + @property + def sheets(self): + return LOCalcSheets(self._sheets, self) + + @property + def names(self): + return self.sheets.names + + @property + def cell_style(self): + obj = self.obj.getStyleFamilies()['CellStyles'] + return LOCellStyles(obj) + + def create(self): + return self.obj.createInstance('com.sun.star.sheet.Spreadsheet') + + def insert(self, name, pos=-1): + # ~ sheet = obj.createInstance('com.sun.star.sheet.Spreadsheet') + # ~ obj.Sheets['New'] = sheet + index = pos + if pos < 0: + index = self._sheets.Count + pos + 1 + if isinstance(name, str): + self._sheets.insertNewByName(name, index) + else: + for n in name: + self._sheets.insertNewByName(n, index) + name = n + return LOCalcSheet(self._sheets[name], self) + + def move(self, name, pos=-1): + return self.sheets.move(name, pos) + + def remove(self, name): + return self.sheets.remove(name) + + def copy(self, source='', target='', pos=-1): + index = pos + if pos < 0: + index = self._sheets.Count + pos + 1 + + names = source + if not names: + names = self.names + elif isinstance(source, str): + names = (source,) + + new_names = target + if not target: + new_names = [n + '_2' for n in names] + elif isinstance(target, str): + new_names = (target,) + + for i, ns in enumerate(names): + self.sheets.copy(ns, new_names[i], index + i) + + return LOCalcSheet(self._sheets[index], self) + + def copy_from(self, doc, source='', target='', pos=-1): + index = pos + if pos < 0: + index = self._sheets.Count + pos + 1 + + names = source + if not names: + names = doc.names + elif isinstance(source, str): + names = (source,) + + new_names = target + if not target: + new_names = names + elif isinstance(target, str): + new_names = (target,) + + for i, n in enumerate(names): + self._sheets.importSheet(doc.obj, n, index + i) + self.sheets[index + i].name = new_names[i] + + # ~ doc.getCurrentController().setActiveSheet(sheet) + # ~ For controls in sheet + # ~ doc.getCurrentController().setFormDesignMode(False) + + return LOCalcSheet(self._sheets[index], self) + + def sort(self, reverse=False): + names = sorted(self.names, reverse=reverse) + for i, n in enumerate(names): + self.sheets.move(n, i) + return + def get_cell(self, index=None): """ index is str 'A1' @@ -608,6 +963,72 @@ class LOCalc(LODocument): self._cc.select(r) return + def create_cell_style(self, name=''): + obj = self.create_instance('com.sun.star.style.CellStyle') + if name: + self.cell_style[name] = obj + return LOCellStyle(obj) + + def clear_undo(self): + self.obj.getUndoManager().clear() + return + + def filter_by_color(self, cell=None): + if cell is None: + cell = self.selection.first + cr = cell.current_region + col = cell.column - cr.column + rangos = cell.get_column(col).visible + for r in rangos: + for row in range(r.rows): + c = r[row, 0] + if c.back_color != cell.back_color: + c.rows_visible = False + return + + +class LOCalcSheets(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + def __getitem__(self, index): + return LOCalcSheet(self.obj[index], self.doc) + + @property + def obj(self): + return self._obj + + @property + def doc(self): + return self._doc + + @property + def count(self): + return self.obj.Count + + @property + def names(self): + return self.obj.ElementNames + + def copy(self, name, new_name, pos): + self.obj.copyByName(name, new_name, pos) + return + + def move(self, name, pos): + index = pos + if pos < 0: + index = self.count + pos + 1 + sheet = self.obj[name] + self.obj.moveByName(sheet.Name, index) + return + + def remove(self, name): + sheet = self.obj[name] + self.obj.removeByName(sheet.Name) + return + class LOCalcSheet(object): @@ -619,7 +1040,15 @@ class LOCalcSheet(object): def __getitem__(self, index): return LOCellRange(self.obj[index], self.doc) + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + def _init_values(self): + self._events = None + self._dp = self.obj.getDrawPage() return @property @@ -630,6 +1059,91 @@ class LOCalcSheet(object): def doc(self): return self._doc + @property + def name(self): + return self._obj.Name + @name.setter + def name(self, value): + self._obj.Name = value + + @property + def code_name(self): + return self._obj.CodeName + @code_name.setter + def code_name(self, value): + self._obj.CodeName = value + + @property + def color(self): + return self._obj.TabColor + @color.setter + def color(self, value): + self._obj.TabColor = get_color(value) + + @property + def active(self): + return self.doc.selection.first + + def activate(self): + self.doc.activate(self.obj) + return + + @property + def visible(self): + return self.obj.IsVisible + @visible.setter + def visible(self, value): + self.obj.IsVisible = value + + @property + def is_protected(self): + return self._obj.isProtected() + + @property + def password(self): + return '' + @visible.setter + def password(self, value): + self.obj.protect(value) + + def unprotect(self, value): + try: + self.obj.unprotect(value) + return True + except: + pass + return False + + def get_cursor(self, cell): + return self.obj.createCursorByRange(cell) + + def exists_chart(self, name): + return name in self.obj.Charts.ElementNames + + @property + def forms(self): + return LOForms(self._dp.getForms(), self.doc) + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers + self._connect_listeners() + + def _connect_listeners(self): + if self.events is None: + return + + listeners = { + 'addModifyListener': EventsModify, + } + for key, value in listeners.items(): + getattr(self.obj, key)(listeners[key](self.events)) + print('add_listener') + return + class LOWriter(LODocument): @@ -652,18 +1166,46 @@ class LOWriter(LODocument): def cursor(self): return self.text.createTextCursor() + @property + def paragraphs(self): + return [LOTextRange(p) for p in self.text] + @property def selection(self): - sel = self._cc.getSelection() + sel = self.obj.getCurrentSelection() return LOTextRange(sel[0]) + def write(self, data, cursor=None): + cursor = cursor or self.selection.cursor.getEnd() + if data.startswith('\n'): + c = data.split('\n') + for i in range(len(c)-1): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + else: + self.text.insertString(cursor, data, False) + return + + def insert_table(self, data, cursor=None): + cursor = cursor or self.selection.cursor.getEnd() + table = self.obj.createInstance('com.sun.star.text.TextTable') + rows = len(data) + cols = len(data[0]) + table.initialize(rows, cols) + self.insert_content(cursor, table) + table.DataArray = data + return WriterTable(table) + + def create_chart(self, tipo, cursor=None): + cursor = cursor or self.selection.cursor.getEnd() + chart = LOChart(None, tipo) + chart.cursor = cursor + chart.doc = self + return chart + 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)) @@ -679,16 +1221,40 @@ class LOWriter(LODocument): self.insert_content(cursor, image) return + def go_start(self): + cursor = self._cc.getViewCursor() + cursor.gotoStart(False) + return cursor + + def go_end(self): + cursor = self._cc.getViewCursor() + cursor.gotoEnd(False) + return cursor + + def select(self, text): + self._cc.select(text) + return + class LOTextRange(object): def __init__(self, obj): self._obj = obj + self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph' + self._is_table = self.obj.ImplementationName == 'SwXTextTable' @property def obj(self): return self._obj + @property + def is_paragraph(self): + return self._is_paragraph + + @property + def is_table(self): + return self._is_table + @property def string(self): return self.obj.String @@ -702,10 +1268,139 @@ class LOTextRange(object): return self.text.createTextCursorByRange(self.obj) -class LOBase(LODocument): +class LOBase(object): + TYPES = { + str: 'setString', + int: 'setInt', + float: 'setFloat', + bool: 'setBoolean', + Date: 'setDate', + Time: 'setTime', + DateTime: 'setTimestamp', + } + # ~ setArray + # ~ setBinaryStream + # ~ setBlob + # ~ setByte + # ~ setBytes + # ~ setCharacterStream + # ~ setClob + # ~ setNull + # ~ setObject + # ~ setObjectNull + # ~ setObjectWithInfo + # ~ setPropertyValue + # ~ setRef + def __init__(self, name, path='', **kwargs): + self._name = name + self._path = path + self._dbc = create_instance('com.sun.star.sdb.DatabaseContext') + if path: + path_url = _path_url(path) + db = self._dbc.createInstance() + db.URL = 'sdbc:embedded:firebird' + db.DatabaseDocument.storeAsURL(path_url, ()) + if not self.exists: + self._dbc.registerDatabaseLocation(name, path_url) + else: + if name.startswith('odbc:'): + self._con = self._odbc(name, kwargs) + else: + db = self._dbc.getByName(name) + self.path = _path_system(self._dbc.getDatabaseLocation(name)) + self._con = db.getConnection('', '') - def __init__(self, obj): - super().__init__(obj) + if self._con is None: + msg = 'Not connected to: {}'.format(name) + else: + msg = 'Connected to: {}'.format(name) + debug(msg) + + def _odbc(self, name, kwargs): + dm = create_instance('com.sun.star.sdbc.DriverManager') + args = dict_to_property(kwargs) + try: + con = dm.getConnectionWithInfo('sdbc:{}'.format(name), args) + return con + except Exception as e: + error(str(e)) + return None + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def connection(self): + return self._con + + @property + def path(self): + return self._path + @path.setter + def path(self, value): + self._path = value + + @property + def exists(self): + return self._dbc.hasRegisteredDatabase(self.name) + + @classmethod + def register(self, path, name): + if not self._dbc.hasRegisteredDatabase(name): + self._dbc.registerDatabaseLocation(name, _path_url(path)) + return + + def revoke(self, name): + self._dbc.revokeDatabaseLocation(name) + return True + + def save(self): + # ~ self._db.connection.commit() + # ~ self._db.connection.getTables().refresh() + # ~ oDisp.executeDispatch(oFrame,".uno:DBRefreshTables", "", 0, Array()) + self._obj.DatabaseDocument.store() + self.refresh() + return + + def close(self): + self._con.close() + return + + def refresh(self): + self._con.getTables().refresh() + return + + def get_tables(self): + tables = self._con.getTables() + tables = [tables.getByIndex(i) for i in range(tables.Count)] + return tables + + def cursor(self, sql, params): + cursor = self._con.prepareStatement(sql) + for i, v in enumerate(params, 1): + if not type(v) in self.TYPES: + error('Type not support') + debug((i, type(v), v, self.TYPES[type(v)])) + getattr(cursor, self.TYPES[type(v)])(i, v) + return cursor + + def execute(self, sql, params): + debug(sql) + if params: + cursor = self.cursor(sql, params) + cursor.execute() + else: + cursor = self._con.createStatement() + cursor.execute(sql) + # ~ resulset = cursor.executeQuery(sql) + # ~ rows = cursor.executeUpdate(sql) + self.save() + return cursor class LODrawImpress(LODocument): @@ -770,7 +1465,7 @@ class LOCellRange(object): def __enter__(self): return self - def __exit__(self, *args): + def __exit__(self, exc_type, exc_value, traceback): pass def __getitem__(self, index): @@ -827,6 +1522,16 @@ class LOCellRange(object): self.obj.setString(data) elif isinstance(data, (int, float)): self.obj.setValue(data) + elif isinstance(data, datetime.datetime): + d = data.toordinal() + t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY + self.obj.setValue(d - DATE_OFFSET + t) + elif isinstance(data, datetime.date): + d = data.toordinal() + self.obj.setValue(d - DATE_OFFSET) + elif isinstance(data, datetime.time): + d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY + self.obj.setValue(d) @property def data(self): @@ -837,19 +1542,96 @@ class LOCellRange(object): values = tuple(values) self.obj.setDataArray(values) - def offset(self, col=1, row=0): + @property + def formula(self): + return self.obj.getFormulaArray() + @formula.setter + def formula(self, values): + if isinstance(values, list): + values = tuple(values) + self.obj.setFormulaArray(values) + + @property + def column(self): a = self.address - col = a.Column + col - row = a.Row + row - return LOCellRange(self.sheet[row,col], self.doc) + if hasattr(a, 'Column'): + c = a.Column + else: + c = a.StartColumn + return c + + @property + def columns(self): + return self._obj.Columns.Count + + @property + def rows(self): + return self._obj.Rows.Count + + def to_size(self, rows, cols): + cursor = self.sheet.get_cursor(self.obj[0,0]) + cursor.collapseToSize(cols, rows) + return LOCellRange(self.sheet[cursor.AbsoluteName].obj, self.doc) + + def copy_from(self, rango): + data = rango + if isinstance(rango, LOCellRange): + data = rango.data + rows = len(data) + cols = len(data[0]) + self.to_size(rows, cols).data = data + return + + def copy_to(self, cell, formula=False): + rango = cell.to_size(self.rows, self.columns) + if formula: + rango.formula = self.data + else: + rango.data = self.data + return + + def offset(self, row=1, col=0): + ra = self.obj.getRangeAddress() + col = ra.EndColumn + col + row = ra.EndRow + row + return LOCellRange(self.sheet[row, col].obj, self.doc) + + @property + def next_cell(self): + a = self.current_region.address + if hasattr(a, 'StartColumn'): + col = a.StartColumn + else: + col = a.Column + if hasattr(a, 'EndRow'): + row = a.EndRow + 1 + else: + row = a.Row + 1 + + return LOCellRange(self.sheet[row, col].obj, self.doc) @property def sheet(self): - return self.obj.Spreadsheet + return LOCalcSheet(self.obj.Spreadsheet, self.doc) + + @property + def charts(self): + return self.obj.Spreadsheet.Charts + + @property + def ps(self): + ps = Rectangle() + s = self.obj.Size + p = self.obj.Position + ps.X = p.X + ps.Y = p.Y + ps.Width = s.Width + ps.Height = s.Height + return ps @property def draw_page(self): - return self.sheet.getDrawPage() + return self.sheet.obj.getDrawPage() @property def name(self): @@ -867,9 +1649,44 @@ class LOCellRange(object): @property def current_region(self): - cursor = self.sheet.createCursorByRange(self.obj[0,0]) + cursor = self.sheet.get_cursor(self.obj[0,0]) cursor.collapseToCurrentRegion() - return LOCellRange(self.sheet[cursor.AbsoluteName], self.doc) + return LOCellRange(self.sheet[cursor.AbsoluteName].obj, self.doc) + + @property + def visible(self): + cursor = self.sheet.get_cursor(self.obj) + rangos = [LOCellRange(self.sheet[r.AbsoluteName].obj, self.doc) + for r in cursor.queryVisibleCells()] + return tuple(rangos) + + @property + def empty(self): + cursor = self.sheet.get_cursor(self.obj) + rangos = [LOCellRange(self.sheet[r.AbsoluteName].obj, self.doc) + for r in cursor.queryEmptyCells()] + return tuple(rangos) + + @property + def back_color(self): + return self._obj.CellBackColor + @back_color.setter + def back_color(self, value): + self._obj.CellBackColor = get_color(value) + + @property + def cell_style(self): + return self.obj.CellStyle + @cell_style.setter + def cell_style(self, value): + self.obj.CellStyle = value + + @property + def auto_format(self): + return self.obj.CellStyle + @auto_format.setter + def auto_format(self, value): + self.obj.autoFormat(value) def insert_image(self, path, **kwargs): s = self.obj.Size @@ -882,15 +1699,92 @@ class LOCellRange(object): img.setSize(Size(w, h)) return + def insert_shape(self, tipo, **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.{}Shape'.format(tipo)) + set_properties(img, kwargs) + 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 + def in_range(self, rango): + if isinstance(rango, LOCellRange): + address = rango.address + else: + address = rango.getRangeAddress() + cursor = self.sheet.get_cursor(self.obj) + result = cursor.queryIntersection(address) + return bool(result.Count) + + def fill(self, source=1): + self.obj.fillAuto(0, source) + return + + def clear(self, what=31): + # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html + self.obj.clearContents(what) + return + + @property + def rows_visible(self): + return self._obj.getRows().IsVisible + @rows_visible.setter + def rows_visible(self, value): + self._obj.getRows().IsVisible = value + + @property + def columns_visible(self): + return self._obj.getColumns().IsVisible + @columns_visible.setter + def columns_visible(self, value): + self._obj.getColumns().IsVisible = value + + def get_column(self, index=0, first=False): + ca = self.address + ra = self.current_region.address + if hasattr(ca, 'Column'): + col = ca.Column + else: + col = ca.StartColumn + index + start = 1 + if first: + start = 0 + if hasattr(ra, 'Row'): + row_start = ra.Row + start + row_end = ra.Row + 1 + else: + row_start = ra.StartRow + start + row_end = ra.EndRow + 1 + return LOCellRange(self.sheet[row_start:row_end, col:col+1].obj, self.doc) + + def import_csv(self, path, **kwargs): + data = import_csv(path, **kwargs) + self.copy_from(data) + return + + def export_csv(self, path, **kwargs): + data = self.current_region.data + export_csv(path, data, **kwargs) + return + + def create_chart(self, tipo): + chart = LOChart(None, tipo) + chart.cell = self + return chart + class EventsListenerBase(unohelper.Base, XEventListener): - def __init__(self, controller, window=None): + def __init__(self, controller, name, window=None): self._controller = controller + self._name = name self._window = window def disposing(self, event): @@ -901,25 +1795,23 @@ class EventsListenerBase(unohelper.Base, XEventListener): class EventsButton(EventsListenerBase, XActionListener): - def __init__(self, controller): - super().__init__(controller) + def __init__(self, controller, name): + super().__init__(controller, name) def actionPerformed(self, event): - name = event.Source.Model.Name - event_name = '{}_action'.format(name) + event_name = '{}_action'.format(self._name) if hasattr(self._controller, event_name): getattr(self._controller, event_name)(event) return -class EventsMouse(EventsListenerBase, XMouseListener): +class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): - def __init__(self, controller): - super().__init__(controller) + def __init__(self, controller, name): + super().__init__(controller, name) def mousePressed(self, event): - name = event.Source.Model.Name - event_name = '{}_click'.format(name) + event_name = '{}_click'.format(self._name) if event.ClickCount == 2: event_name = '{}_double_click'.format(name) if hasattr(self._controller, event_name): @@ -935,6 +1827,26 @@ class EventsMouse(EventsListenerBase, XMouseListener): def mouseExited(self, event): pass + # ~ XMouseMotionListener + def mouseMoved(self, event): + pass + + def mouseDragged(self, event): + pass + + +class EventsMouseLink(EventsMouse): + + def mouseEntered(self, event): + obj = event.Source.Model + obj.TextColor = get_color('blue') + return + + def mouseExited(self, event): + obj = event.Source.Model + obj.TextColor = 0 + return + class EventsMouseGrid(EventsMouse): selected = False @@ -954,13 +1866,178 @@ class EventsMouseGrid(EventsMouse): 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) + # ~ 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 EventsModify(EventsListenerBase, XModifyListener): + + def __init__(self, controller): + super().__init__(controller) + + def modified(self, event): + event_name = '{}_modified'.format(event.Source.Name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsItem(EventsListenerBase, XItemListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def disposing(self, event): + pass + + def itemStateChanged(self, event): + pass + + +class EventsItemRoadmap(EventsItem): + + def itemStateChanged(self, event): + dialog = event.Source.Context.Model + dialog.Step = event.ItemId + 1 + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + + def focusLost(self, event): + obj = event.Source.Model + obj.BackgroundColor = -1 + + +class EventsKey(EventsListenerBase, XKeyListener): + """ + event.KeyChar + event.KeyCode + event.KeyFunc + event.Modifiers + """ + + def __init__(self, cls): + super().__init__(cls.events, cls.name) + self._cls = cls + + def keyPressed(self, event): + pass + + def keyReleased(self, event): + event_name = '{}_key_released'.format(self._cls.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + else: + if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): + self._cls.close() + return + + +class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): + + def __init__(self, cls): + self._cls = cls + super().__init__(cls.events, cls.name, cls._window) + + def windowOpened(self, event): + event_name = '{}_opened'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowActivated(self, event): + control_name = '{}_activated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowDeactivated(self, event): + control_name = '{}_deactivated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowMinimized(self, event): + pass + + def windowNormalized(self, event): + pass + + def windowClosing(self, event): + if self._window: + control_name = 'window_closing' + else: + control_name = '{}_closing'.format(event.Source.Model.Name) + + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + # ~ else: + # ~ if not self._modal and not self._block: + # ~ event.Source.Visible = False + return + + def windowClosed(self, event): + control_name = '{}_closed'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + # ~ XWindowListener + def windowResized(self, event): + # ~ sb = self._container.getControl('subcontainer') + # ~ sb.setPosSize(0, 0, event.Width, event.Height, SIZE) + event_name = '{}_resized'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowMoved(self, event): + pass + + def windowShown(self, event): + pass + + def windowHidden(self, event): + pass + + +class EventsMenu(EventsListenerBase, XMenuListener): + + def __init__(self, controller): + super().__init__(controller, '') + + def itemHighlighted(self, event): + pass + + @catch_exception + def itemSelected(self, event): + name = event.Source.getCommand(event.MenuId) + if name.startswith('menu'): + event_name = '{}_selected'.format(name) + else: + event_name = 'menu_{}_selected'.format(name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def itemActivated(self, event): + return + + def itemDeactivated(self, event): return @@ -987,19 +2064,39 @@ class UnoBaseObject(object): def parent(self): return self.obj.getContext() + def _get_possize(self, name): + ps = self.obj.getPosSize() + return getattr(ps, name) + + def _set_possize(self, name, value): + ps = self.obj.getPosSize() + setattr(ps, name, value) + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + return + @property def x(self): - return self.model.PositionX + if hasattr(self.model, 'PositionX'): + return self.model.PositionX + return self._get_possize('X') @x.setter def x(self, value): - self.model.PositionX = value + if hasattr(self.model, 'PositionX'): + self.model.PositionX = value + else: + self._set_possize('X', value) @property def y(self): - return self.model.PositionY + if hasattr(self.model, 'PositionY'): + return self.model.PositionY + return self._get_possize('Y') @y.setter def y(self, value): - self.model.PositionY = value + if hasattr(self.model, 'PositionY'): + self.model.PositionY = value + else: + self._set_possize('Y', value) @property def width(self): @@ -1010,7 +2107,10 @@ class UnoBaseObject(object): @property def height(self): - return self._model.Height + if hasattr(self._model, 'Height'): + return self._model.Height + ps = self.obj.getPosSize() + return ps.Height @height.setter def height(self, value): self._model.Height = value @@ -1029,6 +2129,13 @@ class UnoBaseObject(object): def step(self, value): self.model.Step = value + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + @property def rules(self): return self._rules @@ -1083,6 +2190,16 @@ class UnoLabel(UnoBaseObject): self.model.Label = value +class UnoLabelLink(UnoLabel): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'link' + + class UnoButton(UnoBaseObject): def __init__(self, obj): @@ -1257,9 +2374,517 @@ class UnoGrid(UnoBaseObject): return +class UnoRoadmap(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._options = () + + @property + def options(self): + return self._options + @options.setter + def options(self, values): + self._options = values + for i, v in enumerate(values): + opt = self.model.createInstance() + opt.ID = i + opt.Label = v + self.model.insertByIndex(i, opt) + return + + def set_enabled(self, index, value): + self.model.getByIndex(index).Enabled = value + return + + +def get_custom_class(tipo, obj): + classes = { + 'label': UnoLabel, + 'button': UnoButton, + 'text': UnoText, + 'listbox': UnoListBox, + 'grid': UnoGrid, + 'link': UnoLabelLink, + 'roadmap': UnoRoadmap, + # ~ 'tab': UnoTab, + # ~ 'image': UnoImage, + # ~ 'radio': UnoRadio, + # ~ 'groupbox': UnoGroupBox, + # ~ 'tree': UnoTree, + } + return classes[tipo](obj) + + +def add_listeners(events, control, name=''): + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + 'addItemListener': EventsItem, + 'addFocusListener': EventsFocus, + } + if hasattr(control, 'obj'): + control = contro.obj + # ~ debug(control.ImplementationName) + is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' + is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' + is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' + + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue + getattr(control, key)(listeners[key](events, name)) + return + + +class WriterTable(ObjectBase): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, key): + obj = super().__getitem__(key) + return WriterTableRange(obj, key, self.name) + + @property + def name(self): + return self.obj.Name + @name.setter + def name(self, value): + self.obj.Name = value + + +class WriterTableRange(ObjectBase): + + def __init__(self, obj, index, table_name): + self._index = index + self._table_name = table_name + super().__init__(obj) + self._is_cell = hasattr(self.obj, 'CellName') + + def __getitem__(self, key): + obj = super().__getitem__(key) + return WriterTableRange(obj, key, self._table_name) + + @property + def value(self): + return self.obj.String + @value.setter + def value(self, value): + self.obj.String = value + + @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) + + @property + def rows(self): + return len(self.data) + + @property + def columns(self): + return len(self.data[0]) + + @property + def name(self): + if self._is_cell: + name = '{}.{}'.format(self._table_name, self.obj.CellName) + elif isinstance(self._index, str): + name = '{}.{}'.format(self._table_name, self._index) + else: + c1 = self.obj[0,0].CellName + c2 = self.obj[self.rows-1,self.columns-1].CellName + name = '{}.{}:{}'.format(self._table_name, c1, c2) + return name + + def get_cell(self, *index): + return self[index] + + def get_column(self, index=0, start=1): + return self[start:self.rows,index:index+1] + + def get_series(self): + class Serie(): + pass + series = [] + for i in range(self.columns): + serie = Serie() + serie.label = self.get_cell(0,i).name + serie.data = self.get_column(i).data + serie.values = self.get_column(i).name + series.append(serie) + return series + + +class ChartFormat(object): + + def __call__(self, obj): + for k, v in self.__dict__.items(): + if hasattr(obj, k): + setattr(obj, k, v) + + +class LOChart(object): + BASE = 'com.sun.star.chart.{}Diagram' + + def __init__(self, obj, tipo=''): + self._obj = obj + self._type = tipo + self._name = '' + self._table = None + self._data = () + self._data_series = () + self._cell = None + self._cursor = None + self._doc = None + self._title = ChartFormat() + self._subtitle = ChartFormat() + self._legend = ChartFormat() + self._xaxistitle = ChartFormat() + self._yaxistitle = ChartFormat() + self._xaxis = ChartFormat() + self._yaxis = ChartFormat() + self._xmaingrid = ChartFormat() + self._ymaingrid = ChartFormat() + self._xhelpgrid = ChartFormat() + self._yhelpgrid = ChartFormat() + self._area = ChartFormat() + self._wall = ChartFormat() + self._dim3d = False + self._series = () + self._labels = () + return + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.insert() + + @property + def obj(self): + return self._obj + @obj.setter + def obj(self, value): + self._obj = value + + @property + def name(self): + return self._name + @name.setter + def name(self, value): + self._name = value + + @property + def type(self): + return self._type + @type.setter + def type(self, value): + self._type = value + + @property + def table(self): + return self._table + @table.setter + def table(self, value): + self._table = value + + @property + def data(self): + return self._data + @data.setter + def data(self, value): + self._data = value + + @property + def cell(self): + return self._cell + @cell.setter + def cell(self, value): + self._cell = value + self.doc = value.doc + + @property + def cursor(self): + return self._cursor + @cursor.setter + def cursor(self, value): + self._cursor = value + + @property + def doc(self): + return self._doc + @doc.setter + def doc(self, value): + self._doc = value + + @property + def width(self): + return self._width + @width.setter + def width(self, value): + self._width = value + + @property + def height(self): + return self._height + @height.setter + def height(self, value): + self._height = value + + @property + def title(self): + return self._title + + @property + def subtitle(self): + return self._subtitle + + @property + def legend(self): + return self._legend + + @property + def xaxistitle(self): + return self._xaxistitle + + @property + def yaxistitle(self): + return self._yaxistitle + + @property + def xaxis(self): + return self._xaxis + + @property + def yaxis(self): + return self._yaxis + + @property + def xmaingrid(self): + return self._xmaingrid + + @property + def ymaingrid(self): + return self._ymaingrid + + @property + def xhelpgrid(self): + return self._xhelpgrid + + @property + def yhelpgrid(self): + return self._yhelpgrid + + @property + def area(self): + return self._area + + @property + def wall(self): + return self._wall + + @property + def dim3d(self): + return self._dim3d + @dim3d.setter + def dim3d(self, value): + self._dim3d = value + + @property + def series(self): + return self._series + @series.setter + def series(self, value): + self._series = value + + @property + def data_series(self): + return self._series + @data_series.setter + def data_series(self, value): + self._data_series = value + + @property + def labels(self): + return self._labels + @labels.setter + def labels(self, value): + self._labels = value + + def _add_series_writer(self, chart): + dp = self.doc.create_instance('com.sun.star.chart2.data.DataProvider') + chart.attachDataProvider(dp) + chart_type = chart.getFirstDiagram().getCoordinateSystems()[0].getChartTypes()[0] + self._data_series = self.table[self.data].get_series() + series = [self._create_serie(dp, s) for s in self._data_series[1:]] + chart_type.setDataSeries(tuple(series)) + chart_data = chart.getData() + chart_data.ComplexRowDescriptions = self._data_series[0].data + return + + def _get_series(self): + rango = self._data_series + class Serie(): + pass + series = [] + for i in range(0, rango.columns, 2): + serie = Serie() + serie.label = rango[0, i+1].name + serie.xvalues = rango.get_column(i).name + serie.values = rango.get_column(i+1).name + series.append(serie) + return series + + def _add_series_calc(self, chart): + dp = self.doc.create_instance('com.sun.star.chart2.data.DataProvider') + chart.attachDataProvider(dp) + chart_type = chart.getFirstDiagram().getCoordinateSystems()[0].getChartTypes()[0] + series = self._get_series() + series = [self._create_serie(dp, s) for s in series] + chart_type.setDataSeries(tuple(series)) + return + + def _create_serie(self, dp, data): + serie = create_instance('com.sun.star.chart2.DataSeries') + rango = data.values + is_x = hasattr(data, 'xvalues') + if is_x: + xrango = data.xvalues + rango_label = data.label + + lds = create_instance('com.sun.star.chart2.data.LabeledDataSequence') + values = self._create_data(dp, rango, 'values-y') + lds.setValues(values) + if data.label: + label = self._create_data(dp, rango_label, '') + lds.setLabel(label) + + xlds = () + if is_x: + xlds = create_instance('com.sun.star.chart2.data.LabeledDataSequence') + values = self._create_data(dp, xrango, 'values-x') + xlds.setValues(values) + + if is_x: + serie.setData((lds, xlds)) + else: + serie.setData((lds,)) + + return serie + + def _create_data(self, dp, rango, role): + data = dp.createDataSequenceByRangeRepresentation(rango) + if not data is None: + data.Role = role + return data + + def _from_calc(self): + ps = self.cell.ps + ps.Width = self.width + ps.Height = self.height + charts = self.cell.charts + data = () + if self.data: + data = (self.data.address,) + charts.addNewByName(self.name, ps, data, True, True) + self.obj = charts.getByName(self.name) + chart = self.obj.getEmbeddedObject() + chart.setDiagram(chart.createInstance(self.BASE.format(self.type))) + if not self.data: + self._add_series_calc(chart) + return chart + + def _from_writer(self): + obj = self.doc.create_instance('com.sun.star.text.TextEmbeddedObject') + obj.setPropertyValue('CLSID', '12DCAE26-281F-416F-a234-c3086127382e') + obj.Name = self.name + obj.setSize(Size(self.width, self.height)) + self.doc.insert_content(self.cursor, obj) + self.obj = obj + chart = obj.getEmbeddedObject() + tipo = self.type + if self.type == 'Column': + tipo = 'Bar' + chart.Diagram.Vertical = True + chart.setDiagram(chart.createInstance(self.BASE.format(tipo))) + chart.DataSourceLabelsInFirstColumn = True + if isinstance(self.data, str): + self._add_series_writer(chart) + else: + chart_data = chart.getData() + labels = [r[0] for r in self.data] + data = [(r[1],) for r in self.data] + chart_data.setData(data) + chart_data.RowDescriptions = labels + + if tipo == 'Pie': + chart.setDiagram(chart.createInstance(self.BASE.format('Bar'))) + chart.setDiagram(chart.createInstance(self.BASE.format('Pie'))) + + return chart + + def insert(self): + if not self.cell is None: + chart = self._from_calc() + elif not self.cursor is None: + chart = self._from_writer() + + diagram = chart.Diagram + + if self.type == 'Bar': + diagram.Vertical = True + + if hasattr(self.title, 'String'): + chart.HasMainTitle = True + self.title(chart.Title) + + if hasattr(self.subtitle, 'String'): + chart.HasSubTitle = True + self.subtitle(chart.SubTitle) + + if self.legend.__dict__: + chart.HasLegend = True + self.legend(chart.Legend) + + if self.xaxistitle.__dict__: + diagram.HasXAxisTitle = True + self.xaxistitle(diagram.XAxisTitle) + + if self.yaxistitle.__dict__: + diagram.HasYAxisTitle = True + self.yaxistitle(diagram.YAxisTitle) + + if self.dim3d: + diagram.Dim3D = True + + if self.series: + data_series = chart.getFirstDiagram( + ).getCoordinateSystems( + )[0].getChartTypes()[0].DataSeries + for i, serie in enumerate(data_series): + for k, v in self.series[i].items(): + if hasattr(serie, k): + setattr(serie, k, v) + return self + + class LODialog(object): - def __init__(self, properties): + def __init__(self, **properties): self._obj = self._create(properties) self._init_values() @@ -1267,22 +2892,27 @@ class LODialog(object): self._model = self._obj.Model self._init_controls() self._events = None - # ~ self._response = None + self._color_on_focus = -1 return def _create(self, properties): path = properties.pop('Path', '') if path: - dp = create_instance('com.sun.star.awt.DialogProvider2', True) + dp = create_instance('com.sun.star.awt.DialogProvider', True) return dp.createDialog(_path_url(path)) - if 'Library' in properties: - location = properties['Location'] + if 'Location' in properties: + location = properties.get('Location', 'application') + library = properties.get('Library', 'Standard') if location == 'user': location = 'application' - dp = create_instance('com.sun.star.awt.DialogProvider2', True) + dp = create_instance('com.sun.star.awt.DialogProvider', True) path = 'vnd.sun.star.script:{}.{}?location={}'.format( - properties['Library'], properties['Name'], location) + library, properties['Name'], location) + if location == 'document': + uid = get_document().uid + path = 'vnd.sun.star.tdoc:/{}/Dialogs/{}/{}.xml'.format( + uid, library, properties['Name']) return dp.createDialog(path) dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) @@ -1295,8 +2925,22 @@ class LODialog(object): return dlg - def _init_controls(self): + def _get_type_control(self, name): + types = { + 'stardiv.Toolkit.UnoFixedTextControl': 'label', + 'stardiv.Toolkit.UnoButtonControl': 'button', + 'stardiv.Toolkit.UnoEditControl': 'text', + 'stardiv.Toolkit.UnoRoadmapControl': 'roadmap', + 'stardiv.Toolkit.UnoFixedHyperlinkControl': 'link', + } + return types[name] + def _init_controls(self): + for control in self.obj.getControls(): + tipo = self._get_type_control(control.ImplementationName) + name = control.Model.Name + control = get_custom_class(tipo, control) + setattr(self, name, control) return @property @@ -1307,6 +2951,29 @@ class LODialog(object): def model(self): return self._model + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + global COLOR_ON_FOCUS + COLOR_ON_FOCUS = get_color(value) + self._color_on_focus = COLOR_ON_FOCUS + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + @property def events(self): return self._events @@ -1316,23 +2983,8 @@ class LODialog(object): 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)) + for control in self.obj.getControls(): + add_listeners(self._events, control, control.Model.Name) return def open(self): @@ -1343,47 +2995,28 @@ class LODialog(object): 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', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', } 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): @@ -1391,25 +3024,191 @@ class LODialog(object): return _path_url(path) return '' - def add_control(self, properties): - tipo = properties.pop('Type').lower() - + def _special_properties(self, tipo, properties): columns = properties.pop('Columns', ()) if tipo == 'grid': properties['ColumnModel'] = self._set_column_model(columns) - if tipo == 'button' and 'ImageURL' in properties: + elif tipo == 'button' and 'ImageURL' in properties: properties['ImageURL'] = self._set_image_url(properties['ImageURL']) + elif tipo == 'roadmap' and not 'Height' in properties: + properties['Height'] = self.height + return properties + def add_control(self, properties): + tipo = properties.pop('Type').lower() + properties = self._special_properties(tipo, properties) 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) + control = self.obj.getControl(name) + add_listeners(self.events, control, name) + control = get_custom_class(tipo, control) setattr(self, name, control) return +class LOWindow(object): + + def __init__(self, **kwargs): + self._events = None + self._menu = None + self._container = None + self._obj = self._create(kwargs) + + def _create(self, properties): + ps = ( + properties.get('X', 0), + properties.get('Y', 0), + properties.get('Width', 500), + properties.get('Height', 500), + ) + self._title = properties.get('Title', TITLE) + self._create_frame(ps) + self._create_container(ps) + # ~ self._create_splitter(ps) + return + + def _create_frame(self, ps): + service = 'com.sun.star.frame.TaskCreator' + tc = create_instance(service, True) + self._frame = tc.createInstanceWithArguments(( + NamedValue('FrameName', 'EasyMacroWin'), + NamedValue('PosSize', Rectangle(*ps)), + )) + self._window = self._frame.getContainerWindow() + self._toolkit = self._window.getToolkit() + desktop = get_desktop() + self._frame.setCreator(desktop) + desktop.getFrames().append(self._frame) + self._frame.Title = self._title + return + + def _create_container(self, ps): + # ~ toolkit = self._window.getToolkit() + service = 'com.sun.star.awt.UnoControlContainer' + self._container = create_instance(service, True) + service = 'com.sun.star.awt.UnoControlContainerModel' + model = create_instance(service, True) + model.BackgroundColor = get_color(225, 225, 225) + self._container.setModel(model) + self._container.createPeer(self._toolkit, self._window) + self._container.setPosSize(*ps, POSSIZE) + self._frame.setComponent(self._container, None) + return + + def _get_base_control(self, tipo): + services = { + 'label': 'com.sun.star.awt.UnoControlFixedText', + 'button': 'com.sun.star.awt.UnoControlButton', + 'text': 'com.sun.star.awt.UnoControlEdit', + 'listbox': 'com.sun.star.awt.UnoControlListBox', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlink', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmap', + 'image': 'com.sun.star.awt.UnoControlImageControl', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBox', + 'radio': 'com.sun.star.awt.UnoControlRadioButton', + 'tree': 'com.sun.star.awt.tree.TreeControl', + 'grid': 'com.sun.star.awt.grid.UnoControlGrid', + } + return services[tipo] + + def add_control(self, properties): + tipo = properties.pop('Type').lower() + base = self._get_base_control(tipo) + obj = create_instance(base, True) + model = create_instance('{}Model'.format(base), True) + set_properties(model, properties) + obj.setModel(model) + x = properties.get('X', 5) + y = properties.get('Y', 5) + w = properties.get('Width', 200) + h = properties.get('Height', 25) + obj.setPosSize(x, y, w, h, POSSIZE) + name = properties['Name'] + self._container.addControl(name, obj) + add_listeners(self.events, obj, name) + control = get_custom_class(tipo, obj) + setattr(self, name, control) + return + + def _create_popupmenu(self, menus): + menu = create_instance('com.sun.star.awt.PopupMenu', True) + for i, m in enumerate(menus): + label = m['label'] + cmd = m.get('event', '') + if not cmd: + cmd = label.lower().replace(' ', '_') + if label == '-': + menu.insertSeparator(i) + else: + menu.insertItem(i, label, m.get('style', 0), i) + menu.setCommand(i, cmd) + # ~ menu.setItemImage(i, path?, True) + menu.addMenuListener(EventsMenu(self.events)) + return menu + + def _create_menu(self, menus): + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMenu.html + #~ nItemId specifies the ID of the menu item to be inserted. + #~ aText specifies the label of the menu item. + #~ nItemStyle 0 = Standard, CHECKABLE = 1, RADIOCHECK = 2, AUTOCHECK = 4 + #~ nItemPos specifies the position where the menu item will be inserted. + self._menu = create_instance('com.sun.star.awt.MenuBar', True) + for i, m in enumerate(menus): + self._menu.insertItem(i, m['label'], m.get('style', 0), i) + cmd = m['label'].lower().replace(' ', '_') + self._menu.setCommand(i, cmd) + submenu = self._create_popupmenu(m['submenu']) + self._menu.setPopupMenu(i, submenu) + + self._window.setMenuBar(self._menu) + return + + def add_menu(self, menus): + self._create_menu(menus) + return + + def _add_listeners(self, control=None): + if self.events is None: + return + controller = EventsWindow(self) + self._window.addTopWindowListener(controller) + self._window.addWindowListener(controller) + self._container.addKeyListener(EventsKey(self)) + return + + @property + def name(self): + return self._title.lower().replace(' ', '_') + + @property + def events(self): + return self._events + @events.setter + def events(self, value): + self._events = value + self._add_listeners() + + @property + def width(self): + return self._container.Size.Width + + @property + def height(self): + return self._container.Size.Height + + def open(self): + self._window.setVisible(True) + return + + def close(self): + self._window.setMenuBar(None) + self._window.dispose() + self._frame.close(True) + return + + # ~ Python >= 3.7 # ~ def __getattr__(name): @@ -1447,7 +3246,6 @@ def get_document(title=''): return _get_class_doc(doc) -# ~ Export ok def get_documents(custom=True): docs = [] desktop = get_desktop() @@ -1479,18 +3277,11 @@ def active_cell(): def create_dialog(properties): - return LODialog(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 create_window(kwargs): + return LOWindow(**kwargs) # ~ Export ok @@ -1617,6 +3408,16 @@ def from_json(path): return data +# ~ Export ok +def json_dumps(data): + return json.dumps(data, indent=4, sort_keys=True) + + +# ~ Export ok +def json_loads(data): + return json.loads(data) + + def get_path_extension(id): pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') path = _path_system(pip.getPackageLocation(id)) @@ -1640,7 +3441,7 @@ def inputbox(message, default='', title=TITLE, echochar=''): 'Width': 200, 'Height': 80, } - dlg = LODialog(args) + dlg = LODialog(**args) dlg.events = ControllersInput(dlg) args = { @@ -1706,12 +3507,29 @@ def new_doc(type_doc=CALC, **kwargs): # ~ Export ok -def new_db(path): +def new_db(path, name=''): + p, fn, n, e = get_info_path(path) + if not name: + name = n + return LOBase(name, path) + + +# ~ Todo +def exists_db(name): dbc = create_instance('com.sun.star.sdb.DatabaseContext') - db = dbc.createInstance() - db.URL = 'sdbc:embedded:firebird' # hsqldb - db.DatabaseDocument.storeAsURL(_path_url(path), ()) - return _get_class_doc(db) + return dbc.hasRegisteredDatabase(name) + + +# ~ Todo +def register_db(name, path): + dbc = create_instance('com.sun.star.sdb.DatabaseContext') + dbc.registerDatabaseLocation(name, _path_url(path)) + return + + +# ~ Todo +def get_db(name): + return LOBase(name) # ~ Export ok @@ -1784,7 +3602,6 @@ def zip_content(path): return names -# ~ Export ok def run(command, wait=False): # ~ debug(command) # ~ debug(shlex.split(command)) @@ -1922,7 +3739,6 @@ def kill(path): return -# ~ Export ok def get_size_screen(): if IS_WIN: user32 = ctypes.windll.user32 @@ -1933,7 +3749,6 @@ def get_size_screen(): return res.strip() -# ~ Export ok def get_clipboard(): df = None text = '' @@ -1987,7 +3802,7 @@ def set_clipboard(value): return -# ~ Todo +# ~ Export ok def copy(): call_dispatch('.uno:Copy') return @@ -2012,12 +3827,16 @@ def file_copy(source, target='', name=''): return path_new -# ~ Export ok -def get_path_content(path, filters='*'): +def get_path_content(path, filters=''): paths = [] + if filters in ('*', '*.*'): + filters = '' for folder, _, files in os.walk(path): - pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) - paths += [join(folder, f) for f in files if pattern.search(f)] + if filters: + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [join(folder, f) for f in files if pattern.search(f)] + else: + paths += files return paths @@ -2175,7 +3994,12 @@ def end(): # ~ Export ok # ~ https://en.wikipedia.org/wiki/Web_colors -def get_color(value): +def get_color(*value): + if len(value) == 1 and isinstance(value[0], int): + return value[0] + if len(value) == 1 and isinstance(value[0], tuple): + value = value[0] + COLORS = { 'aliceblue': 15792383, 'antiquewhite': 16444375, @@ -2326,14 +4150,19 @@ def get_color(value): 'yellowgreen': 10145074, } - if isinstance(value, tuple): - return (value[0] << 16) + (value[1] << 8) + value[2] + if len(value) == 3: + color = (value[0] << 16) + (value[1] << 8) + value[2] + else: + value = value[0] + if value[0] == '#': + r, g, b = bytes.fromhex(value[1:]) + color = (r << 16) + (g << 8) + b + else: + color = COLORS.get(value.lower(), -1) + return color - if isinstance(value, str) and value[0] == '#': - r, g, b = bytes.fromhex(value[1:]) - return (r << 16) + (g << 8) + b - return COLORS.get(value.lower(), -1) +COLOR_ON_FOCUS = get_color('LightYellow') # ~ Export ok @@ -2355,6 +4184,24 @@ def _to_date(value): return new_value +def date_to_struct(value): + # ~ print(type(value)) + if isinstance(value, datetime.datetime): + d = DateTime() + d.Seconds = value.second + d.Minutes = value.minute + d.Hours = value.hour + d.Day = value.day + d.Month = value.month + d.Year = value.year + elif isinstance(value, datetime.date): + d = Date() + d.Day = value.day + d.Month = value.month + d.Year = value.year + return d + + # ~ Export ok def format(template, data): """ @@ -2381,10 +4228,12 @@ def _call_macro(macro): name = 'com.sun.star.script.provider.MasterScriptProviderFactory' factory = create_instance(name, False) + macro['language'] = macro.get('language', 'Python') + macro['location'] = macro.get('location', 'user') data = macro.copy() - if macro['language'] == 'Python': + if data['language'] == 'Python': data['module'] = '.py$' - elif macro['language'] == 'Basic': + elif data['language'] == 'Basic': data['module'] = '.{}.'.format(macro['module']) if macro['location'] == 'user': data['location'] = 'application' @@ -2477,7 +4326,7 @@ class SmtpServer(object): def __enter__(self): return self - def __exit__(self, *args): + def __exit__(self, exc_type, exc_value, traceback): self.close() @property @@ -2612,14 +4461,112 @@ def server_smtp_test(config): return server.error -# ~ 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 +def import_csv(path, **kwargs): + """ + See https://docs.python.org/3.5/library/csv.html#csv.reader + """ + with open(path) as f: + rows = tuple(csv.reader(f, **kwargs)) + return rows + +def export_csv(path, data, **kwargs): + with open(path, 'w') as f: + writer = csv.writer(f, **kwargs) + writer.writerows(data) + return + + +class LIBOServer(object): + HOST = 'localhost' + PORT = '8100' + ARG = 'socket,host={},port={};urp;StarOffice.ComponentContext'.format(HOST, PORT) + CMD = ['soffice', + '-env:SingleAppInstance=false', + '-env:UserInstallation=file:///tmp/LIBO_Process8100', + '--headless', '--norestore', '--invisible', + '--accept={}'.format(ARG)] + + def __init__(self): + self._server = None + self._ctx = None + self._sm = None + self._start_server() + self._init_values() + + def _init_values(self): + global CTX + global SM + + if not self.is_running: + return + + ctx = uno.getComponentContext() + service = 'com.sun.star.bridge.UnoUrlResolver' + resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx) + self._ctx = resolver.resolve('uno:{}'.format(self.ARG)) + self._sm = self._ctx.getServiceManager() + CTX = self._ctx + SM = self._sm + return + + @property + def is_running(self): + try: + s = socket.create_connection((self.HOST, self.PORT), 5.0) + s.close() + debug('LibreOffice is running...') + return True + except ConnectionRefusedError: + return False + + def _start_server(self): + if self.is_running: + return + + for i in range(3): + self._server = subprocess.Popen(self.CMD, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + time.sleep(3) + if self.is_running: + break + return + + def stop(self): + if self._server is None: + print('Search pgrep soffice') + else: + self._server.terminate() + debug('LibreOffice is stop...') + return + + def create_instance(self, name, with_context=True): + if with_context: + instance = self._sm.createInstanceWithContext(name, self._ctx) + else: + instance = self._sm.createInstance(name) + return instance + + + + +# ~ controls = { + # ~ 'CheckBox': 'com.sun.star.awt.UnoControlCheckBoxModel', + # ~ 'ComboBox': 'com.sun.star.awt.UnoControlComboBoxModel', + # ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', + # ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', + # ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', + # ~ 'FixedLine': 'com.sun.star.awt.UnoControlFixedLineModel', + # ~ 'FixedText': 'com.sun.star.awt.UnoControlFixedTextModel', + # ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', + # ~ 'GroupBox': 'com.sun.star.awt.UnoControlGroupBoxModel', + # ~ 'ImageControl': 'com.sun.star.awt.UnoControlImageControlModel', + # ~ 'ListBox': 'com.sun.star.awt.UnoControlListBoxModel', + # ~ 'NumericField': 'com.sun.star.awt.UnoControlNumericFieldModel', + # ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', + # ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', + # ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', + # ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', + # ~ 'SpinButton': 'com.sun.star.awt.UnoControlSpinButtonModel', + # ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', + # ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', +# ~ } diff --git a/files/ZAZFavorites_v0.3.0.oxt b/files/ZAZFavorites_v0.3.0.oxt new file mode 100644 index 0000000000000000000000000000000000000000..1b8f5e68e874b21995a1fbb012094f8f529c0bdc GIT binary patch literal 65455 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 literal 0 HcmV?d00001 diff --git a/source/Jobs.xcu b/source/Jobs.xcu deleted file mode 100755 index 2acfa90..0000000 --- a/source/Jobs.xcu +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - service:net.elmau.zaz.Favorites - - - - first - - - - - - - - - - - - diff --git a/source/META-INF/manifest.xml b/source/META-INF/manifest.xml index ef10414..ce03a57 100644 --- a/source/META-INF/manifest.xml +++ b/source/META-INF/manifest.xml @@ -1,7 +1,6 @@ - - - - - - + + + + + diff --git a/source/ZAZFavorites.py b/source/ZAZFavorites.py index b70d1c2..46c993e 100644 --- a/source/ZAZFavorites.py +++ b/source/ZAZFavorites.py @@ -70,9 +70,8 @@ class Controllers(object): 'After': '.uno:RecentFileList', 'Submenu': submenus, } - doc = app.get_document() - for doc_type in ('calc', 'writer'): + for doc_type in ('main', 'calc', 'writer'): app.remove_menu(doc_type, 'File', command) app.insert_menu(doc_type, 'File', **data) app.set_config('paths', paths) @@ -115,6 +114,7 @@ class ZAZFavorites(unohelper.Base, XJobExecutor): self.path_ext = app.get_path_extension(ID_EXTENSION) self.IMAGES = app.join(self.path_ext, self.IMAGES) + @app.catch_exception def trigger(self, args): if args == 'config': return self._config() diff --git a/source/description.xml b/source/description.xml index 30abecd..d0da032 100644 --- a/source/description.xml +++ b/source/description.xml @@ -1,7 +1,7 @@ - + Favorites files Archivos favoritos diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py index 3bf53b9..bd5f1aa 100644 --- a/source/pythonpath/easymacro.py +++ b/source/pythonpath/easymacro.py @@ -18,6 +18,7 @@ # ~ along with ZAZ. If not, see . import base64 +import csv import ctypes import datetime import errno @@ -30,6 +31,7 @@ import platform import re import shlex import shutil +import socket import subprocess import sys import tempfile @@ -59,25 +61,37 @@ import mailbox import uno import unohelper from com.sun.star.util import Time, Date, DateTime -from com.sun.star.beans import PropertyValue +from com.sun.star.beans import PropertyValue, NamedValue 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.awt import Rectangle +from com.sun.star.awt import KeyEvent +from com.sun.star.awt.KeyFunction import QUIT from com.sun.star.datatransfer import XTransferable, DataFlavor from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK 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 +from com.sun.star.awt import XMouseMotionListener +from com.sun.star.util import XModifyListener +from com.sun.star.awt import XTopWindowListener +from com.sun.star.awt import XWindowListener +from com.sun.star.awt import XMenuListener +from com.sun.star.awt import XKeyListener +from com.sun.star.awt import XItemListener +from com.sun.star.awt import XFocusListener + try: from fernet import Fernet, InvalidToken - CRYPTO = True except ImportError: - CRYPTO = False + pass MSG_LANG = { @@ -119,9 +133,15 @@ TYPE_DOC = { 'base': 'com.sun.star.sdb.DocumentDataSource', 'math': 'com.sun.star.formula.FormulaProperties', 'basic': 'com.sun.star.script.BasicIDE', + 'main': 'com.sun.star.frame.StartModule', } NODE_MENUBAR = 'private:resource/menubar/menubar' +MENUS_MAIN = { + 'file': '.uno:PickList', + 'tools': '.uno:ToolsMenu', + 'help': '.uno:HelpMenu', +} MENUS_CALC = { 'file': '.uno:PickList', 'edit': '.uno:EditMenu', @@ -150,6 +170,7 @@ MENUS_WRITER = { } MENUS_APP = { + 'main': MENUS_MAIN, 'calc': MENUS_CALC, 'writer': MENUS_WRITER, } @@ -174,13 +195,13 @@ log = logging.getLogger(__name__) _start = 0 _stop_thread = {} TIMEOUT = 10 +SECONDS_DAY = 60 * 60 * 24 CTX = uno.getComponentContext() SM = CTX.getServiceManager() -# ~ Export ok def create_instance(name, with_context=False): if with_context: instance = SM.createInstanceWithContext(name, CTX) @@ -189,28 +210,35 @@ def create_instance(name, with_context=False): return instance -def _get_app_config(key, node_name): +def get_app_config(node_name, key=''): name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationAccess' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) try: ca = cp.createInstanceWithArguments(service, (node,)) - if ca and (ca.hasByName(key)): - data = ca.getPropertyValue(key) - return data + if ca and not key: + return ca + if ca and ca.hasByName(key): + return ca.getPropertyValue(key) except Exception as e: - log.error(e) + error(e) return '' -LANGUAGE = _get_app_config('ooLocale', 'org.openoffice.Setup/L10N/') +# ~ FILTER_PDF = '/org.openoffice.Office.Common/Filter/PDF/Export/' +LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') LANG = LANGUAGE.split('-')[0] -NAME = TITLE = _get_app_config('ooName', 'org.openoffice.Setup/Product') -VERSION = _get_app_config('ooSetupVersion', 'org.openoffice.Setup/Product') +NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') +VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') + +nd = '/org.openoffice.Office.Calc/Calculate/Other/Date' +d = get_app_config(nd, 'DD') +m = get_app_config(nd, 'MM') +y = get_app_config(nd, 'YY') +DATE_OFFSET = datetime.date(y, m, d).toordinal() -# ~ Export ok def mri(obj): m = create_instance('mytools.Mri') if m is None: @@ -239,7 +267,6 @@ class LogWin(object): def __init__(self, doc): self.doc = doc - self.doc.Title = FILE_NAME_DEBUG def write(self, info): text = self.doc.Text @@ -249,18 +276,15 @@ class LogWin(object): return -# ~ Export ok def info(data): log.info(data) return -# ~ Export ok def debug(info): if IS_WIN: doc = get_document(FILE_NAME_DEBUG) if doc is None: - # ~ doc = new_doc('writer') return doc = LogWin(doc.obj) doc.write(info) @@ -270,13 +294,11 @@ def debug(info): return -# ~ Export ok def error(info): log.error(info) return -# ~ Export ok def save_log(path, data): with open(path, 'a') as out: out.write('{} -{}- '.format(str(now())[:19], LOG_NAME)) @@ -296,7 +318,31 @@ def now(): return datetime.datetime.now() -# ~ Export ok +def today(): + return datetime.date.today() + + +def time(): + return datetime.datetime.now().time() + + +def get_date(year, month, day, hour=-1, minute=-1, second=-1): + if hour > -1 or minute > -1 or second > -1: + h = hour + m = minute + s = second + if h == -1: + h = 0 + if m == -1: + m = 0 + if s == -1: + s = 0 + d = datetime.datetime(year, month, day, h, m, s) + else: + d = datetime.date(year, month, day) + return d + + def get_config(key='', default=None, prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) values = None @@ -314,7 +360,6 @@ def get_config(key='', default=None, prefix='config'): return values -# ~ Export ok def set_config(key, value, prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) path = join(get_config_path('UserConfig'), path_json) @@ -322,10 +367,9 @@ def set_config(key, value, prefix='config'): 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 + return True -# ~ Export ok def sleep(seconds): time.sleep(seconds) return @@ -342,7 +386,6 @@ def _(msg): return MSG_LANG[L][msg] -# ~ Export ok def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): """ Create message box type_msg: infobox, warningbox, errorbox, querybox, messbox @@ -354,18 +397,15 @@ def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infob return mb.execute() -# ~ Export ok def question(message, title=TITLE): res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') return res == YES -# ~ Export ok def warning(message, title=TITLE): return msgbox(message, title, type_msg='warningbox') -# ~ Export ok def errorbox(message, title=TITLE): return msgbox(message, title, type_msg='errorbox') @@ -374,12 +414,10 @@ def get_desktop(): return create_instance('com.sun.star.frame.Desktop', True) -# ~ Export ok def get_dispatch(): return create_instance('com.sun.star.frame.DispatchHelper') -# ~ Export ok def call_dispatch(url, args=()): frame = get_document().frame dispatch = get_dispatch() @@ -387,7 +425,6 @@ def call_dispatch(url, args=()): return -# ~ Export ok def get_temp_file(): delete = True if IS_WIN: @@ -407,7 +444,6 @@ def _path_system(path): return path -# ~ Export ok def exists_app(name): try: dn = subprocess.DEVNULL @@ -418,12 +454,10 @@ def exists_app(name): return True -# ~ Export ok def exists_path(path): return Path(path).exists() -# ~ Export ok def get_type_doc(obj): for k, v in TYPE_DOC.items(): if obj.supportsService(v): @@ -443,12 +477,99 @@ def property_to_dict(values): return d +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 array_to_dict(values): d = {r[0]: r[1] for r in values} return d # ~ Custom classes +class ObjectBase(object): + + def __init__(self, obj): + self._obj = obj + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __getitem__(self, index): + return self.obj[index] + + def __getattr__(self, name): + a = None + if name == 'obj': + a = super().__getattr__(name) + else: + if hasattr(self.obj, name): + a = getattr(self.obj, name) + return a + + @property + def obj(self): + return self._obj + @obj.setter + def obj(self, value): + self._obj = value + + +class LOObjectBase(object): + + def __init__(self, obj): + self.__dict__['_obj'] = obj + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + return True + + def __setattr__(self, name, value): + print('BASE__setattr__', name) + if name == '_obj': + super().__setattr__(name, value) + else: + self.obj.setPropertyValue(name, value) + + # ~ def _try_for_method(self, name): + # ~ a = None + # ~ m = 'get{}'.format(name) + # ~ if hasattr(self.obj, m): + # ~ a = getattr(self.obj, m)() + # ~ else: + # ~ a = getattr(self.obj, name) + # ~ return a + + def __getattr__(self, name): + print('BASE__getattr__', name) + if name == 'obj': + a = super().__getattr__(name) + else: + a = self.obj.getPropertyValue(name) + # ~ Bug + if a is None: + msg = 'Error get: {} - {}'.format(self.obj.ImplementationName, name) + error(msg) + raise Exception(msg) + return a + + @property + def obj(self): + return self._obj + + class LODocument(object): def __init__(self, obj): @@ -457,23 +578,30 @@ class LODocument(object): def _init_values(self): self._type_doc = get_type_doc(self.obj) - if self._type_doc == 'base': - self._cc = self.obj.DatabaseDocument.getCurrentController() - else: - self._cc = self.obj.getCurrentController() + # ~ if self._type_doc == 'base': + # ~ self._cc = self.obj.DatabaseDocument.getCurrentController() + # ~ else: + 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() + @title.setter + def title(self, value): + self.obj.setTitle(value) + + @property + def uid(self): + return self.obj.RuntimeUID + + @property + def type(self): + return self._type_doc @property def frame(self): @@ -502,7 +630,7 @@ class LODocument(object): @property def visible(self): w = self._cc.getFrame().getContainerWindow() - return w.Visible + return w.isVisible() @visible.setter def visible(self, value): w = self._cc.getFrame().getContainerWindow() @@ -515,6 +643,11 @@ class LODocument(object): def zoom(self, value): self._cc.ZoomValue = value + @property + def table_auto_formats(self): + taf = create_instance('com.sun.star.sheet.TableAutoFormats') + return taf.ElementNames + def create_instance(self, name): obj = self.obj.createInstance(name) return obj @@ -568,19 +701,150 @@ class LODocument(object): return path_pdf -class LOCalc(LODocument): +class LOForm(ObjectBase): def __init__(self, obj): super().__init__(obj) + @property + def name(self): + return self._obj.getName() + @name.setter + def name(self, value): + self._obj.setName(value) + + +class LOForms(ObjectBase): + + def __init__(self, obj, doc): + self._doc = doc + super().__init__(obj) + + def __getitem__(self, index): + form = super().__getitem__(index) + return LOForm(form) + + @property + def doc(self): + return self._doc + + @property + def count(self): + return self.obj.getCount() + + @property + def names(self): + return self.obj.getElementNames() + + def exists(self, name): + return name in self.names + + def insert(self, name): + form = self.doc.create_instance('com.sun.star.form.component.Form') + self.obj.insertByName(name, form) + return self[name] + + def remove(self, index): + if isinstance(index, int): + self.obj.removeByIndex(index) + else: + self.obj.removeByName(index) + return + + +class LOCellStyle(LOObjectBase): + + def __init__(self, obj): + super().__init__(obj) + + @property + def name(self): + return self.obj.Name + + def apply(self, properties): + set_properties(self.obj, properties) + return + + +class LOCellStyles(object): + + def __init__(self, obj): + self._obj = obj + + 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 apply(self, style, properties): + set_properties(style, properties) + return + + +class LOCalc(LODocument): + + def __init__(self, obj): + super().__init__(obj) + self._sheets = obj.getSheets() + + def __getitem__(self, index): + if isinstance(index, str): + index = [s.Name for s in self._sheets if s.CodeName == index][0] or index + return LOCalcSheet(self._sheets[index], self) + + def __setitem__(self, key, value): + self._sheets[key] = value + + def __contains__(self, item): + return item in self.obj.Sheets + + @property + def headers(self): + return self._cc.ColumnRowHeaders + @headers.setter + def headers(self, value): + self._cc.ColumnRowHeaders = value + + @property + def tabs(self): + return self._cc.SheetTabs + @tabs.setter + def tabs(self, value): + self._cc.SheetTabs = value + @property def active(self): return LOCalcSheet(self._cc.getActiveSheet(), self) + def activate(self, sheet): + obj = sheet + if isinstance(sheet, LOCalcSheet): + obj = sheet.obj + elif isinstance(sheet, str): + obj = self[sheet].obj + self._cc.setActiveSheet(obj) + return + @property def selection(self): sel = self.obj.getCurrentSelection() @@ -588,6 +852,97 @@ class LOCalc(LODocument): sel = LOCellRange(sel, self) return sel + @property + def sheets(self): + return LOCalcSheets(self._sheets, self) + + @property + def names(self): + return self.sheets.names + + @property + def cell_style(self): + obj = self.obj.getStyleFamilies()['CellStyles'] + return LOCellStyles(obj) + + def create(self): + return self.obj.createInstance('com.sun.star.sheet.Spreadsheet') + + def insert(self, name, pos=-1): + # ~ sheet = obj.createInstance('com.sun.star.sheet.Spreadsheet') + # ~ obj.Sheets['New'] = sheet + index = pos + if pos < 0: + index = self._sheets.Count + pos + 1 + if isinstance(name, str): + self._sheets.insertNewByName(name, index) + else: + for n in name: + self._sheets.insertNewByName(n, index) + name = n + return LOCalcSheet(self._sheets[name], self) + + def move(self, name, pos=-1): + return self.sheets.move(name, pos) + + def remove(self, name): + return self.sheets.remove(name) + + def copy(self, source='', target='', pos=-1): + index = pos + if pos < 0: + index = self._sheets.Count + pos + 1 + + names = source + if not names: + names = self.names + elif isinstance(source, str): + names = (source,) + + new_names = target + if not target: + new_names = [n + '_2' for n in names] + elif isinstance(target, str): + new_names = (target,) + + for i, ns in enumerate(names): + self.sheets.copy(ns, new_names[i], index + i) + + return LOCalcSheet(self._sheets[index], self) + + def copy_from(self, doc, source='', target='', pos=-1): + index = pos + if pos < 0: + index = self._sheets.Count + pos + 1 + + names = source + if not names: + names = doc.names + elif isinstance(source, str): + names = (source,) + + new_names = target + if not target: + new_names = names + elif isinstance(target, str): + new_names = (target,) + + for i, n in enumerate(names): + self._sheets.importSheet(doc.obj, n, index + i) + self.sheets[index + i].name = new_names[i] + + # ~ doc.getCurrentController().setActiveSheet(sheet) + # ~ For controls in sheet + # ~ doc.getCurrentController().setFormDesignMode(False) + + return LOCalcSheet(self._sheets[index], self) + + def sort(self, reverse=False): + names = sorted(self.names, reverse=reverse) + for i, n in enumerate(names): + self.sheets.move(n, i) + return + def get_cell(self, index=None): """ index is str 'A1' @@ -608,6 +963,72 @@ class LOCalc(LODocument): self._cc.select(r) return + def create_cell_style(self, name=''): + obj = self.create_instance('com.sun.star.style.CellStyle') + if name: + self.cell_style[name] = obj + return LOCellStyle(obj) + + def clear_undo(self): + self.obj.getUndoManager().clear() + return + + def filter_by_color(self, cell=None): + if cell is None: + cell = self.selection.first + cr = cell.current_region + col = cell.column - cr.column + rangos = cell.get_column(col).visible + for r in rangos: + for row in range(r.rows): + c = r[row, 0] + if c.back_color != cell.back_color: + c.rows_visible = False + return + + +class LOCalcSheets(object): + + def __init__(self, obj, doc): + self._obj = obj + self._doc = doc + + def __getitem__(self, index): + return LOCalcSheet(self.obj[index], self.doc) + + @property + def obj(self): + return self._obj + + @property + def doc(self): + return self._doc + + @property + def count(self): + return self.obj.Count + + @property + def names(self): + return self.obj.ElementNames + + def copy(self, name, new_name, pos): + self.obj.copyByName(name, new_name, pos) + return + + def move(self, name, pos): + index = pos + if pos < 0: + index = self.count + pos + 1 + sheet = self.obj[name] + self.obj.moveByName(sheet.Name, index) + return + + def remove(self, name): + sheet = self.obj[name] + self.obj.removeByName(sheet.Name) + return + class LOCalcSheet(object): @@ -619,7 +1040,15 @@ class LOCalcSheet(object): def __getitem__(self, index): return LOCellRange(self.obj[index], self.doc) + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + def _init_values(self): + self._events = None + self._dp = self.obj.getDrawPage() return @property @@ -630,6 +1059,91 @@ class LOCalcSheet(object): def doc(self): return self._doc + @property + def name(self): + return self._obj.Name + @name.setter + def name(self, value): + self._obj.Name = value + + @property + def code_name(self): + return self._obj.CodeName + @code_name.setter + def code_name(self, value): + self._obj.CodeName = value + + @property + def color(self): + return self._obj.TabColor + @color.setter + def color(self, value): + self._obj.TabColor = get_color(value) + + @property + def active(self): + return self.doc.selection.first + + def activate(self): + self.doc.activate(self.obj) + return + + @property + def visible(self): + return self.obj.IsVisible + @visible.setter + def visible(self, value): + self.obj.IsVisible = value + + @property + def is_protected(self): + return self._obj.isProtected() + + @property + def password(self): + return '' + @visible.setter + def password(self, value): + self.obj.protect(value) + + def unprotect(self, value): + try: + self.obj.unprotect(value) + return True + except: + pass + return False + + def get_cursor(self, cell): + return self.obj.createCursorByRange(cell) + + def exists_chart(self, name): + return name in self.obj.Charts.ElementNames + + @property + def forms(self): + return LOForms(self._dp.getForms(), self.doc) + + @property + def events(self): + return self._events + @events.setter + def events(self, controllers): + self._events = controllers + self._connect_listeners() + + def _connect_listeners(self): + if self.events is None: + return + + listeners = { + 'addModifyListener': EventsModify, + } + for key, value in listeners.items(): + getattr(self.obj, key)(listeners[key](self.events)) + print('add_listener') + return + class LOWriter(LODocument): @@ -652,18 +1166,46 @@ class LOWriter(LODocument): def cursor(self): return self.text.createTextCursor() + @property + def paragraphs(self): + return [LOTextRange(p) for p in self.text] + @property def selection(self): - sel = self._cc.getSelection() + sel = self.obj.getCurrentSelection() return LOTextRange(sel[0]) + def write(self, data, cursor=None): + cursor = cursor or self.selection.cursor.getEnd() + if data.startswith('\n'): + c = data.split('\n') + for i in range(len(c)-1): + self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + else: + self.text.insertString(cursor, data, False) + return + + def insert_table(self, data, cursor=None): + cursor = cursor or self.selection.cursor.getEnd() + table = self.obj.createInstance('com.sun.star.text.TextTable') + rows = len(data) + cols = len(data[0]) + table.initialize(rows, cols) + self.insert_content(cursor, table) + table.DataArray = data + return WriterTable(table) + + def create_chart(self, tipo, cursor=None): + cursor = cursor or self.selection.cursor.getEnd() + chart = LOChart(None, tipo) + chart.cursor = cursor + chart.doc = self + return chart + 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)) @@ -679,16 +1221,40 @@ class LOWriter(LODocument): self.insert_content(cursor, image) return + def go_start(self): + cursor = self._cc.getViewCursor() + cursor.gotoStart(False) + return cursor + + def go_end(self): + cursor = self._cc.getViewCursor() + cursor.gotoEnd(False) + return cursor + + def select(self, text): + self._cc.select(text) + return + class LOTextRange(object): def __init__(self, obj): self._obj = obj + self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph' + self._is_table = self.obj.ImplementationName == 'SwXTextTable' @property def obj(self): return self._obj + @property + def is_paragraph(self): + return self._is_paragraph + + @property + def is_table(self): + return self._is_table + @property def string(self): return self.obj.String @@ -702,10 +1268,139 @@ class LOTextRange(object): return self.text.createTextCursorByRange(self.obj) -class LOBase(LODocument): +class LOBase(object): + TYPES = { + str: 'setString', + int: 'setInt', + float: 'setFloat', + bool: 'setBoolean', + Date: 'setDate', + Time: 'setTime', + DateTime: 'setTimestamp', + } + # ~ setArray + # ~ setBinaryStream + # ~ setBlob + # ~ setByte + # ~ setBytes + # ~ setCharacterStream + # ~ setClob + # ~ setNull + # ~ setObject + # ~ setObjectNull + # ~ setObjectWithInfo + # ~ setPropertyValue + # ~ setRef + def __init__(self, name, path='', **kwargs): + self._name = name + self._path = path + self._dbc = create_instance('com.sun.star.sdb.DatabaseContext') + if path: + path_url = _path_url(path) + db = self._dbc.createInstance() + db.URL = 'sdbc:embedded:firebird' + db.DatabaseDocument.storeAsURL(path_url, ()) + if not self.exists: + self._dbc.registerDatabaseLocation(name, path_url) + else: + if name.startswith('odbc:'): + self._con = self._odbc(name, kwargs) + else: + db = self._dbc.getByName(name) + self.path = _path_system(self._dbc.getDatabaseLocation(name)) + self._con = db.getConnection('', '') - def __init__(self, obj): - super().__init__(obj) + if self._con is None: + msg = 'Not connected to: {}'.format(name) + else: + msg = 'Connected to: {}'.format(name) + debug(msg) + + def _odbc(self, name, kwargs): + dm = create_instance('com.sun.star.sdbc.DriverManager') + args = dict_to_property(kwargs) + try: + con = dm.getConnectionWithInfo('sdbc:{}'.format(name), args) + return con + except Exception as e: + error(str(e)) + return None + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self._name + + @property + def connection(self): + return self._con + + @property + def path(self): + return self._path + @path.setter + def path(self, value): + self._path = value + + @property + def exists(self): + return self._dbc.hasRegisteredDatabase(self.name) + + @classmethod + def register(self, path, name): + if not self._dbc.hasRegisteredDatabase(name): + self._dbc.registerDatabaseLocation(name, _path_url(path)) + return + + def revoke(self, name): + self._dbc.revokeDatabaseLocation(name) + return True + + def save(self): + # ~ self._db.connection.commit() + # ~ self._db.connection.getTables().refresh() + # ~ oDisp.executeDispatch(oFrame,".uno:DBRefreshTables", "", 0, Array()) + self._obj.DatabaseDocument.store() + self.refresh() + return + + def close(self): + self._con.close() + return + + def refresh(self): + self._con.getTables().refresh() + return + + def get_tables(self): + tables = self._con.getTables() + tables = [tables.getByIndex(i) for i in range(tables.Count)] + return tables + + def cursor(self, sql, params): + cursor = self._con.prepareStatement(sql) + for i, v in enumerate(params, 1): + if not type(v) in self.TYPES: + error('Type not support') + debug((i, type(v), v, self.TYPES[type(v)])) + getattr(cursor, self.TYPES[type(v)])(i, v) + return cursor + + def execute(self, sql, params): + debug(sql) + if params: + cursor = self.cursor(sql, params) + cursor.execute() + else: + cursor = self._con.createStatement() + cursor.execute(sql) + # ~ resulset = cursor.executeQuery(sql) + # ~ rows = cursor.executeUpdate(sql) + self.save() + return cursor class LODrawImpress(LODocument): @@ -770,7 +1465,7 @@ class LOCellRange(object): def __enter__(self): return self - def __exit__(self, *args): + def __exit__(self, exc_type, exc_value, traceback): pass def __getitem__(self, index): @@ -827,6 +1522,16 @@ class LOCellRange(object): self.obj.setString(data) elif isinstance(data, (int, float)): self.obj.setValue(data) + elif isinstance(data, datetime.datetime): + d = data.toordinal() + t = (data - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY + self.obj.setValue(d - DATE_OFFSET + t) + elif isinstance(data, datetime.date): + d = data.toordinal() + self.obj.setValue(d - DATE_OFFSET) + elif isinstance(data, datetime.time): + d = (data.hour * 3600 + data.minute * 60 + data.second) / SECONDS_DAY + self.obj.setValue(d) @property def data(self): @@ -837,19 +1542,96 @@ class LOCellRange(object): values = tuple(values) self.obj.setDataArray(values) - def offset(self, col=1, row=0): + @property + def formula(self): + return self.obj.getFormulaArray() + @formula.setter + def formula(self, values): + if isinstance(values, list): + values = tuple(values) + self.obj.setFormulaArray(values) + + @property + def column(self): a = self.address - col = a.Column + col - row = a.Row + row - return LOCellRange(self.sheet[row,col], self.doc) + if hasattr(a, 'Column'): + c = a.Column + else: + c = a.StartColumn + return c + + @property + def columns(self): + return self._obj.Columns.Count + + @property + def rows(self): + return self._obj.Rows.Count + + def to_size(self, rows, cols): + cursor = self.sheet.get_cursor(self.obj[0,0]) + cursor.collapseToSize(cols, rows) + return LOCellRange(self.sheet[cursor.AbsoluteName].obj, self.doc) + + def copy_from(self, rango): + data = rango + if isinstance(rango, LOCellRange): + data = rango.data + rows = len(data) + cols = len(data[0]) + self.to_size(rows, cols).data = data + return + + def copy_to(self, cell, formula=False): + rango = cell.to_size(self.rows, self.columns) + if formula: + rango.formula = self.data + else: + rango.data = self.data + return + + def offset(self, row=1, col=0): + ra = self.obj.getRangeAddress() + col = ra.EndColumn + col + row = ra.EndRow + row + return LOCellRange(self.sheet[row, col].obj, self.doc) + + @property + def next_cell(self): + a = self.current_region.address + if hasattr(a, 'StartColumn'): + col = a.StartColumn + else: + col = a.Column + if hasattr(a, 'EndRow'): + row = a.EndRow + 1 + else: + row = a.Row + 1 + + return LOCellRange(self.sheet[row, col].obj, self.doc) @property def sheet(self): - return self.obj.Spreadsheet + return LOCalcSheet(self.obj.Spreadsheet, self.doc) + + @property + def charts(self): + return self.obj.Spreadsheet.Charts + + @property + def ps(self): + ps = Rectangle() + s = self.obj.Size + p = self.obj.Position + ps.X = p.X + ps.Y = p.Y + ps.Width = s.Width + ps.Height = s.Height + return ps @property def draw_page(self): - return self.sheet.getDrawPage() + return self.sheet.obj.getDrawPage() @property def name(self): @@ -867,9 +1649,44 @@ class LOCellRange(object): @property def current_region(self): - cursor = self.sheet.createCursorByRange(self.obj[0,0]) + cursor = self.sheet.get_cursor(self.obj[0,0]) cursor.collapseToCurrentRegion() - return LOCellRange(self.sheet[cursor.AbsoluteName], self.doc) + return LOCellRange(self.sheet[cursor.AbsoluteName].obj, self.doc) + + @property + def visible(self): + cursor = self.sheet.get_cursor(self.obj) + rangos = [LOCellRange(self.sheet[r.AbsoluteName].obj, self.doc) + for r in cursor.queryVisibleCells()] + return tuple(rangos) + + @property + def empty(self): + cursor = self.sheet.get_cursor(self.obj) + rangos = [LOCellRange(self.sheet[r.AbsoluteName].obj, self.doc) + for r in cursor.queryEmptyCells()] + return tuple(rangos) + + @property + def back_color(self): + return self._obj.CellBackColor + @back_color.setter + def back_color(self, value): + self._obj.CellBackColor = get_color(value) + + @property + def cell_style(self): + return self.obj.CellStyle + @cell_style.setter + def cell_style(self, value): + self.obj.CellStyle = value + + @property + def auto_format(self): + return self.obj.CellStyle + @auto_format.setter + def auto_format(self, value): + self.obj.autoFormat(value) def insert_image(self, path, **kwargs): s = self.obj.Size @@ -882,15 +1699,92 @@ class LOCellRange(object): img.setSize(Size(w, h)) return + def insert_shape(self, tipo, **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.{}Shape'.format(tipo)) + set_properties(img, kwargs) + 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 + def in_range(self, rango): + if isinstance(rango, LOCellRange): + address = rango.address + else: + address = rango.getRangeAddress() + cursor = self.sheet.get_cursor(self.obj) + result = cursor.queryIntersection(address) + return bool(result.Count) + + def fill(self, source=1): + self.obj.fillAuto(0, source) + return + + def clear(self, what=31): + # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html + self.obj.clearContents(what) + return + + @property + def rows_visible(self): + return self._obj.getRows().IsVisible + @rows_visible.setter + def rows_visible(self, value): + self._obj.getRows().IsVisible = value + + @property + def columns_visible(self): + return self._obj.getColumns().IsVisible + @columns_visible.setter + def columns_visible(self, value): + self._obj.getColumns().IsVisible = value + + def get_column(self, index=0, first=False): + ca = self.address + ra = self.current_region.address + if hasattr(ca, 'Column'): + col = ca.Column + else: + col = ca.StartColumn + index + start = 1 + if first: + start = 0 + if hasattr(ra, 'Row'): + row_start = ra.Row + start + row_end = ra.Row + 1 + else: + row_start = ra.StartRow + start + row_end = ra.EndRow + 1 + return LOCellRange(self.sheet[row_start:row_end, col:col+1].obj, self.doc) + + def import_csv(self, path, **kwargs): + data = import_csv(path, **kwargs) + self.copy_from(data) + return + + def export_csv(self, path, **kwargs): + data = self.current_region.data + export_csv(path, data, **kwargs) + return + + def create_chart(self, tipo): + chart = LOChart(None, tipo) + chart.cell = self + return chart + class EventsListenerBase(unohelper.Base, XEventListener): - def __init__(self, controller, window=None): + def __init__(self, controller, name, window=None): self._controller = controller + self._name = name self._window = window def disposing(self, event): @@ -901,25 +1795,23 @@ class EventsListenerBase(unohelper.Base, XEventListener): class EventsButton(EventsListenerBase, XActionListener): - def __init__(self, controller): - super().__init__(controller) + def __init__(self, controller, name): + super().__init__(controller, name) def actionPerformed(self, event): - name = event.Source.Model.Name - event_name = '{}_action'.format(name) + event_name = '{}_action'.format(self._name) if hasattr(self._controller, event_name): getattr(self._controller, event_name)(event) return -class EventsMouse(EventsListenerBase, XMouseListener): +class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): - def __init__(self, controller): - super().__init__(controller) + def __init__(self, controller, name): + super().__init__(controller, name) def mousePressed(self, event): - name = event.Source.Model.Name - event_name = '{}_click'.format(name) + event_name = '{}_click'.format(self._name) if event.ClickCount == 2: event_name = '{}_double_click'.format(name) if hasattr(self._controller, event_name): @@ -935,6 +1827,26 @@ class EventsMouse(EventsListenerBase, XMouseListener): def mouseExited(self, event): pass + # ~ XMouseMotionListener + def mouseMoved(self, event): + pass + + def mouseDragged(self, event): + pass + + +class EventsMouseLink(EventsMouse): + + def mouseEntered(self, event): + obj = event.Source.Model + obj.TextColor = get_color('blue') + return + + def mouseExited(self, event): + obj = event.Source.Model + obj.TextColor = 0 + return + class EventsMouseGrid(EventsMouse): selected = False @@ -954,13 +1866,178 @@ class EventsMouseGrid(EventsMouse): 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) + # ~ 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 EventsModify(EventsListenerBase, XModifyListener): + + def __init__(self, controller): + super().__init__(controller) + + def modified(self, event): + event_name = '{}_modified'.format(event.Source.Name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsItem(EventsListenerBase, XItemListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def disposing(self, event): + pass + + def itemStateChanged(self, event): + pass + + +class EventsItemRoadmap(EventsItem): + + def itemStateChanged(self, event): + dialog = event.Source.Context.Model + dialog.Step = event.ItemId + 1 + return + + +class EventsFocus(EventsListenerBase, XFocusListener): + + def __init__(self, controller, name): + super().__init__(controller, name) + + def focusGained(self, event): + obj = event.Source.Model + obj.BackgroundColor = COLOR_ON_FOCUS + + def focusLost(self, event): + obj = event.Source.Model + obj.BackgroundColor = -1 + + +class EventsKey(EventsListenerBase, XKeyListener): + """ + event.KeyChar + event.KeyCode + event.KeyFunc + event.Modifiers + """ + + def __init__(self, cls): + super().__init__(cls.events, cls.name) + self._cls = cls + + def keyPressed(self, event): + pass + + def keyReleased(self, event): + event_name = '{}_key_released'.format(self._cls.name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + else: + if event.KeyFunc == QUIT and hasattr(self._cls, 'close'): + self._cls.close() + return + + +class EventsWindow(EventsListenerBase, XTopWindowListener, XWindowListener): + + def __init__(self, cls): + self._cls = cls + super().__init__(cls.events, cls.name, cls._window) + + def windowOpened(self, event): + event_name = '{}_opened'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowActivated(self, event): + control_name = '{}_activated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowDeactivated(self, event): + control_name = '{}_deactivated'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + def windowMinimized(self, event): + pass + + def windowNormalized(self, event): + pass + + def windowClosing(self, event): + if self._window: + control_name = 'window_closing' + else: + control_name = '{}_closing'.format(event.Source.Model.Name) + + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + # ~ else: + # ~ if not self._modal and not self._block: + # ~ event.Source.Visible = False + return + + def windowClosed(self, event): + control_name = '{}_closed'.format(event.Source.Model.Name) + if hasattr(self._controller, control_name): + getattr(self._controller, control_name)(event) + return + + # ~ XWindowListener + def windowResized(self, event): + # ~ sb = self._container.getControl('subcontainer') + # ~ sb.setPosSize(0, 0, event.Width, event.Height, SIZE) + event_name = '{}_resized'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def windowMoved(self, event): + pass + + def windowShown(self, event): + pass + + def windowHidden(self, event): + pass + + +class EventsMenu(EventsListenerBase, XMenuListener): + + def __init__(self, controller): + super().__init__(controller, '') + + def itemHighlighted(self, event): + pass + + @catch_exception + def itemSelected(self, event): + name = event.Source.getCommand(event.MenuId) + if name.startswith('menu'): + event_name = '{}_selected'.format(name) + else: + event_name = 'menu_{}_selected'.format(name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + def itemActivated(self, event): + return + + def itemDeactivated(self, event): return @@ -987,19 +2064,39 @@ class UnoBaseObject(object): def parent(self): return self.obj.getContext() + def _get_possize(self, name): + ps = self.obj.getPosSize() + return getattr(ps, name) + + def _set_possize(self, name, value): + ps = self.obj.getPosSize() + setattr(ps, name, value) + self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE) + return + @property def x(self): - return self.model.PositionX + if hasattr(self.model, 'PositionX'): + return self.model.PositionX + return self._get_possize('X') @x.setter def x(self, value): - self.model.PositionX = value + if hasattr(self.model, 'PositionX'): + self.model.PositionX = value + else: + self._set_possize('X', value) @property def y(self): - return self.model.PositionY + if hasattr(self.model, 'PositionY'): + return self.model.PositionY + return self._get_possize('Y') @y.setter def y(self, value): - self.model.PositionY = value + if hasattr(self.model, 'PositionY'): + self.model.PositionY = value + else: + self._set_possize('Y', value) @property def width(self): @@ -1010,7 +2107,10 @@ class UnoBaseObject(object): @property def height(self): - return self._model.Height + if hasattr(self._model, 'Height'): + return self._model.Height + ps = self.obj.getPosSize() + return ps.Height @height.setter def height(self, value): self._model.Height = value @@ -1029,6 +2129,13 @@ class UnoBaseObject(object): def step(self, value): self.model.Step = value + @property + def back_color(self): + return self.model.BackgroundColor + @back_color.setter + def back_color(self, value): + self.model.BackgroundColor = value + @property def rules(self): return self._rules @@ -1083,6 +2190,16 @@ class UnoLabel(UnoBaseObject): self.model.Label = value +class UnoLabelLink(UnoLabel): + + def __init__(self, obj): + super().__init__(obj) + + @property + def type(self): + return 'link' + + class UnoButton(UnoBaseObject): def __init__(self, obj): @@ -1257,9 +2374,517 @@ class UnoGrid(UnoBaseObject): return +class UnoRoadmap(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._options = () + + @property + def options(self): + return self._options + @options.setter + def options(self, values): + self._options = values + for i, v in enumerate(values): + opt = self.model.createInstance() + opt.ID = i + opt.Label = v + self.model.insertByIndex(i, opt) + return + + def set_enabled(self, index, value): + self.model.getByIndex(index).Enabled = value + return + + +def get_custom_class(tipo, obj): + classes = { + 'label': UnoLabel, + 'button': UnoButton, + 'text': UnoText, + 'listbox': UnoListBox, + 'grid': UnoGrid, + 'link': UnoLabelLink, + 'roadmap': UnoRoadmap, + # ~ 'tab': UnoTab, + # ~ 'image': UnoImage, + # ~ 'radio': UnoRadio, + # ~ 'groupbox': UnoGroupBox, + # ~ 'tree': UnoTree, + } + return classes[tipo](obj) + + +def add_listeners(events, control, name=''): + listeners = { + 'addActionListener': EventsButton, + 'addMouseListener': EventsMouse, + 'addItemListener': EventsItem, + 'addFocusListener': EventsFocus, + } + if hasattr(control, 'obj'): + control = contro.obj + # ~ debug(control.ImplementationName) + is_grid = control.ImplementationName == 'stardiv.Toolkit.GridControl' + is_link = control.ImplementationName == 'stardiv.Toolkit.UnoFixedHyperlinkControl' + is_roadmap = control.ImplementationName == 'stardiv.Toolkit.UnoRoadmapControl' + + for key, value in listeners.items(): + if hasattr(control, key): + if is_grid and key == 'addMouseListener': + control.addMouseListener(EventsMouseGrid(events, name)) + continue + if is_link and key == 'addMouseListener': + control.addMouseListener(EventsMouseLink(events, name)) + continue + if is_roadmap and key == 'addItemListener': + control.addItemListener(EventsItemRoadmap(events, name)) + continue + getattr(control, key)(listeners[key](events, name)) + return + + +class WriterTable(ObjectBase): + + def __init__(self, obj): + super().__init__(obj) + + def __getitem__(self, key): + obj = super().__getitem__(key) + return WriterTableRange(obj, key, self.name) + + @property + def name(self): + return self.obj.Name + @name.setter + def name(self, value): + self.obj.Name = value + + +class WriterTableRange(ObjectBase): + + def __init__(self, obj, index, table_name): + self._index = index + self._table_name = table_name + super().__init__(obj) + self._is_cell = hasattr(self.obj, 'CellName') + + def __getitem__(self, key): + obj = super().__getitem__(key) + return WriterTableRange(obj, key, self._table_name) + + @property + def value(self): + return self.obj.String + @value.setter + def value(self, value): + self.obj.String = value + + @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) + + @property + def rows(self): + return len(self.data) + + @property + def columns(self): + return len(self.data[0]) + + @property + def name(self): + if self._is_cell: + name = '{}.{}'.format(self._table_name, self.obj.CellName) + elif isinstance(self._index, str): + name = '{}.{}'.format(self._table_name, self._index) + else: + c1 = self.obj[0,0].CellName + c2 = self.obj[self.rows-1,self.columns-1].CellName + name = '{}.{}:{}'.format(self._table_name, c1, c2) + return name + + def get_cell(self, *index): + return self[index] + + def get_column(self, index=0, start=1): + return self[start:self.rows,index:index+1] + + def get_series(self): + class Serie(): + pass + series = [] + for i in range(self.columns): + serie = Serie() + serie.label = self.get_cell(0,i).name + serie.data = self.get_column(i).data + serie.values = self.get_column(i).name + series.append(serie) + return series + + +class ChartFormat(object): + + def __call__(self, obj): + for k, v in self.__dict__.items(): + if hasattr(obj, k): + setattr(obj, k, v) + + +class LOChart(object): + BASE = 'com.sun.star.chart.{}Diagram' + + def __init__(self, obj, tipo=''): + self._obj = obj + self._type = tipo + self._name = '' + self._table = None + self._data = () + self._data_series = () + self._cell = None + self._cursor = None + self._doc = None + self._title = ChartFormat() + self._subtitle = ChartFormat() + self._legend = ChartFormat() + self._xaxistitle = ChartFormat() + self._yaxistitle = ChartFormat() + self._xaxis = ChartFormat() + self._yaxis = ChartFormat() + self._xmaingrid = ChartFormat() + self._ymaingrid = ChartFormat() + self._xhelpgrid = ChartFormat() + self._yhelpgrid = ChartFormat() + self._area = ChartFormat() + self._wall = ChartFormat() + self._dim3d = False + self._series = () + self._labels = () + return + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.insert() + + @property + def obj(self): + return self._obj + @obj.setter + def obj(self, value): + self._obj = value + + @property + def name(self): + return self._name + @name.setter + def name(self, value): + self._name = value + + @property + def type(self): + return self._type + @type.setter + def type(self, value): + self._type = value + + @property + def table(self): + return self._table + @table.setter + def table(self, value): + self._table = value + + @property + def data(self): + return self._data + @data.setter + def data(self, value): + self._data = value + + @property + def cell(self): + return self._cell + @cell.setter + def cell(self, value): + self._cell = value + self.doc = value.doc + + @property + def cursor(self): + return self._cursor + @cursor.setter + def cursor(self, value): + self._cursor = value + + @property + def doc(self): + return self._doc + @doc.setter + def doc(self, value): + self._doc = value + + @property + def width(self): + return self._width + @width.setter + def width(self, value): + self._width = value + + @property + def height(self): + return self._height + @height.setter + def height(self, value): + self._height = value + + @property + def title(self): + return self._title + + @property + def subtitle(self): + return self._subtitle + + @property + def legend(self): + return self._legend + + @property + def xaxistitle(self): + return self._xaxistitle + + @property + def yaxistitle(self): + return self._yaxistitle + + @property + def xaxis(self): + return self._xaxis + + @property + def yaxis(self): + return self._yaxis + + @property + def xmaingrid(self): + return self._xmaingrid + + @property + def ymaingrid(self): + return self._ymaingrid + + @property + def xhelpgrid(self): + return self._xhelpgrid + + @property + def yhelpgrid(self): + return self._yhelpgrid + + @property + def area(self): + return self._area + + @property + def wall(self): + return self._wall + + @property + def dim3d(self): + return self._dim3d + @dim3d.setter + def dim3d(self, value): + self._dim3d = value + + @property + def series(self): + return self._series + @series.setter + def series(self, value): + self._series = value + + @property + def data_series(self): + return self._series + @data_series.setter + def data_series(self, value): + self._data_series = value + + @property + def labels(self): + return self._labels + @labels.setter + def labels(self, value): + self._labels = value + + def _add_series_writer(self, chart): + dp = self.doc.create_instance('com.sun.star.chart2.data.DataProvider') + chart.attachDataProvider(dp) + chart_type = chart.getFirstDiagram().getCoordinateSystems()[0].getChartTypes()[0] + self._data_series = self.table[self.data].get_series() + series = [self._create_serie(dp, s) for s in self._data_series[1:]] + chart_type.setDataSeries(tuple(series)) + chart_data = chart.getData() + chart_data.ComplexRowDescriptions = self._data_series[0].data + return + + def _get_series(self): + rango = self._data_series + class Serie(): + pass + series = [] + for i in range(0, rango.columns, 2): + serie = Serie() + serie.label = rango[0, i+1].name + serie.xvalues = rango.get_column(i).name + serie.values = rango.get_column(i+1).name + series.append(serie) + return series + + def _add_series_calc(self, chart): + dp = self.doc.create_instance('com.sun.star.chart2.data.DataProvider') + chart.attachDataProvider(dp) + chart_type = chart.getFirstDiagram().getCoordinateSystems()[0].getChartTypes()[0] + series = self._get_series() + series = [self._create_serie(dp, s) for s in series] + chart_type.setDataSeries(tuple(series)) + return + + def _create_serie(self, dp, data): + serie = create_instance('com.sun.star.chart2.DataSeries') + rango = data.values + is_x = hasattr(data, 'xvalues') + if is_x: + xrango = data.xvalues + rango_label = data.label + + lds = create_instance('com.sun.star.chart2.data.LabeledDataSequence') + values = self._create_data(dp, rango, 'values-y') + lds.setValues(values) + if data.label: + label = self._create_data(dp, rango_label, '') + lds.setLabel(label) + + xlds = () + if is_x: + xlds = create_instance('com.sun.star.chart2.data.LabeledDataSequence') + values = self._create_data(dp, xrango, 'values-x') + xlds.setValues(values) + + if is_x: + serie.setData((lds, xlds)) + else: + serie.setData((lds,)) + + return serie + + def _create_data(self, dp, rango, role): + data = dp.createDataSequenceByRangeRepresentation(rango) + if not data is None: + data.Role = role + return data + + def _from_calc(self): + ps = self.cell.ps + ps.Width = self.width + ps.Height = self.height + charts = self.cell.charts + data = () + if self.data: + data = (self.data.address,) + charts.addNewByName(self.name, ps, data, True, True) + self.obj = charts.getByName(self.name) + chart = self.obj.getEmbeddedObject() + chart.setDiagram(chart.createInstance(self.BASE.format(self.type))) + if not self.data: + self._add_series_calc(chart) + return chart + + def _from_writer(self): + obj = self.doc.create_instance('com.sun.star.text.TextEmbeddedObject') + obj.setPropertyValue('CLSID', '12DCAE26-281F-416F-a234-c3086127382e') + obj.Name = self.name + obj.setSize(Size(self.width, self.height)) + self.doc.insert_content(self.cursor, obj) + self.obj = obj + chart = obj.getEmbeddedObject() + tipo = self.type + if self.type == 'Column': + tipo = 'Bar' + chart.Diagram.Vertical = True + chart.setDiagram(chart.createInstance(self.BASE.format(tipo))) + chart.DataSourceLabelsInFirstColumn = True + if isinstance(self.data, str): + self._add_series_writer(chart) + else: + chart_data = chart.getData() + labels = [r[0] for r in self.data] + data = [(r[1],) for r in self.data] + chart_data.setData(data) + chart_data.RowDescriptions = labels + + if tipo == 'Pie': + chart.setDiagram(chart.createInstance(self.BASE.format('Bar'))) + chart.setDiagram(chart.createInstance(self.BASE.format('Pie'))) + + return chart + + def insert(self): + if not self.cell is None: + chart = self._from_calc() + elif not self.cursor is None: + chart = self._from_writer() + + diagram = chart.Diagram + + if self.type == 'Bar': + diagram.Vertical = True + + if hasattr(self.title, 'String'): + chart.HasMainTitle = True + self.title(chart.Title) + + if hasattr(self.subtitle, 'String'): + chart.HasSubTitle = True + self.subtitle(chart.SubTitle) + + if self.legend.__dict__: + chart.HasLegend = True + self.legend(chart.Legend) + + if self.xaxistitle.__dict__: + diagram.HasXAxisTitle = True + self.xaxistitle(diagram.XAxisTitle) + + if self.yaxistitle.__dict__: + diagram.HasYAxisTitle = True + self.yaxistitle(diagram.YAxisTitle) + + if self.dim3d: + diagram.Dim3D = True + + if self.series: + data_series = chart.getFirstDiagram( + ).getCoordinateSystems( + )[0].getChartTypes()[0].DataSeries + for i, serie in enumerate(data_series): + for k, v in self.series[i].items(): + if hasattr(serie, k): + setattr(serie, k, v) + return self + + class LODialog(object): - def __init__(self, properties): + def __init__(self, **properties): self._obj = self._create(properties) self._init_values() @@ -1267,22 +2892,27 @@ class LODialog(object): self._model = self._obj.Model self._init_controls() self._events = None - # ~ self._response = None + self._color_on_focus = -1 return def _create(self, properties): path = properties.pop('Path', '') if path: - dp = create_instance('com.sun.star.awt.DialogProvider2', True) + dp = create_instance('com.sun.star.awt.DialogProvider', True) return dp.createDialog(_path_url(path)) - if 'Library' in properties: - location = properties['Location'] + if 'Location' in properties: + location = properties.get('Location', 'application') + library = properties.get('Library', 'Standard') if location == 'user': location = 'application' - dp = create_instance('com.sun.star.awt.DialogProvider2', True) + dp = create_instance('com.sun.star.awt.DialogProvider', True) path = 'vnd.sun.star.script:{}.{}?location={}'.format( - properties['Library'], properties['Name'], location) + library, properties['Name'], location) + if location == 'document': + uid = get_document().uid + path = 'vnd.sun.star.tdoc:/{}/Dialogs/{}/{}.xml'.format( + uid, library, properties['Name']) return dp.createDialog(path) dlg = create_instance('com.sun.star.awt.UnoControlDialog', True) @@ -1295,8 +2925,22 @@ class LODialog(object): return dlg - def _init_controls(self): + def _get_type_control(self, name): + types = { + 'stardiv.Toolkit.UnoFixedTextControl': 'label', + 'stardiv.Toolkit.UnoButtonControl': 'button', + 'stardiv.Toolkit.UnoEditControl': 'text', + 'stardiv.Toolkit.UnoRoadmapControl': 'roadmap', + 'stardiv.Toolkit.UnoFixedHyperlinkControl': 'link', + } + return types[name] + def _init_controls(self): + for control in self.obj.getControls(): + tipo = self._get_type_control(control.ImplementationName) + name = control.Model.Name + control = get_custom_class(tipo, control) + setattr(self, name, control) return @property @@ -1307,6 +2951,29 @@ class LODialog(object): def model(self): return self._model + @property + def height(self): + return self.model.Height + @height.setter + def height(self, value): + self.model.Height = value + + @property + def color_on_focus(self): + return self._color_on_focus + @color_on_focus.setter + def color_on_focus(self, value): + global COLOR_ON_FOCUS + COLOR_ON_FOCUS = get_color(value) + self._color_on_focus = COLOR_ON_FOCUS + + @property + def step(self): + return self.model.Step + @step.setter + def step(self, value): + self.model.Step = value + @property def events(self): return self._events @@ -1316,23 +2983,8 @@ class LODialog(object): 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)) + for control in self.obj.getControls(): + add_listeners(self._events, control, control.Model.Name) return def open(self): @@ -1343,47 +2995,28 @@ class LODialog(object): 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', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel', + 'image': 'com.sun.star.awt.UnoControlImageControlModel', + 'label': 'com.sun.star.awt.UnoControlFixedTextModel', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel', + 'listbox': 'com.sun.star.awt.UnoControlListBoxModel', + 'radio': 'com.sun.star.awt.UnoControlRadioButtonModel', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel', + 'text': 'com.sun.star.awt.UnoControlEditModel', + 'tree': 'com.sun.star.awt.tree.TreeControlModel', } 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): @@ -1391,25 +3024,191 @@ class LODialog(object): return _path_url(path) return '' - def add_control(self, properties): - tipo = properties.pop('Type').lower() - + def _special_properties(self, tipo, properties): columns = properties.pop('Columns', ()) if tipo == 'grid': properties['ColumnModel'] = self._set_column_model(columns) - if tipo == 'button' and 'ImageURL' in properties: + elif tipo == 'button' and 'ImageURL' in properties: properties['ImageURL'] = self._set_image_url(properties['ImageURL']) + elif tipo == 'roadmap' and not 'Height' in properties: + properties['Height'] = self.height + return properties + def add_control(self, properties): + tipo = properties.pop('Type').lower() + properties = self._special_properties(tipo, properties) 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) + control = self.obj.getControl(name) + add_listeners(self.events, control, name) + control = get_custom_class(tipo, control) setattr(self, name, control) return +class LOWindow(object): + + def __init__(self, **kwargs): + self._events = None + self._menu = None + self._container = None + self._obj = self._create(kwargs) + + def _create(self, properties): + ps = ( + properties.get('X', 0), + properties.get('Y', 0), + properties.get('Width', 500), + properties.get('Height', 500), + ) + self._title = properties.get('Title', TITLE) + self._create_frame(ps) + self._create_container(ps) + # ~ self._create_splitter(ps) + return + + def _create_frame(self, ps): + service = 'com.sun.star.frame.TaskCreator' + tc = create_instance(service, True) + self._frame = tc.createInstanceWithArguments(( + NamedValue('FrameName', 'EasyMacroWin'), + NamedValue('PosSize', Rectangle(*ps)), + )) + self._window = self._frame.getContainerWindow() + self._toolkit = self._window.getToolkit() + desktop = get_desktop() + self._frame.setCreator(desktop) + desktop.getFrames().append(self._frame) + self._frame.Title = self._title + return + + def _create_container(self, ps): + # ~ toolkit = self._window.getToolkit() + service = 'com.sun.star.awt.UnoControlContainer' + self._container = create_instance(service, True) + service = 'com.sun.star.awt.UnoControlContainerModel' + model = create_instance(service, True) + model.BackgroundColor = get_color(225, 225, 225) + self._container.setModel(model) + self._container.createPeer(self._toolkit, self._window) + self._container.setPosSize(*ps, POSSIZE) + self._frame.setComponent(self._container, None) + return + + def _get_base_control(self, tipo): + services = { + 'label': 'com.sun.star.awt.UnoControlFixedText', + 'button': 'com.sun.star.awt.UnoControlButton', + 'text': 'com.sun.star.awt.UnoControlEdit', + 'listbox': 'com.sun.star.awt.UnoControlListBox', + 'link': 'com.sun.star.awt.UnoControlFixedHyperlink', + 'roadmap': 'com.sun.star.awt.UnoControlRoadmap', + 'image': 'com.sun.star.awt.UnoControlImageControl', + 'groupbox': 'com.sun.star.awt.UnoControlGroupBox', + 'radio': 'com.sun.star.awt.UnoControlRadioButton', + 'tree': 'com.sun.star.awt.tree.TreeControl', + 'grid': 'com.sun.star.awt.grid.UnoControlGrid', + } + return services[tipo] + + def add_control(self, properties): + tipo = properties.pop('Type').lower() + base = self._get_base_control(tipo) + obj = create_instance(base, True) + model = create_instance('{}Model'.format(base), True) + set_properties(model, properties) + obj.setModel(model) + x = properties.get('X', 5) + y = properties.get('Y', 5) + w = properties.get('Width', 200) + h = properties.get('Height', 25) + obj.setPosSize(x, y, w, h, POSSIZE) + name = properties['Name'] + self._container.addControl(name, obj) + add_listeners(self.events, obj, name) + control = get_custom_class(tipo, obj) + setattr(self, name, control) + return + + def _create_popupmenu(self, menus): + menu = create_instance('com.sun.star.awt.PopupMenu', True) + for i, m in enumerate(menus): + label = m['label'] + cmd = m.get('event', '') + if not cmd: + cmd = label.lower().replace(' ', '_') + if label == '-': + menu.insertSeparator(i) + else: + menu.insertItem(i, label, m.get('style', 0), i) + menu.setCommand(i, cmd) + # ~ menu.setItemImage(i, path?, True) + menu.addMenuListener(EventsMenu(self.events)) + return menu + + def _create_menu(self, menus): + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMenu.html + #~ nItemId specifies the ID of the menu item to be inserted. + #~ aText specifies the label of the menu item. + #~ nItemStyle 0 = Standard, CHECKABLE = 1, RADIOCHECK = 2, AUTOCHECK = 4 + #~ nItemPos specifies the position where the menu item will be inserted. + self._menu = create_instance('com.sun.star.awt.MenuBar', True) + for i, m in enumerate(menus): + self._menu.insertItem(i, m['label'], m.get('style', 0), i) + cmd = m['label'].lower().replace(' ', '_') + self._menu.setCommand(i, cmd) + submenu = self._create_popupmenu(m['submenu']) + self._menu.setPopupMenu(i, submenu) + + self._window.setMenuBar(self._menu) + return + + def add_menu(self, menus): + self._create_menu(menus) + return + + def _add_listeners(self, control=None): + if self.events is None: + return + controller = EventsWindow(self) + self._window.addTopWindowListener(controller) + self._window.addWindowListener(controller) + self._container.addKeyListener(EventsKey(self)) + return + + @property + def name(self): + return self._title.lower().replace(' ', '_') + + @property + def events(self): + return self._events + @events.setter + def events(self, value): + self._events = value + self._add_listeners() + + @property + def width(self): + return self._container.Size.Width + + @property + def height(self): + return self._container.Size.Height + + def open(self): + self._window.setVisible(True) + return + + def close(self): + self._window.setMenuBar(None) + self._window.dispose() + self._frame.close(True) + return + + # ~ Python >= 3.7 # ~ def __getattr__(name): @@ -1447,7 +3246,6 @@ def get_document(title=''): return _get_class_doc(doc) -# ~ Export ok def get_documents(custom=True): docs = [] desktop = get_desktop() @@ -1479,18 +3277,11 @@ def active_cell(): def create_dialog(properties): - return LODialog(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 create_window(kwargs): + return LOWindow(**kwargs) # ~ Export ok @@ -1617,6 +3408,16 @@ def from_json(path): return data +# ~ Export ok +def json_dumps(data): + return json.dumps(data, indent=4, sort_keys=True) + + +# ~ Export ok +def json_loads(data): + return json.loads(data) + + def get_path_extension(id): pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') path = _path_system(pip.getPackageLocation(id)) @@ -1640,7 +3441,7 @@ def inputbox(message, default='', title=TITLE, echochar=''): 'Width': 200, 'Height': 80, } - dlg = LODialog(args) + dlg = LODialog(**args) dlg.events = ControllersInput(dlg) args = { @@ -1706,12 +3507,29 @@ def new_doc(type_doc=CALC, **kwargs): # ~ Export ok -def new_db(path): +def new_db(path, name=''): + p, fn, n, e = get_info_path(path) + if not name: + name = n + return LOBase(name, path) + + +# ~ Todo +def exists_db(name): dbc = create_instance('com.sun.star.sdb.DatabaseContext') - db = dbc.createInstance() - db.URL = 'sdbc:embedded:firebird' # hsqldb - db.DatabaseDocument.storeAsURL(_path_url(path), ()) - return _get_class_doc(db) + return dbc.hasRegisteredDatabase(name) + + +# ~ Todo +def register_db(name, path): + dbc = create_instance('com.sun.star.sdb.DatabaseContext') + dbc.registerDatabaseLocation(name, _path_url(path)) + return + + +# ~ Todo +def get_db(name): + return LOBase(name) # ~ Export ok @@ -1784,7 +3602,6 @@ def zip_content(path): return names -# ~ Export ok def run(command, wait=False): # ~ debug(command) # ~ debug(shlex.split(command)) @@ -1922,7 +3739,6 @@ def kill(path): return -# ~ Export ok def get_size_screen(): if IS_WIN: user32 = ctypes.windll.user32 @@ -1933,7 +3749,6 @@ def get_size_screen(): return res.strip() -# ~ Export ok def get_clipboard(): df = None text = '' @@ -1987,7 +3802,7 @@ def set_clipboard(value): return -# ~ Todo +# ~ Export ok def copy(): call_dispatch('.uno:Copy') return @@ -2012,12 +3827,16 @@ def file_copy(source, target='', name=''): return path_new -# ~ Export ok -def get_path_content(path, filters='*'): +def get_path_content(path, filters=''): paths = [] + if filters in ('*', '*.*'): + filters = '' for folder, _, files in os.walk(path): - pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) - paths += [join(folder, f) for f in files if pattern.search(f)] + if filters: + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [join(folder, f) for f in files if pattern.search(f)] + else: + paths += files return paths @@ -2175,7 +3994,12 @@ def end(): # ~ Export ok # ~ https://en.wikipedia.org/wiki/Web_colors -def get_color(value): +def get_color(*value): + if len(value) == 1 and isinstance(value[0], int): + return value[0] + if len(value) == 1 and isinstance(value[0], tuple): + value = value[0] + COLORS = { 'aliceblue': 15792383, 'antiquewhite': 16444375, @@ -2326,14 +4150,19 @@ def get_color(value): 'yellowgreen': 10145074, } - if isinstance(value, tuple): - return (value[0] << 16) + (value[1] << 8) + value[2] + if len(value) == 3: + color = (value[0] << 16) + (value[1] << 8) + value[2] + else: + value = value[0] + if value[0] == '#': + r, g, b = bytes.fromhex(value[1:]) + color = (r << 16) + (g << 8) + b + else: + color = COLORS.get(value.lower(), -1) + return color - if isinstance(value, str) and value[0] == '#': - r, g, b = bytes.fromhex(value[1:]) - return (r << 16) + (g << 8) + b - return COLORS.get(value.lower(), -1) +COLOR_ON_FOCUS = get_color('LightYellow') # ~ Export ok @@ -2355,6 +4184,24 @@ def _to_date(value): return new_value +def date_to_struct(value): + # ~ print(type(value)) + if isinstance(value, datetime.datetime): + d = DateTime() + d.Seconds = value.second + d.Minutes = value.minute + d.Hours = value.hour + d.Day = value.day + d.Month = value.month + d.Year = value.year + elif isinstance(value, datetime.date): + d = Date() + d.Day = value.day + d.Month = value.month + d.Year = value.year + return d + + # ~ Export ok def format(template, data): """ @@ -2381,10 +4228,12 @@ def _call_macro(macro): name = 'com.sun.star.script.provider.MasterScriptProviderFactory' factory = create_instance(name, False) + macro['language'] = macro.get('language', 'Python') + macro['location'] = macro.get('location', 'user') data = macro.copy() - if macro['language'] == 'Python': + if data['language'] == 'Python': data['module'] = '.py$' - elif macro['language'] == 'Basic': + elif data['language'] == 'Basic': data['module'] = '.{}.'.format(macro['module']) if macro['location'] == 'user': data['location'] = 'application' @@ -2477,7 +4326,7 @@ class SmtpServer(object): def __enter__(self): return self - def __exit__(self, *args): + def __exit__(self, exc_type, exc_value, traceback): self.close() @property @@ -2612,14 +4461,112 @@ def server_smtp_test(config): return server.error -# ~ 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 +def import_csv(path, **kwargs): + """ + See https://docs.python.org/3.5/library/csv.html#csv.reader + """ + with open(path) as f: + rows = tuple(csv.reader(f, **kwargs)) + return rows + +def export_csv(path, data, **kwargs): + with open(path, 'w') as f: + writer = csv.writer(f, **kwargs) + writer.writerows(data) + return + + +class LIBOServer(object): + HOST = 'localhost' + PORT = '8100' + ARG = 'socket,host={},port={};urp;StarOffice.ComponentContext'.format(HOST, PORT) + CMD = ['soffice', + '-env:SingleAppInstance=false', + '-env:UserInstallation=file:///tmp/LIBO_Process8100', + '--headless', '--norestore', '--invisible', + '--accept={}'.format(ARG)] + + def __init__(self): + self._server = None + self._ctx = None + self._sm = None + self._start_server() + self._init_values() + + def _init_values(self): + global CTX + global SM + + if not self.is_running: + return + + ctx = uno.getComponentContext() + service = 'com.sun.star.bridge.UnoUrlResolver' + resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx) + self._ctx = resolver.resolve('uno:{}'.format(self.ARG)) + self._sm = self._ctx.getServiceManager() + CTX = self._ctx + SM = self._sm + return + + @property + def is_running(self): + try: + s = socket.create_connection((self.HOST, self.PORT), 5.0) + s.close() + debug('LibreOffice is running...') + return True + except ConnectionRefusedError: + return False + + def _start_server(self): + if self.is_running: + return + + for i in range(3): + self._server = subprocess.Popen(self.CMD, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + time.sleep(3) + if self.is_running: + break + return + + def stop(self): + if self._server is None: + print('Search pgrep soffice') + else: + self._server.terminate() + debug('LibreOffice is stop...') + return + + def create_instance(self, name, with_context=True): + if with_context: + instance = self._sm.createInstanceWithContext(name, self._ctx) + else: + instance = self._sm.createInstance(name) + return instance + + + + +# ~ controls = { + # ~ 'CheckBox': 'com.sun.star.awt.UnoControlCheckBoxModel', + # ~ 'ComboBox': 'com.sun.star.awt.UnoControlComboBoxModel', + # ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', + # ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', + # ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', + # ~ 'FixedLine': 'com.sun.star.awt.UnoControlFixedLineModel', + # ~ 'FixedText': 'com.sun.star.awt.UnoControlFixedTextModel', + # ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', + # ~ 'GroupBox': 'com.sun.star.awt.UnoControlGroupBoxModel', + # ~ 'ImageControl': 'com.sun.star.awt.UnoControlImageControlModel', + # ~ 'ListBox': 'com.sun.star.awt.UnoControlListBoxModel', + # ~ 'NumericField': 'com.sun.star.awt.UnoControlNumericFieldModel', + # ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', + # ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', + # ~ 'ScrollBar': 'com.sun.star.awt.UnoControlScrollBarModel', + # ~ 'SimpleAnimation': 'com.sun.star.awt.UnoControlSimpleAnimationModel', + # ~ 'SpinButton': 'com.sun.star.awt.UnoControlSpinButtonModel', + # ~ 'Throbber': 'com.sun.star.awt.UnoControlThrobberModel', + # ~ 'TimeField': 'com.sun.star.awt.UnoControlTimeFieldModel', +# ~ } diff --git a/zaz.py b/zaz.py index 5669ec1..2cac1bc 100644 --- a/zaz.py +++ b/zaz.py @@ -19,11 +19,16 @@ import argparse import os +import re import sys +import zipfile +from datetime import datetime from pathlib import Path from shutil import copyfile from subprocess import call -import zipfile +from xml.etree import ElementTree as ET +from xml.dom.minidom import parseString + from conf import ( DATA, @@ -37,6 +42,84 @@ from conf import ( log) +class LiboXML(object): + TYPES = { + 'py': 'application/vnd.sun.star.uno-component;type=Python', + 'zip': 'application/binary', + 'xcu': 'application/vnd.sun.star.configuration-data', + 'rdb': 'application/vnd.sun.star.uno-typelibrary;type=RDB', + 'xcs': 'application/vnd.sun.star.configuration-schema', + 'help': 'application/vnd.sun.star.help', + 'component': 'application/vnd.sun.star.uno-components', + } + NAME_SPACES = { + 'manifest_version': '1.2', + 'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0', + 'xmlns:loext': 'urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0', + } + + def __init__(self): + self._manifest = None + self._paths = [] + + def _save_path(self, attr): + self._paths.append(attr['{{{}}}full-path'.format(self.NAME_SPACES['manifest'])]) + return + + def _clean(self, name, nodes): + has_words = re.compile('\\w') + + if not re.search(has_words, str(nodes.tail)): + nodes.tail = '' + if not re.search(has_words, str(nodes.text)): + nodes.text = '' + + for node in nodes: + if name == 'manifest': + self._save_path(node.attrib) + if not re.search(has_words, str(node.tail)): + node.tail = '' + if not re.search(has_words, str(node.text)): + node.text = '' + return + + def new_manifest(self, data): + attr = { + 'manifest:version': self.NAME_SPACES['manifest_version'], + 'xmlns:manifest': self.NAME_SPACES['manifest'], + 'xmlns:loext': self.NAME_SPACES['xmlns:loext'], + } + self._manifest = ET.Element('manifest:manifest', attr) + return self.add_data_manifest(data) + + def parse_manifest(self, data): + ET.register_namespace('manifest', self.NAME_SPACES['manifest']) + self._manifest = ET.fromstring(data) + data = {'xmlns:loext': self.NAME_SPACES['xmlns:loext']} + self._manifest.attrib.update(**data) + self._clean('manifest', self._manifest) + return + + def add_data_manifest(self, data): + node_name = 'manifest:file-entry' + attr = { + 'manifest:full-path': '', + 'manifest:media-type': '', + } + for path in data: + if path in self._paths: + continue + ext = path.split('.')[-1] + attr['manifest:full-path'] = path + attr['manifest:media-type'] = self.TYPES.get(ext, '') + ET.SubElement(self._manifest, node_name, attr) + return self._get_xml(self._manifest) + + def _get_xml(self, doc): + xml = parseString(ET.tostring(doc, encoding='utf-8')) + return xml.toprettyxml(indent=' ', encoding='utf-8').decode('utf-8') + + def _exists(path): return os.path.exists(path) @@ -222,8 +305,11 @@ def _update_files(): target = _join(path_source, 'pythonpath', source) copyfile(source, target) + xml = LiboXML() + path = _join(path_source, DIRS['meta'], FILES['manifest']) - _save(path, DATA['manifest']) + data = xml.new_manifest(DATA['manifest']) + _save(path, data) path = _join(path_source, DIRS['office']) _mkdir(path) @@ -265,7 +351,96 @@ def _new(): return +def _get_info_path(path): + path, filename = os.path.split(path) + name, extension = os.path.splitext(filename) + return (path, filename, name, extension) + + +def _zip_embed(source, files): + PATH = 'Scripts/python/' + EASYMACRO = 'easymacro.' + + p, f, name, e = _get_info_path(source) + now = datetime.now().strftime('_%Y%m%d_%H%M%S') + path_source = _join(p, name + now + e) + copyfile(source, path_source) + target = source + + with zipfile.PyZipFile(EASYMACRO + 'zip', mode='w') as zf: + zf.writepy(EASYMACRO + 'py') + + xml = LiboXML() + + path_easymacro = PATH + EASYMACRO + 'zip' + names = [f[1] for f in files] + [path_easymacro] + nodes = [] + with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as zt: + with zipfile.ZipFile(path_source, compression=zipfile.ZIP_DEFLATED) as zs: + for name in zs.namelist(): + if FILES['manifest'] in name: + path_manifest = name + xml_manifest = zs.open(name).read() + elif name in names: + continue + else: + zt.writestr(name, zs.open(name).read()) + + data = [] + for path, name in files: + data.append(name) + zt.write(path, name) + + zt.write(EASYMACRO + 'zip', path_easymacro) + data.append(path_easymacro) + + xml.parse_manifest(xml_manifest) + xml_manifest = xml.add_data_manifest(data) + zt.writestr(path_manifest, xml_manifest) + + os.unlink(EASYMACRO + 'zip') + return + + +def _embed(args): + PATH = 'Scripts/python' + PYTHONPATH = 'pythonpath' + + doc = args.document + if not doc: + msg = '-d/--document Path file to embed is mandatory' + log.error(msg) + return + if not _exists(doc): + msg = 'Path file not exists' + log.error(msg) + return + + files = [] + if args.files: + files = args.files.split(',') + source = _join(PATHS['profile'], PATH) + content = os.listdir(source) + if PYTHONPATH in content: + content.remove(PYTHONPATH) + + if files: + files = [(_join(source, f), _join(PATH, f)) for f in files if f in content] + else: + files = [(_join(source, f), _join(PATH, f)) for f in content] + + _zip_embed(doc, files) + + log.info('Embedded macros successfully...') + return + + + def main(args): + if args.embed: + _embed(args) + return + if args.new: _new() return @@ -279,7 +454,7 @@ def main(args): if args.install: _install_and_test() - log.info('Extension make sucesfully...') + log.info('Extension make successfully...') return @@ -290,6 +465,10 @@ def _process_command_line_arguments(): default=False, required=False) parser.add_argument('-n', '--new', dest='new', action='store_true', default=False, required=False) + parser.add_argument('-e', '--embed', dest='embed', action='store_true', + default=False, required=False) + parser.add_argument('-d', '--document', dest='document', default='') + parser.add_argument('-f', '--files', dest='files', default='') return parser.parse_args()