#!/usr/bin/env python3

# == Rapid Develop Macros in LibreOffice ==

# ~ This file is part of ZAZ.

# ~ https://git.cuates.net/elmau/zaz

# ~ ZAZ is free software: you can redistribute it and/or modify
# ~ it under the terms of the GNU General Public License as published by
# ~ the Free Software Foundation, either version 3 of the License, or
# ~ (at your option) any later version.

# ~ ZAZ is distributed in the hope that it will be useful,
# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# ~ GNU General Public License for more details.

# ~ You should have received a copy of the GNU General Public License
# ~ along with ZAZ.  If not, see <https://www.gnu.org/licenses/>.

import base64
import csv
import ctypes
import datetime
import getpass
import gettext
import hashlib
import json
import logging
import os
import platform
import re
import shlex
import shutil
import socket
import ssl
import subprocess
import sys
import tempfile
import threading
import time
import traceback
import zipfile

from collections import OrderedDict
from collections.abc import MutableMapping
from decimal import Decimal
from enum import IntEnum
from functools import wraps
from pathlib import Path
from pprint import pprint
from socket import timeout
from string import Template
from typing import Any, Union
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

import imaplib
import smtplib
from smtplib import SMTPException, SMTPAuthenticationError
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.utils import formatdate
from email import encoders
import mailbox

import uno
import unohelper
from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS
from com.sun.star.awt.MessageBoxResults import YES
from com.sun.star.awt import Rectangle, Size, Point
from com.sun.star.awt.PosSize import POSSIZE, SIZE
from com.sun.star.awt import Key, KeyModifier, KeyEvent
from com.sun.star.container import NoSuchElementException
from com.sun.star.datatransfer import XTransferable, DataFlavor

from com.sun.star.beans import PropertyValue, NamedValue
from com.sun.star.sheet import TableFilterField
from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA
from com.sun.star.util import Time, Date, DateTime

from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK
from com.sun.star.text.TextContentAnchorType import AS_CHARACTER

from com.sun.star.lang import Locale
from com.sun.star.lang import XEventListener
from com.sun.star.awt import XActionListener
from com.sun.star.awt import XMenuListener
from com.sun.star.awt import XMouseListener
from com.sun.star.awt import XMouseMotionListener
from com.sun.star.awt import XFocusListener
from com.sun.star.awt import XKeyListener
from com.sun.star.awt import XItemListener
from com.sun.star.awt import XTabListener
from com.sun.star.awt import XWindowListener
from com.sun.star.awt import XTopWindowListener
from com.sun.star.awt.grid import XGridDataListener
from com.sun.star.awt.grid import XGridSelectionListener
from com.sun.star.script import ScriptEventDescriptor

# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1FontUnderline.html
from com.sun.star.awt import FontUnderline
from com.sun.star.style.VerticalAlignment import TOP, MIDDLE, BOTTOM

from com.sun.star.view.SelectionType import SINGLE, MULTI, RANGE

from com.sun.star.sdb.CommandType import TABLE, QUERY, COMMAND

try:
    from peewee import Database, DateTimeField, DateField, TimeField, \
        __exception_wrapper__
except ImportError as e:
    Database = DateField = TimeField = DateTimeField = object
    print('You need install peewee, only if you will develop with Base')


LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
log = logging.getLogger(__name__)


# ~ You can get custom salt
# ~ codecs.encode(os.urandom(16), 'hex')
# ~ but, not modify this file, modify in import file
SALT = b'c9548699d4e432dfd2b46adddafbb06d'

TIMEOUT = 10
LOG_NAME = 'ZAZ'
FILE_NAME_CONFIG = 'zaz-{}.json'

LEFT = 0
CENTER = 1
RIGHT = 2

CALC = 'calc'
WRITER = 'writer'
DRAW = 'draw'
IMPRESS = 'impress'
BASE = 'base'
MATH = 'math'
BASIC = 'basic'
MAIN = 'main'
TYPE_DOC = {
    CALC: 'com.sun.star.sheet.SpreadsheetDocument',
    WRITER: 'com.sun.star.text.TextDocument',
    DRAW: 'com.sun.star.drawing.DrawingDocument',
    IMPRESS: 'com.sun.star.presentation.PresentationDocument',
    BASE: 'com.sun.star.sdb.DocumentDataSource',
    MATH: 'com.sun.star.formula.FormulaProperties',
    BASIC: 'com.sun.star.script.BasicIDE',
    MAIN: 'com.sun.star.frame.StartModule',
}

OBJ_CELL = 'ScCellObj'
OBJ_RANGE = 'ScCellRangeObj'
OBJ_RANGES = 'ScCellRangesObj'
TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_RANGES)

OBJ_SHAPES = 'com.sun.star.drawing.SvxShapeCollection'
OBJ_SHAPE = 'com.sun.star.comp.sc.ScShapeObj'
OBJ_GRAPHIC = 'SwXTextGraphicObject'

OBJ_TEXTS = 'SwXTextRanges'
OBJ_TEXT = 'SwXTextRange'


# ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL
class FilterOperator(IntEnum):
    EMPTY = 0
    NO_EMPTY = 1
    EQUAL = 2
    NOT_EQUAL = 3

# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html#a54d3ff280d892218d71e667f81ce99d4
class Border(IntEnum):
    NO_BORDER = 0
    BORDER = 1
    SIMPLE = 2


# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aa5aa6dbecaeb5e18a476b0a58279c57a
class ValidationType():
    from com.sun.star.sheet.ValidationType \
        import ANY, WHOLE, DECIMAL, DATE, TIME, TEXT_LEN, LIST, CUSTOM
VT = ValidationType


# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html#aecf58149730f4c8c5c18c70f3c7c5db7
class ValidationAlertStyle():
    from com.sun.star.sheet.ValidationAlertStyle \
        import STOP, WARNING, INFO, MACRO
VAS = ValidationAlertStyle


# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1ConditionOperator2.html
class ConditionOperator():
    from com.sun.star.sheet.ConditionOperator2 \
        import NONE, EQUAL, NOT_EQUAL, GREATER, GREATER_EQUAL, LESS, \
            LESS_EQUAL, BETWEEN, NOT_BETWEEN, FORMULA, DUPLICATE, NOT_DUPLICATE
CO = ConditionOperator


class DataPilotFieldOrientation():
    from com.sun.star.sheet.DataPilotFieldOrientation \
        import HIDDEN, COLUMN, ROW, PAGE, DATA
DPFO = DataPilotFieldOrientation


OS = platform.system()
IS_WIN = OS == 'Windows'
IS_MAC = OS == 'Darwin'
USER = getpass.getuser()
PC = platform.node()
DESKTOP = os.environ.get('DESKTOP_SESSION', '')
INFO_DEBUG = f"{sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path)

PYTHON = 'python'
if IS_WIN:
    PYTHON = 'python.exe'

_MACROS = {}
_start = 0

SECONDS_DAY = 60 * 60 * 24
DIR = {
    'images': 'images',
    'locales': 'locales',
}

KEY = {
    'enter': 1280,
}

MODIFIERS = {
    'shift': KeyModifier.SHIFT,
    'ctrl': KeyModifier.MOD1,
    'alt': KeyModifier.MOD2,
    'ctrlmac': KeyModifier.MOD3,
}

# ~ Menus
NODE_MENUBAR = 'private:resource/menubar/menubar'
MENUS = {
    'file': '.uno:PickList',
    'tools': '.uno:ToolsMenu',
    'help': '.uno:HelpMenu',
    'windows': '.uno:WindowList',
    'edit': '.uno:EditMenu',
    'view': '.uno:ViewMenu',
    'insert': '.uno:InsertMenu',
    'format': '.uno:FormatMenu',
    'styles': '.uno:FormatStylesMenu',
    'sheet': '.uno:SheetMenu',
    'data': '.uno:DataMenu',
    'table': '.uno:TableMenu',
    'form': '.uno:FormatFormMenu',
    'page': '.uno:PageMenu',
    'shape': '.uno:ShapeMenu',
    'slide': '.uno:SlideMenu',
    'show': '.uno:SlideShowMenu',
}

DEFAULT_MIME_TYPE = 'png'
MIME_TYPE = {
    'png': 'image/png',
    'jpg': 'image/jpeg',
}

MESSAGES = {
    'es': {
        'OK': 'Aceptar',
        'Cancel': 'Cancelar',
        'Select path': 'Seleccionar ruta',
        'Select directory': 'Seleccionar directorio',
        'Select file': 'Seleccionar archivo',
        'Incorrect user or password': 'Nombre de usuario o contraseña inválidos',
        'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail',
    }
}


CTX = uno.getComponentContext()
SM = CTX.getServiceManager()


def create_instance(name: str, with_context: bool=False, args: Any=None) -> Any:
    if with_context:
        instance = SM.createInstanceWithContext(name, CTX)
    elif args:
        instance = SM.createInstanceWithArguments(name, (args,))
    else:
        instance = SM.createInstance(name)
    return instance


def get_app_config(node_name: str, key: str=''):
    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 not key:
            return ca
        if ca and ca.hasByName(key):
            return ca.getPropertyValue(key)
    except Exception as e:
        error(e)
        return ''


LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale')
LANG = LANGUAGE.split('-')[0]
try:
    COUNTRY = LANGUAGE.split('-')[1]
except:
    COUNTRY = ''
LOCALE = Locale(LANG, COUNTRY, '')
NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName')
VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion')

INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{INFO_DEBUG}"

node = '/org.openoffice.Office.Calc/Calculate/Other/Date'
y = get_app_config(node, 'YY')
m = get_app_config(node, 'MM')
d = get_app_config(node, 'DD')
DATE_OFFSET = datetime.date(y, m, d).toordinal()


def error(info):
    log.error(info)
    return


def debug(*args):
    data = [str(a) for a in args]
    log.debug('\t'.join(data))
    return


def info(*args):
    data = [str(a) for a in args]
    log.info('\t'.join(data))
    return


def save_log(path: str, data):
    with open(path, 'a') as f:
        f.write(f'{str(now())[:19]} -{LOG_NAME}- ')
        pprint(data, stream=f)
    return


def catch_exception(f):
    @wraps(f)
    def func(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except Exception as e:
            name = f.__name__
            if IS_WIN:
                msgbox(traceback.format_exc())
            log.error(name, exc_info=True)
    return func


def inspect(obj: Any) -> None:
    zaz = create_instance('net.elmau.zaz.inspect')
    if hasattr(obj, 'obj'):
        obj = obj.obj
    zaz.inspect(obj)
    return


def mri(obj: Any) -> None:
    m = create_instance('mytools.Mri')
    if m is None:
        msg = 'Extension MRI not found'
        error(msg)
        return

    if hasattr(obj, 'obj'):
        obj = obj.obj
    m.inspect(obj)
    return


def run_in_thread(fn):
    def run(*k, **kw):
        t = threading.Thread(target=fn, args=k, kwargs=kw)
        t.start()
        return t
    return run


def now(only_time: bool=False):
    now = datetime.datetime.now()
    if only_time:
        now = now.time()
    return now


def today():
    return datetime.date.today()


def _(msg):
    if LANG == 'en':
        return msg

    if not LANG in MESSAGES:
        return msg

    return MESSAGES[LANG][msg]


def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'):
    """ Create message box
        type_msg: infobox, warningbox, errorbox, querybox, messbox
        http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMessageBoxFactory.html
    """
    toolkit = create_instance('com.sun.star.awt.Toolkit')
    parent = toolkit.getDesktopWindow()
    box = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message))
    return box.execute()


def question(message, title=TITLE):
    result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox')
    return result == YES


def warning(message, title=TITLE):
    return msgbox(message, title, type_msg='warningbox')


def errorbox(message, title=TITLE):
    return msgbox(message, title, type_msg='errorbox')


def get_type_doc(obj: Any) -> str:
    for k, v in TYPE_DOC.items():
        if obj.supportsService(v):
            return k
    return ''


def _get_class_doc(obj: Any) -> Any:
    classes = {
        CALC: LOCalc,
        WRITER: LOWriter,
        DRAW: LODraw,
        IMPRESS: LOImpress,
        BASE: LOBase,
        MATH: LOMath,
        BASIC: LOBasic,
    }
    type_doc = get_type_doc(obj)
    return classes[type_doc](obj)


def dict_to_property(values: dict, uno_any: bool=False):
    ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()])
    if uno_any:
        ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps)
    return ps


def _array_to_dict(values):
    d = {v[0]: v[1] for v in values}
    return d


def _property_to_dict(values):
    d = {v.Name: v.Value for v in values}
    return d


def json_dumps(data):
    return json.dumps(data, indent=4, sort_keys=True)


def json_loads(data):
    return json.loads(data)


def data_to_dict(data):
    if isinstance(data, (tuple, list)) and isinstance(data[0], (tuple, list)):
        return _array_to_dict(data)

    if isinstance(data, (tuple, list)) and isinstance(data[0], (PropertyValue, NamedValue)):
        return _property_to_dict(data)
    return {}


def _get_dispatch() -> Any:
    return create_instance('com.sun.star.frame.DispatchHelper')


# ~ https://wiki.documentfoundation.org/Development/DispatchCommands
# ~ Used only if not exists in API
def call_dispatch(frame: Any, url: str, args: dict={}) -> None:
    dispatch = _get_dispatch()
    if hasattr(frame, 'frame'):
        frame = frame.frame
    opt = dict_to_property(args)
    dispatch.executeDispatch(frame, url, '', 0, opt)
    return


def get_desktop():
    return create_instance('com.sun.star.frame.Desktop', True)


def _date_to_struct(value):
    if isinstance(value, datetime.datetime):
        d = DateTime()
        d.Year = value.year
        d.Month = value.month
        d.Day = value.day
        d.Hours = value.hour
        d.Minutes = value.minute
        d.Seconds = value.second
    elif isinstance(value, datetime.date):
        d = Date()
        d.Day = value.day
        d.Month = value.month
        d.Year = value.year
    elif isinstance(value, datetime.time):
        d = Time()
        d.Hours = value.hour
        d.Minutes = value.minute
        d.Seconds = value.second
    return d


def _struct_to_date(value):
    d = None
    if isinstance(value, Time):
        d = datetime.time(value.Hours, value.Minutes, value.Seconds)
    elif isinstance(value, Date):
        if value != Date():
            d = datetime.date(value.Year, value.Month, value.Day)
    elif isinstance(value, DateTime):
        if value.Year > 0:
            d = datetime.datetime(
                value.Year, value.Month, value.Day,
                value.Hours, value.Minutes, value.Seconds)
    return d


def _get_url_script(args: dict):
    library = args['library']
    name = args['name']
    language = args.get('language', 'Python')
    location = args.get('location', 'user')
    module = args.get('module', '.')

    if language == 'Python':
        module = '.py$'
    elif language == 'Basic':
        module = f".{module}."
        if location == 'user':
            location = 'application'

    url = 'vnd.sun.star.script'
    url = f'{url}:{library}{module}{name}?language={language}&location={location}'
    return url


def _call_macro(args: dict):
    #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification

    url = _get_url_script(args)
    args = args.get('args', ())

    service = 'com.sun.star.script.provider.MasterScriptProviderFactory'
    factory = create_instance(service)
    script = factory.createScriptProvider('').getScript(url)
    result = script.invoke(args, None, None)[0]

    return result


def call_macro(args, in_thread=False):
    result = None
    if in_thread:
        t = threading.Thread(target=_call_macro, args=(args,))
        t.start()
    else:
        result = _call_macro(args)
    return result


def run(command, capture=False, split=False):
    if split:
        cmd = shlex.split(command)
        result = subprocess.run(cmd, capture_output=capture, text=True, shell=IS_WIN)
        if capture:
            result = result.stdout
        else:
            result = result.returncode
    else:
        if capture:
            result = subprocess.check_output(command, shell=True).decode()
        else:
            result = subprocess.Popen(command)
    return result


def popen(command):
    try:
        proc = subprocess.Popen(shlex.split(command), shell=IS_WIN,
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        for line in proc.stdout:
            yield line.decode().rstrip()
    except Exception as e:
        error(e)
        yield (e.errno, e.strerror)


def sleep(seconds):
    time.sleep(seconds)
    return


class TimerThread(threading.Thread):

    def __init__(self, event, seconds, macro):
        threading.Thread.__init__(self)
        self.stopped = event
        self.seconds = seconds
        self.macro = macro

    def run(self):
        info('Timer started... {}'.format(self.macro['name']))
        while not self.stopped.wait(self.seconds):
            _call_macro(self.macro)
        info('Timer stopped... {}'.format(self.macro['name']))
        return


def start_timer(name, seconds, macro):
    global _MACROS
    _MACROS[name] = threading.Event()
    thread = TimerThread(_MACROS[name], seconds, macro)
    thread.start()
    return


def stop_timer(name):
    global _MACROS
    _MACROS[name].set()
    del _MACROS[name]
    return


def install_locales(path: str, domain: str='base', dir_locales=DIR['locales']):
    path_locales = _P.join(_P(path).path, dir_locales)
    try:
        lang = gettext.translation(domain, path_locales, languages=[LANG])
        lang.install()
        _ = lang.gettext
    except Exception as e:
        from gettext import gettext as _
        error(e)
    return _


def _export_image(obj, args):
    name = 'com.sun.star.drawing.GraphicExportFilter'
    exporter = create_instance(name)
    path = _P.to_system(args['URL'])
    args = dict_to_property(args)
    exporter.setSourceDocument(obj)
    exporter.filter(args)
    return _P.exists(path)


def sha256(data):
    result = hashlib.sha256(data.encode()).hexdigest()
    return result


def sha512(data):
    result = hashlib.sha512(data.encode()).hexdigest()
    return result


def get_config(key='', prefix='conf', default={}):
    name_file = FILE_NAME_CONFIG.format(prefix)
    values = None
    path = _P.join(_P.config('UserConfig'), name_file)
    if not _P.exists(path):
        return default

    values = _P.from_json(path)
    if key:
        values = values.get(key, default)

    return values


def set_config(key, value, prefix='conf'):
    name_file = FILE_NAME_CONFIG.format(prefix)
    path = _P.join(_P.config('UserConfig'), name_file)
    values = get_config(default={}, prefix=prefix)
    values[key] = value
    result = _P.to_json(path, values)
    return result


def start():
    global _start

    _start = now()
    info(_start)
    return


def end(get_seconds: bool=False):
    global _start

    e = now()
    td = e - _start
    result = str(td)
    if get_seconds:
        result = td.total_seconds()
    return result


def get_epoch():
    n = now()
    return int(time.mktime(n.timetuple()))


def render(template, data):
    s = Template(template)
    return s.safe_substitute(**data)


def get_size_screen():
    res = ''
    if IS_WIN:
        user32 = ctypes.windll.user32
        res = f'{user32.GetSystemMetrics(0)}x{user32.GetSystemMetrics(1)}'
    else:
        try:
            args = 'xrandr | grep "*" | cut -d " " -f4'
            res = run(args, split=False)
        except Exception as e:
            error(e)
    return res.strip()


def url_open(url, data=None, headers={}, verify=True, get_json=False, timeout=TIMEOUT):
    err = ''
    req = Request(url)
    for k, v in headers.items():
        req.add_header(k, v)
    try:
        # ~ debug(url)
        if verify:
            if not data is None and isinstance(data, str):
                data = data.encode()
            response = urlopen(req, data=data, timeout=timeout)
        else:
            context = ssl._create_unverified_context()
            response = urlopen(req, data=data, timeout=timeout, context=context)
    except HTTPError as e:
        error(e)
        err = str(e)
    except URLError as e:
        error(e.reason)
        err = str(e.reason)
    except timeout:
        err = 'timeout'
        error(err)
    else:
        headers = dict(response.info())
        result = response.read().decode()
        if get_json:
            result = json.loads(result)

    return result, headers, err


def _get_key(password):
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=SALT,
        iterations=100000)
    key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
    return key


