Add Event Listener only one time?

Is there a way to make sure that a listener is added to a uno object only once?

For instance I have class that represents a cell. When the cell is created I want to add a listener to the cell.

class MyCellClass:
    def __init__(self, cell_name: str):
        # other code ...
        # how to only add if there is not already a MyCellModifyListener attached?
        modify_listener = MyCellModifyListener()
        self._cell.component.addModifyListener(modify_listener)

First Instance

my_first_cell = MyCellClass("A2")
# now my_first_cell "A2" has a listener attached

Later I may want to create another instance of cell on “A2”.

my_second_cell = MyCellClass("A2")
# now my_second_cell "A2" has a listener attached. Not preferred!

At this point there would be two cell modify listeners. I would like to ensure there is only one.
Is there a way to do this?

class MyCellClass:
    def __init__(self, cell_name: str):
        # other code ...
        # how to only add if there is not already a MyCellModifyListener attached?
        modify_listener = MyCellModifyListener()
        self._cell.component.removeModifyListener(modify_listener)
        self._cell.component.addModifyListener(modify_listener)

This not working for the scenario I am trying to solve.

class CellCustomPropListener(unohelper.Base, XModifyListener):
    def __init__(self) -> None:
        XModifyListener.__init__(self)

    def modified(self, event: EventObject) -> None:
        try:
            _ = event.Source.AbsoluteName  # type: ignore
            log.debug(f"CellCustomPropListener: modified: {event.Source.AbsoluteName}")
        except RuntimeException:
            # cell is deleted
            log.debug("CellCustomPropListener: modified: Cell is deleted")

Then I am running something like:

def main():
    # other code ...
    cell = sheet["B2"]
    cell.value = 10
    print()
    cell2 = sheet["B2"]
    cell2.value = 12
    print()
    cell3 = sheet["B2"]
    cell3.value = 14
    print()

Outputs

30/05/2024 16:50:11 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

30/05/2024 16:50:11 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2
30/05/2024 16:50:11 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

30/05/2024 16:50:11 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2
30/05/2024 16:50:11 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2
30/05/2024 16:50:11 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

In Cell class

class Cell:
    def __init__(self):
        # other code ...
        modify_listener = CellCustomPropListener()
        self.component.removeModifyListener(modify_listener)
        self.component.addModifyListener(modify_listener)

Not sure if I’m missing something, but wouldn’t keeping track i.e. a map of cell addresses and ranges listeners where added to and allowing only unique entries be the solution?

That most likely is possible, but also kinda defeats the purpose listeners. My inquiry to to figure out I there is another way to discover if a listener has been added via the API.


I can see that mapping and tracking between lets say cell adddress or cell.AbsoluteName as a possible solution. But this has drawbacks. The biggest I can think of right now is What if a cell were to be moved, Now its cell address and AbsoluteName are changed and the map is invalid. Preferable a solution that did not require any tracking at all. Is there any know way to query a uno object for its listeners?

True, with the cell moving around that’s not feasible unless you track every move. Still, I don’t see why calling removeModifyListener() before addModifyListener() with one single instance listener object is not an option. But no, a SheetCell object (or any supporting css::util::XModifyListener) can’t be queried for its listeners.

Hmm… maybe if the CellCustomPropListener class were a singleton this may work. I will have to give this a try later. The cell that listener is attached to is not passed directly to the listener so I suspect a singleton will work.

Well I gave it a shot. Exact same results.

Singleton Instance.

class CellCustomPropListener(unohelper.Base, XModifyListener):
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._is_init = False
        return cls._instance

    def __init__(self) -> None:
        if self._is_init:
            return
        XModifyListener.__init__(self)
        self._is_init = True

    def modified(self, event: EventObject) -> None:
        try:
            _ = event.Source.AbsoluteName  # type: ignore
            log.debug(f"CellCustomPropListener: modified: {event.Source.AbsoluteName}")
        except RuntimeException:
            # cell is deleted
            log.debug("CellCustomPropListener: modified: Cell is deleted")

On Cell Class:

Class Cell:
    def __init__(self):
        # other code ...
        self.component.removeModifyListener(CellCustomPropListener())
        self.component.addModifyListener(CellCustomPropListener())

Test ONE:

def main():

    loader = Lo.load_office(connector=Lo.ConnectPipe(), opt=Options(log_level=logging.DEBUG))
    doc = CalcDoc.create_doc(loader=loader, visible=True)
    try:
        sheet = doc.sheets[0]
        cell = sheet["B2"]
        cell.value = 10
        print()
        cell2 = sheet["B2"]
        cell2.value = 12
        print()
        cell3 = sheet["B2"]
        cell3.value = 14
        print()
    finally:
        doc.close()
        Lo.close_office()

