diff --git a/README b/README index 9ed5467..44f0213 100644 --- a/README +++ b/README @@ -17,3 +17,15 @@ You're supposed to be using a Debian-based GNU/Linux distribution, other environ Tests assume you have your OpenOffice.org in English - USA default language. +See also : + * https://github.com/CosminEugenDinu/libreoffice-python-library + +now possible to + * extend append_string() to muliple cells + * add methods for color (background and fore), modify all cell at once + +Addendum to the doc : + * in case you use oosheet in python macros, please be cautious since a global var. is used (see OODoc ctor) and it could be a bug's nest ... + * in this case, do not hesitate to call doc.connect() at the beginnig of each macro you write :-) + * see the discussion in https://github.com/tudstlennkozh/oosheet/issues/1 to get details + diff --git a/docs/index.rst b/docs/index.rst index 3d0a5e8..8222b1d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -95,6 +95,11 @@ Luc Jean sent patch that allows OOSheet to run on Windows Vista with default Pyt Changelog ========= +- 1.3.1 + - added OODoc.alert() + - Works with LO >= 6 + - adding code to access annotation for a cell or a selector via propterty: annotation (R/W) + - 1.3 - Works with Python 3 and LibreOffice 5 diff --git a/oosheet/__init__.py b/oosheet/__init__.py index 1989bd4..e5d7640 100644 --- a/oosheet/__init__.py +++ b/oosheet/__init__.py @@ -2,46 +2,66 @@ import sys import os +from enum import Flag from .columns import name as col_name, index as col_index if sys.platform == 'win32': - #This is required in order to make pyuno usable with the default python interpreter under windows - #Some environment variables must be modified + # This is required in order to make pyuno usable with the default python interpreter under windows + # Some environment variables must be modified + + # get the install path from registry + import winreg - #get the install path from registry - import _winreg # try with OpenOffice, LibreOffice on W7 - for _key in [# OpenOffice 3.3 - "SOFTWARE\\OpenOffice.org\\UNO\\InstallPath", - # LibreOffice 3.4.5 on W7 - "SOFTWARE\\Wow6432Node\\LibreOffice\\UNO\\InstallPath"]: + for key in [ # OpenOffice 3.3 + "SOFTWARE\\OpenOffice.org\\UNO\\InstallPath", + # LibreOffice 3.4.5 on W7 + "SOFTWARE\\Wow6432Node\\LibreOffice\\UNO\\InstallPath", + # LibreOffice >= 6 + "SOFTWARE\\LibreOffice\\UNO\\InstallPath"]: try: - value = _winreg.QueryValue(_winreg.HKEY_LOCAL_MACHINE, _key) + value = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, key) except Exception as detail: _errMess = "%s" % detail else: - break # first existing key will do - install_folder = '\\'.join(value.split('\\')[:-1]) # 'C:\\Program Files\\OpenOffice.org 3' + break # first existing key will do + install_folder = '\\'.join(value.split('\\')[:-1]) # 'C:\\Program Files\\OpenOffice.org 3' - #modify the environment variables + # modify the environment variables os.environ['URE_BOOTSTRAP'] = 'vnd.sun.star.pathname:{0}\\program\\fundamental.ini'.format(install_folder) - os.environ['UNO_PATH'] = install_folder+'\\program\\' + os.environ['UNO_PATH'] = install_folder + '\\program\\' - sys.path.append(install_folder+'\\Basis\\program') - sys.path.append(install_folder+'\\program') + sys.path.append(install_folder + '\\Basis\\program') + sys.path.append(install_folder + '\\program') paths = '' for path in ("\\URE\\bin;", "\\Basis\\program;", "'\\program;"): paths += install_folder + path - os.environ['PATH'] = paths+ os.environ['PATH'] + os.environ['PATH'] = paths + os.environ['PATH'] import uno, re, zipfile, types, inspect, tempfile, shutil, subprocess from datetime import datetime, timedelta -# http://codesnippets.services.openoffice.org/Office/Office.MessageBoxWithTheUNOBasedToolkit.snip -from com.sun.star.awt import WindowDescriptor -from com.sun.star.awt.WindowClass import MODALTOP -from com.sun.star.awt.VclWindowPeerAttribute import OK +# for alert() +from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS +from com.sun.star.awt.MessageBoxType import MESSAGEBOX + + +# for append_string() + +class FontSlant: + from com.sun.star.awt.FontSlant import NONE, ITALIC + + +class FontWeight: + from com.sun.star.awt.FontWeight import NORMAL, BOLD + + +class Format(Flag): + RESET = 0 + ITALIC = 0b01 + BOLD = 0b10 + class OODoc(object): """ @@ -73,9 +93,10 @@ def load_cache(self): self.model = OODoc._model self.dispatcher = OODoc._dispatcher - def _detect_macro_environment(self): + @staticmethod + def _detect_macro_environment(): for layer in inspect.stack(): - if layer[1].startswith('vnd.sun.star.tdoc:'): + if layer[1].startswith('vnd.sun.star.tdoc:') or 'pythonscript.py' in layer[1]: return True return False @@ -86,8 +107,9 @@ def get_context(self): return localContext else: # We have to connect by socket - resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) - return resolver.resolve( "uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext" ) + resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", + localContext) + return resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") def get_model(self): """ @@ -102,7 +124,7 @@ def get_model(self): """ smgr = self.context.ServiceManager - desktop = smgr.createInstanceWithContext( "com.sun.star.frame.Desktop", self.context) + desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", self.context) return desktop.getCurrentComponent() def get_dispatcher(self): @@ -118,9 +140,10 @@ def get_dispatcher(self): The current environment is detected to decide to connect either via socket or directly. """ smgr = self.context.ServiceManager - return smgr.createInstanceWithContext( "com.sun.star.frame.DispatchHelper", self.context) + return smgr.createInstanceWithContext("com.sun.star.frame.DispatchHelper", self.context) - def args(self, name, *args): + @staticmethod + def args(name, *args): """ Receives a list of tupples and returns a list of com.sun.star.beans.PropertyValue objects corresponding to those tupples. This result can be passed to OODoc.dispatcher. @@ -140,6 +163,23 @@ def args(self, name, *args): return tuple(uno_struct) + def has_interface(self, name): + """ returns True if 'name' interface is supported by this object + """ + m = self.model + if hasattr(m, "getSupportedServiceNames"): + sn = m.getSupportedServiceNames() + for n in sn: + if str(n) == name: + return True + + return False + + def is_spreadsheet(self): + """ returns True if document is of type com.sun.star.sheet.SpreadsheetDocument + """ + return self.has_interface("com.sun.star.sheet.SpreadsheetDocument") + def dispatch(self, cmd, *args): """ Combines OODoc.dispatcher and OODoc.args to dispatch a event. @@ -169,26 +209,25 @@ def dispatch(self, cmd, *args): self.dispatcher.executeDispatch(self.model.getCurrentController(), '.uno:%s' % cmd, '', 0, args) - def alert(self, msg, title = u'Alert'): - """Opens an alert window with a message and title, and requires user to click 'Ok'""" - parentWin = self.model.CurrentController.Frame.ContainerWindow - - aDescriptor = WindowDescriptor() - aDescriptor.Type = MODALTOP - aDescriptor.WindowServiceName = 'messbox' - aDescriptor.ParentIndex = -1 - aDescriptor.Parent = parentWin - aDescriptor.WindowAttributes = OK + def _msgBox(self, msg, title, box_type, button_type): + """call to uno API to display a messageBox""" + ctx = self.get_context() + toolkit = ctx.ServiceManager.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) + parentWin = toolkit.getDesktopWindow() + box = toolkit.createMessageBox(parentWin, box_type, button_type, title, msg) - tk = parentWin.getToolkit() - box = tk.createWindow(aDescriptor) + return box.execute() - box.setMessageText(msg) - - if title: - box.setCaptionText(title) + def alert(self, msg, title=u'Alert'): + """Opens an alert window with a message and title, and requires user to click 'Ok'""" + self._msgBox(str(msg), title, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK) - box.execute() + def alertOkCancel(self, msg, title=u'Alert'): + """Opens an alert window and allows user to choose between ok or cancel + Use this in your code to get constants for return values : + from com.sun.star.awt.MessageBoxResults import CANCEL, OK + """ + return self._msgBox(str(msg), title, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK_CANCEL) def undo(self): """Undo the last action""" @@ -198,7 +237,8 @@ def redo(self): """Redo the last undo""" self.dispatch('.uno:Redo') - def _file_url(self, filename): + @staticmethod + def _file_url(filename): if not filename.startswith('/'): filename = os.path.join(os.environ['PWD'], filename) @@ -222,13 +262,14 @@ def quit(self): """Closes the OpenOffice.org instance""" self.dispatch('Quit') + class OOSheet(OODoc): """ Interacts with an OpenOffice.org Spreadsheet instance. This high-level library works with a group of cells defined by a selector. """ - def __init__(self, selector = None, _row_sliced = False): + def __init__(self, selector=None, _row_sliced=False): """ Constructor gets a selector as parameter. Selector can be one of the following forms: a10 @@ -256,8 +297,10 @@ def __init__(self, selector = None, _row_sliced = False): sheet_name, cells = selector.split('.') self.sheet = self.model.Sheets.getByName(sheet_name) except ValueError: - self.sheet = self.model.Sheets.getByIndex(0) + address = self.model.CurrentSelection.RangeAddress + self.sheet = self.model.Sheets.getByIndex(address.Sheet) cells = selector + cells.replace('$', '') cells = cells.upper() @@ -310,8 +353,8 @@ def _cells(self): A generator of all cells of this selector. Each cell returned will be a python-uno com.sun.star.table.XCell object. """ - for col in range(self.start_col, self.end_col+1): - for row in range(self.start_row, self.end_row+1): + for col in range(self.start_col, self.end_col + 1): + for row in range(self.start_row, self.end_row + 1): yield self.sheet.getCellByPosition(col, row) def __iter__(self): @@ -340,14 +383,13 @@ def __getitem__(self, key): self.end_col, self.start_row + start, self.start_row + stop), - _row_sliced = True) + _row_sliced=True) else: return OOSheet(self._generate_selector(self.start_col + start, self.start_col + stop, self.start_row, self.end_row)) - def __eq__(self, peer): return ((self.sheet.Name == peer.sheet.Name) and (self.start_row == peer.start_row) and @@ -355,16 +397,14 @@ def __eq__(self, peer): (self.end_row == peer.end_row) and (self.end_col == peer.end_col)) - - @property def cells(self): """ A generator of all cells of this selector. Each cell returned will be a single-cell OOSheet object """ - for col in range(self.start_col, self.end_col+1): - for row in range(self.start_row, self.end_row+1): + for col in range(self.start_col, self.end_col + 1): + for row in range(self.start_row, self.end_row + 1): yield OOSheet(self._generate_selector(col, col, row, row)) @property @@ -373,9 +413,9 @@ def rows(self): A generator of all cells of this selector. Each cell returned will be a single-cell OOSheet object """ - for row in range(self.start_row, self.end_row+1): + for row in range(self.start_row, self.end_row + 1): yield OOSheet(self._generate_selector(self.start_col, self.end_col, row, row), - _row_sliced = True) + _row_sliced=True) @property def columns(self): @@ -383,11 +423,9 @@ def columns(self): A generator of all cells of this selector. Each cell returned will be a single-cell OOSheet object """ - for col in range(self.start_col, self.end_col+1): + for col in range(self.start_col, self.end_col + 1): yield OOSheet(self._generate_selector(col, col, self.start_row, self.end_row)) - - @property def data_array(self): """ @@ -396,7 +434,6 @@ def data_array(self): """ return self.sheet.getCellRangeByName(self.selector).getDataArray() - def __repr__(self): try: return self.selector @@ -467,6 +504,34 @@ def string(self): assert self.cell is not None return self.cell.getString() + def _cellInsertString(self, where, text, s_format=None, w_format=None): + """insert text into LO cell with a specific slant of weight attribute""" + if s_format is not None: + where.CharPosture = s_format + if w_format is not None: + where.CharWeight = w_format + self.cell.insertString(where, text, False) + + def append_string(self, text, text_format=None): + """ append :text to the end of the current cell string, using format. + Only works - for the moment - for single-cell selectors + """ + assert self.cell is not None + + s_format = None + w_format = None + if text_format is not None: + if text_format == Format.RESET: + s_format = FontSlant.NONE + w_format = FontWeight.NORMAL + else: + if text_format & Format.BOLD: + w_format = FontWeight.BOLD + if text_format & Format.ITALIC: + s_format = FontSlant.ITALIC + + self._cellInsertString(self.cell.End, text, s_format, w_format) + @string.setter def string(self, string): """Sets the string of all cells affected by this selector. Expects a string.""" @@ -478,6 +543,31 @@ def set_string(self, string): self.string = string return self + @property + def annotation(self): + """The annotation associated with the cell. Only works for single-cell selectors""" + assert self.cell is not None + aCell = self.cell + return aCell.getAnnotation().getString() + + @staticmethod + def _setCellAnnotation(aCell, supplier, string): + """private function to set annotation via UNO API""" + aCellAddr = aCell.CellAddress + supplier.insertNew(aCellAddr, string) + + @annotation.setter + def annotation(self, string): + """Sets annotation associated of all cells affected by this selector. Expects a string.""" + annotationsSupplier = self.sheet.getAnnotations() + for cell in self._cells: + self._setCellAnnotation(cell, annotationsSupplier, string) + + def set_annotation(self, string): + """Sets the annotation of all cells affected by this selector. Expects a string.""" + self.annotation = string + return self + @property def date(self): """The date representation of a cell. Only works for single-cell selectors""" @@ -490,15 +580,14 @@ def date(self, date): delta = date - self.basedate self.value = delta.days - date_format = uno.getConstantByName( "com.sun.star.util.NumberFormat.DATE" ) + date_format = uno.getConstantByName("com.sun.star.util.NumberFormat.DATE") formats = self.model.getNumberFormats() cells = self.sheet.getCellRangeByName(self.selector) - #if formats.getByKey(cells).Type != date_format: + # if formats.getByKey(cells).Type != date_format: for cell in self._cells: if formats.getByKey(cell.NumberFormat).Type != date_format: - locale = uno.createUnoStruct( "com.sun.star.lang.Locale" ) - cell.NumberFormat = formats.getStandardFormat( date_format, locale ) - + locale = uno.createUnoStruct("com.sun.star.lang.Locale") + cell.NumberFormat = formats.getStandardFormat(date_format, locale) def set_date(self, date): """Sets the date of all cells affected by this selector. Expects a datetime.datetime object.""" @@ -588,12 +677,15 @@ def flatten(self): @property def first_row(self): return self.clone().shrink_down(self.height - 1) + @property def last_row(self): return self.clone().shrink_up(self.height - 1) + @property def first_column(self): return self.clone().shrink_right(self.width - 1) + @property def last_column(self): return self.clone().shrink_left(self.width - 1) @@ -700,16 +792,19 @@ def each(self, function): for cell in self.cells: function(cell) - def shift_right(self, num = 1): + def shift_right(self, num=1): """Moves the selector to right, but number of columns given by "num" parameter.""" return self.shift(num, 0) - def shift_left(self, num = 1): + + def shift_left(self, num=1): """Moves the selector to left, but number of columns given by "num" parameter.""" return self.shift(-num, 0) - def shift_down(self, num = 1): + + def shift_down(self, num=1): """Moves the selector down, but number of rows given by "num" parameter.""" return self.shift(0, num) - def shift_up(self, num = 1): + + def shift_up(self, num=1): """Moves the selector up, but number of rows given by "num" parameter.""" return self.shift(0, -num) @@ -798,12 +893,15 @@ def shift_until(self, col, row, *args, **kwargs): def shift_right_until(self, *args, **kwargs): """Moves selector to right until condition is matched. See shift_until()""" return self.shift_until(1, 0, *args, **kwargs) + def shift_left_until(self, *args, **kwargs): """Moves selector to left until condition is matched. See shift_until()""" return self.shift_until(-1, 0, *args, **kwargs) + def shift_down_until(self, *args, **kwargs): """Moves selector down until condition is matched. See shift_until()""" return self.shift_until(0, 1, *args, **kwargs) + def shift_up_until(self, *args, **kwargs): """Moves selector up until condition is matched. See shift_until()""" return self.shift_until(0, -1, *args, **kwargs) @@ -824,16 +922,19 @@ def grow(self, col, row): return self - def grow_right(self, num = 1): + def grow_right(self, num=1): """Add columns to right of selector""" return self.grow(num, 0) - def grow_left(self, num = 1): + + def grow_left(self, num=1): """Add columns to left of selector""" return self.grow(-num, 0) - def grow_up(self, num = 1): + + def grow_up(self, num=1): """Add rows before selector""" return self.grow(0, -num) - def grow_down(self, num = 1): + + def grow_down(self, num=1): """Add rows after selector""" return self.grow(0, num) @@ -852,12 +953,15 @@ def grow_until(self, col, row, *args, **kwargs): def grow_right_until(self, *args, **kwargs): """Expands selection to right until condition is matched. Conditions are same as shift_until()""" return self.grow_until(1, 0, *args, **kwargs) + def grow_left_until(self, *args, **kwargs): """Expands selection to left until condition is matched. Conditions are same as shift_until()""" return self.grow_until(-1, 0, *args, **kwargs) + def grow_down_until(self, *args, **kwargs): """Expands selection down until condition is matched. Conditions are same as shift_until()""" return self.grow_until(0, 1, *args, **kwargs) + def grow_up_until(self, *args, **kwargs): """Expands selection up until condition is matched. Conditions are same as shift_until()""" return self.grow_until(0, -1, *args, **kwargs) @@ -878,16 +982,19 @@ def shrink(self, col, row): return self - def shrink_right(self, num = 1): + def shrink_right(self, num=1): """Removes columns from right of selector. Does not afect data, only the selector.""" return self.shrink(num, 0) - def shrink_left(self, num = 1): + + def shrink_left(self, num=1): """Removes columns from left of selector. Does not afect data, only the selector.""" return self.shrink(-num, 0) - def shrink_up(self, num = 1): + + def shrink_up(self, num=1): """Removes rows from top of selector. Does not afect data, only the selector.""" return self.shrink(0, -num) - def shrink_down(self, num = 1): + + def shrink_down(self, num=1): """Removes rows from bottom of selector. Does not afect data, only the selector.""" return self.shrink(0, num) @@ -895,14 +1002,17 @@ def shrink_right_until(self, *args, **kwargs): """Reduces selection on right border until condition is matched. Conditions are same as shift_until()""" self.end_col = self.last_column.shift_left_until(*args, **kwargs).end_col return self + def shrink_left_until(self, *args, **kwargs): """Reduces selection on left border until condition is matched. Conditions are same as shift_until()""" self.start_col = self.first_column.shift_right_until(*args, **kwargs).start_col return self + def shrink_down_until(self, *args, **kwargs): """Reduces selection on bottom border until condition is matched. Conditions are same as shift_until()""" self.end_row = self.last_row.shift_up_until(*args, **kwargs).end_row return self + def shrink_up_until(self, *args, **kwargs): """Reduces selection on top border until condition is matched. Conditions are same as shift_until()""" self.start_row = self.first_row.shift_down_until(*args, **kwargs).start_row @@ -915,7 +1025,7 @@ def clone(self): """ return OOSheet(self.selector) - def protect_sheet(self, password = ""): + def protect_sheet(self, password=""): """ Protects selection's sheet against editions. When sheet is protected, only unprotected cells can be modified. @@ -924,7 +1034,7 @@ def protect_sheet(self, password = ""): self.sheet.protect(password) return self - def unprotect_sheet(self, password = ""): + def unprotect_sheet(self, password=""): """ Unprotects selection's sheet against editions. When sheet is unprotected, all cells can be modified. @@ -970,7 +1080,7 @@ def __init__(self, document_path, script_path): def open(self, path, mode='r'): fullpath = os.path.join(self.tmp, path) if not os.path.exists(fullpath): - os.makedirs(os.path.dirname(fullpath)) + os.makedirs(os.path.dirname(fullpath)) return open(os.path.join(self.tmp, path), mode) @property @@ -986,7 +1096,8 @@ def manifest_add(self, path): manifest = [] for line in self.open('META-INF/manifest.xml'): if '' in line: - manifest.append(' ' % path) + manifest.append( + ' ' % path) elif ('full-path="%s"' % path) in line: return @@ -1004,7 +1115,7 @@ def pack(self): fh.write(open(self.script).read()) fh.close() - #Backup + # Backup open(self.document + '.bak', 'wb').write(open(self.document, 'rb').read()) os.remove(self.document) @@ -1017,6 +1128,7 @@ def pack(self): shutil.rmtree(self.tmp) + def pack(): """Command line to pack the script in a document. Acessed as "oosheet-pack".""" try: @@ -1035,12 +1147,14 @@ def pack(): OOPacker(document, script).pack() + def print_help(): """Prints help message for pack()""" script_name = sys.argv[0].split('/')[-1] print("Usage: %s document script.py" % script_name) sys.exit(1) + def launch(): print(""" # This is just a reminder of the complicated command needed to launch diff --git a/setup.py b/setup.py index 5b6465e..0df3b2a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ import os setup(name = 'oosheet', - version = '1.3', + version = '1.4.0', description = 'LibreOffice Spreadsheet scripting library', long_description = open(os.path.join(os.path.dirname(__file__), "README")).read(), author = "Luis Fagundes",