def encrypt(data, password):
    from cryptography.fernet import Fernet

    f = Fernet(_get_key(password))
    if isinstance(data, str):
        data = data.encode()
    token = f.encrypt(data).decode()
    return token


def decrypt(token, password):
    from cryptography.fernet import Fernet, InvalidToken

    data = ''
    f = Fernet(_get_key(password))
    try:
        data = f.decrypt(token.encode()).decode()
    except InvalidToken as e:
        error('Invalid Token')
    return data


def switch_design_mode(doc):
    call_dispatch(doc.frame, '.uno:SwitchControlDesignMode')
    return


class SmtpServer(object):

    def __init__(self, config):
        self._server = None
        self._error = ''
        self._sender = ''
        self._is_connect = self._login(config)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    @property
    def is_connect(self):
        return self._is_connect

    @property
    def error(self):
        return self._error

    def _login(self, config):
        name = config['server']
        port = config['port']
        is_ssl = config['ssl']
        self._sender = config['user']
        hosts = ('gmail' in name or 'outlook' in name)
        try:
            if is_ssl and hosts:
                self._server = smtplib.SMTP(name, port, timeout=TIMEOUT)
                self._server.ehlo()
                self._server.starttls()
                self._server.ehlo()
            elif is_ssl:
                self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT)
                self._server.ehlo()
            else:
                self._server = smtplib.SMTP(name, port, timeout=TIMEOUT)

            self._server.login(self._sender, config['password'])
            msg = 'Connect to: {}'.format(name)
            debug(msg)
            return True
        except smtplib.SMTPAuthenticationError as e:
            if '535' in str(e):
                self._error = _('Incorrect user or password')
                return False
            if '534' in str(e) and 'gmail' in name:
                self._error = _('Allow less secure apps in GMail')
                return False
        except smtplib.SMTPException as e:
            self._error = str(e)
            return False
        except Exception as e:
            self._error = str(e)
            return False
        return False

    def _body(self, msg):
        body = msg.replace('\\n', '<BR>')
        return body

    def send(self, message):
        file_name = 'attachment; filename={}'
        email = MIMEMultipart()
        email['From'] = self._sender
        email['To'] = message['to']
        email['Cc'] = message.get('cc', '')
        email['Subject'] = message['subject']
        email['Date'] = formatdate(localtime=True)
        if message.get('confirm', False):
            email['Disposition-Notification-To'] = email['From']
        email.attach(MIMEText(self._body(message['body']), 'html'))

        for path in message.get('files', ()):
            fn = _P(path).file_name
            part = MIMEBase('application', 'octet-stream')
            part.set_payload(_P.read_bin(path))
            encoders.encode_base64(part)
            part.add_header('Content-Disposition', f'attachment; filename={fn}')
            email.attach(part)

        receivers = (
            email['To'].split(',') +
            email['CC'].split(',') +
            message.get('bcc', '').split(','))
        try:
            self._server.sendmail(self._sender, receivers, email.as_string())
            msg = 'Email sent...'
            debug(msg)
            if message.get('path', ''):
                self.save_message(email, message['path'])
            return True
        except Exception as e:
            self._error = str(e)
            return False
        return False

    def save_message(self, email, path):
        mbox = mailbox.mbox(path, create=True)
        mbox.lock()
        try:
            msg = mailbox.mboxMessage(email)
            mbox.add(msg)
            mbox.flush()
        finally:
            mbox.unlock()
        return

    def close(self):
        try:
            self._server.quit()
            msg = 'Close connection...'
            debug(msg)
        except:
            pass
        return


def _send_email(server, messages):
    with SmtpServer(server) as server:
        if server.is_connect:
            for msg in messages:
                server.send(msg)
        else:
            error(server.error)
    return server.error


def send_email(server, message):
    messages = message
    if isinstance(message, dict):
        messages = (message,)
    t = threading.Thread(target=_send_email, args=(server, messages))
    t.start()
    return


class ImapServer(object):

    def __init__(self, config):
        self._server = None
        self._error = ''
        self._is_connect = self._login(config)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    @property
    def is_connect(self):
        return self._is_connect

    @property
    def error(self):
        return self._error

    def _login(self, config):
        try:
            # ~ hosts = 'gmail' in config['server']
            if config['ssl']:
                self._server = imaplib.IMAP4_SSL(config['server'], config['port'])
            else:
                self._server = imaplib.IMAP4(config['server'], config['port'])
            self._server.login(config['user'], config['password'])
            self._server.select()
            return True
        except imaplib.IMAP4.error as e:
            self._error = str(e)
            return False
        except Exception as e:
            self._error = str(e)
            return False
        return False

    def get_folders(self, exclude=()):
        folders = {}
        result, subdir = self._server.list()
        for s in subdir:
            print(s.decode('utf-8'))
        return folders

    def close(self):
        try:
            self._server.close()
            self._server.logout()
            msg = 'Close connection...'
            debug(msg)
        except:
            pass
        return


# ~ Classes

class LOBaseObject(object):

    def __init__(self, obj):
        self._obj = obj

    def __setattr__(self, name, value):
        exists = hasattr(self, name)
        if not exists and not name in ('_obj', '_index', '_view'):
            setattr(self._obj, name, value)
        else:
            super().__setattr__(name, value)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    @property
    def obj(self):
        return self._obj


class LODocument(object):
    FILTERS = {
        'doc': 'MS Word 97',
        'docx': 'MS Word 2007 XML',
    }

    def __init__(self, obj):
        self._obj = obj
        self._cc = self.obj.getCurrentController()
        self._undo = True

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    @property
    def obj(self):
        return self._obj

    @property
    def title(self):
        return self.obj.getTitle()
    @title.setter
    def title(self, value):
        self.obj.setTitle(value)

    @property
    def type(self):
        return self._type

    @property
    def uid(self):
        return self.obj.RuntimeUID

    @property
    def frame(self):
        return self._cc.getFrame()

    @property
    def is_saved(self):
        return self.obj.hasLocation()

    @property
    def is_modified(self):
        return self.obj.isModified()

    @property
    def is_read_only(self):
        return self.obj.isReadOnly()

    @property
    def path(self):
        return _P.to_system(self.obj.URL)

    @property
    def dir(self):
        return _P(self.path).path

    @property
    def file_name(self):
        return _P(self.path).file_name

    @property
    def name(self):
        return _P(self.path).name

    @property
    def status_bar(self):
        return self._cc.getStatusIndicator()

    @property
    def visible(self):
        w = self.frame.ContainerWindow
        return w.isVisible()
    @visible.setter
    def visible(self, value):
        w = self.frame.ContainerWindow
        w.setVisible(value)

    @property
    def zoom(self):
        return self._cc.ZoomValue
    @zoom.setter
    def zoom(self, value):
        self._cc.ZoomValue = value

    @property
    def undo(self):
        return self._undo
    @undo.setter
    def undo(self, value):
        self._undo = value
        um = self.obj.UndoManager
        if value:
            try:
                um.leaveUndoContext()
            except:
                pass
        else:
            um.enterHiddenUndoContext()

    def clear_undo(self):
        self.obj.getUndoManager().clear()
        return

    @property
    def selection(self):
        sel = self.obj.CurrentSelection
        # ~ return _get_class_uno(sel)
        return sel

    @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

    def set_focus(self):
        w = self.frame.ComponentWindow
        w.setFocus()
        return

    def copy(self):
        call_dispatch(self.frame, '.uno:Copy')
        return

    def insert_contents(self, args={}):
        call_dispatch(self.frame, '.uno:InsertContents', args)
        return

    def paste(self):
        sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard')
        transferable = sc.getContents()
        self._cc.insertTransferable(transferable)
        # ~ return self.obj.getCurrentSelection()
        return

    def select(self, obj):
        self._cc.select(obj)
        return

    def to_pdf(self, path: str='', args: dict={}):
        """
            https://wiki.documentfoundation.org/Macros/Python_Guide/PDF_export_filter_data
        """
        path_pdf = path
        filter_name = '{}_pdf_Export'.format(self.type)
        filter_data = dict_to_property(args, True)
        args = {
            'FilterName': filter_name,
            'FilterData': filter_data,
        }
        opt = dict_to_property(args)
        try:
            self.obj.storeToURL(_P.to_url(path), opt)
        except Exception as e:
            error(e)
            path_pdf = ''

        return _P.exists(path_pdf)

    def export(self, path: str, ext: str='', args: dict={}):
        if not ext:
            ext = _P(path).ext
        filter_name = self.FILTERS[ext]
        filter_data = dict_to_property(args, True)
        args = {
            'FilterName': filter_name,
            'FilterData': filter_data,
        }
        opt = dict_to_property(args)
        try:
            self.obj.storeToURL(_P.to_url(path), opt)
        except Exception as e:
            error(e)
            path = ''
        return _P.exists(path)

    def save(self, path: str='', args: dict={}) -> bool:
        result = True
        opt = dict_to_property(args)
        if path:
            try:
                self.obj.storeAsURL(_P.to_url(path), opt)
            except Exception as e:
                error(e)
                result = False
        else:
            self.obj.store()
        return result

    def close(self):
        self.obj.close(True)
        return


class LOCellStyle(LOBaseObject):

    def __init__(self, obj):
        super().__init__(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(object):

    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')
        if name:
            self.obj[name] = obj
            obj = LOCellStyle(obj)
        return obj


class LOCalc(LODocument):

    def __init__(self, obj):
        super().__init__(obj)
        self._type = CALC
        self._sheets = obj.Sheets

    def __getitem__(self, index):
        return LOCalcSheet(self._sheets[index])

    def __setitem__(self, key, value):
        self._sheets[key] = value

    def __len__(self):
        return self._sheets.Count

    def __contains__(self, item):
        return item in self._sheets

    @property
    def names(self):
        names = self.obj.Sheets.ElementNames
        return names

    @property
    def selection(self):
        sel = self.obj.CurrentSelection
        if sel.ImplementationName in TYPE_RANGES:
            sel = LOCalcRange(sel)
        elif sel.ImplementationName == OBJ_SHAPES:
            if len(sel) == 1:
                sel = sel[0]
                sel = LODrawPage(sel.Parent)[sel.Name]
        else:
            debug(sel.ImplementationName)
        return sel

    @property
    def active(self):
        return LOCalcSheet(self._cc.ActiveSheet)

    @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 cs(self):
        return self.cell_styles
    @property
    def cell_styles(self):
        obj = self.obj.StyleFamilies['CellStyles']
        return LOCellStyles(obj, self)

    @property
    def db_ranges(self):
        # ~ return LOCalcDataBaseRanges(self.obj.DataBaseRanges)
        return self.obj.DatabaseRanges

    def activate(self, sheet):
        obj = sheet
        if isinstance(sheet, LOCalcSheet):
            obj = sheet.obj
        elif isinstance(sheet, str):
            obj = self._sheets[sheet]
        self._cc.setActiveSheet(obj)
        return

    def new_sheet(self):
        s = self.create_instance('com.sun.star.sheet.Spreadsheet')
        return s

    def insert(self, name):
        names = name
        if isinstance(name, str):
            names = (name,)
        for n in names:
            self._sheets[n] = self.new_sheet()
        return LOCalcSheet(self._sheets[n])

    def move(self, name, pos=-1):
        index = pos
        if pos < 0:
            index = len(self)
        if isinstance(name, LOCalcSheet):
            name = name.name
        self._sheets.moveByName(name, index)
        return

    def remove(self, name):
        if isinstance(name, LOCalcSheet):
            name = name.name
        self._sheets.removeByName(name)
        return

    def copy(self, name, new_name='', pos=-1):
        if isinstance(name, LOCalcSheet):
            name = name.name
        index = pos
        if pos < 0:
            index = len(self)
        self._sheets.copyByName(name, new_name, index)
        return LOCalcSheet(self._sheets[new_name])

    def copy_from(self, doc, source='', target='', pos=-1):
        index = pos
        if pos < 0:
            index = len(self)

        names = source
        if not source:
            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, 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):
        names = sorted(self.names, reverse=reverse)
        for i, n in enumerate(names):
            self.move(n, i)
        return

    def render(self, data, sheet=None, clean=True):
        if sheet is None:
            sheet = self.active
        return sheet.render(data, clean=clean)


class LOChart(object):

    def __init__(self, name, obj, draw_page):
        self._name = name
        self._obj = obj
        self._eobj = self._obj.EmbeddedObject
        self._type = 'Column'
        self._cell = None
        self._shape = self._get_shape(draw_page)
        self._pos = self._shape.Position

    def __getitem__(self, index):
        return LOBaseObject(self.diagram.getDataRowProperties(index))

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    @property
    def obj(self):
        return self._obj

    @property
    def name(self):
        return self._name

    @property
    def diagram(self):
        return self._eobj.Diagram

    @property
    def type(self):
        return self._type
    @type.setter
    def type(self, value):
        self._type = value
        if value == 'Bar':
            self.diagram.Vertical = True
            return
        type_chart = f'com.sun.star.chart.{value}Diagram'
        self._eobj.setDiagram(self._eobj.createInstance(type_chart))

    @property
    def cell(self):
        return self._cell
    @cell.setter
    def cell(self, value):
        self._cell = value
        self._shape.Anchor = value.obj

    @property
    def position(self):
        return self._pos
    @position.setter
    def position(self, value):
        self._pos = value
        self._shape.Position = value

    def _get_shape(self, draw_page):
        for shape in draw_page:
            if shape.PersistName == self.name:
                break
        return shape


class LOSheetCharts(object):

    def __init__(self, obj, sheet):
        self._obj = obj
        self._sheet = sheet

    def __getitem__(self, index):
        return LOChart(index, self.obj[index], self._sheet.draw_page)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __contains__(self, item):
        return item in self.obj

    def __len__(self):
        return len(self.obj)

    @property
    def obj(self):
        return self._obj

    def new(self, name, pos_size, data):
        self.obj.addNewByName(name, pos_size, data, True, True)
        return LOChart(name, self.obj[name], self._sheet.draw_page)


class LOSheetTableField(object):

    def __init__(self, obj):
        self._obj = obj

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    @property
    def obj(self):
        return self._obj

    @property
    def name(self):
        return self.obj.Name

    @property
    def orientation(self):
        return self.obj.Orientation
    @orientation.setter
    def orientation(self, value):
        self.obj.Orientation = value


# ~ com.sun.star.sheet.DataPilotFieldOrientation.ROW
class LOSheetTable(object):

    def __init__(self, obj):
        self._obj = obj
        self._source = None

    def __getitem__(self, index):
        field = self.obj.DataPilotFields[index]
        return LOSheetTableField(field)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    @property
    def obj(self):
        return self._obj

    @property
    def filter(self):
        return self.obj.ShowFilterButton
    @filter.setter
    def filter(self, value):
        self.obj.ShowFilterButton = value

    @property
    def source(self):
        return self._source
    @source.setter
    def source(self, value):
        self._source = value
        self.obj.SourceRange = value.range_address

    @property
    def rows(self):
        return self.obj.RowFields
    @rows.setter
    def rows(self, values):
        if not isinstance(values, tuple):
            values = (values,)
        for v in values:
            with self[v] as f:
                f.orientation = DPFO.ROW
    @property
    def columns(self):
        return self.obj.ColumnFields
    @columns.setter
    def columns(self, values):
        if not isinstance(values, tuple):
            values = (values,)
        for v in values:
            with self[v] as f:
                f.orientation = DPFO.COLUMN

    @property
    def data(self):
        return self.obj.DataFields
    @data.setter
    def data(self, values):
        if not isinstance(values, tuple):
            values = (values,)
        for v in values:
            with self[v] as f:
                f.orientation = DPFO.DATA


class LOSheetTables(object):

    def __init__(self, obj, sheet):
        self._obj = obj
        self._sheet = sheet

    def __getitem__(self, index):
        return LOSheetTable(self.obj[index])

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __contains__(self, item):
        return item in self.obj

    @property
    def obj(self):
        return self._obj

    @property
    def count(self):
        return self.obj.Count

    @property
    def names(self):
        return self.obj.ElementNames

    def new(self, name, target):
        table = self.obj.createDataPilotDescriptor()
        self.obj.insertNewByName(name, target.address, table)
        return LOSheetTable(self.obj[name])

    def remove(self, name):
        self.obj.removeByName(name)
        return