Test Two: Use only one cell var.

def main():

    loader = Lo.load_office(connector=Lo.ConnectPipe(), opt=Options(log_level=logging.DEBUG))
    doc = CalcDoc.create_doc(loader=loader, visible=True)
    try:
        sheet = doc.sheets[0]
        cell = sheet["B2"]
        cell.value = 10
        print()
        cell = sheet["B2"]
        cell.value = 12
        print()
        cell = sheet["B2"]
        cell.value = 14
        print()
    finally:
        doc.close()
        Lo.close_office()

Same output for both test.

31/05/2024 11:15:29 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

31/05/2024 11:15:29 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2
31/05/2024 11:15:29 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

31/05/2024 11:15:29 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2
31/05/2024 11:15:29 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2
31/05/2024 11:15:29 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

Oh! So you create a new instance of the listener every time, and try to remove that new, never existed before, instance from the pre-existed listeners list, then try to add yet another instance?

Not exactly the CellCustomPropListener() is a Singleton class, so it is always the same instance.

1 Like

Hallo
Maybe something like:

import uno, unohelper
from com.sun.star.util import XModifyListener

global INSTANCES
INSTANCES = set()

class Observe(unohelper.Base, XModifyListener):
    
    def __init__(self, cell_range=None):
        unique = cell_range.AbsoluteName
        if unique in INSTANCES:
            return
        INSTANCES.add( unique )
        self.cell_range = cell_range
        XModifyListener.__init__(self)
        self.cell_range.addModifyListener(self)
        self._current = unique
        
    def modified(self, event):
        self.cell_range = event.Source
        if self._current != self.cell_range.AbsoluteName:
            INSTANCES.remove(self._current)
            INSTANCES.add(self.cell_range.AbsoluteName)
            self._current = self.cell_range.AbsoluteName
        # the following pattern prevents endless loops meanwhile modifying
        # the cell itself
        # … stolen from
        # https://forum.openoffice.org/en/forum/viewtopic.php?p=175732#p175732    
        event.Source.removeModifyListener(self)
        _string = event.Source.String 
        if _string.islower():
            _string = _string.upper()
        else:
            _string = _string.lower()
        event.Source.String = _string
        event.Source.addModifyListener(self)
        print(event.Source.AbsoluteName, flush=True)
            
    def disposing(self):
        pass

…addModifyListener is called in the __init__ so you have only:

def main():
    doc = XSCRIPTCONTEXT.getDocument()
    cell = doc.Sheets[0]['A10']
    Observe( cell )
    # do something and try again and again

with thanks to @Villeroy … from this really old topic

Don’t you think that this pattern would fail a soon as the involved cells tracked in INSTANCE have there AbsolutName changed such as when a new row or column are inserted?

try and test yourself!

did i really write that code?

Not the whole but essential parts inside the modified-method !

This mostly working. Except cause error when terminating. See the bottom here.

from __future__ import annotations
from typing import Any, cast, TYPE_CHECKING
import uno
import unohelper
from com.sun.star.util import XModifyListener
from com.sun.star.uno import RuntimeException

from ooodev.io.log import logging as log

if TYPE_CHECKING:
    from com.sun.star.lang import EventObject


class CellCustomPropListener(unohelper.Base, XModifyListener):
    _instances = {}

    def __new__(cls, cell_range: Any):
        unique = cell_range.AbsoluteName
        if unique not in cls._instances:
            inst = super().__new__(cls)
            inst._is_init = False
            cls._instances[unique] = inst
        return cls._instances[unique]

    def __init__(self, cell_range: Any) -> None:
        if self._is_init:
            return
        self._cell_range = cell_range
        XModifyListener.__init__(self)
        self._cell_range.addModifyListener(self)
        self._current = cell_range.AbsoluteName
        self._is_init = True

    def modified(self, event: EventObject) -> None:
        try:
            src = cast(Any, event.Source)
            self._cell_range = event.Source
            if self._current != self._cell_range.AbsoluteName:  # type: ignore
                self.__class__._instances.pop(self._current)
                self.__class__._instances[self._cell_range.AbsoluteName] = self  # type: ignore
                self._current = self._cell_range.AbsoluteName  # type: ignore
            src.removeModifyListener(self)
            # the following pattern prevents endless loops meanwhile modifying
            # the cell itself
            # … stolen from
            # https://forum.openoffice.org/en/forum/viewtopic.php?p=175732#p175732
            # update cell if needed here
            log.debug(f"CellCustomPropListener: modified: {src.AbsoluteName}")
            src.addModifyListener(self)

        except RuntimeException:
            # cell is deleted
            log.debug("CellCustomPropListener: modified: Cell is deleted")
            try:
                log.debug("CellCustomPropListener: modified: Attempting to clean up")
                if self._current and self._current in self.__class__._instances:
                    self.__class__._instances.pop(self._current)
                    log.debug(f"CellCustomPropListener: modified: Cleaned up {self._current}")
                    self._current = ""
            except Exception as e:
                log.error(f"CellCustomPropListener: modified: {e}")

    def disposing(self):
        if self._current in self.__class__._instances:
            self.__class__._instances.pop(self._current)

    @classmethod
    def clear(cls):
        cls._instances.clear()
        log.debug("CellCustomPropListener: clear: Cleared all instances")

