#!/usr/bin/env python3 import datetime from decimal import Decimal from typing import Any, Union from com.sun.star.sheet import CellFlags from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA from .easymain import (log, DATE_OFFSET, BaseObject, Color, dict_to_property, run_in_thread, set_properties ) from .easydoc import LODocument from .easyevents import EventsRangeSelectionListener, LOEvents from .easyshape import LOShapes, LOShape from .easydrawpage import LODrawPage SECONDS_DAY = 60 * 60 * 24 ONLY_VALUES = CellFlags.VALUE + CellFlags.DATETIME + CellFlags.STRING class LOCellStyle(): def __init__(self, obj): self._obj = obj def __str__(self): return f'CellStyle: {self.name}' @property def obj(self): return self._obj @property def name(self): return self.obj.Name @property def properties(self): properties = self.obj.PropertySetInfo.Properties data = {p.Name: getattr(self.obj, p.Name) for p in properties} return data @properties.setter def properties(self, values): set_properties(self.obj, values) class LOCellStyles(): def __init__(self, obj, doc): self._obj = obj self._doc = doc def __len__(self): return len(self.obj) def __getitem__(self, index): return LOCellStyle(self.obj[index]) def __setitem__(self, key, value): self.obj[key] = value def __delitem__(self, key): if not isinstance(key, str): key = key.Name del self.obj[key] def __contains__(self, item): return item in self.obj @property def obj(self): return self._obj @property def names(self): return self.obj.ElementNames def new(self, name: str): obj = self._doc.create_instance('com.sun.star.style.CellStyle') self.obj[name] = obj return LOCellStyle(obj) # ~ IsFiltered, # ~ IsManualPageBreak, # ~ IsStartOfNewPage class LOSheetRows(): def __init__(self, sheet, obj): self._sheet = sheet self._obj = obj def __getitem__(self, index): if isinstance(index, int): rows = LOSheetRows(self._sheet, self.obj[index]) else: rango = self._sheet[index.start:index.stop,0:] rows = LOSheetRows(self._sheet, rango.obj.Rows) return rows def __len__(self): return self.obj.Count def __str__(self): return self.obj.Name @property def obj(self): return self._obj @property def visible(self): return self._obj.IsVisible @visible.setter def visible(self, value): self._obj.IsVisible = value @property def color(self): return self.obj.CellBackColor @color.setter def color(self, value): self.obj.CellBackColor = value @property def is_transparent(self): return self.obj.IsCellBackgroundTransparent @is_transparent.setter def is_transparent(self, value): self.obj.IsCellBackgroundTransparent = value @property def height(self): return self.obj.Height @height.setter def height(self, value): self.obj.Height = value def optimal(self): self.obj.OptimalHeight = True return def insert(self, index, count): self.obj.insertByIndex(index, count) return def remove(self, index, count): self.obj.removeByIndex(index, count) return class LOCalcRanges(object): def __init__(self, obj): self._obj = obj def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __len__(self): return self._obj.Count def __iter__(self): self._index = 0 return self def __next__(self): try: r = self.obj[self._index] rango = LOCalcRange(r) except IndexError: raise StopIteration self._index += 1 return rango def __contains__(self, item): return self._obj.hasByName(item.name) def __getitem__(self, index): r = self.obj[index] rango = LOCalcRange(r) return rango def __str__(self): s = f'Ranges: {self.names}' return s @property def obj(self): return self._obj @property def names(self): return self.obj.ElementNames @property def data(self): rows = [r.data for r in self] return rows @data.setter def data(self, values): for i, data in enumerate(values): self[i].data = data @property def style(self): return '' @style.setter def style(self, value): for r in self: r.style = value def add(self, rangos: Any): if isinstance(rangos, LOCalcRange): rangos = (rangos,) for r in rangos: self.obj.addRangeAddress(r.range_address, False) return def remove(self, rangos: Any): if isinstance(rangos, LOCalcRange): rangos = (rangos,) for r in rangos: self.obj.removeRangeAddress(r.range_address) return class LOCalcRange(): CELL = 'ScCellObj' def __init__(self, obj): self._obj = obj self._is_cell = obj.ImplementationName == self.CELL def __contains__(self, rango): if isinstance(rango, LOCalcRange): address = rango.range_address else: address = rango.RangeAddress result = self.cursor.queryIntersection(address) return bool(result.Count) def __getitem__(self, index): return LOCalcRange(self.obj[index]) def __iter__(self): self._r = 0 self._c = 0 return self def __next__(self): try: rango = self[self._r, self._c] except Exception as e: raise StopIteration self._c += 1 if self._c == self.obj.Columns.Count: self._c = 0 self._r +=1 return rango def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __len__(self): ra = self.range_address rows = ra.EndRow - ra.StartRow + 1 return rows def __str__(self): s = f'Range: {self.name}' if self.is_cell: s = f'Cell: {self.name}' return s @property def obj(self): """Get original object pyUNO""" return self._obj @property def len_columns(self): """Get len columns""" ra = self.range_address cols = ra.EndColumn - ra.StartColumn + 1 return cols @property def is_cell(self): """True if range is cell""" return self._is_cell @property def is_range(self): return not self._is_cell @property def name(self): """Return the range address like string""" return self.obj.AbsoluteName @property def address(self): """Return com.sun.star.table.CellAddress""" return self.obj.CellAddress @property def range_address(self): """Return com.sun.star.table.CellRangeAddress""" return self.obj.RangeAddress @property def sheet(self): """Return sheet parent""" return LOCalcSheet(self.obj.Spreadsheet) @property def doc(self): """Return doc parent""" doc = self.obj.Spreadsheet.DrawPage.Forms.Parent return LOCalc(doc) @property def style(self): """Get or set cell style""" return self.obj.CellStyle @style.setter def style(self, value): self.obj.CellStyle = value @property def current_region(self): """Return current region""" cursor = self.cursor cursor.collapseToCurrentRegion() rango = self.obj.Spreadsheet[cursor.AbsoluteName] return LOCalcRange(rango) @property def back_color(self): """Get or set cell back color""" return self._obj.CellBackColor @back_color.setter def back_color(self, value): self._obj.CellBackColor = Color()(value) @property def type(self): """Get type of content. See https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1table.html#affea688ab9e00781fa35d8a790d10f0e """ return self.obj.Type @property def error(self): """Get number error in formulas""" return self.obj.getError() @property def string(self): """Get or set content like string""" return self.obj.String @string.setter def string(self, value): self.obj.setString(value) @property def float(self): """Get or set content like value""" return self.obj.Value @float.setter def float(self, value): self.obj.setValue(value) @property def formula(self): """Get or set formula""" return self.obj.Formula @formula.setter def formula(self, value): self.obj.setFormula(value) @property def date(self): """Get or set date""" value = int(self.obj.Value) date = datetime.date.fromordinal(value + DATE_OFFSET) return date @date.setter def date(self, value): d = value.toordinal() self.float = d - DATE_OFFSET @property def time(self): """Get or set time""" seconds = self.obj.Value * SECONDS_DAY time_delta = datetime.timedelta(seconds=seconds) time = (datetime.datetime.min + time_delta).time() return time @time.setter def time(self, value): d = (value.hour * 3600 + value.minute * 60 + value.second) / SECONDS_DAY self.float = d @property def datetime(self): """Get or set datetime""" return datetime.datetime.combine(self.date, self.time) @datetime.setter def datetime(self, value): d = value.toordinal() t = (value - datetime.datetime.fromordinal(d)).seconds / SECONDS_DAY self.float = d - DATE_OFFSET + t @property def value(self): """Get or set value, automatically get type data""" v = None if self.type == VALUE: v = self.float elif self.type == TEXT: v = self.string elif self.type == FORMULA: v = self.formula return v @value.setter def value(self, value): if isinstance(value, str): if value[0] in '=': self.formula = value else: self.string = value elif isinstance(value, Decimal): self.float = float(value) elif isinstance(value, (int, float, bool)): self.float = value elif isinstance(value, datetime.datetime): self.datetime = value elif isinstance(value, datetime.date): self.date = value elif isinstance(value, datetime.time): self.time = value @property def data_array(self): """Get or set DataArray""" return self.obj.DataArray @data_array.setter def data_array(self, data): self.obj.DataArray = data @property def formula_array(self): """Get or set FormulaArray""" return self.obj.FormulaArray @formula_array.setter def formula_array(self, data): self.obj.FormulaArray = data @property def data(self): """Get like data_array, for set, if range is cell, automatically ajust size range. """ return self.data_array @data.setter def data(self, values): if self.is_cell: cols = len(values[0]) rows = len(values) if cols == 1 and rows == 1: self.data_array = values else: self.to_size(cols, rows).data = values else: self.data_array = values @property def dict(self): """Get or set data from to dict""" rows = self.data k = rows[0] data = [dict(zip(k, r)) for r in rows[1:]] return data @dict.setter def dict(self, values): data = [tuple(values[0].keys())] data += [tuple(d.values()) for d in values] self.data = data @property def next_free(self): """Get next free cell""" ra = self.current_region.range_address col = ra.StartColumn row = ra.EndRow + 1 return LOCalcRange(self.sheet[row, col].obj) @property def range_data(self): """Get range without headers""" rango = self.current_region ra = rango.range_address row1 = ra.StartRow + 1 row2 = ra.EndRow + 1 col1 = ra.StartColumn col2 = ra.EndColumn + 1 return LOCalcRange(self.sheet[row1:row2, col1:col2].obj) @property def array_formula(self): return self.obj.ArrayFormula @array_formula.setter def array_formula(self, value): self.obj.ArrayFormula = value @property def cursor(self): cursor = self.obj.Spreadsheet.createCursorByRange(self.obj) return cursor # ~ To doc @property def position(self): return self.obj.Position # ~ To doc @property def size(self): return self.obj.Size @property def height(self): return self.obj.Size.Height @property def width(self): return self.obj.Size.Width @property def x(self): return self.obj.Position.X @property def y(self): return self.obj.Position.Y # ~ To doc @property def possize(self): data = { 'Width': self.size.Width, 'Height': self.size.Height, 'X': self.position.X, 'Y': self.position.Y, } return data @property def rows(self): return LOSheetRows(self.sheet, self.obj.Rows) def clear(self, what: int=ONLY_VALUES): """Clear contents""" # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html self.obj.clearContents(what) return def offset(self, rows=0, cols=1): ra = self.range_address col = ra.EndColumn + cols row = ra.EndRow + rows return LOCalcRange(self.sheet[row, col].obj) def to_size(self, cols: int, rows: int): cursor = self.cursor cursor.collapseToSize(cols, rows) rango = self.obj.Spreadsheet[cursor.AbsoluteName] return LOCalcRange(rango) class LOCalcSheet(BaseObject): def __init__(self, obj): super().__init__(obj) def __getitem__(self, index): return LOCalcRange(self.obj[index]) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): pass def __str__(self): return f'Sheet: {self.name}' @property def doc(self): return LOCalc(self.obj.DrawPage.Forms.Parent) @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 visible(self): return self._obj.IsVisible @visible.setter def visible(self, value): self._obj.IsVisible = value @property def color(self): return self._obj.TabColor @color.setter def color(self, value): self._obj.TabColor = Color()(value) @property def used_area(self): cursor = self.create_cursor() cursor.gotoEndOfUsedArea(True) return self[cursor.AbsoluteName] @property def is_protected(self): return self._obj.isProtected() @property def password(self): return '' @password.setter def password(self, value): self.obj.protect(value) @property def draw_page(self): return LODrawPage(self.obj.DrawPage) @property def dp(self): return self.draw_page @property def shapes(self): return self.draw_page @property def events(self): return LOEvents(self.obj.Events) @property def height(self): return self.obj.Size.Height @property def width(self): return self.obj.Size.Width def protect(self, value): self.obj.protect(value) return def unprotect(self, value): try: self.obj.unprotect(value) return True except: pass return False def move(self, pos: int=-1): index = pos if pos < 0: index = len(self.doc) self.doc.move(self.name, index) return def remove(self): self.doc.remove(self.name) return def copy(self, new_name: str='', pos: int=-1): index = pos if pos < 0: index = len(self.doc) new_sheet = self.doc.copy_sheet(self.name, new_name, index) return new_sheet def copy_to(self, doc: Any, target: str='', pos: int=-1): """Copy sheet to other document. """ index = pos if pos < 0: index = len(doc) new_name = target or self.name sheet = doc.copy_from(self.doc, self.name, new_name, index) return sheet def activate(self): """Activate sheet""" self.doc.activate(self.obj) return def create_cursor(self, rango: Any=None): if rango is None: cursor = self.obj.createCursor() else: obj = rango if hasattr(rango, 'obj'): obj = rango.obj cursor = self.obj.createCursorByRange(obj) return cursor class LOCalcSheetsCodeName(BaseObject): """Access by code name sheet""" def __init__(self, obj): super().__init__(obj) self._sheets = obj.Sheets def __getitem__(self, index): """Index access""" for sheet in self._sheets: if sheet.CodeName == index: return LOCalcSheet(sheet) raise IndexError def __iter__(self): self._i = 0 return self def __next__(self): """Interation sheets""" try: sheet = LOCalcSheet(self._sheets[self._i]) except Exception as e: raise StopIteration self._i += 1 return sheet def __contains__(self, item): """Contains""" for sheet in self._sheets: if sheet.CodeName == item: return True return False class LOCalc(LODocument): """Classe for Calc module""" TYPE_RANGES = ('ScCellObj', 'ScCellRangeObj') RANGES = 'ScCellRangesObj' SHAPE = 'com.sun.star.drawing.SvxShapeCollection' _type = 'calc' def __init__(self, obj): super().__init__(obj) self._sheets = obj.Sheets self._listener_range_selection = None def __getitem__(self, index): """Index access""" return LOCalcSheet(self._sheets[index]) def __setitem__(self, key: str, value: Any): """Insert new sheet""" self._sheets[key] = value def __len__(self): """Count sheets""" return self._sheets.Count def __contains__(self, item): """Contains""" return item in self._sheets def __iter__(self): self._i = 0 return self def __next__(self): """Interation sheets""" try: sheet = LOCalcSheet(self._sheets[self._i]) except Exception as e: raise StopIteration self._i += 1 return sheet def __str__(self): return f'Calc: {self.title}' @property def sheets(self): """Acces by code name sheet""" return LOCalcSheetsCodeName(self.obj) @property def headers(self): """Get true if is visible columns/rows headers""" return self._cc.ColumnRowHeaders @headers.setter def headers(self, value): """Set visible columns/rows headers""" self._cc.ColumnRowHeaders = value @property def tabs(self): """Get true if is visible tab sheets""" return self._cc.SheetTabs @tabs.setter def tabs(self, value): """Set visible tab sheets""" self._cc.SheetTabs = value @property def selection(self): """Get current seleccion""" sel = None selection = self.obj.CurrentSelection type_obj = selection.ImplementationName if type_obj in self.TYPE_RANGES: sel = LOCalcRange(selection) elif type_obj == self.RANGES: sel = LOCalcRanges(selection) elif type_obj == self.SHAPE: if len(selection) == 1: sel = LOShape(selection[0]) else: sel = LOShapes(selection) else: log.debug(type_obj) return sel @property def active(self): """Get active sheet""" return LOCalcSheet(self._cc.ActiveSheet) @property def names(self): """Get all sheet names""" names = self.obj.Sheets.ElementNames return names @property def events(self): """Get class events""" return LOEvents(self.obj.Events) def ranges(self): """Create ranges container""" obj = self._create_instance('com.sun.star.sheet.SheetCellRanges') return LOCalcRanges(obj) def new_sheet(self): """Create new instance sheet""" s = self._create_instance('com.sun.star.sheet.Spreadsheet') return s def select(self, rango: Any): """Select range""" obj = rango if hasattr(rango, 'obj'): obj = rango.obj self._cc.select(obj) return @run_in_thread def start_range_selection(self, controllers: Any, args: dict={}): """Start select range selection by user `See Api RangeSelectionArguments `_ """ if args: args['CloseOnMouseRelease'] = args.get('CloseOnMouseRelease', True) else: args = dict( Title = 'Please select a range', CloseOnMouseRelease = True) properties = dict_to_property(args) self._listener_range_selection = EventsRangeSelectionListener(controllers(self)) self._cc.addRangeSelectionListener(self._listener_range_selection) self._cc.startRangeSelection(properties) return def remove_range_selection_listener(self): """Remove range listener""" if not self._listener_range_selection is None: self._cc.removeRangeSelectionListener(self._listener_range_selection) return def activate(self, sheet: Union[str, LOCalcSheet]): """Activate sheet :param sheet: Sheet to activate :type sheet: str, pyUno or LOCalcSheet """ obj = sheet if isinstance(sheet, LOCalcSheet): obj = sheet.obj elif isinstance(sheet, str): obj = self._sheets[sheet] self._cc.setActiveSheet(obj) return def insert(self, name: Union[str, list, tuple]): """Insert new sheet :param name: Name new sheet, or iterable with names. :type name: str, list or tuple :return: New last instance sheet. :rtype: LOCalcSheet """ names = name if isinstance(name, str): names = (name,) for n in names: self._sheets[n] = self._create_instance('com.sun.star.sheet.Spreadsheet') return LOCalcSheet(self._sheets[n]) def move(self, sheet: Union[str, LOCalcSheet], pos: int=-1): """Move sheet name to position :param sheet: Instance or Name sheet to move :type sheet: LOCalcSheet or str :param pos: New position, if pos=-1 move to end :type pos: int """ name = sheet index = pos if pos < 0: index = len(self) if isinstance(sheet, LOCalcSheet): name = sheet.name self._sheets.moveByName(name, index) return def remove(self, sheet: Union[str, LOCalcSheet]): """Remove sheet by name :param sheet: Instance or Name sheet to move :type sheet: LOCalcSheet or str """ name = sheet if isinstance(sheet, LOCalcSheet): name = sheet.name self._sheets.removeByName(name) return def _get_new_name_sheet(self, name): i = 1 new_name = f'{name}_{i}' while new_name in self: i += 1 new_name = f'{name}_{i}' return new_name def copy_sheet(self, sheet: Union[str, LOCalcSheet], new_name: str='', pos: int=-1): """Copy sheet """ name = sheet if isinstance(sheet, LOCalcSheet): name = sheet.name index = pos if pos < 0: index = len(self) if not new_name: new_name = self._get_new_name_sheet(name) self._sheets.copyByName(name, new_name, index) return LOCalcSheet(self._sheets[new_name]) def copy_from(self, doc: Any, source: Any=None, target: Any=None, pos: int=-1): """Copy sheet from document """ index = pos if pos < 0: index = len(self) names = source if not source: names = doc.names elif isinstance(source, str): names = (source,) elif isinstance(source, LOCalcSheet): names = (source.name,) new_names = target if not target: new_names = names elif isinstance(target, str): new_names = (target,) for i, name in enumerate(names): self._sheets.importSheet(doc.obj, name, index + i) self[index + i].name = new_names[i] return LOCalcSheet(self._sheets[index]) def sort(self, reverse=False): """Sort sheets by name :param reverse: For order in reverse :type reverse: bool """ names = sorted(self.names, reverse=reverse) for i, n in enumerate(names): self.move(n, i) return def get_ranges(self, address: str): """Get the same range address in each sheet. """ ranges = self.ranges() ranges.add([sheet[address] for sheet in self]) return ranges @property def cs(self): return self.cell_styles @property def cell_styles(self): obj = self.obj.StyleFamilies['CellStyles'] return LOCellStyles(obj, self)