class LOFormControl(LOBaseObject):
    EVENTS = {
        'action': 'actionPerformed',
        'click': 'mousePressed',
    }
    TYPES = {
        'actionPerformed': 'XActionListener',
        'mousePressed': 'XMouseListener',
    }

    def __init__(self, obj, view, form):
        super().__init__(obj)
        self._view = view
        self._form = form
        self._m = view.Model
        self._index = -1

    def __setattr__(self, name, value):
        if name in ('_form', '_view', '_m', '_index'):
            self.__dict__[name] = value
        else:
            super().__setattr__(name, value)

    def __str__(self):
        return f'{self.name} ({self.type}) {[self.index]}'

    @property
    def form(self):
        return self._form

    @property
    def doc(self):
        return self.obj.Parent.Forms.Parent

    @property
    def name(self):
        return self._m.Name
    @name.setter
    def name(self, value):
        self._m.Name = value

    @property
    def tag(self):
        return self._m.Tag
    @tag.setter
    def tag(self, value):
        self._m.Tag = value

    @property
    def index(self):
        return self._index
    @index.setter
    def index(self, value):
        self._index = value

    @property
    def enabled(self):
        return self._m.Enabled
    @enabled.setter
    def enabled(self, value):
        self._m.Enabled = value

    @property
    def events(self):
        return self.form.getScriptEvents(self.index)
    def add_event(self, name, macro):
        if not 'name' in macro:
            macro['name'] = '{}_{}'.format(self.name, name)

        event = ScriptEventDescriptor()
        event.AddListenerParam = ''
        event.EventMethod = self.EVENTS[name]
        event.ListenerType = self.TYPES[event.EventMethod]
        event.ScriptCode = _get_url_script(macro)
        event.ScriptType = 'Script'

        for ev in self.events:
            if ev.EventMethod == event.EventMethod and \
                ev.ListenerType == event.ListenerType:
                self.form.revokeScriptEvent(self.index,
                    event.ListenerType, event.EventMethod, event.AddListenerParam)
                break

        self.form.registerScriptEvent(self.index, event)
        return

    def set_focus(self):
        self._view.setFocus()
        return


class LOFormControlLabel(LOFormControl):

    def __init__(self, obj, view, form):
        super().__init__(obj, view, form)

    @property
    def type(self):
        return 'label'

    @property
    def value(self):
        return self._m.Label
    @value.setter
    def value(self, value):
        self._m.Label = value


class LOFormControlText(LOFormControl):

    def __init__(self, obj, view, form):
        super().__init__(obj, view, form)

    @property
    def type(self):
        return 'text'

    @property
    def value(self):
        return self._m.Text
    @value.setter
    def value(self, value):
        self._m.Text = value


class LOFormControlButton(LOFormControl):

    def __init__(self, obj, view, form):
        super().__init__(obj, view, form)

    @property
    def type(self):
        return 'button'

    @property
    def value(self):
        return self._m.Label
    @value.setter
    def value(self, value):
        self._m.Text = Label


FORM_CONTROL_CLASS = {
    'label': LOFormControlLabel,
    'text': LOFormControlText,
    'button': LOFormControlButton,
}


class LOForm(object):
    MODELS = {
        'label': 'com.sun.star.form.component.FixedText',
        'text': 'com.sun.star.form.component.TextField',
        'button': 'com.sun.star.form.component.CommandButton',
    }

    def __init__(self, obj, draw_page):
        self._obj = obj
        self._dp = draw_page
        self._controls = {}
        self._init_controls()

    def __getitem__(self, index):
        control = self.obj[index]
        return self._controls[control.Name]

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __contains__(self, item):
        return item in self.obj

    def __len__(self):
        return len(self.obj)

    def __str__(self):
        return f'Form: {self.name}'

    def _init_controls(self):
        types = {
            'com.sun.star.form.OFixedTextModel': 'label',
            'com.sun.star.form.OEditModel': 'text',
            'com.sun.star.form.OButtonModel': 'button',
        }
        for i, control in enumerate(self.obj):
            name = control.Name
            tipo = types[control.ImplementationName]
            view = self.doc.CurrentController.getControl(control)
            control = FORM_CONTROL_CLASS[tipo](control, view)
            control.index = i
            setattr(self, name, control)
            self._controls[name] = control
        return

    @property
    def obj(self):
        return self._obj

    @property
    def name(self):
        return self.obj.Name
    @name.setter
    def name(self, value):
        self.obj.Name = value

    @property
    def source(self):
        return self.obj.DataSourceName
    @source.setter
    def source(self, value):
        self.obj.DataSourceName = value

    @property
    def type(self):
        return self.obj.CommandType
    @type.setter
    def type(self, value):
        self.obj.CommandType = value

    @property
    def command(self):
        return self.obj.Command
    @command.setter
    def command(self, value):
        self.obj.Command = value

    @property
    def doc(self):
        return self.obj.Parent.Parent

    def _special_properties(self, tipo, args):
        if tipo == 'button':
            # ~ if 'ImageURL' in args:
                # ~ args['ImageURL'] = self._set_image_url(args['ImageURL'])
            args['FocusOnClick'] = args.get('FocusOnClick', False)
            return args
        return args

    def add(self, args):
        name = args['Name']
        tipo = args.pop('Type').lower()
        w = args.pop('Width')
        h = args.pop('Height')
        x = args.pop('X', 0)
        y = args.pop('Y', 0)
        control = self.doc.createInstance('com.sun.star.drawing.ControlShape')
        control.setSize(Size(w, h))
        control.setPosition(Point(x, y))
        model = self.doc.createInstance(self.MODELS[tipo])
        args = self._special_properties(tipo, args)
        _set_properties(model, args)
        control.Control = model
        index = len(self)
        self.obj.insertByIndex(index, model)
        self._dp.add(control)
        view = self.doc.CurrentController.getControl(self.obj.getByName(name))
        control = FORM_CONTROL_CLASS[tipo](control, view, self.obj)
        control.index = index
        setattr(self, name, control)
        self._controls[name] = control
        return control


class LOSheetForms(object):

    def __init__(self, draw_page):
        self._dp = draw_page
        self._obj = draw_page.Forms

    def __getitem__(self, index):
        return LOForm(self.obj[index], self._dp)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __contains__(self, item):
        return item in self.obj

    def __len__(self):
        return len(self.obj)

    @property
    def obj(self):
        return self._obj

    @property
    def doc(self):
        return self.obj.Parent

    @property
    def count(self):
        return len(self)

    @property
    def names(self):
        return self.obj.ElementNames

    def insert(self, name):
        form = self.doc.createInstance('com.sun.star.form.component.Form')
        self.obj.insertByName(name, form)
        return LOForm(form, self._dp)

    def remove(self, index):
        if isinstance(index, int):
            self.obj.removeByIndex(index)
        else:
            self.obj.removeByName(index)
        return


# ~ IsFiltered,
# ~ IsManualPageBreak,
# ~ IsStartOfNewPage
class LOSheetRows(object):

    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

    @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


# ~ IsManualPageBreak,
# ~ IsStartOfNewPage
class LOSheetColumns(object):

    def __init__(self, sheet, obj):
        self._sheet = sheet
        self._obj = obj

    def __getitem__(self, index):
        if isinstance(index, (int, str)):
            rows = LOSheetColumns(self._sheet, self.obj[index])
        else:
            rango = self._sheet[0,index.start:index.stop]
            rows = LOSheetColumns(self._sheet, rango.obj.Columns)
        return rows

    def __len__(self):
        return self.obj.Count

    @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 width(self):
        return self.obj.Width
    @width.setter
    def width(self, value):
        self.obj.Width = value

    def optimal(self):
        self.obj.OptimalWidth = 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 LOCalcSheet(object):

    def __init__(self, obj):
        self._obj = 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'easymacro.LOCalcSheet: {self.name}'

    @property
    def obj(self):
        return self._obj

    @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 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

    @property
    def color(self):
        return self._obj.TabColor
    @color.setter
    def color(self, value):
        self._obj.TabColor = get_color(value)

    @property
    def used_area(self):
        cursor = self.get_cursor()
        cursor.gotoEndOfUsedArea(True)
        return LOCalcRange(self[cursor.AbsoluteName].obj)

    @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 doc(self):
        return LOCalc(self.obj.DrawPage.Forms.Parent)

    @property
    def charts(self):
        return LOSheetCharts(self.obj.Charts, self)

    @property
    def tables(self):
        return LOSheetTables(self.obj.DataPilotTables, self)

    @property
    def rows(self):
        return LOSheetRows(self, self.obj.Rows)

    @property
    def columns(self):
        return LOSheetColumns(self, self.obj.Columns)

    @property
    def forms(self):
        return LOSheetForms(self.obj.DrawPage)

    @property
    def events(self):
        names = ('OnFocus', 'OnUnfocus', 'OnSelect', 'OnDoubleClick',
            'OnRightClick', 'OnChange', 'OnCalculate')
        evs = self.obj.Events
        events = {n: _property_to_dict(evs.getByName(n)) for n in names
            if evs.getByName(n)}
        return events
    @events.setter
    def events(self, values):
        pv = '[]com.sun.star.beans.PropertyValue'
        ev = self.obj.Events
        for name, v in values.items():
            url = _get_url_script(v)
            args = dict_to_property(dict(EventType='Script', Script=url))
            # ~ e.replaceByName(k, args)
            uno.invoke(ev, 'replaceByName', (name, uno.Any(pv, args)))

    @property
    def search_descriptor(self):
        return self.obj.createSearchDescriptor()

    @property
    def replace_descriptor(self):
        return self.obj.createReplaceDescriptor()

    def activate(self):
        self.doc.activate(self.obj)
        return

    def clean(self):
        doc = self.doc
        sheet = doc.create_instance('com.sun.star.sheet.Spreadsheet')
        doc._sheets.replaceByName(self.name, sheet)
        return

    def move(self, pos=-1):
        index = pos
        if pos < 0:
            index = len(self.doc)
        self.doc._sheets.moveByName(self.name, index)
        return

    def remove(self):
        self.doc._sheets.removeByName(self.name)
        return

    def copy(self, new_name='', pos=-1):
        index = pos
        if pos < 0:
            index = len(self.doc)
        self.doc._sheets.copyByName(self.name, new_name, index)
        return LOCalcSheet(self.doc._sheets[new_name])

    def copy_to(self, doc, target='', pos=-1):
        index = pos
        if pos < 0:
            index = len(doc)
        name = self.name
        if not target:
            new_name = name

        doc._sheets.importSheet(self.doc.obj, name, index)
        sheet = doc[name]
        sheet.name = new_name
        return sheet

    def get_cursor(self, cell=None):
        if cell is None:
            cursor = self.obj.createCursor()
        else:
            cursor = self.obj.createCursorByRange(cell)
        return cursor

    def render(self, data, rango=None, clean=True):
        if rango is None:
            rango = self.used_area
        return rango.render(data, clean)

    def find(self, search_string, rango=None):
        if rango is None:
            rango = self.used_area
        return rango.find(search_string)


class LOCalcRange(object):

    def __init__(self, obj):
        self._obj = obj
        self._sd = None
        self._is_cell = obj.ImplementationName == OBJ_CELL

    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.columns:
            self._c = 0
            self._r +=1
        return rango

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __contains__(self, item):
        return item.in_range(self)

    def __str__(self):
        if self.is_none:
            s = 'Range: None'
        else:
            s = f'Range: {self.name}'
        return s

    @property
    def obj(self):
        return self._obj

    @property
    def is_none(self):
        return self.obj is None

    @property
    def is_cell(self):
        return self._is_cell

    @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 dp(self):
        return self.sheet.dp

    @property
    def sheet(self):
        return LOCalcSheet(self.obj.Spreadsheet)

    @property
    def doc(self):
        doc = self.obj.Spreadsheet.DrawPage.Forms.Parent
        return LODocument(doc)

    @property
    def name(self):
        return self.obj.AbsoluteName

    @property
    def code_name(self):
        name = self.name.replace('$', '').replace('.', '_').replace(':', '')
        return name

    @property
    def columns(self):
        return self.obj.Columns.Count

    @property
    def column(self):
        c1 = self.address.Column
        c2 = c1 + 1
        ra = self.current_region.range_address
        r1 = ra.StartRow
        r2 = ra.EndRow + 1
        return LOCalcRange(self.sheet[r1:r2, c1:c2].obj)

    @property
    def rows(self):
        return LOSheetRows(self.sheet, self.obj.Rows)

    @property
    def row(self):
        r1 = self.address.Row
        r2 = r1 + 1
        ra = self.current_region.range_address
        c1 = ra.StartColumn
        c2 = ra.EndColumn + 1
        return LOCalcRange(self.sheet[r1:r2, c1:c2].obj)

    @property
    def type(self):
        return self.obj.Type

    @property
    def value(self):
        v = None
        if self.type == VALUE:
            v = self.obj.getValue()
        elif self.type == TEXT:
            v = self.obj.getString()
        elif self.type == FORMULA:
            v = self.obj.getFormula()
        return v
    @value.setter
    def value(self, data):
        if isinstance(data, str):
            # ~ print(isinstance(data, str), data[0])
            if data[0] in '=':
                self.obj.setFormula(data)
                # ~ print('Set Formula')
            else:
                self.obj.setString(data)
        elif isinstance(data, Decimal):
            self.obj.setValue(float(data))
        elif isinstance(data, (int, float, bool)):
            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 date(self):
        value = int(self.obj.Value)
        date = datetime.date.fromordinal(value + DATE_OFFSET)
        return date

    @property
    def time(self):
        seconds = self.obj.Value * SECONDS_DAY
        time_delta = datetime.timedelta(seconds=seconds)
        time = (datetime.datetime.min + time_delta).time()
        return time

    @property
    def datetime(self):
        return datetime.datetime.combine(self.date, self.time)

    @property
    def data(self):
        return self.obj.getDataArray()
    @data.setter
    def data(self, values):
        if self._is_cell:
            self.to_size(len(values), len(values[0])).data = values
        else:
            self.obj.setDataArray(values)

    @property
    def dict(self):
        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 formula(self):
        return self.obj.getFormulaArray()
    @formula.setter
    def formula(self, values):
        self.obj.setFormulaArray(values)

    @property
    def array_formula(self):
        return self.obj.ArrayFormula
    @array_formula.setter
    def array_formula(self, value):
        self.obj.ArrayFormula = value

    @property
    def address(self):
        return self.obj.CellAddress

    @property
    def range_address(self):
        return self.obj.RangeAddress

    @property
    def cursor(self):
        cursor = self.obj.Spreadsheet.createCursorByRange(self.obj)
        return cursor

    @property
    def current_region(self):
        cursor = self.cursor
        cursor.collapseToCurrentRegion()
        return LOCalcRange(self.sheet[cursor.AbsoluteName].obj)

    @property
    def next_cell(self):
        a = self.current_region.range_address
        col = a.StartColumn
        row = a.EndRow + 1
        return LOCalcRange(self.sheet[row, col].obj)

    @property
    def position(self):
        return self.obj.Position

    @property
    def size(self):
        return self.obj.Size

    @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 visible(self):
        cursor = self.cursor
        rangos = cursor.queryVisibleCells()
        rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos]
        return tuple(rangos)

    @property
    def merged_area(self):
        cursor = self.cursor
        cursor.collapseToMergedArea()
        rango = LOCalcRange(self.sheet[cursor.AbsoluteName].obj)
        return rango

    @property
    def empty(self):
        cursor = self.sheet.get_cursor(self.obj)
        cursor = self.cursor
        rangos = cursor.queryEmptyCells()
        rangos = [LOCalcRange(self.sheet[r.AbsoluteName].obj) for r in rangos]
        return tuple(rangos)

    @property
    def merge(self):
        return self.obj.IsMerged
    @merge.setter
    def merge(self, value):
        self.obj.merge(value)

    @property
    def style(self):
        return self.obj.CellStyle
    @style.setter
    def style(self, value):
        self.obj.CellStyle = value

    @property
    def auto_format(self):
        return ''
    @auto_format.setter
    def auto_format(self, value):
        self.obj.autoFormat(value)

    @property
    def validation(self):
        return self.obj.Validation
    @validation.setter
    def validation(self, values):
        current = self.validation
        if not values:
            current.Type = ValidationType.ANY
            current.ShowInputMessage = False
        else:
            is_list = False
            for k, v in values.items():
                if k == 'Type' and v == VT.LIST:
                    is_list = True
                if k == 'Formula1' and is_list:
                    if isinstance(v, (tuple, list)):
                        v = ';'.join(['"{}"'.format(i) for i in v])
                setattr(current, k, v)
        self.obj.Validation = current

    def select(self):
        self.doc.select(self.obj)
        return

    def search(self, options, find_all=True):
        rangos = None

        descriptor = self.sheet.search_descriptor
        descriptor.setSearchString(options['Search'])
        descriptor.SearchCaseSensitive = options.get('CaseSensitive', False)
        descriptor.SearchWords = options.get('Words', False)
        if hasattr(descriptor, 'SearchRegularExpression'):
            descriptor.SearchRegularExpression = options.get('RegularExpression', False)
        if hasattr(descriptor, 'SearchType') and 'Type' in options:
            descriptor.SearchType = options['Type']

        if find_all:
            found = self.obj.findAll(descriptor)
        else:
            found = self.obj.findFirst(descriptor)

        if found:
            if found.ImplementationName == OBJ_CELL:
                rangos = LOCalcRange(found)
            else:
                rangos = [LOCalcRange(f) for f in found]

        return rangos

    def replace(self, options):
        descriptor = self.sheet.replace_descriptor
        descriptor.setSearchString(options['Search'])
        descriptor.setReplaceString(options['Replace'])
        descriptor.SearchCaseSensitive = options.get('CaseSensitive', False)
        descriptor.SearchWords = options.get('Words', False)
        if hasattr(descriptor, 'SearchRegularExpression'):
            descriptor.SearchRegularExpression = options.get('RegularExpression', False)
        if hasattr(descriptor, 'SearchType') and 'Type' in options:
            descriptor.SearchType = options['Type']
        count = self.obj.replaceAll(descriptor)
        return count

    def in_range(self, rango):
        if isinstance(rango, LOCalcRange):
            address = rango.range_address
        else:
            address = rango.RangeAddress
        result = self.cursor.queryIntersection(address)
        return bool(result.Count)

    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, rows, cols):
        cursor = self.cursor
        cursor.collapseToSize(cols, rows)
        return LOCalcRange(self.sheet[cursor.AbsoluteName].obj)

    def copy(self, source):
        self.sheet.obj.copyRange(self.address, source.range_address)
        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 copy_from(self, rango, formula=False):
        data = rango
        if isinstance(rango, LOCalcRange):
            if formula:
                data = rango.formula
            else:
                data = rango.data
        rows = len(data)
        cols = len(data[0])
        if formula:
            self.to_size(rows, cols).formula = data
        else:
            self.to_size(rows, cols).data = data
        return

    def optimal_width(self):
        self.obj.Columns.OptimalWidth = True
        return

    def clean_render(self, template='\{(\w.+)\}'):
        self._sd.SearchRegularExpression = True
        self._sd.setSearchString(template)
        self.obj.replaceAll(self._sd)
        return

    def render(self, data, clean=True):
        self._sd = self.sheet.obj.createSearchDescriptor()
        self._sd.SearchCaseSensitive = False
        for k, v in data.items():
            cell = self._render_value(k, v)
        return cell

    def _render_value(self, key, value, parent=''):
        cell = None
        if isinstance(value, dict):
            for k, v in value.items():
                # ~ print(1, 'RENDER', k, v)
                cell = self._render_value(k, v, key)
            return cell
        elif isinstance(value, (list, tuple)):
            self._render_list(key, value)
            return

        search = f'{{{key}}}'
        if parent:
            search = f'{{{parent}.{key}}}'
        ranges = self.find_all(search)

        if ranges is None:
            return

        # ~ for cell in ranges or range(0):
        for cell in ranges:
            self._set_new_value(cell, search, value)
        return LOCalcRange(cell)

    def _set_new_value(self, cell, search, value):
        if not cell.ImplementationName == 'ScCellObj':
            return

        if isinstance(value, str):
            pattern = re.compile(search, re.IGNORECASE)
            new_value = pattern.sub(value, cell.String)
            cell.String = new_value
        else:
            LOCalcRange(cell).value = value
        return

    def _render_list(self, key, rows):
        for row in rows:
            for k, v in row.items():
                self._render_value(k, v)
        return

    def find(self, search_string):
        if self._sd is None:
            self._sd = self.sheet.obj.createSearchDescriptor()
            self._sd.SearchCaseSensitive = False

        self._sd.setSearchString(search_string)
        cell = self.obj.findFirst(self._sd)
        if cell:
            cell = LOCalcRange(cell)
        return cell

    def find_all(self, search_string):
        if self._sd is None:
            self._sd = self.sheet.obj.createSearchDescriptor()
            self._sd.SearchCaseSensitive = False

        self._sd.setSearchString(search_string)
        ranges = self.obj.findAll(self._sd)
        return ranges

    def filter(self, args, with_headers=True):
        ff = TableFilterField()
        ff.Field = args['Field']
        ff.Operator = args['Operator']
        if isinstance(args['Value'], str):
            ff.IsNumeric = False
            ff.StringValue = args['Value']
        else:
            ff.IsNumeric = True
            ff.NumericValue = args['Value']

        fd = self.obj.createFilterDescriptor(True)
        fd.ContainsHeader = with_headers
        fd.FilterFields = ((ff,))
        # ~ self.obj.AutoFilter = True
        self.obj.filter(fd)
        return

    def copy_format_from(self, rango):
        rango.select()
        self.doc.copy()
        self.select()
        args = {
            'Flags': 'T',
            'MoveMode': 4,
        }
        url = '.uno:InsertContents'
        call_dispatch(self.doc.frame, url, args)
        return

    def to_image(self):
        self.select()
        self.doc.copy()
        args = {'SelectedFormat': 141}
        url = '.uno:ClipboardFormatItems'
        call_dispatch(self.doc.frame, url, args)
        return self.sheet.shapes[-1]

    def insert_image(self, path, args={}):
        ps = self.possize
        args['Width'] = args.get('Width', ps['Width'])
        args['Height'] = args.get('Height', ps['Height'])
        args['X'] = args.get('X', ps['X'])
        args['Y'] = args.get('Y', ps['Y'])
        # ~ img.ResizeWithCell = True
        img = self.sheet.dp.insert_image(path, args)
        img.anchor = self.obj
        args.clear()
        return img

    def insert_shape(self, tipo, args={}):
        ps = self.possize
        args['Width'] = args.get('Width', ps['Width'])
        args['Height'] = args.get('Height', ps['Height'])
        args['X'] = args.get('X', ps['X'])
        args['Y'] = args.get('Y', ps['Y'])

        shape = self.sheet.dp.add(tipo, args)
        shape.anchor = self.obj
        args.clear()
        return

    def filter_by_color(self, cell):
        rangos = cell.column[1:,:].visible
        for r in rangos:
            for c in r:
                if c.back_color != cell.back_color:
                    c.rows.visible = False
        return

    def clear(self, what=1023):
        # ~ http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html
        self.obj.clearContents(what)
        return

    def transpose(self):
        # ~ 'Flags': 'A',
        # ~ 'FormulaCommand': 0,
        # ~ 'SkipEmptyCells': False,
        # ~ 'AsLink': False,
        # ~ 'MoveMode': 4,
        self.select()
        self.doc.copy()
        self.clear(1023)
        self[0,0].select()
        self.doc.insert_contents({'Transpose': True})
        _CB.set('')
        return

    def transpose_data(self, formula=False):
        data = self.data
        if formula:
            data = self.formula
        data = tuple(zip(*data))
        self.clear(1023)
        self[0,0].copy_from(data, formula=formula)
        return

    def merge_by_row(self):
        for r in range(len(self.rows)):
            self[r].merge = True
        return

    def fill(self, source=1):
        self.obj.fillAuto(0, source)
        return

    def _cast(self, t, v):
        if not t:
            return v

        if t == datetime.date:
            nv = datetime.date.fromordinal(int(v) + DATE_OFFSET)
        else:
            nv = t(v)
        return nv

    def get_data(self, types):
        values = [
            [self._cast(types[i], v)  for i, v in enumerate(row)]
                for row in self.data
            ]
        return values