Python runner

from __future__ import annotations
import logging
import uno

from ooodev.calc import CalcDoc
from ooodev.loader import Lo
from ooodev.loader.inst.options import Options
from ooodev.calc.cell.cell_custom_prop_listener import CellCustomPropListener

def main():

    loader = Lo.load_office(connector=Lo.ConnectPipe(), opt=Options(log_level=logging.DEBUG))
    doc = CalcDoc.create_doc(loader=loader, visible=True)
    try:
        sheet = doc.sheets[0]
        sheet2 = doc.sheets.insert_sheet("Sheet2")
        cell1 = sheet["B2"]
        CellCustomPropListener(cell1.component)
        cell1.value = 10
        print()
        cell2 = sheet["B2"]
        CellCustomPropListener(cell2.component)
        cell2.value = 12

        print()
        cell_c4 = sheet["C4"]
        CellCustomPropListener(cell_c4.component)
        cell_c4.value = 12

        print()
        cell3 = sheet["B2"]
        CellCustomPropListener(cell3.component)
        cell3.value = 14
        print()
        cell_c4.value = "Done"
        print()

        cell = sheet2["A1"]
        CellCustomPropListener(cell.component)
        cell.value = 10

        cell = sheet2["B1"]
        CellCustomPropListener(cell.component)
        cell.value = 12

        cell_b3 = sheet2["B3"]
        CellCustomPropListener(cell_b3.component)
        cell_b3.value = 12
        CellCustomPropListener.clear()

    finally:
        doc.close()
        Lo.close_office()

if __name__ == "__main__":
    main()

Log output

31/05/2024 15:21:12 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

31/05/2024 15:21:12 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

31/05/2024 15:21:12 - DEBUG - CellCustomPropListener: modified: $Sheet1.$C$4

31/05/2024 15:21:12 - DEBUG - CellCustomPropListener: modified: $Sheet1.$B$2

31/05/2024 15:21:12 - DEBUG - CellCustomPropListener: modified: $Sheet1.$C$4

31/05/2024 15:21:12 - DEBUG - CellCustomPropListener: modified: $Sheet2.$A$1
31/05/2024 15:21:12 - DEBUG - CellCustomPropListener: modified: $Sheet2.$B$1
31/05/2024 15:22:08 - DEBUG - CellCustomPropListener: modified: $Sheet2.$B$3
31/05/2024 15:22:08 - DEBUG - CellCustomPropListener: clear: Cleared all instances

Error output.

double free or corruption (out)
Unspecified Application Error