class LOWriterStyles(object):

    def __init__(self, styles):
        self._styles = styles

    @property
    def names(self):
        return {s.DisplayName: s.Name for s in  self._styles}

    def __str__(self):
        return '\n'.join(tuple(self.names.values()))


class LOWriterStylesFamilies(object):

    def __init__(self, styles):
        self._styles = styles

    def __getitem__(self, index):
        styles = {
            'Character': 'CharacterStyles',
            'Paragraph': 'ParagraphStyles',
            'Page': 'PageStyles',
            'Frame': 'FrameStyles',
            'Numbering': 'NumberingStyles',
            'Table': 'TableStyles',
            'Cell': 'CellStyles',
        }
        name = styles.get(index, index)
        return LOWriterStyles(self._styles[name])

    def __iter__(self):
        self._index = 0
        return self

    def __next__(self):
        obj = LOWriterStyles(self._styles[self._index])
        self._index += 1
        return obj
        # ~ raise StopIteration

    @property
    def names(self):
        return self._styles.ElementNames

    def __str__(self):
        return '\n'.join(self.names)


class LOWriterPageStyle(LOBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    def __str__(self):
        return f'Page Style: {self.name}'

    @property
    def name(self):
        return self._obj.Name


class LOWriterPageStyles(object):

    def __init__(self, styles):
        self._styles = styles

    def __getitem__(self, index):
        return LOWriterPageStyle(self._styles[index])

    @property
    def names(self):
        return self._styles.ElementNames

    def __str__(self):
        return '\n'.join(self.names)


class LOWriterTextRange(object):

    def __init__(self, obj, doc):
        self._obj = obj
        self._doc = doc
        self._is_paragraph = self.obj.ImplementationName == 'SwXParagraph'
        self._is_table = self.obj.ImplementationName == 'SwXTextTable'
        self._is_text = self.obj.ImplementationName == 'SwXTextPortion'
        self._parts = []
        if self._is_paragraph:
            self._parts = [LOWriterTextRange(p, doc) for p in obj]

    def __iter__(self):
        self._index = 0
        return self

    def __next__(self):
        try:
            obj = self._parts[self._index]
        except IndexError:
            raise StopIteration

        self._index += 1
        return obj

    @property
    def obj(self):
        return self._obj

    @property
    def string(self):
        s = ''
        if not self._is_table:
            s = self.obj.String
        return s
    @string.setter
    def string(self, value):
        self.obj.String = value

    @property
    def value(self):
        return self.string
    @value.setter
    def value(self, value):
        self.string = value

    @property
    def style(self):
        s = ''
        if self.is_paragraph:
            s = self.obj.ParaStyleName
        elif self.is_text:
            s = self.obj.CharStyleName
        return s
    @style.setter
    def style(self, value):
        if self.is_paragraph:
            self.obj.ParaStyleName = value
        elif self.is_text:
            self.obj.CharStyleName = value

    @property
    def is_paragraph(self):
        return self._is_paragraph

    @property
    def is_table(self):
        return self._is_table

    @property
    def is_text(self):
        return self._is_text

    @property
    def text(self):
        return self.obj.Text

    @property
    def cursor(self):
        return self.text.createTextCursorByRange(self.obj)

    @property
    def dp(self):
        return self._doc.dp

    def delete(self):
        cursor = self.cursor
        cursor.gotoStartOfParagraph(False)
        cursor.gotoNextParagraph(True)
        cursor.String = ''
        return

    def offset(self):
        cursor = self.cursor.getEnd()
        return LOWriterTextRange(cursor, self._doc)

    def insert_content(self, data, cursor=None, replace=False):
        if cursor is None:
            cursor = self.cursor
        self.text.insertTextContent(cursor, data, replace)
        return

    def new_line(self, count=1):
        cursor = self.cursor
        for i in range(count):
            self.text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False)
        return self._doc.selection

    def insert_table(self, data):
        table = self._doc.create_instance('com.sun.star.text.TextTable')
        rows = len(data)
        cols = len(data[0])
        table.initialize(rows, cols)
        self.insert_content(table)
        table.DataArray = data
        name = table.Name
        table = LOWriterTextTable(self._doc.tables[name], self._doc)
        return table

    def insert_image(self, path, args={}):
        w = args.get('Width', 1000)
        h = args.get('Height', 1000)
        image = self._doc.create_instance('com.sun.star.text.GraphicObject')
        image.GraphicURL = _P.to_url(path)
        image.AnchorType = AS_CHARACTER
        image.Width = w
        image.Height = h
        self.insert_content(image)
        return self._doc.dp.last


class LOWriterTextRanges(object):

    def __init__(self, obj, doc):
        self._obj = obj
        self._doc = doc
        self._paragraphs = [LOWriterTextRange(p, doc) for p in obj]

    def __len__(self):
        return len(self._paragraphs)

    def __getitem__(self, index):
        return self._paragraphs[index]

    def __iter__(self):
        self._index = 0
        return self

    def __next__(self):
        try:
            obj = self._paragraphs[self._index]
        except IndexError:
            raise StopIteration

        self._index += 1
        return obj

    @property
    def obj(self):
        return self._obj


class LOWriterTextTable(object):

    def __init__(self, obj, doc):
        self._obj = obj
        self._doc = doc

    @property
    def obj(self):
        return self._obj

    @property
    def name(self):
        return self._obj.Name

    @property
    def data(self):
        return self.obj.DataArray
    @data.setter
    def data(self, values):
        self.obj.DataArray = values

    @property
    def style(self):
        return self.obj.TableTemplateName
    @style.setter
    def style(self, value):
        self.obj.autoFormat(value)


class LOWriterTextTables(object):

    def __init__(self, doc):
        self._doc = doc
        self._obj = doc.obj.TextTables

    def __getitem__(self, key):
        return LOWriterTextTable(self._obj[key], self._doc)

    def __len__(self):
        return self._obj.Count

    def insert(self, data, text_range=None):
        if text_range is None:
            text_range = self._doc.selection
        text_range.insert_table(data)
        return


class LOWriter(LODocument):

    def __init__(self, obj):
        super().__init__(obj)
        self._type = WRITER

    @property
    def text(self):
        return self.paragraphs

    @property
    def paragraphs(self):
        return LOWriterTextRanges(self.obj.Text, self)

    @property
    def tables(self):
        return LOWriterTextTables(self)

    @property
    def selection(self):
        sel = self.obj.CurrentSelection
        if sel.ImplementationName == OBJ_TEXTS:
            if len(sel) == 1:
                sel = LOWriterTextRanges(sel, self)[0]
            else:
                sel = LOWriterTextRanges(sel, self)
            return sel

        if sel.ImplementationName == OBJ_SHAPES:
            if len(sel) == 1:
                sel = sel[0]
                sel = LODrawPage(sel.Parent)[sel.Name]
            return sel

        if sel.ImplementationName == OBJ_GRAPHIC:
            sel = self.dp[sel.Name]
        else:
            debug(sel.ImplementationName)

        return sel

    @property
    def dp(self):
        return self.draw_page
    @property
    def draw_page(self):
        return LODrawPage(self.obj.DrawPage)

    @property
    def view_cursor(self):
        return self._cc.ViewCursor

    @property
    def cursor(self):
        return self.obj.Text.createTextCursor()

    @property
    def page_styles(self):
        ps = self.obj.StyleFamilies['PageStyles']
        return LOWriterPageStyles(ps)

    @property
    def styles(self):
        return LOWriterStylesFamilies(self.obj.StyleFamilies)

    @property
    def search_descriptor(self):
        return self.obj.createSearchDescriptor()

    @property
    def replace_descriptor(self):
        return self.obj.createReplaceDescriptor()

    def goto_start(self):
        self.view_cursor.gotoStart(False)
        return self.selection

    def goto_end(self):
        self.view_cursor.gotoEnd(False)
        return self.selection

    def search(self, options, find_all=True):
        descriptor = self.search_descriptor
        descriptor.setSearchString(options.get('Search', ''))
        descriptor.SearchCaseSensitive = options.get('CaseSensitive', False)
        descriptor.SearchWords = options.get('Words', False)
        if 'Attributes' in options:
            attr = dict_to_property(options['Attributes'])
            descriptor.setSearchAttributes(attr)
        if hasattr(descriptor, 'SearchRegularExpression'):
            descriptor.SearchRegularExpression = options.get('RegularExpression', False)
        if hasattr(descriptor, 'SearchType') and 'Type' in options:
            descriptor.SearchType = options['Type']

        result = False
        if find_all:
            found = self.obj.findAll(descriptor)
            if len(found):
                result = [LOWriterTextRange(f, self) for f in found]
        else:
            found = self.obj.findFirst(descriptor)
            if found:
                result = LOWriterTextRange(found, self)

        return result

    def replace(self, options):
        descriptor = self.replace_descriptor
        descriptor.setSearchString(options['Search'])
        descriptor.setReplaceString(options['Replace'])
        descriptor.SearchCaseSensitive = options.get('CaseSensitive', False)
        descriptor.SearchWords = options.get('Words', False)
        if 'Attributes' in options:
            attr = dict_to_property(options['Attributes'])
            descriptor.setSearchAttributes(attr)
        if hasattr(descriptor, 'SearchRegularExpression'):
            descriptor.SearchRegularExpression = options.get('RegularExpression', False)
        if hasattr(descriptor, 'SearchType') and 'Type' in options:
            descriptor.SearchType = options['Type']
        found = self.obj.replaceAll(descriptor)
        return found

    def select(self, text):
        if hasattr(text, 'obj'):
            text = text.obj
        self._cc.select(text)
        return


class LOShape(LOBaseObject):
    IMAGE = 'com.sun.star.drawing.GraphicObjectShape'

    def __init__(self, obj, index):
        self._index = index
        super().__init__(obj)

    @property
    def type(self):
        t = self.shape_type[21:]
        if self.is_image:
            t = 'image'
        return t

    @property
    def shape_type(self):
        return self.obj.ShapeType

    @property
    def properties(self):
        return {}
    @properties.setter
    def properties(self, values):
        _set_properties(self.obj, values)

    @property
    def is_image(self):
        return self.shape_type == self.IMAGE

    @property
    def name(self):
        return self.obj.Name or f'{self.type}{self.index}'
    @name.setter
    def name(self, value):
        self.obj.Name = value

    @property
    def index(self):
        return self._index

    @property
    def size(self):
        s = self.obj.Size
        a = dict(Width=s.Width, Height=s.Height)
        return a

    @property
    def string(self):
        return self.obj.String
    @string.setter
    def string(self, value):
        self.obj.String = value

    @property
    def description(self):
        return self.obj.Description
    @description.setter
    def description(self, value):
        self.obj.Description = value

    @property
    def cell(self):
        return self.anchor

    @property
    def anchor(self):
        obj = self.obj.Anchor
        if obj.ImplementationName == OBJ_CELL:
            obj = LOCalcRange(obj)
        elif obj.ImplementationName == OBJ_TEXT:
            obj = LOWriterTextRange(obj, LODocs().active)
        else:
            debug('Anchor', obj.ImplementationName)
        return obj
    @anchor.setter
    def anchor(self, value):
        if hasattr(value, 'obj'):
            value = value.obj
        self.obj.Anchor = value

    @property
    def visible(self):
        return self.obj.Visible
    @visible.setter
    def visible(self, value):
        self.obj.Visible = value

    @property
    def path(self):
        return self.url
    @property
    def url(self):
        url = ''
        if self.is_image:
            url = _P.to_system(self.obj.GraphicURL.OriginURL)
        return url

    @property
    def mimetype(self):
        mt = ''
        if self.is_image:
            mt = self.obj.GraphicURL.MimeType
        return mt

    @property
    def linked(self):
        l = False
        if self.is_image:
            l = self.obj.GraphicURL.Linked
        return l

    def delete(self):
        self.remove()
        return
    def remove(self):
        self.obj.Parent.remove(self.obj)
        return

    def save(self, path: str, mimetype=DEFAULT_MIME_TYPE):
        if _P.is_dir(path):
            name = self.name
            ext = mimetype.lower()
        else:
            p = _P(path)
            path = p.path
            name = p.name
            ext = p.ext.lower()

        path = _P.join(path, f'{name}.{ext}')
        args = dict(
            URL = _P.to_url(path),
            MimeType = MIME_TYPE[ext],
        )
        if not _export_image(self.obj, args):
            path = ''
        return path

    # ~ def save2(self, path: str):
        # ~ size = len(self.obj.Bitmap.DIB)
        # ~ data = self.obj.GraphicStream.readBytes((), size)
        # ~ data = data[-1].value
        # ~ path = _P.join(path, f'{self.name}.png')
        # ~ _P.save_bin(path, b'')
        # ~ return


class LODrawPage(LOBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    def __getitem__(self, index):
        if isinstance(index, int):
            shape = LOShape(self.obj[index], index)
        else:
            for i, o in enumerate(self.obj):
                shape = self.obj[i]
                name = shape.Name or f'shape{i}'
                if name == index:
                    shape = LOShape(shape, i)
                    break
        return shape

    def __iter__(self):
        self._index = 0
        return self

    def __next__(self):
        if self._index == self.count:
            raise StopIteration
        shape = self[self._index]
        self._index += 1
        return shape


    @property
    def name(self):
        return self.obj.Name

    @property
    def doc(self):
        return self.obj.Forms.Parent

    @property
    def width(self):
        return self.obj.Width

    @property
    def height(self):
        return self.obj.Height

    @property
    def count(self):
        return self.obj.Count

    @property
    def last(self):
        return self[self.count - 1]

    def create_instance(self, name):
        return self.doc.createInstance(name)

    def add(self, type_shape, options={}):
        args = options.copy()
        """Insert a shape in page, type shapes:
            Line
            Rectangle
            Ellipse
            Text
            Connector
        """
        index = self.count
        w = args.pop('Width', 3000)
        h = args.pop('Height', 3000)
        x = args.pop('X', 1000)
        y = args.pop('Y', 1000)
        name = args.pop('Name', f'{type_shape.lower()}{index}')

        service = f'com.sun.star.drawing.{type_shape}Shape'
        shape = self.create_instance(service)
        shape.Size = Size(w, h)
        shape.Position = Point(x, y)
        shape.Name = name
        self.obj.add(shape)

        if args:
            _set_properties(shape, args)

        return LOShape(self.obj[index], index)

    def remove(self, shape):
        if hasattr(shape, 'obj'):
            shape = shape.obj
        return self.obj.remove(shape)

    def remove_all(self):
        while self.count:
            self.obj.remove(self.obj[0])
        return

    def insert_image(self, path, args={}):
        index = self.count
        w = args.get('Width', 3000)
        h = args.get('Height', 3000)
        x = args.get('X', 1000)
        y = args.get('Y', 1000)
        name = args.get('Name', f'image{index}')

        image = self.create_instance('com.sun.star.drawing.GraphicObjectShape')
        image.GraphicURL = _P.to_url(path)
        image.Size = Size(w, h)
        image.Position = Point(x, y)
        image.Name = name
        self.obj.add(image)
        return LOShape(self.obj[index], index)


class LODrawImpress(LODocument):

    def __init__(self, obj):
        super().__init__(obj)

    def __getitem__(self, index):
        if isinstance(index, int):
            page = self.obj.DrawPages[index]
        else:
            page = self.obj.DrawPages.getByName(index)
        return LODrawPage(page)

    @property
    def selection(self):
        sel = self.obj.CurrentSelection[0]
        # ~ return _get_class_uno(sel)
        return sel

    @property
    def current_page(self):
        return LODrawPage(self._cc.getCurrentPage())

    def paste(self):
        call_dispatch(self.frame, '.uno:Paste')
        return self.current_page[-1]

    def add(self, type_shape, args={}):
        return self.current_page.add(type_shape, args)

    def insert_image(self, path, args={}):
        self.current_page.insert_image(path, args)
        return

    # ~ def export(self, path, mimetype='png'):
        # ~ args = dict(
            # ~ URL = _P.to_url(path),
            # ~ MimeType = MIME_TYPE[mimetype],
        # ~ )
        # ~ result = _export_image(self.obj, args)
        # ~ return result


class LODraw(LODrawImpress):

    def __init__(self, obj):
        super().__init__(obj)
        self._type = DRAW


class LOImpress(LODrawImpress):

    def __init__(self, obj):
        super().__init__(obj)
        self._type = IMPRESS


class BaseDateField(DateField):

    def db_value(self, value):
        return _date_to_struct(value)

    def python_value(self, value):
        return _struct_to_date(value)


class BaseTimeField(TimeField):

    def db_value(self, value):
        return _date_to_struct(value)

    def python_value(self, value):
        return _struct_to_date(value)


class BaseDateTimeField(DateTimeField):

    def db_value(self, value):
        return _date_to_struct(value)

    def python_value(self, value):
        return _struct_to_date(value)


class FirebirdDatabase(Database):
    field_types = {'BOOL': 'BOOLEAN', 'DATETIME': 'TIMESTAMP'}

    def __init__(self, database, **kwargs):
        super().__init__(database, **kwargs)
        self._db = database

    def _connect(self):
        return self._db

    def create_tables(self, models, **options):
        options['safe'] = False
        tables = self._db.tables
        models = [m for m in models if not m.__name__.lower() in tables]
        super().create_tables(models, **options)

    def execute_sql(self, sql, params=None, commit=True):
        with __exception_wrapper__:
            cursor = self._db.execute(sql, params)
        return cursor

    def last_insert_id(self, cursor, query_type=None):
        # ~ debug('LAST_ID', cursor)
        return 0

    def rows_affected(self, cursor):
        return self._db.rows_affected

    @property
    def path(self):
        return self._db.path


class BaseRow:
    pass


class BaseQuery(object):
    PY_TYPES = {
        'VARCHAR': 'getString',
        'INTEGER': 'getLong',
        'DATE': 'getDate',
        # ~ 'SQL_LONG': 'getLong',
        # ~ 'SQL_VARYING': 'getString',
        # ~ 'SQL_FLOAT': 'getFloat',
        # ~ 'SQL_BOOLEAN': 'getBoolean',
        # ~ 'SQL_TYPE_DATE': 'getDate',
        # ~ 'SQL_TYPE_TIME': 'getTime',
        # ~ 'SQL_TIMESTAMP': 'getTimestamp',
    }
    # ~ TYPES_DATE = ('SQL_TYPE_DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP')
    TYPES_DATE = ('DATE', 'SQL_TYPE_TIME', 'SQL_TIMESTAMP')

    def __init__(self, query):
        self._query = query
        self._meta = query.MetaData
        self._cols = self._meta.ColumnCount
        self._names = query.Columns.ElementNames
        self._data = self._get_data()

    def __getitem__(self, index):
        return self._data[index]

    def __iter__(self):
        self._index = 0
        return self

    def __next__(self):
        try:
            row = self._data[self._index]
        except IndexError:
            raise StopIteration
        self._index += 1
        return row

    def _to_python(self, index):
        type_field = self._meta.getColumnTypeName(index)
        # ~ print('TF', type_field)
        value = getattr(self._query, self.PY_TYPES[type_field])(index)
        if type_field in self.TYPES_DATE:
            value = _struct_to_date(value)
        return value

    def _get_row(self):
        row = BaseRow()
        for i in range(1, self._cols + 1):
            column_name = self._meta.getColumnName(i)
            value = self._to_python(i)
            setattr(row, column_name, value)
        return row

    def _get_data(self):
        data = []
        while self._query.next():
            row = self._get_row()
            data.append(row)
        return data

    @property
    def tuples(self):
        data = [tuple(r.__dict__.values()) for r in self._data]
        return tuple(data)

    @property
    def dicts(self):
        data = [r.__dict__ for r in self._data]
        return tuple(data)


class LOBase(object):
    DB_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, obj, args={}):
        self._obj = obj
        self._type = BASE
        self._dbc = create_instance('com.sun.star.sdb.DatabaseContext')
        self._rows_affected = 0
        path = args.get('path', '')
        self._path = _P(path)
        self._name = self._path.name
        if _P.exists(path):
            if not self.is_registered:
                self.register()
            db = self._dbc.getByName(self.name)
        else:
            db = self._dbc.createInstance()
            db.URL = 'sdbc:embedded:firebird'
            db.DatabaseDocument.storeAsURL(self._path.url, ())
            self.register()
        self._obj = db
        self._con = db.getConnection('', '')

    def __contains__(self, item):
        return item in self.tables

    @property
    def obj(self):
        return self._obj

    @property
    def name(self):
        return self._name

    @property
    def path(self):
        return str(self._path)

    @property
    def is_registered(self):
        return self._dbc.hasRegisteredDatabase(self.name)

    @property
    def tables(self):
        tables = [t.Name.lower() for t in self._con.getTables()]
        return tables

    @property
    def rows_affected(self):
        return self._rows_affected

    def register(self):
        if not self.is_registered:
            self._dbc.registerDatabaseLocation(self.name, self._path.url)
        return

    def revoke(self, name):
        self._dbc.revokeDatabaseLocation(name)
        return True

    def save(self):
        self.obj.DatabaseDocument.store()
        self.refresh()
        return

    def close(self):
        self._con.close()
        return

    def refresh(self):
        self._con.getTables().refresh()
        return

    def initialize(self, database_proxy, tables=[]):
        db = FirebirdDatabase(self)
        database_proxy.initialize(db)
        if tables:
            db.create_tables(tables)
        return

    def _validate_sql(self, sql, params):
        limit = ' LIMIT '
        for p in params:
            sql = sql.replace('?', f"'{p}'", 1)
        if limit in sql:
            sql = sql.split(limit)[0]
            sql = sql.replace('SELECT', f'SELECT FIRST {params[-1]}')
        return sql

    def cursor(self, sql, params):
        if sql.startswith('SELECT'):
            sql = self._validate_sql(sql, params)
            cursor = self._con.prepareStatement(sql)
            return cursor

        if not params:
            cursor = self._con.createStatement()
            return cursor

        cursor = self._con.prepareStatement(sql)
        for i, v in enumerate(params, 1):
            t = type(v)
            if not t in self.DB_TYPES:
                error('Type not support')
                debug((i, t, v, self.DB_TYPES[t]))
            getattr(cursor, self.DB_TYPES[t])(i, v)
        return cursor

    def execute(self, sql, params):
        debug(sql, params)
        cursor = self.cursor(sql, params)

        if sql.startswith('SELECT'):
            result = cursor.executeQuery()
        elif params:
            result = cursor.executeUpdate()
            self._rows_affected = result
            self.save()
        else:
            result = cursor.execute(sql)
            self.save()

        return result

    def select(self, sql):
        debug('SELECT', sql)
        if not sql.startswith('SELECT'):
            return ()

        cursor = self._con.prepareStatement(sql)
        query = cursor.executeQuery()
        return BaseQuery(query)

    def get_query(self, query):
        sql, args = query.sql()
        sql = self._validate_sql(sql, args)
        return self.select(sql)


class LOMath(LODocument):

    def __init__(self, obj):
        super().__init__(obj)
        self._type = MATH


class LOBasic(LODocument):

    def __init__(self, obj):
        super().__init__(obj)
        self._type = BASIC


class LODocs(object):
    _desktop = None

    def __init__(self):
        self._desktop = get_desktop()
        LODocs._desktop = self._desktop

    def __getitem__(self, index):
        document = None
        for i, doc in enumerate(self._desktop.Components):
            if isinstance(index, int) and i == index:
                document = _get_class_doc(doc)
                break
            elif isinstance(index, str) and doc.Title == index:
                document = _get_class_doc(doc)
                break
        return document

    def __contains__(self, item):
        doc = self[item]
        return not doc is None

    def __iter__(self):
        self._i = -1
        return self

    def __next__(self):
        self._i += 1
        doc = self[self._i]
        if doc is None:
            raise StopIteration
        else:
            return doc

    def __len__(self):
        # ~ len(self._desktop.Components)
        for i, _ in enumerate(self._desktop.Components):
            pass
        return i + 1

    @property
    def active(self):
        return _get_class_doc(self._desktop.getCurrentComponent())

    @classmethod
    def new(cls, type_doc=CALC, args={}):
        if type_doc == BASE:
            return LOBase(None, args)

        path = f'private:factory/s{type_doc}'
        opt = dict_to_property(args)
        doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt)
        return _get_class_doc(doc)

    @classmethod
    def open(cls, path, args={}):
        """ Open document in path
            Usually options:
                Hidden: True or False
                AsTemplate: True or False
                ReadOnly: True or False
                Password: super_secret
                MacroExecutionMode: 4 = Activate macros
                Preview: True or False

            http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1frame_1_1XComponentLoader.html
            http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html
        """
        path = _P.to_url(path)
        opt = dict_to_property(args)
        doc = cls._desktop.loadComponentFromURL(path, '_default', 0, opt)
        if doc is None:
            return

        return _get_class_doc(doc)

    def connect(self, path):
        db = LOBase(None, {'path': path})
        return  db


def _add_listeners(events, control, name=''):
    listeners = {
        'addActionListener': EventsButton,
        'addMouseListener': EventsMouse,
        'addFocusListener': EventsFocus,
        'addItemListener': EventsItem,
        'addKeyListener': EventsKey,
        'addTabListener': EventsTab,
    }
    if hasattr(control, 'obj'):
        control = control.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'
    is_pages = control.ImplementationName == 'stardiv.Toolkit.UnoMultiPageControl'

    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))

    if is_grid:
        controllers = EventsGrid(events, name)
        control.addSelectionListener(controllers)
        control.Model.GridDataModel.addGridDataListener(controllers)
    return


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


class EventsListenerBase(unohelper.Base, XEventListener):

    def __init__(self, controller, name, window=None):
        self._controller = controller
        self._name = name
        self._window = window

    @property
    def name(self):
        return self._name

    def disposing(self, event):
        self._controller = None
        if not self._window is None:
            self._window.setMenuBar(None)


class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener):

    def __init__(self, controller, name):
        super().__init__(controller, name)

    def mousePressed(self, event):
        event_name = '{}_click'.format(self._name)
        if event.ClickCount == 2:
            event_name = '{}_double_click'.format(self._name)
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(event)
        return

    def mouseReleased(self, event):
        pass

    def mouseEntered(self, event):
        pass

    def mouseExited(self, event):
        pass

    # ~ XMouseMotionListener
    def mouseMoved(self, event):
        pass

    def mouseDragged(self, event):
        pass


class EventsMouseLink(EventsMouse):

    def __init__(self, controller, name):
        super().__init__(controller, name)
        self._text_color = 0

    def mouseEntered(self, event):
        model = event.Source.Model
        self._text_color = model.TextColor or 0
        model.TextColor = get_color('blue')
        return

    def mouseExited(self, event):
        model = event.Source.Model
        model.TextColor = self._text_color
        return


class EventsButton(EventsListenerBase, XActionListener):

    def __init__(self, controller, name):
        super().__init__(controller, name)

    def actionPerformed(self, event):
        event_name = f'{self.name}_action'
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(event)
        return


class EventsFocus(EventsListenerBase, XFocusListener):
    CONTROLS = (
        'stardiv.Toolkit.UnoControlEditModel',
    )

    def __init__(self, controller, name):
        super().__init__(controller, name)

    def focusGained(self, event):
        service = event.Source.Model.ImplementationName
        # ~ print('Focus enter', service)
        if service in self.CONTROLS:
            obj = event.Source.Model
            obj.BackgroundColor = COLOR_ON_FOCUS
        return

    def focusLost(self, event):
        service = event.Source.Model.ImplementationName
        if service in self.CONTROLS:
            obj = event.Source.Model
            obj.BackgroundColor = -1
        return


class EventsKey(EventsListenerBase, XKeyListener):
    """
        event.KeyChar
        event.KeyCode
        event.KeyFunc
        event.Modifiers
    """

    def __init__(self, controller, name):
        super().__init__(controller, name)

    def keyPressed(self, event):
        pass

    def keyReleased(self, event):
        event_name = '{}_key_released'.format(self._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 EventsItem(EventsListenerBase, XItemListener):

    def __init__(self, controller, name):
        super().__init__(controller, name)

    def disposing(self, event):
        pass

    def itemStateChanged(self, event):
        event_name = '{}_item_changed'.format(self.name)
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(event)
        return


class EventsItemRoadmap(EventsItem):

    def itemStateChanged(self, event):
        dialog = event.Source.Context.Model
        dialog.Step = event.ItemId + 1
        return


class EventsGrid(EventsListenerBase, XGridDataListener, XGridSelectionListener):

    def __init__(self, controller, name):
        super().__init__(controller, name)

    def dataChanged(self, event):
        event_name = '{}_data_changed'.format(self.name)
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(event)
        return

    def rowHeadingChanged(self, event):
        pass

    def rowsInserted(self, event):
        pass

    def rowsRemoved(self, evemt):
        pass

    def selectionChanged(self, event):
        event_name = '{}_selection_changed'.format(self.name)
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(event)
        return


class EventsMouseGrid(EventsMouse):
    selected = False

    def mousePressed(self, event):
        super().mousePressed(event)
        # ~ obj = event.Source
        # ~ col = obj.getColumnAtPoint(event.X, event.Y)
        # ~ row = obj.getRowAtPoint(event.X, event.Y)
        # ~ print(col, row)
        # ~ if col == -1 and row == -1:
            # ~ if self.selected:
                # ~ obj.deselectAllRows()
            # ~ else:
                # ~ obj.selectAllRows()
            # ~ self.selected = not self.selected
        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)
        return


class EventsTab(EventsListenerBase, XTabListener):

    def __init__(self, controller, name):
        super().__init__(controller, name)

    def activated(self, id):
        event_name = '{}_activated'.format(self.name)
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(id)
        return


class EventsMenu(EventsListenerBase, XMenuListener):

    def __init__(self, controller):
        super().__init__(controller, '')

    def itemHighlighted(self, event):
        pass

    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


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._cls._subcont
        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