Fatal exception: Signal 6
Stack:
/usr/lib/libreoffice/program/libuno_sal.so.3(+0x41be2)[0x78f108a18be2]
/usr/lib/libreoffice/program/libuno_sal.so.3(+0x41d9a)[0x78f108a18d9a]
...
/lib/x86_64-linux-gnu/libc.so.6(+0x94ac3)[0x78f103094ac3]
/lib/x86_64-linux-gnu/libc.so.6(+0x126850)[0x78f103126850]
Leaking python objects bridged to UNO for reason pyuno runtime is not initialized, (the pyuno.bootstrap needs to be called before using any uno classes) at ./pyuno/source/module/pyuno_runtime.cxx:358
Leaking python objects bridged to UNO for reason pyuno runtime is not initialized, (the pyuno.bootstrap needs to be called before using any uno classes) at ./pyuno/source/module/pyuno_runtime.cxx:358
Traceback (most recent call last):
  File "/home/paul/.pyenv/versions/3.10.5/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/home/paul/.pyenv/versions/3.10.5/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/home/paul/.vscode/extensions/ms-python.debugpy-2024.6.0-linux-x64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/__main__.py", line 39, in <module>
    cli.main()
  File "/home/paul/.vscode/extensions/ms-python.debugpy-2024.6.0-linux-x64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 430, in main
    run()
  File "/home/paul/.vscode/extensions/ms-python.debugpy-2024.6.0-linux-x64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 284, in run_file
    runpy.run_path(target, run_name="__main__")
  File "/home/paul/.vscode/extensions/ms-python.debugpy-2024.6.0-linux-x64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 321, in run_path
    return _run_module_code(code, init_globals, run_name,
  File "/home/paul/.vscode/extensions/ms-python.debugpy-2024.6.0-linux-x64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 135, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "/home/paul/.vscode/extensions/ms-python.debugpy-2024.6.0-linux-x64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 124, in _run_code
    exec(code, run_globals)
  File "/home/paul/Documents/Projects/Python/LibreOffice/ooouno-dev-tools/tmp/calc_prop_listen.py", line 65, in <module>
    main()
  File "/home/paul/Documents/Projects/Python/LibreOffice/ooouno-dev-tools/tmp/calc_prop_listen.py", line 60, in main
    doc.close()
  File "/home/paul/Documents/Projects/Python/LibreOffice/ooouno-dev-tools/ooodev/utils/partial/doc_io_partial.py", line 89, in close
    result = self.__lo_inst.close(closable, deliver_ownership)
  File "/home/paul/Documents/Projects/Python/LibreOffice/ooouno-dev-tools/ooodev/loader/inst/lo_inst.py", line 1379, in close
    closeable.close(cargs.event_data)

I am getting errors when LibreOffice is terminating. I suspect all the listeners need to be released from the cells before closing.

I managed to get rid of errors except if a cell is deleted.

CellCustomPropListener.clear() must be called before the document is closed.


If I try and clean up deleted cell event.Source.removeModifyListener(self) I get error.
and closing has the same error as posted above. If I don’t cleanup I still get the same error when closing. Do you have any improvements for this? I think the listener is not being released from the deleted cell.

from __future__ import annotations
from typing import Any, cast, TYPE_CHECKING
import contextlib
import uno
import unohelper
from com.sun.star.util import XModifyListener
from com.sun.star.uno import RuntimeException

from ooodev.io.log import logging as log

if TYPE_CHECKING:
    from com.sun.star.lang import EventObject


class CellCustomPropListener(unohelper.Base, XModifyListener):
    _instances = {}

    def __new__(cls, cell_range: Any):
        unique = cell_range.AbsoluteName
        if unique not in cls._instances:
            inst = super().__new__(cls)
            inst._is_init = False
            cls._instances[unique] = inst
        return cls._instances[unique]

    def __init__(self, cell_range: Any) -> None:
        if self._is_init:
            return
        self._cell_range = cell_range
        XModifyListener.__init__(self)
        self._cell_range.addModifyListener(self)
        self._current = cell_range.AbsoluteName
        self._is_init = True

    def modified(self, event: EventObject) -> None:
        try:
            src = cast(Any, event.Source)
            self._cell_range = src
            if self._current != self._cell_range.AbsoluteName:  # type: ignore
                inst = self.__class__._instances.pop(self._current)
                inst.removeModifyListener(self)
                inst = None
                self.__class__._instances[self._cell_range.AbsoluteName] = self  # type: ignore
                self._current = self._cell_range.AbsoluteName  # type: ignore
            src.removeModifyListener(self)
            # the following pattern prevents endless loops meanwhile modifying
            # the cell itself
            # … stolen from
            # https://forum.openoffice.org/en/forum/viewtopic.php?p=175732#p175732
            # update cell if needed here
            log.debug(f"CellCustomPropListener: modified: {src.AbsoluteName}")
            src.addModifyListener(self)

        except RuntimeException:
            # cell is deleted
            log.debug("CellCustomPropListener: modified: Cell is deleted")
            try:
                log.debug("CellCustomPropListener: modified: Attempting to clean up")
                if self._current and self._current in self.__class__._instances:
                    self.__class__._instances.pop(self._current)
                    log.debug(f"CellCustomPropListener: modified: Cleaning up {self._current}")
                    event.Source.removeModifyListener(self)
                    log.debug(f"CellCustomPropListener: modified: Cleaned up {self._current}")
                    self._current = ""
            except Exception as e:
                log.error(f"CellCustomPropListener: modified: {e}")

    def disposing(self):
        if self._current in self.__class__._instances:
            self.__class__._instances.pop(self._current)

    @classmethod
    def clear(cls):
        for inst in cls._instances.values():
            with contextlib.suppress(Exception):
                inst._cell_range.removeModifyListener(inst)
        cls._instances.clear()
        log.debug("CellCustomPropListener: clear: Cleared all instances")