# ~ BorderColor = ?
# ~ FontStyleName = ?
# ~ HelpURL = ?
class UnoBaseObject(object):

    def __init__(self, obj, path=''):
        self._obj = obj
        self._model = obj.Model

    def __setattr__(self, name, value):
        exists = hasattr(self, name)
        if not exists and not name in ('_obj', '_model'):
            setattr(self._model, name, value)
        else:
            super().__setattr__(name, value)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    @property
    def obj(self):
        return self._obj

    @property
    def model(self):
        return self._model
    @property
    def m(self):
        return self._model

    @property
    def properties(self):
        return {}
    @properties.setter
    def properties(self, values):
        _set_properties(self.model, values)

    @property
    def name(self):
        return self.model.Name

    @property
    def parent(self):
        return self.obj.Context

    @property
    def tag(self):
        return self.model.Tag
    @tag.setter
    def tag(self, value):
        self.model.Tag = value

    @property
    def visible(self):
        return self.obj.Visible
    @visible.setter
    def visible(self, value):
        self.obj.setVisible(value)

    @property
    def enabled(self):
        return self.model.Enabled
    @enabled.setter
    def enabled(self, value):
        self.model.Enabled = value

    @property
    def step(self):
        return self.model.Step
    @step.setter
    def step(self, value):
        self.model.Step = value

    @property
    def align(self):
        return self.model.Align
    @align.setter
    def align(self, value):
        self.model.Align = value

    @property
    def valign(self):
        return self.model.VerticalAlign
    @valign.setter
    def valign(self, value):
        self.model.VerticalAlign = value

    @property
    def font_weight(self):
        return self.model.FontWeight
    @font_weight.setter
    def font_weight(self, value):
        self.model.FontWeight = value

    @property
    def font_height(self):
        return self.model.FontHeight
    @font_height.setter
    def font_height(self, value):
        self.model.FontHeight = value

    @property
    def font_name(self):
        return self.model.FontName
    @font_name.setter
    def font_name(self, value):
        self.model.FontName = value

    @property
    def font_underline(self):
        return self.model.FontUnderline
    @font_underline.setter
    def font_underline(self, value):
        self.model.FontUnderline = value

    @property
    def text_color(self):
        return self.model.TextColor
    @text_color.setter
    def text_color(self, value):
        self.model.TextColor = value

    @property
    def back_color(self):
        return self.model.BackgroundColor
    @back_color.setter
    def back_color(self, value):
        self.model.BackgroundColor = value

    @property
    def multi_line(self):
        return self.model.MultiLine
    @multi_line.setter
    def multi_line(self, value):
        self.model.MultiLine = value

    @property
    def help_text(self):
        return self.model.HelpText
    @help_text.setter
    def help_text(self, value):
        self.model.HelpText = value

    @property
    def border(self):
        return self.model.Border
    @border.setter
    def border(self, value):
        # ~ Bug for report
        self.model.Border = value

    @property
    def width(self):
        return self._model.Width
    @width.setter
    def width(self, value):
        self.model.Width = value

    @property
    def height(self):
        return self.model.Height
    @height.setter
    def height(self, value):
        self.model.Height = value

    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):
        if hasattr(self.model, 'PositionX'):
            return self.model.PositionX
        return self._get_possize('X')
    @x.setter
    def x(self, value):
        if hasattr(self.model, 'PositionX'):
            self.model.PositionX = value
        else:
            self._set_possize('X', value)

    @property
    def y(self):
        if hasattr(self.model, 'PositionY'):
            return self.model.PositionY
        return self._get_possize('Y')
    @y.setter
    def y(self, value):
        if hasattr(self.model, 'PositionY'):
            self.model.PositionY = value
        else:
            self._set_possize('Y', value)

    @property
    def tab_index(self):
        return self._model.TabIndex
    @tab_index.setter
    def tab_index(self, value):
        self.model.TabIndex = value

    @property
    def tab_stop(self):
        return self._model.Tabstop
    @tab_stop.setter
    def tab_stop(self, value):
        self.model.Tabstop = value

    @property
    def ps(self):
        ps = self.obj.getPosSize()
        return ps
    @ps.setter
    def ps(self, ps):
        self.obj.setPosSize(ps.X, ps.Y, ps.Width, ps.Height, POSSIZE)

    def set_focus(self):
        self.obj.setFocus()
        return

    def ps_from(self, source):
        self.ps = source.ps
        return

    def center(self, horizontal=True, vertical=False):
        p = self.parent.Model
        w = p.Width
        h = p.Height
        if horizontal:
            x = w / 2 - self.width / 2
            self.x = x
        if vertical:
            y = h / 2 - self.height / 2
            self.y = y
        return

    def move(self, origin, x=0, y=5, center=False):
        if x:
            self.x = origin.x + origin.width + x
        else:
            self.x = origin.x
        if y:
            self.y = origin.y + origin.height + y
        else:
            self.y = origin.y

        if center:
            self.center()
        return


class UnoLabel(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def type(self):
        return 'label'

    @property
    def value(self):
        return self.model.Label
    @value.setter
    def value(self, value):
        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):
        super().__init__(obj)

    @property
    def type(self):
        return 'button'

    @property
    def value(self):
        return self.model.Label
    @value.setter
    def value(self, value):
        self.model.Label = value


class UnoRadio(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def type(self):
        return 'radio'

    @property
    def value(self):
        return self.model.Label
    @value.setter
    def value(self, value):
        self.model.Label = value


class UnoCheckBox(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def type(self):
        return 'checkbox'

    @property
    def value(self):
        return self.model.State
    @value.setter
    def value(self, value):
        self.model.State = value

    @property
    def label(self):
        return self.model.Label
    @label.setter
    def label(self, value):
        self.model.Label = value

    @property
    def tri_state(self):
        return self.model.TriState
    @tri_state.setter
    def tri_state(self, value):
        self.model.TriState = value


# ~ https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1awt_1_1UnoControlEditModel.html
class UnoText(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def type(self):
        return 'text'

    @property
    def value(self):
        return self.model.Text
    @value.setter
    def value(self, value):
        self.model.Text = value

    @property
    def echochar(self):
        return chr(self.model.EchoChar)
    @echochar.setter
    def echochar(self, value):
        self.model.EchoChar = ord(value[0])

    def validate(self):
        return


class UnoImage(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def type(self):
        return 'image'

    @property
    def value(self):
        return self.url
    @value.setter
    def value(self, value):
        self.url = value

    @property
    def url(self):
        return self.m.ImageURL
    @url.setter
    def url(self, value):
        self.m.ImageURL = None
        self.m.ImageURL = _P.to_url(value)


class UnoListBox(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)
        self._path = ''

    def __setattr__(self, name, value):
        if name in ('_path',):
            self.__dict__[name] = value
        else:
            super().__setattr__(name, value)

    @property
    def type(self):
        return 'listbox'

    @property
    def value(self):
        return self.obj.getSelectedItem()

    @property
    def count(self):
        return len(self.data)

    @property
    def data(self):
        return self.model.StringItemList
    @data.setter
    def data(self, values):
        self.model.StringItemList = list(sorted(values))

    @property
    def path(self):
        return self._path
    @path.setter
    def path(self, value):
        self._path = value

    def unselect(self):
        self.obj.selectItem(self.value, False)
        return

    def select(self, pos=0):
        if isinstance(pos, str):
            self.obj.selectItem(pos, True)
        else:
            self.obj.selectItemPos(pos, True)
        return

    def clear(self):
        self.model.removeAllItems()
        return

    def _set_image_url(self, image):
        if _P.exists(image):
            return _P.to_url(image)

        path = _P.join(self._path, DIR['images'], image)
        return _P.to_url(path)

    def insert(self, value, path='', pos=-1, show=True):
        if pos < 0:
            pos = self.count
        if path:
            self.model.insertItem(pos, value, self._set_image_url(path))
        else:
            self.model.insertItemText(pos, value)
        if show:
            self.select(pos)
        return


class UnoRoadmap(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)
        self._options = ()

    def __setattr__(self, name, value):
        if name in ('_options',):
            self.__dict__[name] = value
        else:
            super().__setattr__(name, value)

    @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

    @property
    def enabled(self):
        return True
    @enabled.setter
    def enabled(self, value):
        for m in self.model:
            m.Enabled = value
        return

    def set_enabled(self, index, value):
        self.model.getByIndex(index).Enabled = value
        return


class UnoTree(UnoBaseObject):

    def __init__(self, obj, ):
        super().__init__(obj)
        self._tdm = None
        self._data = []

    def __setattr__(self, name, value):
        if name in ('_tdm', '_data'):
            self.__dict__[name] = value
        else:
            super().__setattr__(name, value)

    @property
    def selection(self):
        sel = self.obj.Selection
        return sel.DataValue, sel.DisplayValue

    @property
    def parent(self):
        parent = self.obj.Selection.Parent
        if parent is None:
            return ()
        return parent.DataValue, parent.DisplayValue

    def _get_parents(self, node):
        value = (node.DisplayValue,)
        parent = node.Parent
        if parent is None:
            return value
        return self._get_parents(parent) + value

    @property
    def parents(self):
        values = self._get_parents(self.obj.Selection)
        return values

    @property
    def root(self):
        if self._tdm is None:
            return ''
        return self._tdm.Root.DisplayValue
    @root.setter
    def root(self, value):
        self._add_data_model(value)

    def _add_data_model(self, name):
        tdm = create_instance('com.sun.star.awt.tree.MutableTreeDataModel')
        root = tdm.createNode(name, True)
        root.DataValue = 0
        tdm.setRoot(root)
        self.model.DataModel = tdm
        self._tdm = self.model.DataModel
        return

    @property
    def path(self):
        return self.root
    @path.setter
    def path(self, value):
        self.data = _P.walk_dir(value, True)

    @property
    def data(self):
        return self._data
    @data.setter
    def data(self, values):
        self._data = list(values)
        self._add_data()

    def _add_data(self):
        if not self.data:
            return

        parents = {}
        for node in self.data:
            parent = parents.get(node[1], self._tdm.Root)
            child = self._tdm.createNode(node[2], False)
            child.DataValue = node[0]
            parent.appendChild(child)
            parents[node[0]] = child
        self.obj.expandNode(self._tdm.Root)
        return


# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1grid.html
class UnoGrid(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)
        self._gdm = self.model.GridDataModel
        self._data = []
        self._formats = ()

    def __setattr__(self, name, value):
        if name in ('_gdm', '_data', '_formats'):
            self.__dict__[name] = value
        else:
            super().__setattr__(name, value)

    def __getitem__(self, key):
        value = self._gdm.getCellData(key[0], key[1])
        return value

    def __setitem__(self, key, value):
        self._gdm.updateCellData(key[0], key[1], value)
        return

    @property
    def type(self):
        return 'grid'

    @property
    def columns(self):
        return {}
    @columns.setter
    def columns(self, values):
        # ~ self._columns = values
        #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html
        model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True)
        for properties in values:
            column = create_instance('com.sun.star.awt.grid.GridColumn', True)
            for k, v in properties.items():
                setattr(column, k, v)
            model.addColumn(column)
        self.model.ColumnModel = model
        return

    @property
    def data(self):
        return self._data
    @data.setter
    def data(self, values):
        self._data = values
        self.clear()
        headings = tuple(range(1, len(values) + 1))
        self._gdm.addRows(headings, values)
        # ~ rows = range(grid_dm.RowCount)
        # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows]
        # ~ grid.Model.RowBackgroundColors = tuple(colors)
        return

    @property
    def value(self):
        if self.column == -1 or self.row == -1:
            return ''
        return self[self.column, self.row]
    @value.setter
    def value(self, value):
        if self.column > -1 and self.row > -1:
            self[self.column, self.row] = value

    @property
    def row(self):
        return self.obj.CurrentRow

    @property
    def row_count(self):
        return self._gdm.RowCount

    @property
    def column(self):
        return self.obj.CurrentColumn

    @property
    def column(self):
        return self.obj.CurrentColumn

    @property
    def is_valid(self):
        return not (self.row == -1 or self.column == -1)

    @property
    def formats(self):
        return self._formats
    @formats.setter
    def formats(self, values):
        self._formats = values

    def clear(self):
        self._gdm.removeAllRows()
        return

    def _format_columns(self, data):
        row = data
        if self.formats:
            for i, f in enumerate(formats):
                if f:
                    row[i] = f.format(data[i])
        return row

    def add_row(self, data):
        self._data.append(data)
        row = self._format_columns(data)
        self._gdm.addRow(self.row_count + 1, row)
        return

    def set_cell_tooltip(self, col, row, value):
        self._gdm.updateCellToolTip(col, row, value)
        return

    def get_cell_tooltip(self, col, row):
        value = self._gdm.getCellToolTip(col, row)
        return value

    def sort(self, column, asc=True):
        self._gdm.sortByColumn(column, asc)
        self.update_row_heading()
        return

    def update_row_heading(self):
        for i in range(self.row_count):
            self._gdm.updateRowHeading(i, i + 1)
        return

    def remove_row(self, row):
        self._gdm.removeRow(row)
        del self._data[row]
        self.update_row_heading()
        return


class UnoPage(object):

    def __init__(self, obj):
        self._obj = obj
        self._events = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    @property
    def obj(self):
        return self._obj

    @property
    def model(self):
        return self._obj.Model

    # ~ @property
    # ~ def id(self):
        # ~ return self.m.TabPageID

    @property
    def parent(self):
        return self.obj.Context

    def _set_image_url(self, image):
        if _P.exists(image):
            return _P.to_url(image)

        path = _P.join(self._path, DIR['images'], image)
        return _P.to_url(path)

    def _special_properties(self, tipo, args):
        if tipo == 'link' and not 'Label' in args:
            args['Label'] = args['URL']
            return args

        if tipo == 'button':
            if 'ImageURL' in args:
                args['ImageURL'] = self._set_image_url(args['ImageURL'])
            args['FocusOnClick'] = args.get('FocusOnClick', False)
            return args

        if tipo == 'roadmap':
            args['Height'] = args.get('Height', self.height)
            if 'Title' in args:
                args['Text'] = args.pop('Title')
            return args

        if tipo == 'tree':
            args['SelectionType'] = args.get('SelectionType', SINGLE)
            return args

        if tipo == 'grid':
            args['ShowRowHeader'] = args.get('ShowRowHeader', True)
            return args

        if tipo == 'pages':
            args['Width'] = args.get('Width', self.width)
            args['Height'] = args.get('Height', self.height)

        return args

    def add_control(self, args):
        tipo = args.pop('Type').lower()
        root = args.pop('Root', '')
        sheets = args.pop('Sheets', ())
        columns = args.pop('Columns', ())

        args = self._special_properties(tipo, args)
        model = self.model.createInstance(UNO_MODELS[tipo])
        _set_properties(model, args)
        name = args['Name']
        self.model.insertByName(name, model)
        control = self.obj.getControl(name)
        _add_listeners(self._events, control, name)
        control = UNO_CLASSES[tipo](control)

        if tipo in ('listbox',):
            control.path = self.path

        if tipo == 'tree' and root:
            control.root = root
        elif tipo == 'grid' and columns:
            control.columns = columns
        elif tipo == 'pages' and sheets:
            control.sheets = sheets
            control.events = self.events

        setattr(self, name, control)
        return control


class UnoPages(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)
        self._sheets = []
        self._events = None

    def __setattr__(self, name, value):
        if name in ('_sheets', '_events'):
            self.__dict__[name] = value
        else:
            super().__setattr__(name, value)

    def __getitem__(self, index):
        name = index
        if isinstance(index, int):
            name = f'sheet{index}'
        sheet = self.obj.getControl(name)
        page = UnoPage(sheet)
        page._events = self._events
        return page

    @property
    def type(self):
        return 'pages'

    @property
    def current(self):
        return self.obj.ActiveTabID
    @property
    def active(self):
        return self.current

    @property
    def sheets(self):
        return self._sheets
    @sheets.setter
    def sheets(self, values):
        self._sheets = values
        for i, title in enumerate(values):
            sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel')
            sheet.Title = title
            self.m.insertByName(f'sheet{i + 1}', sheet)
        return

    @property
    def events(self):
        return self._events
    @events.setter
    def events(self, controllers):
        self._events = controllers

    @property
    def visible(self):
        return self.obj.Visible
    @visible.setter
    def visible(self, value):
        self.obj.Visible = value

    def insert(self, title):
        self._sheets.append(title)
        id = len(self._sheets)
        sheet = self.m.createInstance('com.sun.star.awt.UnoPageModel')
        sheet.Title = title
        self.m.insertByName(f'sheet{id}', sheet)
        return self[id]

    def remove(self, id):
        self.obj.removeTab(id)
        return

    def activate(self, id):
        self.obj.activateTab(id)
        return


UNO_CLASSES = {
    'label': UnoLabel,
    'link': UnoLabelLink,
    'button': UnoButton,
    'radio': UnoRadio,
    'checkbox': UnoCheckBox,
    'text': UnoText,
    'image': UnoImage,
    'listbox': UnoListBox,
    'roadmap': UnoRoadmap,
    'tree': UnoTree,
    'grid': UnoGrid,
    'pages': UnoPages,
}

UNO_MODELS = {
    'label': 'com.sun.star.awt.UnoControlFixedTextModel',
    'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel',
    'button': 'com.sun.star.awt.UnoControlButtonModel',
    'radio': 'com.sun.star.awt.UnoControlRadioButtonModel',
    'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel',
    'text': 'com.sun.star.awt.UnoControlEditModel',
    'image': 'com.sun.star.awt.UnoControlImageControlModel',
    'listbox': 'com.sun.star.awt.UnoControlListBoxModel',
    'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel',
    'tree': 'com.sun.star.awt.tree.TreeControlModel',
    'grid': 'com.sun.star.awt.grid.UnoControlGridModel',
    'pages': 'com.sun.star.awt.UnoMultiPageModel',
    'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel',
    'combobox': 'com.sun.star.awt.UnoControlComboBoxModel',
}
# ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel',
# ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel',
# ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel',
# ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel',
# ~ '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',


class LODialog(object):
    SEPARATION = 5
    MODELS = {
        'label': 'com.sun.star.awt.UnoControlFixedTextModel',
        'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel',
        'button': 'com.sun.star.awt.UnoControlButtonModel',
        'radio': 'com.sun.star.awt.UnoControlRadioButtonModel',
        'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel',
        'text': 'com.sun.star.awt.UnoControlEditModel',
        'image': 'com.sun.star.awt.UnoControlImageControlModel',
        'listbox': 'com.sun.star.awt.UnoControlListBoxModel',
        'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel',
        'tree': 'com.sun.star.awt.tree.TreeControlModel',
        'grid': 'com.sun.star.awt.grid.UnoControlGridModel',
        'pages': 'com.sun.star.awt.UnoMultiPageModel',
        'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel',
        'combobox': 'com.sun.star.awt.UnoControlComboBoxModel',
    }

    def __init__(self, args):
        self._obj = self._create(args)
        self._model = self.obj.Model
        self._events = None
        self._modal = True
        self._controls = {}
        self._color_on_focus = COLOR_ON_FOCUS
        self._id = ''
        self._path = ''
        self._init_controls()

    def _create(self, args):
        service = 'com.sun.star.awt.DialogProvider'
        path = args.pop('Path', '')
        if path:
            dp = create_instance(service, True)
            dlg = dp.createDialog(_P.to_url(path))
            return dlg

        if 'Location' in args:
            name = args['Name']
            library = args.get('Library', 'Standard')
            location = args.get('Location', 'application').lower()
            if location == 'user':
                location = 'application'
            url = f'vnd.sun.star.script:{library}.{name}?location={location}'
            if location == 'document':
                dp = create_instance(service, args=docs.active.obj)
            else:
                dp = create_instance(service, True)
                # ~ uid = docs.active.uid
                # ~ url = f'vnd.sun.star.tdoc:/{uid}/Dialogs/{library}/{name}.xml'
            dlg = dp.createDialog(url)
            return dlg

        dlg = create_instance('com.sun.star.awt.UnoControlDialog', True)
        model = create_instance('com.sun.star.awt.UnoControlDialogModel', True)
        toolkit = create_instance('com.sun.star.awt.Toolkit', True)
        _set_properties(model, args)
        dlg.setModel(model)
        dlg.setVisible(False)
        dlg.createPeer(toolkit, None)
        return dlg

    def _get_type_control(self, name):
        name = name.split('.')[2]
        types = {
            'UnoFixedTextControl': 'label',
            'UnoEditControl': 'text',
            'UnoButtonControl': 'button',
        }
        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 = UNO_CLASSES[tipo](control)
            setattr(self, name, control)
        return

    @property
    def obj(self):
        return self._obj

    @property
    def model(self):
        return self._model

    @property
    def controls(self):
        return self._controls

    @property
    def path(self):
        return self._path
    @property
    def id(self):
        return self._id
    @id.setter
    def id(self, value):
        self._id = value
        self._path = _P.from_id(value)

    @property
    def height(self):
        return self.model.Height
    @height.setter
    def height(self, value):
        self.model.Height = value

    @property
    def width(self):
        return self.model.Width
    @width.setter
    def width(self, value):
        self.model.Width = value

    @property
    def visible(self):
        return self.obj.Visible
    @visible.setter
    def visible(self, value):
        self.obj.Visible = value

    @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
    @events.setter
    def events(self, controllers):
        self._events = controllers(self)
        self._connect_listeners()

    @property
    def color_on_focus(self):
        return self._color_on_focus
    @color_on_focus.setter
    def color_on_focus(self, value):
        self._color_on_focus = get_color(value)

    def _connect_listeners(self):
        for control in self.obj.Controls:
            _add_listeners(self.events, control, control.Model.Name)
        return

    def _set_image_url(self, image):
        if _P.exists(image):
            return _P.to_url(image)

        path = _P.join(self._path, DIR['images'], image)
        return _P.to_url(path)

    def _special_properties(self, tipo, args):
        if tipo == 'link' and not 'Label' in args:
            args['Label'] = args['URL']
            return args

        if tipo == 'button':
            if 'ImageURL' in args:
                args['ImageURL'] = self._set_image_url(args['ImageURL'])
            args['FocusOnClick'] = args.get('FocusOnClick', False)
            return args

        if tipo == 'roadmap':
            args['Height'] = args.get('Height', self.height)
            if 'Title' in args:
                args['Text'] = args.pop('Title')
            return args

        if tipo == 'tree':
            args['SelectionType'] = args.get('SelectionType', SINGLE)
            return args

        if tipo == 'grid':
            args['ShowRowHeader'] = args.get('ShowRowHeader', True)
            return args

        if tipo == 'pages':
            args['Width'] = args.get('Width', self.width)
            args['Height'] = args.get('Height', self.height)

        return args

    def add_control(self, args):
        tipo = args.pop('Type').lower()
        root = args.pop('Root', '')
        sheets = args.pop('Sheets', ())
        columns = args.pop('Columns', ())

        args = self._special_properties(tipo, args)
        model = self.model.createInstance(self.MODELS[tipo])
        _set_properties(model, args)
        name = args['Name']
        self.model.insertByName(name, model)
        control = self.obj.getControl(name)
        _add_listeners(self.events, control, name)
        control = UNO_CLASSES[tipo](control)

        if tipo in ('listbox',):
            control.path = self.path

        if tipo == 'tree' and root:
            control.root = root
        elif tipo == 'grid' and columns:
            control.columns = columns
        elif tipo == 'pages' and sheets:
            control.sheets = sheets
            control.events = self.events

        setattr(self, name, control)
        self._controls[name] = control
        return control

    def center(self, control, x=0, y=0):
        w = self.width
        h = self.height

        if isinstance(control, tuple):
            wt = self.SEPARATION * -1
            for c in control:
                wt += c.width + self.SEPARATION
            x = w / 2 - wt / 2
            for c in control:
                c.x = x
                x = c.x + c.width + self.SEPARATION
            return

        if x < 0:
            x = w + x - control.width
        elif x == 0:
            x = w / 2 - control.width / 2
        if y < 0:
            y = h + y - control.height
        elif y == 0:
            y = h / 2 - control.height / 2
        control.x = x
        control.y = y
        return

    def open(self, modal=True):
        self._modal = modal
        if modal:
            return self.obj.execute()
        else:
            self.visible = True
        return

    def close(self, value=0):
        if self._modal:
            value = self.obj.endDialog(value)
        else:
            self.visible = False
            self.obj.dispose()
        return value

    def set_values(self, data):
        for k, v in data.items():
            self._controls[k].value = v
        return


class LOSheets(object):

    def __getitem__(self, index):
        return LODocs().active[index]

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass


class LOCells(object):

    def __getitem__(self, index):
        return LODocs().active.active[index]


class LOShortCut(object):
# ~ getKeyEventsByCommand

    def __init__(self, app):
        self._app = app
        self._scm = None
        self._init_values()

    def _init_values(self):
        name = 'com.sun.star.ui.GlobalAcceleratorConfiguration'
        instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier'
        service = TYPE_DOC[self._app]
        manager = create_instance(instance, True)
        uicm = manager.getUIConfigurationManager(service)
        self._scm = uicm.ShortCutManager
        return

    def __contains__(self, item):
        cmd = self._get_command(item)
        return bool(cmd)

    def _get_key_event(self, command):
        events = self._scm.AllKeyEvents
        for event in events:
            cmd = self._scm.getCommandByKeyEvent(event)
            if cmd == command:
                break
        return event

    def _to_key_event(self, shortcut):
        key_event = KeyEvent()
        keys = shortcut.split('+')
        for v in keys[:-1]:
            key_event.Modifiers += MODIFIERS[v.lower()]
        key_event.KeyCode = getattr(Key, keys[-1].upper())
        return key_event

    def _get_command(self, shortcut):
        command = ''
        key_event = self._to_key_event(shortcut)
        try:
            command = self._scm.getCommandByKeyEvent(key_event)
        except NoSuchElementException:
            debug(f'No exists: {shortcut}')
        return command

    def add(self, shortcut, command):
        if isinstance(command, dict):
            command = _get_url_script(command)
        key_event = self._to_key_event(shortcut)
        self._scm.setKeyEvent(key_event, command)
        self._scm.store()
        return

    def reset(self):
        self._scm.reset()
        self._scm.store()
        return

    def remove(self, shortcut):
        key_event = self._to_key_event(shortcut)
        try:
            self._scm.removeKeyEvent(key_event)
            self._scm.store()
        except NoSuchElementException:
            debug(f'No exists: {shortcut}')
        return

    def remove_by_command(self, command):
        if isinstance(command, dict):
            command = _get_url_script(command)
        try:
            self._scm.removeCommandFromAllKeyEvents(command)
            self._scm.store()
        except NoSuchElementException:
            debug(f'No exists: {command}')
        return


class LOShortCuts(object):

    def __getitem__(self, index):
        return LOShortCut(index)


class LOMenu(object):

    def __init__(self, app):
        self._app = app
        self._ui = None
        self._pymenus = None
        self._menu = None
        self._menus = self._get_menus()

    def __getitem__(self, index):
        if isinstance(index, int):
            self._menu = self._menus[index]
        else:
            for menu in self._menus:
                cmd = menu.get('CommandURL', '')
                if MENUS[index.lower()] == cmd:
                    self._menu = menu
                    break
        # ~ line = self._menu.get('CommandURL', '')
        # ~ line += self._get_submenus(self._menu['ItemDescriptorContainer'])
        return self._menu

    def _get_menus(self):
        instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier'
        service = TYPE_DOC[self._app]
        manager = create_instance(instance, True)
        self._ui = manager.getUIConfigurationManager(service)
        self._pymenus = self._ui.getSettings(NODE_MENUBAR, True)
        data = []
        for menu in self._pymenus:
            data.append(data_to_dict(menu))
        return data

    def _get_info(self, menu):
        line = menu.get('CommandURL', '')
        line += self._get_submenus(menu['ItemDescriptorContainer'])
        return line

    def _get_submenus(self, menu, level=1):
        line = ''
        for i, v in enumerate(menu):
            data = data_to_dict(v)
            cmd = data.get('CommandURL', '----------')
            line += f'\n{"  " * level}├─ ({i}) {cmd}'
            submenu = data.get('ItemDescriptorContainer', None)
            if not submenu is None:
                line += self._get_submenus(submenu, level + 1)
        return line

    def __str__(self):
        info = '\n'.join([self._get_info(m) for m in self._menus])
        return info

    def _get_index_menu(self, menu, command):
        index = -1
        for i, v in enumerate(menu):
            data = data_to_dict(v)
            cmd = data.get('CommandURL', '')
            if cmd == command:
                index = i
                break
        return index

    def insert(self, name, args):
        idc = None
        replace = False
        command = args['CommandURL']
        label = args['Label']

        self[name]
        menu = self._menu['ItemDescriptorContainer']
        submenu = args.get('Submenu', False)
        if submenu:
            idc = self._ui.createSettings()

        index = self._get_index_menu(menu, command)
        if index == -1:
            if 'Index' in args:
                index = args['Index']
            else:
                index = self._get_index_menu(menu, args['After']) + 1
        else:
            replace = True

        data = dict (
            CommandURL = command,
            Label = label,
            Style = 0,
            Type = 0,
            ItemDescriptorContainer = idc,
        )
        self._save(menu, data, index, replace)
        self._insert_submenu(idc, submenu)
        return

    def _get_command(self, args):
        shortcut = args.get('ShortCut', '')
        cmd = args['CommandURL']
        if isinstance(cmd, dict):
            cmd = _get_url_script(cmd)
        if shortcut:
            LOShortCut(self._app).add(shortcut, cmd)
        return cmd

    def _insert_submenu(self, parent, menus):
        for i, v in enumerate(menus):
            submenu = v.pop('Submenu', False)
            if submenu:
                idc = self._ui.createSettings()
                v['ItemDescriptorContainer'] = idc
            v['Type'] = 0
            if v['Label'] == '-':
                v['Type'] = 1
            else:
                v['CommandURL'] = self._get_command(v)
            self._save(parent, v, i)
            if submenu:
                self._insert_submenu(idc, submenu)
        return

    def remove(self, name, command):
        self[name]
        menu = self._menu['ItemDescriptorContainer']
        index = self._get_index_menu(menu, command)
        if index > -1:
            uno.invoke(menu, 'removeByIndex', (index,))
            self._ui.replaceSettings(NODE_MENUBAR, self._pymenus)
            self._ui.store()
        return

    def _save(self, menu, properties, index, replace=False):
        properties = dict_to_property(properties, True)
        if replace:
            uno.invoke(menu, 'replaceByIndex', (index, properties))
        else:
            uno.invoke(menu, 'insertByIndex', (index, properties))
        self._ui.replaceSettings(NODE_MENUBAR, self._pymenus)
        self._ui.store()
        return


class LOMenus(object):

    def __getitem__(self, index):
        return LOMenu(index)


class LOWindow(object):
    EMPTY = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dlg:window PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "dialog.dtd">
<dlg:window xmlns:dlg="http://openoffice.org/2000/dialog" xmlns:script="http://openoffice.org/2000/script" dlg:id="empty" dlg:left="0" dlg:top="0" dlg:width="0" dlg:height="0" dlg:closeable="true" dlg:moveable="true" dlg:withtitlebar="false"/>"""
    MODELS = {
        'label': 'com.sun.star.awt.UnoControlFixedTextModel',
        'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel',
        'button': 'com.sun.star.awt.UnoControlButtonModel',
        'radio': 'com.sun.star.awt.UnoControlRadioButtonModel',
        'checkbox': 'com.sun.star.awt.UnoControlCheckBoxModel',
        'text': 'com.sun.star.awt.UnoControlEditModel',
        'image': 'com.sun.star.awt.UnoControlImageControlModel',
        'listbox': 'com.sun.star.awt.UnoControlListBoxModel',
        'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel',
        'tree': 'com.sun.star.awt.tree.TreeControlModel',
        'grid': 'com.sun.star.awt.grid.UnoControlGridModel',
        'pages': 'com.sun.star.awt.UnoMultiPageModel',
        'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel',
        'combobox': 'com.sun.star.awt.UnoControlComboBoxModel',
    }

    def __init__(self, args):
        self._events = None
        self._menu = None
        self._container = None
        self._model = None
        self._id = ''
        self._path = ''
        self._obj = self._create(args)

    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_subcontainer(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):
        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 _create_subcontainer(self, ps):
        service = 'com.sun.star.awt.ContainerWindowProvider'
        cwp = create_instance(service, True)

        path_tmp = _P.save_tmp(self.EMPTY)
        subcont = cwp.createContainerWindow(
            _P.to_url(path_tmp), '', self._container.getPeer(), None)
        _P.kill(path_tmp)

        subcont.setPosSize(0, 0, 500, 500, POSSIZE)
        subcont.setVisible(True)
        self._container.addControl('subcont', subcont)
        self._subcont = subcont
        self._model = subcont.Model
        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_listeners(self, control=None):
        if self.events is None:
            return
        controller = EventsWindow(self)
        self._window.addTopWindowListener(controller)
        self._window.addWindowListener(controller)
        # ~ self._container.addKeyListener(EventsKeyWindow(self))
        return

    def _set_image_url(self, image):
        if _P.exists(image):
            return _P.to_url(image)

        path = _P.join(self._path, DIR['images'], image)
        return _P.to_url(path)

    def _special_properties(self, tipo, args):
        if tipo == 'link' and not 'Label' in args:
            args['Label'] = args['URL']
            return args

        if tipo == 'button':
            if 'ImageURL' in args:
                args['ImageURL'] = self._set_image_url(args['ImageURL'])
            args['FocusOnClick'] = args.get('FocusOnClick', False)
            return args

        if tipo == 'roadmap':
            args['Height'] = args.get('Height', self.height)
            if 'Title' in args:
                args['Text'] = args.pop('Title')
            return args

        if tipo == 'tree':
            args['SelectionType'] = args.get('SelectionType', SINGLE)
            return args

        if tipo == 'grid':
            args['ShowRowHeader'] = args.get('ShowRowHeader', True)
            return args

        if tipo == 'pages':
            args['Width'] = args.get('Width', self.width)
            args['Height'] = args.get('Height', self.height)

        return args

    def add_control(self, args):
        tipo = args.pop('Type').lower()
        root = args.pop('Root', '')
        sheets = args.pop('Sheets', ())
        columns = args.pop('Columns', ())

        args = self._special_properties(tipo, args)
        model = self.model.createInstance(self.MODELS[tipo])
        _set_properties(model, args)
        name = args['Name']
        self.model.insertByName(name, model)
        control = self._subcont.getControl(name)
        _add_listeners(self.events, control, name)
        control = UNO_CLASSES[tipo](control)

        # ~ if tipo in ('listbox',):
            # ~ control.path = self.path

        if tipo == 'tree' and root:
            control.root = root
        elif tipo == 'grid' and columns:
            control.columns = columns
        elif tipo == 'pages' and sheets:
            control.sheets = sheets
            control.events = self.events

        setattr(self, name, control)
        return control

    @property
    def events(self):
        return self._events
    @events.setter
    def events(self, controllers):
        self._events = controllers(self)
        self._add_listeners()

    @property
    def model(self):
        return self._model

    @property
    def width(self):
        return self._container.Size.Width

    @property
    def height(self):
        return self._container.Size.Height

    @property
    def name(self):
        return self._title.lower().replace(' ', '_')

    def add_menu(self, menus):
        self._create_menu(menus)
        return

    def open(self):
        self._window.setVisible(True)
        return

    def close(self):
        self._window.setMenuBar(None)
        self._window.dispose()
        self._frame.close(True)
        return


class LODBServer(object):
    DRIVERS = {
        'mysql': 'mysqlc',
        'mariadb': 'mysqlc',
        'postgres': 'postgresql:postgresql',
    }
    PORTS = {
        'mysql': 3306,
        'mariadb': 3306,
        'postgres': 5432,
    }

    def __init__(self):
        self._conn = None
        self._error = 'Not connected'
        self._type = ''

    def __str__(self):
        return f'DB type {self._type}'

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.disconnet()

    @property
    def is_connected(self):
        return not self._conn is None

    @property
    def error(self):
        return self._error

    def disconnet(self):
        if not self._conn is None:
            if not self._conn.isClosed():
                self._conn.close()
                self._conn.dispose()

    def connect(self, args):
        self._error = ''
        self._type = args.get('type', 'postgres')
        driver = self.DRIVERS[self._type]
        server = args.get('server', 'localhost')
        port = args.get('port', self.PORTS[self._type])
        dbname = args.get('dbname', '')
        user = args['user']
        password = args['password']

        data = {'user': user, 'password': password}
        url = f'sdbc:{driver}://{server}:{port}/{dbname}'
        args = dict_to_property(data)
        manager = create_instance('com.sun.star.sdbc.DriverManager')

        try:
            self._conn = manager.getConnectionWithInfo(url, args)
        except Exception as e:
            error(e)
            self._error = str(e)

        return self


def create_window(args):
    return LOWindow(args)


class classproperty:
    def __init__(self, method=None):
        self.fget = method

    def __get__(self, instance, cls=None):
        return self.fget(cls)

    def getter(self, method):
        self.fget = method
        return self


class ClipBoard(object):
    SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard'
    CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16'

    class TextTransferable(unohelper.Base, XTransferable):

        def __init__(self, text):
            df = DataFlavor()
            df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT
            df.HumanPresentableName = "encoded text utf-16"
            self.flavors = (df,)
            self._data = text

        def getTransferData(self, flavor):
            return self._data

        def getTransferDataFlavors(self):
            return self.flavors


    @classmethod
    def set(cls, value):
        ts = cls.TextTransferable(value)
        sc = create_instance(cls.SERVICE)
        sc.setContents(ts, None)
        return

    @classproperty
    def contents(cls):
        df = None
        text = ''
        sc = create_instance(cls.SERVICE)
        transferable = sc.getContents()
        data = transferable.getTransferDataFlavors()
        for df in data:
            if df.MimeType == cls.CLIPBOARD_FORMAT_TEXT:
                break
        if df:
            text = transferable.getTransferData(df)
        return text
_CB = ClipBoard


class Paths(object):
    FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker'
    FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker'

    def __init__(self, path=''):
        if path.startswith('file://'):
            path = str(Path(uno.fileUrlToSystemPath(path)).resolve())
        self._path = Path(path)

    @property
    def path(self):
        return str(self._path.parent)

    @property
    def file_name(self):
        return self._path.name

    @property
    def name(self):
        return self._path.stem

    @property
    def ext(self):
        return self._path.suffix[1:]

    @property
    def info(self):
        return self.path, self.file_name, self.name, self.ext

    @property
    def url(self):
        return self._path.as_uri()

    @property
    def size(self):
        return self._path.stat().st_size

    @classproperty
    def home(self):
        return str(Path.home())

    @classproperty
    def documents(self):
        return self.config()

    @classproperty
    def temp_dir(self):
        return tempfile.gettempdir()

    @classproperty
    def python(self):
        if IS_WIN:
            path = self.join(self.config('Module'), PYTHON)
        elif IS_MAC:
            path = self.join(self.config('Module'), '..', 'Resources', PYTHON)
        else:
            path = sys.executable
        return path

    @classmethod
    def dir_tmp(self, only_name=False):
        dt = tempfile.TemporaryDirectory()
        if only_name:
            dt = dt.name
        return dt

    @classmethod
    def tmp(cls, ext=''):
        tmp = tempfile.NamedTemporaryFile(suffix=ext)
        return tmp.name

    @classmethod
    def save_tmp(cls, data):
        path_tmp = cls.tmp()
        cls.save(path_tmp, data)
        return path_tmp

    @classmethod
    def config(cls, name='Work'):
        """
            Return path from config
            http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html
        """
        path = create_instance('com.sun.star.util.PathSettings')
        path = cls.to_system(getattr(path, name))
        return path

    @classmethod
    def get(cls, init_dir='', filters: str=''):
        """
            Get path for save
            Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html
            filters: 'xml' or 'txt,xml'
        """
        if not init_dir:
            init_dir = cls.documents
        init_dir = cls.to_url(init_dir)
        file_picker = create_instance(cls.FILE_PICKER)
        file_picker.setTitle(_('Select path'))
        file_picker.setDisplayDirectory(init_dir)
        file_picker.initialize((2,))
        if filters:
            filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')]
            file_picker.setCurrentFilter(filters[0][0])
            for f in filters:
                file_picker.appendFilter(f[0], f[1])

        path = ''
        if file_picker.execute():
            path =  cls.to_system(file_picker.getSelectedFiles()[0])
        return path

    @classmethod
    def get_dir(cls, init_dir=''):
        folder_picker = create_instance(cls.FOLDER_PICKER)
        if not init_dir:
            init_dir = cls.documents
        init_dir = cls.to_url(init_dir)
        folder_picker.setTitle(_('Select directory'))
        folder_picker.setDisplayDirectory(init_dir)

        path = ''
        if folder_picker.execute():
            path = cls.to_system(folder_picker.getDirectory())
        return path

    @classmethod
    def get_file(cls, init_dir: str='', filters: str='', multiple: bool=False):
        """
            Get path file

            init_folder: folder default open
            filters: 'xml' or 'xml,txt'
            multiple: True for multiple selected
        """
        if not init_dir:
            init_dir = cls.documents
        init_dir = cls.to_url(init_dir)

        file_picker = create_instance(cls.FILE_PICKER)
        file_picker.setTitle(_('Select file'))
        file_picker.setDisplayDirectory(init_dir)
        file_picker.setMultiSelectionMode(multiple)

        if filters:
            filters = [(f.upper(), f'*.{f.lower()}') for f in filters.split(',')]
            file_picker.setCurrentFilter(filters[0][0])
            for f in filters:
                file_picker.appendFilter(f[0], f[1])

        path = ''
        if file_picker.execute():
            files = file_picker.getSelectedFiles()
            path = [cls.to_system(f) for f in files]
            if not multiple:
                path = path[0]
        return path

    @classmethod
    def replace_ext(cls, path, new_ext):
        p = Paths(path)
        name = f'{p.name}.{new_ext}'
        path = cls.join(p.path, name)
        return path

    @classmethod
    def exists(cls, path):
        result = False
        if path:
            path = cls.to_system(path)
            result = Path(path).exists()
        return result

    @classmethod
    def exists_app(cls, name_app):
        return bool(shutil.which(name_app))

    @classmethod
    def open(cls, path):
        if IS_WIN:
            os.startfile(path)
        else:
            pid = subprocess.Popen(['xdg-open', path]).pid
        return

    @classmethod
    def is_dir(cls, path):
        return Path(path).is_dir()

    @classmethod
    def is_file(cls, path):
        return Path(path).is_file()

    @classmethod
    def join(cls, *paths):
        return str(Path(paths[0]).joinpath(*paths[1:]))

    @classmethod
    def save(cls, path, data, encoding='utf-8'):
        result = bool(Path(path).write_text(data, encoding=encoding))
        return result

    @classmethod
    def save_bin(cls, path, data):
        result = bool(Path(path).write_bytes(data))
        return result

    @classmethod
    def read(cls, path, encoding='utf-8'):
        data = Path(path).read_text(encoding=encoding)
        return data

    @classmethod
    def read_bin(cls, path):
        data = Path(path).read_bytes()
        return data

    @classmethod
    def to_url(cls, path):
        if not path.startswith('file://'):
            path = Path(path).as_uri()
        return path

    @classmethod
    def to_system(cls, path):
        if path.startswith('file://'):
            path = str(Path(uno.fileUrlToSystemPath(path)).resolve())
        return path

    @classmethod
    def kill(cls, path):
        p = Path(path)
        try:
            if p.is_file():
                p.unlink()
            elif p.is_dir():
                shutil.rmtree(path)
            result = True
        except OSError as e:
            log.error(e)
            result = False

        return result

    @classmethod
    def files(cls, path, pattern='*'):
        files = [str(p) for p in Path(path).glob(pattern) if p.is_file()]
        return files

    @classmethod
    def dirs(cls, path):
        dirs = [str(p) for p in Path(path).iterdir() if p.is_dir()]
        return dirs

    @classmethod
    def walk(cls, path, filters=''):
        paths = []
        if filters in ('*', '*.*'):
            filters = ''
        for folder, _, files in os.walk(path):
            if filters:
                pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE)
                paths += [cls.join(folder, f) for f in files if pattern.search(f)]
            else:
                paths += [cls.join(folder, f) for f in files]
        return paths

    @classmethod
    def walk_dirs(cls, path, tree=False):
        """
            Get directories recursively
            path: path source
            tree: get info in a tuple (ID_FOLDER, ID_PARENT, NAME)
        """
        folders = []
        if tree:
            i = 0
            p = 0
            parents = {path: 0}
            for root, dirs, _ in os.walk(path):
                for name in dirs:
                    i += 1
                    rn = cls.join(root, name)
                    if not rn in parents:
                        parents[rn] = i
                    folders.append((i, parents[root], name))
        else:
            for root, dirs, _ in os.walk(path):
                folders += [cls.join(root, name) for name in dirs]
        return folders

    @classmethod
    def from_id(cls, id_ext):
        pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider')
        path = _P.to_system(pip.getPackageLocation(id_ext))
        return path

    @classmethod
    def from_json(cls, path):
        data = json.loads(cls.read(path))
        return data

    @classmethod
    def to_json(cls, path, data):
        data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True)
        return cls.save(path, data)

    @classmethod
    def from_csv(cls, path, args={}):
        # ~ See https://docs.python.org/3.7/library/csv.html#csv.reader
        with open(path) as f:
            rows = tuple(csv.reader(f, **args))
        return rows

    @classmethod
    def to_csv(cls, path, data, args={}):
        with open(path, 'w') as f:
            writer = csv.writer(f, **args)
            writer.writerows(data)
        return

    @classmethod
    def zip(cls, source, target='', pwd=''):
        path_zip = target
        if not isinstance(source, (tuple, list)):
            path, _, name, _ = _P(source).info
            start = len(path) + 1
            if not target:
                path_zip = f'{path}/{name}.zip'

        if isinstance(source, (tuple, list)):
            files = [(f, f[len(_P(f).path)+1:]) for f in source]
        elif _P.is_file(source):
            files = ((source, source[start:]),)
        else:
            files = [(f, f[start:]) for f in _P.walk(source)]

        compression = zipfile.ZIP_DEFLATED
        with zipfile.ZipFile(path_zip, 'w', compression=compression) as z:
            for f in files:
                z.write(f[0], f[1])
        return

    @classmethod
    def zip_content(cls, path):
        with zipfile.ZipFile(path) as z:
            names = z.namelist()
        return names

    @classmethod
    def unzip(cls, source, target='', members=None, pwd=None):
        path = target
        if not target:
            path = _P(source).path
        with zipfile.ZipFile(source) as z:
            if not pwd is None:
                pwd = pwd.encode()
            if isinstance(members, str):
                members = (members,)
            z.extractall(path, members=members, pwd=pwd)
        return True

    @classmethod
    def merge_zip(cls, target, zips):
        try:
            with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t:
                for path in zips:
                    with zipfile.ZipFile(path, compression=zipfile.ZIP_DEFLATED) as s:
                        for name in s.namelist():
                            t.writestr(name, s.open(name).read())
        except Exception as e:
            error(e)
            return False

        return True

    @classmethod
    def image(cls, path):
        gp = create_instance('com.sun.star.graphic.GraphicProvider')
        image = gp.queryGraphic((
            PropertyValue(Name='URL', Value=cls.to_url(path)),
        ))
        return image

    @classmethod
    def copy(cls, source, target='', name=''):
        p, f, n, e = _P(source).info
        if target:
            p = target
        if name:
            e = ''
            n = name
        path_new = cls.join(p, f'{n}{e}')
        shutil.copy(source, path_new)
        return path_new
_P = Paths


class Dates(object):

    @classmethod
    def date(cls, year, month, day):
        d = datetime.date(year, month, day)
        return d


class SpellChecker(object):

    def __init__(self):
        service = 'com.sun.star.linguistic2.SpellChecker'
        self._spellchecker = create_instance(service, True)
        self._locale = LOCALE

    @property
    def locale(self):
        slocal = f'{self._locale.Language}-{self._locale.Country}'
        return slocale
    @locale.setter
    def locale(self, value):
        lang = value.split('-')
        self._locale = Locale(lang[0], lang[1], '')

    def is_valid(self, word):
        result = self._spellchecker.isValid(word, self._locale, ())
        return result

    def spell(self, word):
        result = self._spellchecker.spell(word, self._locale, ())
        if result:
            result = result.getAlternatives()
            if not isinstance(result, tuple):
                result = ()
        return result


def spell(word, locale=''):
    sc = SpellChecker()
    if locale:
        sc.locale = locale
    return sc.spell(word)


def __getattr__(name):
    if name == 'active':
        return LODocs().active
    if name == 'active_sheet':
        return LODocs().active.active
    if name == 'selection':
        return LODocs().active.selection
    if name == 'current_region':
        return LODocs().active.selection.current_region
    if name in ('rectangle', 'pos_size'):
        return Rectangle()
    if name == 'paths':
        return Paths
    if name == 'docs':
        return LODocs()
    if name == 'db':
        return LODBServer()
    if name == 'sheets':
        return LOSheets()
    if name == 'cells':
        return LOCells()
    if name == 'menus':
        return LOMenus()
    if name == 'shortcuts':
        return LOShortCuts()
    if name == 'clipboard':
        return ClipBoard
    if name == 'dates':
        return Dates
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")


def create_dialog(args):
    return LODialog(args)


def inputbox(message, default='', title=TITLE, echochar=''):

    class ControllersInput(object):

        def __init__(self, dlg):
            self.d = dlg

        def cmd_ok_action(self, event):
            self.d.close(1)
            return

    args = {
        'Title': title,
        'Width': 200,
        'Height': 80,
    }
    dlg = LODialog(args)
    dlg.events = ControllersInput

    args = {
        'Type': 'Label',
        'Name': 'lbl_msg',
        'Label': message,
        'Width': 140,
        'Height': 50,
        'X': 5,
        'Y': 5,
        'MultiLine': True,
        'Border': 1,
    }
    dlg.add_control(args)

    args = {
        'Type': 'Text',
        'Name': 'txt_value',
        'Text': default,
        'Width': 190,
        'Height': 15,
    }
    if echochar:
        args['EchoChar'] = ord(echochar[0])
    dlg.add_control(args)
    dlg.txt_value.move(dlg.lbl_msg)

    args = {
        'Type': 'button',
        'Name': 'cmd_ok',
        'Label': _('OK'),
        'Width': 40,
        'Height': 15,
        'DefaultButton': True,
        'PushButtonType': 1,
    }
    dlg.add_control(args)
    dlg.cmd_ok.move(dlg.lbl_msg, 10, 0)

    args = {
        'Type': 'button',
        'Name': 'cmd_cancel',
        'Label': _('Cancel'),
        'Width': 40,
        'Height': 15,
        'PushButtonType': 2,
    }
    dlg.add_control(args)
    dlg.cmd_cancel.move(dlg.cmd_ok)

    if dlg.open():
        return dlg.txt_value.value

    return ''


def get_fonts():
    toolkit = create_instance('com.sun.star.awt.Toolkit')
    device = toolkit.createScreenCompatibleDevice(0, 0)
    return device.FontDescriptors


# ~ https://en.wikipedia.org/wiki/Web_colors
def get_color(value):
    COLORS = {
        'aliceblue': 15792383,
        'antiquewhite': 16444375,
        'aqua': 65535,
        'aquamarine': 8388564,
        'azure': 15794175,
        'beige': 16119260,
        'bisque': 16770244,
        'black': 0,
        'blanchedalmond': 16772045,
        'blue': 255,
        'blueviolet': 9055202,
        'brown': 10824234,
        'burlywood': 14596231,
        'cadetblue': 6266528,
        'chartreuse': 8388352,
        'chocolate': 13789470,
        'coral': 16744272,
        'cornflowerblue': 6591981,
        'cornsilk': 16775388,
        'crimson': 14423100,
        'cyan': 65535,
        'darkblue': 139,
        'darkcyan': 35723,
        'darkgoldenrod': 12092939,
        'darkgray': 11119017,
        'darkgreen': 25600,
        'darkgrey': 11119017,
        'darkkhaki': 12433259,
        'darkmagenta': 9109643,
        'darkolivegreen': 5597999,
        'darkorange': 16747520,
        'darkorchid': 10040012,
        'darkred': 9109504,
        'darksalmon': 15308410,
        'darkseagreen': 9419919,
        'darkslateblue': 4734347,
        'darkslategray': 3100495,
        'darkslategrey': 3100495,
        'darkturquoise': 52945,
        'darkviolet': 9699539,
        'deeppink': 16716947,
        'deepskyblue': 49151,
        'dimgray': 6908265,
        'dimgrey': 6908265,
        'dodgerblue': 2003199,
        'firebrick': 11674146,
        'floralwhite': 16775920,
        'forestgreen': 2263842,
        'fuchsia': 16711935,
        'gainsboro': 14474460,
        'ghostwhite': 16316671,
        'gold': 16766720,
        'goldenrod': 14329120,
        'gray': 8421504,
        'grey': 8421504,
        'green': 32768,
        'greenyellow': 11403055,
        'honeydew': 15794160,
        'hotpink': 16738740,
        'indianred': 13458524,
        'indigo': 4915330,
        'ivory': 16777200,
        'khaki': 15787660,
        'lavender': 15132410,
        'lavenderblush': 16773365,
        'lawngreen': 8190976,
        'lemonchiffon': 16775885,
        'lightblue': 11393254,
        'lightcoral': 15761536,
        'lightcyan': 14745599,
        'lightgoldenrodyellow': 16448210,
        'lightgray': 13882323,
        'lightgreen': 9498256,
        'lightgrey': 13882323,
        'lightpink': 16758465,
        'lightsalmon': 16752762,
        'lightseagreen': 2142890,
        'lightskyblue': 8900346,
        'lightslategray': 7833753,
        'lightslategrey': 7833753,
        'lightsteelblue': 11584734,
        'lightyellow': 16777184,
        'lime': 65280,
        'limegreen': 3329330,
        'linen': 16445670,
        'magenta': 16711935,
        'maroon': 8388608,
        'mediumaquamarine': 6737322,
        'mediumblue': 205,
        'mediumorchid': 12211667,
        'mediumpurple': 9662683,
        'mediumseagreen': 3978097,
        'mediumslateblue': 8087790,
        'mediumspringgreen': 64154,
        'mediumturquoise': 4772300,
        'mediumvioletred': 13047173,
        'midnightblue': 1644912,
        'mintcream': 16121850,
        'mistyrose': 16770273,
        'moccasin': 16770229,
        'navajowhite': 16768685,
        'navy': 128,
        'oldlace': 16643558,
        'olive': 8421376,
        'olivedrab': 7048739,
        'orange': 16753920,
        'orangered': 16729344,
        'orchid': 14315734,
        'palegoldenrod': 15657130,
        'palegreen': 10025880,
        'paleturquoise': 11529966,
        'palevioletred': 14381203,
        'papayawhip': 16773077,
        'peachpuff': 16767673,
        'peru': 13468991,
        'pink': 16761035,
        'plum': 14524637,
        'powderblue': 11591910,
        'purple': 8388736,
        'red': 16711680,
        'rosybrown': 12357519,
        'royalblue': 4286945,
        'saddlebrown': 9127187,
        'salmon': 16416882,
        'sandybrown': 16032864,
        'seagreen': 3050327,
        'seashell': 16774638,
        'sienna': 10506797,
        'silver': 12632256,
        'skyblue': 8900331,
        'slateblue': 6970061,
        'slategray': 7372944,
        'slategrey': 7372944,
        'snow': 16775930,
        'springgreen': 65407,
        'steelblue': 4620980,
        'tan': 13808780,
        'teal': 32896,
        'thistle': 14204888,
        'tomato': 16737095,
        'turquoise': 4251856,
        'violet': 15631086,
        'wheat': 16113331,
        'white': 16777215,
        'whitesmoke': 16119285,
        'yellow': 16776960,
        'yellowgreen': 10145074,
    }

    if isinstance(value, tuple):
        color = (value[0] << 16) + (value[1] << 8) + value[2]
    else:
        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


COLOR_ON_FOCUS = get_color('LightYellow')


class LOServer(object):
    HOST = 'localhost'
    PORT = '8100'
    ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext'
    CMD = ['soffice',
        '-env:SingleAppInstance=false',
        '-env:UserInstallation=file:///tmp/LO_Process8100',
        '--headless', '--norestore', '--invisible',
        f'--accept={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
