# -*- coding: UTF-8 -*-

# TabellioOOo - OpenOffice.org extension
# Copyright (C) 2007-2010  Parlement de la Communauté française de Belgique
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA


import os
import csv
import string
import sys
import tempfile
import xml.dom.minidom
import socket
import random
import time

if hasattr(socket, 'setdefaulttimeout'):
    socket.setdefaulttimeout(30)

import urllib2
import urlparse

import uno
import unohelper

from com.sun.star.container import NoSuchElementException

from com.sun.star.task import XJobExecutor

from com.sun.star.awt import XActionListener, XItemListener
from com.sun.star.frame import XControlNotificationListener
from com.sun.star.frame import XDispatch
from com.sun.star.frame import XDispatchProvider
from com.sun.star.frame import FeatureStateEvent
from com.sun.star.beans import PropertyValue, NamedValue
from com.sun.star.frame import ControlCommand
from com.sun.star.task import XJob

from com.sun.star.awt import WindowDescriptor
from com.sun.star.awt import FontDescriptor

from com.sun.star.awt.FontWeight import BOLD, NORMAL
from com.sun.star.awt.WindowClass import MODALTOP
from com.sun.star.awt.VclWindowPeerAttribute import OK

from com.sun.star.text.ControlCharacter import APPEND_PARAGRAPH

from com.sun.star.ui.dialogs.TemplateDescription import FILESAVE_SIMPLE


### Utility functions

def debug_print(*args):
    '''Print a message on the console (if possible)'''
    if sys.platform.startswith('win'):
        # there is no stdout/stderr on windows
        return
    if type(args) in (str, unicode):
        print >> sys.stderr, args
    else:
        for a in args:
            print >> sys.stderr, a,
        print >> sys.stderr, ''

def debug_unodir(unoobj):
    '''Introspect an object to get all its methods and properties.'''

    from com.sun.star.beans.MethodConcept import ALL as ALLMETHS
    from com.sun.star.beans.PropertyConcept import ALL as ALLPROPS

    ctx = uno.getComponentContext()
    introspection = ctx.ServiceManager.createInstanceWithContext(
                   'com.sun.star.beans.Introspection', ctx)
    access = introspection.inspect(unoobj)

    return {'methods': [x.getName() for x in access.getMethods(ALLMETHS)],
            'properties': [x.getName() for x in access.getMethods(ALLPROPS)]}

xlate = {
    u'\N{ACUTE ACCENT}': "'",
    u'\N{BROKEN BAR}': '|',
    u'\N{DIVISION SIGN}': '/',
    u'\N{LATIN CAPITAL LETTER A WITH ACUTE}': 'A',
    u'\N{LATIN CAPITAL LETTER A WITH CIRCUMFLEX}': 'A',
    u'\N{LATIN CAPITAL LETTER A WITH DIAERESIS}': 'A',
    u'\N{LATIN CAPITAL LETTER A WITH GRAVE}': 'A',
    u'\N{LATIN CAPITAL LETTER A WITH RING ABOVE}': 'A',
    u'\N{LATIN CAPITAL LETTER A WITH TILDE}': 'A',
    u'\N{LATIN CAPITAL LETTER AE}': 'Ae',
    u'\N{LATIN CAPITAL LETTER C WITH CEDILLA}': 'C',
    u'\N{LATIN CAPITAL LETTER E WITH ACUTE}': 'E',
    u'\N{LATIN CAPITAL LETTER E WITH CIRCUMFLEX}': 'E',
    u'\N{LATIN CAPITAL LETTER E WITH DIAERESIS}': 'E',
    u'\N{LATIN CAPITAL LETTER E WITH GRAVE}': 'E',
    u'\N{LATIN CAPITAL LETTER ETH}': 'Th',
    u'\N{LATIN CAPITAL LETTER I WITH ACUTE}': 'I',
    u'\N{LATIN CAPITAL LETTER I WITH CIRCUMFLEX}': 'I',
    u'\N{LATIN CAPITAL LETTER I WITH DIAERESIS}': 'I',
    u'\N{LATIN CAPITAL LETTER I WITH GRAVE}': 'I',
    u'\N{LATIN CAPITAL LETTER N WITH TILDE}': 'N',
    u'\N{LATIN CAPITAL LETTER O WITH ACUTE}': 'O',
    u'\N{LATIN CAPITAL LETTER O WITH CIRCUMFLEX}': 'O',
    u'\N{LATIN CAPITAL LETTER O WITH DIAERESIS}': 'O',
    u'\N{LATIN CAPITAL LETTER O WITH GRAVE}': 'O',
    u'\N{LATIN CAPITAL LETTER O WITH STROKE}': 'O',
    u'\N{LATIN CAPITAL LETTER O WITH TILDE}': 'O',
    u'\N{LATIN CAPITAL LETTER THORN}': 'th',
    u'\N{LATIN CAPITAL LETTER U WITH ACUTE}': 'U',
    u'\N{LATIN CAPITAL LETTER U WITH CIRCUMFLEX}': 'U',
    u'\N{LATIN CAPITAL LETTER U WITH DIAERESIS}': 'U',
    u'\N{LATIN CAPITAL LETTER U WITH GRAVE}': 'U',
    u'\N{LATIN CAPITAL LETTER Y WITH ACUTE}': 'Y',
    u'\N{LATIN SMALL LETTER A WITH ACUTE}': 'a',
    u'\N{LATIN SMALL LETTER A WITH CIRCUMFLEX}': 'a',
    u'\N{LATIN SMALL LETTER A WITH DIAERESIS}': 'a',
    u'\N{LATIN SMALL LETTER A WITH GRAVE}': 'a',
    u'\N{LATIN SMALL LETTER A WITH RING ABOVE}': 'a',
    u'\N{LATIN SMALL LETTER A WITH TILDE}': 'a',
    u'\N{LATIN SMALL LETTER AE}': 'ae',
    u'\N{LATIN SMALL LETTER C WITH CEDILLA}': 'c',
    u'\N{LATIN SMALL LETTER E WITH ACUTE}': 'e',
    u'\N{LATIN SMALL LETTER E WITH CIRCUMFLEX}': 'e',
    u'\N{LATIN SMALL LETTER E WITH DIAERESIS}': 'e',
    u'\N{LATIN SMALL LETTER E WITH GRAVE}': 'e',
    u'\N{LATIN SMALL LETTER ETH}': 'th',
    u'\N{LATIN SMALL LETTER I WITH ACUTE}': 'i',
    u'\N{LATIN SMALL LETTER I WITH CIRCUMFLEX}': 'i',
    u'\N{LATIN SMALL LETTER I WITH DIAERESIS}': 'i',
    u'\N{LATIN SMALL LETTER I WITH GRAVE}': 'i',
    u'\N{LATIN SMALL LETTER N WITH TILDE}': 'n',
    u'\N{LATIN SMALL LETTER O WITH ACUTE}': 'o',
    u'\N{LATIN SMALL LETTER O WITH CIRCUMFLEX}': 'o',
    u'\N{LATIN SMALL LETTER O WITH DIAERESIS}': 'o',
    u'\N{LATIN SMALL LETTER O WITH GRAVE}': 'o',
    u'\N{LATIN SMALL LETTER O WITH STROKE}': 'o',
    u'\N{LATIN SMALL LETTER O WITH TILDE}': 'o',
    u'\N{LATIN SMALL LETTER SHARP S}': 'ss',
    u'\N{LATIN SMALL LETTER THORN}': 'th',
    u'\N{LATIN SMALL LETTER U WITH ACUTE}': 'u',
    u'\N{LATIN SMALL LETTER U WITH CIRCUMFLEX}': 'u',
    u'\N{LATIN SMALL LETTER U WITH DIAERESIS}': 'u',
    u'\N{LATIN SMALL LETTER U WITH GRAVE}': 'u',
    u'\N{LATIN SMALL LETTER Y WITH ACUTE}': 'y',
    u'\N{LATIN SMALL LETTER Y WITH DIAERESIS}': 'y',
    }

def latin1_to_ascii(unicrap):
    """This takes a UNICODE string and replaces Latin-1 characters with
        something equivalent in 7-bit ASCII. It returns a plain ASCII string.
        This function makes a best effort to convert Latin-1 characters into
        ASCII equivalents. It does not just strip out the Latin-1 characters.
        All characters in the standard 7-bit ASCII range are preserved.
        In the 8th bit range all the Latin-1 accented letters are converted
        to unaccented equivalents. Most symbol characters are converted to
        something meaningful. Anything not converted is deleted.

        <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/251871>
    """

    r = ""
    for i in unicrap:
        if xlate.has_key(i):
            r += xlate[i]
        elif ord(i) >= 0x80:
            pass
        else:
            r += str(i)
    return r


def normalize_filename(filename):
    '''Normalize a filename received from OOo (as a RFC 1738 URL) so it can
       get accessed by Python file functions afterwards'''
    filename = unicode(urllib2.unquote(str(filename[7:])), 'utf-8')
    if sys.platform.startswith('win'):
        # Note on behaviour on Windows:
        #  when on a lettered disk, it returns file:///Z:/foobar
        #  on a share, it returns file://name-of-the-share/boobar
        if filename[0] != '/':
            # windows share
            filename = '\\\\' + filename
        elif filename[2] == ':':
            filename = filename[1:]
            filename = filename.replace('/', '\\')
    return filename


class DownloadError(Exception):
    pass


def display_exception(ctx=None):
    '''Display exception in a message dialog'''
    # it allows to trace errors that would otherwise be swallowed by
    # OpenOffice.org, and to get them displayed to terminal for easier
    # handling, on non-Windows platforms.
    import traceback
    if not sys.platform.startswith('win'):
        # also print trace on stdout/stderr on non-Windows platform
        traceback.print_exc()
    if not ctx:
        return
    s = '\n'.join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))

    smgr = ctx.ServiceManager
    desktop = smgr.createInstanceWithContext('com.sun.star.frame.Desktop',ctx )
    doc = desktop.getCurrentComponent()
    parentwin = doc.CurrentController.Frame.ContainerWindow
    MessageBox(parentwin, s, 'Exception')


# from danny/OOoLib:
def makePropertyValue( cName=None, uValue=None, nHandle=None, nState=None ):
    """Create a com.sun.star.beans.PropertyValue struct and return it.
    """
    oPropertyValue = PropertyValue()

    if cName != None:
        oPropertyValue.Name = cName
    if uValue != None:
        oPropertyValue.Value = uValue
    if nHandle != None:
        oPropertyValue.Handle = nHandle
    if nState != None:
        oPropertyValue.State = nState

    return oPropertyValue

def makeNamedList(items):
    p = NamedValue()
    p.Name = 'List'
    p.Value = uno.Any('[]string', items)
    return (p,)

def get_url_opener(ctx):
    '''Return a urllib2 opener object, configured with appropriate proxy
       settings'''
    oConfigAccess = getConfigAccess(ctx,
            '/org.entrouvert.openoffice.tabellio.Configuration', False)
    try:
        proxy_server_url = oConfigAccess.getByName('ProxyServerURL').strip()
    except: # com.sun.star.container.NoSuchElementException
        # this happens in some case of extension misconfiguration, ignore
        proxy_server_url = ''
    if proxy_server_url in ('system', ''):
        return urllib2.build_opener()
    if proxy_server_url == 'none':
        proxy_handler = urllib2.ProxyHandler({})
    else:
        proxy_handler = urllib2.ProxyHandler({'http': proxy_server_url})
    return urllib2.build_opener(proxy_handler)

def download(ctx, href):
    '''Download and cache files'''
    if sys.platform.startswith('win'):
        download_cache_dir = 'c:\\temp'
        filename = urllib2.urlparse.urlparse(href)[2].strip('/')
    else:
        download_cache_dir = os.path.join(tempfile.gettempdir(), 'tabellio-cache')
        filename = '/'.join(urllib2.urlparse.urlparse(href)[1:3])
        filename = filename.replace('/', os.path.sep)
    cache_filename = os.path.join(download_cache_dir, filename)

    cache_dir = os.path.split(cache_filename)[0]
    if not os.path.exists(cache_dir):
        os.makedirs(cache_dir)
    if not os.path.exists(cache_filename) or time.time() - os.stat(cache_filename).st_mtime > 7200:
        try:
            s = get_url_opener(ctx).open(href).read()
        except (urllib2.HTTPError, urllib2.URLError, socket.timeout), e:
            if os.path.exists(cache_filename):
                return cache_filename
            return None
        fd = open(cache_filename, 'wb')
        fd.write(s)
        fd.close()
    return cache_filename


def get_text_node_content(node):
    '''Return the content of a text node'''
    rc = ''
    for n in node.childNodes:
        if n.nodeType == n.TEXT_NODE:
            rc = rc + n.data
    return rc


def get_mode(ctx):
    oConfigAccess = getConfigAccess(ctx,
            '/org.entrouvert.openoffice.tabellio.Configuration', False)
    return oConfigAccess.getByName('Mode')


class RemoteObject:
    '''
    Class to handle objects described in a remote XML file, so they can be
    inserted in the document.
    '''
    _attrs = []
    aspres = 0

    def __init__(self, node):
        for attr in self._attrs:
            try:
                setattr(self, attr, get_text_node_content(
                                        node.getElementsByTagName(attr)[0]).strip())
            except IndexError:
                setattr(self, attr, None)

    def get_name(self):
        return u'%s %s' % (self.name, self.firstname)

    def get_download_url(cls, ctx):
        oConfigAccess = getConfigAccess(ctx,
                '/org.entrouvert.openoffice.tabellio.Configuration', False)
        inserts_base_url = oConfigAccess.getByName('InsertsRootURL')
        download_url = inserts_base_url + cls._download_file
        return download_url
    get_download_url = classmethod(get_download_url)

    def get_nodes(cls, ctx):
        list_filename = download(ctx, cls.get_download_url(ctx))
        if not list_filename:
            raise DownloadError()
        dom = xml.dom.minidom.parseString(open(list_filename).read())
        nodes = dom.childNodes[0].getElementsByTagName(cls._node_name)
        return nodes
    get_nodes = classmethod(get_nodes)

    _cache = None
    def values(cls, ctx):
        if cls._cache:
            return cls._cache
        cls._cache = [cls(x) for x in cls.get_nodes(ctx)]
        return cls._cache
    values = classmethod(values)

    def insert(self, ctx, doc, cursor):
        cursor.Text.insertString(cursor, self.get_name().replace(' ', u' '), 0)

    def insert_speaker_closing(self, ctx, doc, cursor):
        if get_mode(ctx) == 'PFB':
            doc.Text.insertString(cursor, u'.- ', 0)
        else:
            doc.Text.insertString(cursor, u'. – ', 0)


class Deputy(RemoteObject):
    '''
    Class to handle description of a deputy
    '''
    _attrs = ('id', 'firstname', 'name', 'title', 'comppol', 'classname', 'sexe')
    _download_file = 'Parls.xml'
    _node_name = 'SParlSpeaker'

    def get_long_name(self):
        return u'%s %s %s (%s)' % (self.title, self.firstname, self.name, self.comppol)

    def values(cls, ctx):
        if cls._cache:
            return cls._cache
        cls._cache = [cls(x) for x in cls.get_nodes(ctx) if x.getElementsByTagName('firstname')]
        return cls._cache
    values = classmethod(values)

    def insert_as_speaker(self, ctx, doc, cursor):
        # Add an annotation next to the document, with a reference to deputy
        # internal id
        annotation = doc.createInstance('com.sun.star.text.TextField.Annotation')
        annotation.setPropertyValue('Author', '')
        text = 'type: TABELLIO\nref id: %s\nclassname: %s' % (self.id, self.classname)
        annotation.setPropertyValue('Content', text)
        doc.Text.insertTextContent(cursor, annotation, False)
        cursor.setPropertyValue('CharWeight', BOLD)
        doc.Text.insertString(cursor, self.get_long_name().replace(' ', u' '), 0)
        self.insert_speaker_closing(ctx, doc, cursor)
        cursor.setPropertyValue('CharWeight', NORMAL)

    def get_ascii_name(self):
        return latin1_to_ascii('%s %s' % (self.firstname, self.name))

    def get_deputy_id(cls, firstname, lastname, ctx=None):
        '''
        Lookup a deputy while ignoring case and accentuated characters
        '''
        ascii_name = latin1_to_ascii('%s %s' % (firstname, lastname)).lower()
        for deputy in cls.values(ctx=ctx):
            if ascii_name == deputy.get_ascii_name().lower():
                return deputy.id
        return None
    get_deputy_id = classmethod(get_deputy_id)


class Minister(RemoteObject):
    '''
    Class to handle the description of a minister
    '''
    _attrs = ('id', 'firstname', 'name', 'title', 'classname', 'fonc')
    _download_file = 'Ministres.xml'
    _node_name = 'SMinistreSpeaker'

    def get_long_name(self):
        return u'%s %s %s' % (self.title, self.firstname, self.name)

    def get_function_with_correct_case(self, ctx):
        if not self.fonc:
            self.fonc = ''
        fonc = self.fonc.strip()
        fonc = fonc.replace('Ministre', 'ministre')
        if get_mode(ctx) == 'PCF':
            fonc = fonc.replace(u'Président', u'président')
            fonc = fonc.replace(u'Vice-président', u'vice-président')
        return fonc

    def insert_as_speaker(self, ctx, doc, cursor):
        # Add an annotation next to the document, with a reference to minister
        # internal id
        annotation = doc.createInstance('com.sun.star.text.TextField.Annotation')
        annotation.setPropertyValue('Author', '')
        text = 'type: TABELLIO\nref id: %s\nclassname: %s' % (self.id, self.classname)
        annotation.setPropertyValue('Content', text)
        doc.Text.insertTextContent(cursor, annotation, False)
        cursor.setPropertyValue('CharWeight', BOLD)
        doc.Text.insertString(cursor, self.get_long_name().replace(' ', u' '), 0)

        fonc = self.get_function_with_correct_case(ctx)
        if fonc:
            if get_mode(ctx) == 'PCF':
                cursor.setPropertyValue('CharWeight', NORMAL)
            doc.Text.insertString(cursor, u', ', 0)
            doc.Text.insertString(cursor, fonc, 0)
            if get_mode(ctx) == 'PFB':
                cursor.setPropertyValue('CharWeight', NORMAL)
        self.insert_speaker_closing(ctx, doc, cursor)


class President(RemoteObject):
    '''
    Class to handle the description of the president
    '''
    _attrs = ('id', 'firstname', 'name', 'title', 'classname', 'function', 'sexe')
    _download_file = 'President.xml'
    _node_name = 'SParlSpeaker'
    aspres = 1

    def get_long_name(self):
        if self.sexe == 'M':
            return u'M. le Président'
        else:
            return u'Mme la Présidente'

    def get_nodes(cls, ctx):
        list_filename = download(ctx, cls.get_download_url(ctx))
        if not list_filename:
            raise DownloadError()
        dom = xml.dom.minidom.parseString(open(list_filename).read())
        # this document has only one element, and it is not embedded into a
        # list
        return [dom.childNodes[0]]
    get_nodes = classmethod(get_nodes)

    def insert_as_speaker(self, ctx, doc, cursor):
        # Add an annotation next to the document, with a reference to minister
        # internal id
        annotation = doc.createInstance('com.sun.star.text.TextField.Annotation')
        annotation.setPropertyValue('Author', '')
        text = 'type: TABELLIO\nref id: %s\nclassname: %s' % (self.id, self.classname)
        annotation.setPropertyValue('Content', text)
        doc.Text.insertTextContent(cursor, annotation, False)
        cursor.setPropertyValue('CharWeight', BOLD)
        doc.Text.insertString(cursor, self.get_long_name().replace(' ', u' '), 0)
        self.insert_speaker_closing(ctx, doc, cursor)
        cursor.setPropertyValue('CharWeight', NORMAL)


class PresCom(RemoteObject):
    '''
    Class to handle a president of commission
    '''
    _attrs = ('id', 'firstname', 'name', 'title', 'classname', 'com_code', 'sexe', 'function')
    _download_file = 'PresComs.xml'
    _node_name = 'SPresComSpeaker'
    classname = 'PresCom'

    def get_name(self):
        return u'%s %s (%s)' % (self.name, self.firstname, self.com_code)

    def get_long_name(self, ctx):
        if get_mode(ctx) == 'PCF':
            if self.sexe == 'M':
                function = u'le président'
            else:
                function = u'la présidente'
            return u'%s %s' % (self.title, function)
        else:
            if self.function:
                return u'%s %s %s, %s' % (self.title, self.firstname, self.name, self.function)
            if self.sexe == 'M':
                function = u'président'
            else:
                function = u'présidente'
            return u'%s %s %s, %s' % (self.title, self.firstname, self.name, function)

    def insert_as_speaker(self, ctx, doc, cursor):
        # Add an annotation next to the document, with a reference to minister
        # internal id
        annotation = doc.createInstance('com.sun.star.text.TextField.Annotation')
        annotation.setPropertyValue('Author', '')
        text = 'type: TABELLIO\nref id: %s\nclassname: %s' % (self.id, self.classname)
        annotation.setPropertyValue('Content', text)
        doc.Text.insertTextContent(cursor, annotation, False)
        cursor.setPropertyValue('CharWeight', BOLD)
        doc.Text.insertString(cursor, self.get_long_name(ctx).replace(' ', u' '), 0)
        self.insert_speaker_closing(ctx, doc, cursor)
        cursor.setPropertyValue('CharWeight', NORMAL)


class GenericPresCom(PresCom):
    '''
    Class to handle a random president of commission
    '''
    sex = None
    id = 'XXX'
    classname = 'GenericPresCom'

    def __init__(self, sex):
        self.sex = sex

    def get_name(self):
        if self.sex == 'M':
            return u'M. le Président'
        else:
            return u'Mme la Présidente'

    def get_long_name(self, ctx):
        return self.get_name()

    def insert_as_speaker(self, ctx, doc, cursor):
        annotation = doc.createInstance('com.sun.star.text.TextField.Annotation')
        annotation.setPropertyValue('Author', '')
        text = 'type: TABELLIO\nref id: %s\nclassname: %s' % (self.id, self.classname)
        annotation.setPropertyValue('Content', text)
        doc.Text.insertTextContent(cursor, annotation, False)
        cursor.setPropertyValue('CharWeight', BOLD)
        doc.Text.insertString(cursor, self.get_long_name(ctx).replace(' ', u' '), 0)
        self.insert_speaker_closing(ctx, doc, cursor)
        cursor.setPropertyValue('CharWeight', NORMAL)


class Commission(RemoteObject):
    '''
    Class to handle a commission
    '''
    _attrs = ('id', 'nom', 'code', 'classname')
    _download_file = 'Commissions.xml'
    _node_name = 'MCOMSInfo'

    def get_name(self):
        if len(self.nom) > 60:
            return self.nom[:self.nom[:65].rindex(u' ')] + u'... (%s)' % self.code
        return self.nom

    def get_long_name(self):
        return self.nom

    def insert(self, ctx, cursor):
        cursor.Text.insertString(cursor, self.get_long_name(), 0)


def get_min_pres_menu_items(ctx):
    values = Minister.values(ctx) + President.values(ctx) + PresCom.values(ctx)
    values.append(GenericPresCom('M'))
    values.append(GenericPresCom('F'))
    return values


class SnippetDoc(RemoteObject):
    '''
    Class to handle a snippet document
    '''
    _download_file = 'Docs.xml'
    _node_name = 'document'

    def __init__(self, node):
        self.title = get_text_node_content(node)
        self.filename = node.getAttribute('filename')


class CloseListener(unohelper.Base, XActionListener):
    '''
    Listener to close the dialogs
    '''
    def __init__(self, dialog):
        self.dialog = dialog

    def actionPerformed(self, actionEvent):
        self.dialog.endExecute()


class InsertSpeakerDlgListener(unohelper.Base, XActionListener):
    '''
    Listener for the insert speaker dialog box
    '''
    def __init__(self, ctx, doc, combobox, dialog, values):
        self.ctx = ctx
        self.combobox = combobox
        self.dialog = dialog
        self.doc = doc
        self.values = values
        RemoteObject.ctx = ctx

    def actionPerformed(self, actionEvent):
        try:
            cursor = self.doc.getCurrentController().getViewCursor()
            string = self.combobox.Text
            t = [x for x in self.values if x.get_name() == string]
            if not t:
                raise Exception('Unknown')
            t[0].insert_as_speaker(self.ctx, self.doc, cursor)
            self.dialog.endExecute()
        except:
            display_exception(self.ctx)


def MessageBox(ParentWin, MsgText, MsgTitle, MsgType="messbox", MsgButtons=OK):
    '''Show a message box with the UNO based toolkit'''

    MsgType = MsgType.lower()

    #available msg types
    MsgTypes = ("messbox", "infobox", "errorbox", "warningbox", "querybox")

    if not ( MsgType in MsgTypes ):
        MsgType = "messbox"

    #describe window properties.
    aDescriptor = WindowDescriptor()
    aDescriptor.Type = MODALTOP
    aDescriptor.WindowServiceName = MsgType
    aDescriptor.ParentIndex = -1
    aDescriptor.Parent = ParentWin
    aDescriptor.WindowAttributes = MsgButtons

    tk = ParentWin.getToolkit()
    msgbox = tk.createWindow(aDescriptor)

    msgbox.setMessageText(MsgText)
    if MsgTitle :
        msgbox.setCaptionText(MsgTitle)

    return msgbox.execute()


class StyleApply(unohelper.Base, XJobExecutor):
    '''
    Job to apply a style to the current paragraph; it is used in styles and
    legistic styles toolbars.
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        style_name = args
        desktop = self.ctx.ServiceManager.createInstanceWithContext(
                "com.sun.star.frame.Desktop", self.ctx )
        doc = desktop.getCurrentComponent()
        cursor = doc.getCurrentController().getViewCursor()
        try:
            text = doc.Text
            textcursor = text.createTextCursorByRange(cursor.getStart())

            numberingrules = textcursor.getPropertyValue('NumberingRules')
            if numberingrules:
                textcursor.setPropertyValue('NumberingRules', None)

            cursor.setPropertyValue('ParaStyleName', style_name)
            if style_name == 'TitreSynthese':
                cursor.Text.insertString(cursor, u'Résumé', 0)
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
                cursor.setPropertyValue('ParaStyleName', 'Text body')

            dispatchHelper = self.ctx.ServiceManager.createInstanceWithContext(
                        'com.sun.star.frame.DispatchHelper', self.ctx)
            cursor = text.createTextCursor()
            view_cursor = doc.getCurrentController().getViewCursor()
            cursor.gotoRange(view_cursor, False)
            frame = doc.getCurrentController().getFrame()
            dispatchHelper.executeDispatch(frame, '.uno:GoToStartOfLine', '', 0, ())
            dispatchHelper.executeDispatch(frame, '.uno:EndOfParaSel', '', 0, ())
            dispatchHelper.executeDispatch(frame, '.uno:ResetAttributes', '', 0, ())
            view_cursor.gotoRange(cursor, False)
        except:
            display_exception(self.ctx)


class ListStyleApply(unohelper.Base, XJobExecutor):
    '''
    Job to apply a list style.
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        '''
        Style current paragraph as list, argument should be one of ARABIC,
        CHARS_UPPER_LETTER, CHARS_LOWER_LETTER, ROMAN_UPPER, ROMAN_LOWER,
        or DASH.
        '''
        log = []
        try:
            style = args
            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx)
            doc = desktop.getCurrentComponent()
            text = doc.Text
            cursor = doc.getCurrentController().getViewCursor()

            # look at the previous paragraph, if it's already a list item, then
            # it's better not to create a new list, but adding to the existing
            # list; this makes it possible to repeatedly click on the "listize"
            # toolbar button over consecuting paragraphs
            previouspara_textcursor = text.createTextCursorByRange(cursor.getStart())
            previouspara_textcursor.gotoPreviousParagraph(False)
            numberingrules = None
            if previouspara_textcursor.ParaStyleName == 'Text body':
                level = previouspara_textcursor.getPropertyValue('NumberingLevel')
                numberingrules = previouspara_textcursor.getPropertyValue('NumberingRules')
                if numberingrules:
                    props = numberingrules.getByIndex(level)
                    for prop in props:
                        if prop.Name == 'NumberingType' and (
                                (prop.Value == 0 and style == 'CHARS_UPPER_LETTER') or
                                (prop.Value == 1 and style == 'CHARS_LOWER_LETTER') or
                                (prop.Value == 2 and style == 'ROMAN_UPPER') or
                                (prop.Value == 3 and style == 'ROMAN_LOWER') or
                                (prop.Value == 4 and style == 'ARABIC')):
                            # found a similar numbering rule in the
                            # previous paragraph, keep on using it
                            break
                    else:
                        numberingrules = None

            # Back to current paragraph
            textcursor = text.createTextCursorByRange(cursor.getStart())
            level = textcursor.getPropertyValue('NumberingLevel')
            if numberingrules is None:
                numberingrules = textcursor.getPropertyValue('NumberingRules')
                if numberingrules is None:
                    log.append('using new numbering rules array')
                    numberingrules = doc.createInstance('com.sun.star.text.NumberingRules')
                else:
                    log.append('using numbering rules of current paragraph')
            else:
                log.append('using numbering rules of previous paragraph')
            props = numberingrules.getByIndex(level)
            found_bullet_char_prop = False
            found_bullet_fontname_prop = False
            log.append('style level %s as %s' % (level, style))
            for i, prop in enumerate(props):
                log.append('  initial prop: %s: %s' % (prop.Name, prop.Value))
                if prop.Name == 'CharStyleName':
                    if style in ('DASH', 'BULLET'):
                        prop.Value = 'Bullet Symbols'
                    else:
                        prop.Value = 'Numbering Symbols'
                    log.append('  -> changed to %r' % prop.Value)
                elif prop.Name == 'BulletChar':
                    found_bullet_char_prop = True
                    if style == 'DASH':
                        prop.Value = '-'
                    else:
                        prop.Value = '◦'
                    log.append('  -> changed to %r' % prop.Value)
                elif prop.Name == 'NumberingType':
                    if style == 'CHARS_UPPER_LETTER':
                        prop.Value = 0
                    elif style == 'CHARS_LOWER_LETTER':
                        prop.Value = 1
                    elif style == 'ROMAN_UPPER':
                        prop.Value = 2
                    elif style == 'ROMAN_LOWER':
                        prop.Value = 3
                    elif style == 'ARABIC':
                        prop.Value = 4
                    else:
                        prop.Value = 6
                    log.append('  -> changed to %r' % prop.Value)
                elif prop.Name == 'BulletFontName':
                    if style in ('DASH', 'BULLET'):
                        prop.Value = ''
                    log.append('  -> changed to %r' % prop.Value)

            if style in ('DASH', 'BULLET'):
                props = list(props)
                if not found_bullet_char_prop:
                    if style == 'DASH':
                        props.append(makePropertyValue('BulletChar', '-'))
                    else:
                        props.append(makePropertyValue('BulletChar', '◦'))
                    log.append('  -> add BulletChar as %r' % props[-1].Value)
                if not found_bullet_fontname_prop:
                    log.append('  -> add BulletFontName')
                    props.append(makePropertyValue('BulletFontName', ''))
                props = tuple(props)

            uno.invoke(numberingrules, 'replaceByIndex',
                        (level, uno.Any("[]com.sun.star.beans.PropertyValue", props)) )
            textcursor.setPropertyValue('NumberingRules', numberingrules)
        except Exception, e:
            display_exception(self.ctx)

            log.append('\nException data:')
            log.append('%s' % type(e))
            log.append('%r' % e.__dict__)

            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx)
            doc = desktop.getCurrentComponent()
            parentwin = doc.CurrentController.Frame.ContainerWindow
            MessageBox(parentwin, '\n'.join(log), 'Debug')


class StructureError:
    '''
    Class describing an error in the structure of the document
    '''
    type = None
    paragraph_index = 0
    page_no = None
    para_style = None

    def __init__(self, type, *args):
        self.type = type
        if type == 'skipped some level':
            self.lastStyle = args[0].replace('Heading', 'Titre')
            self.currentStyle = args[1].replace('Heading', 'Titre')

    def get_short(self):
        '''Get a short summary of the error'''
        if self.type == 'same level without content':
            return u'Deux titres consécutifs sans contenu intermédiaire'
        if self.type == 'skipped some level':
            return u'Saut de titre (niveau %s à %s)' % (self.lastStyle, self.currentStyle)
        if self.type == 'went below top level':
            return u'Niveau mal placé (trop bas)'
        if self.type == 'started legistic section too low':
            return u'Les parties légistiques doivent démarrer à un niveau supérieur'
        if self.type == 'preface in wrong place':
            return u'La préface est placée à un mauvais endroit'
        if self.type == 'two prefaces':
            return u'Le document contient deux préfaces'
        if self.type == 'word-copy-paste-horizontal-line':
            return u'Ligne horizontale, vraisemblablement copié/collé'
        if self.type == 'paragraph-filled-with-nothing-but-spaces':
            return u'Paragraphe composé uniquement d\'espaces'

    def get_long(self):
        '''Get a longer description of the error'''
        if self.type == 'same level without content':
            return u'''Deux titres de même niveau se suivent et il n'y a aucun contenu entre les deux'''
        if self.type == 'skipped some level':
            return u'''Hiérarchie du document non respectée. Passage sans intermédiaire du niveau %s au niveau %s.''' % (self.lastStyle, self.currentStyle)
        if self.type == 'went below top level':
            return u'Niveau mal placé (trop bas)'
        if self.type == 'started legistic section too low':
            return u'''Erreur de hiérarchie légistique. Une partie légistique ne peut démarrer au niveau section ou sous-section. Les choix possibles sont Partie, Livre, Titre ou chapitre.'''
        if self.type == 'preface in wrong place':
            return u'''La préface doit se trouver à la base du document ou au niveau "Partie".'''
        if self.type == 'two prefaces':
            return u'Une seule préface est autorisée dans le document.'
        if self.type == 'word-copy-paste-horizontal-line':
            return u'''Une ligne horizontale provient généralement d'un '''\
                   u'''copié/collé depuis Microsoft Word d'un texte '''\
                   u'''contenant une note de bas de page.'''
        if self.type == 'paragraph-filled-with-nothing-but-spaces':
            return u'Paragraphe composé uniquement d\'espaces'


class StructureCheckListListener(unohelper.Base, XItemListener):
    '''
    Listener for the structure check dialog
    '''
    def __init__(self, dialog, doc, errors):
        self.dialog = dialog
        self.doc = doc
        self.errors = errors

    def itemStateChanged(self, event):
        try:
            label = self.dialog.getControl('detail')
            error = self.errors[event.Selected]
            label.setText(error.get_long())

            text = self.doc.Text
            cursor = text.createTextCursor()
            cursor.gotoStart(False)
            for i in range(error.paragraph_index):
                cursor.gotoNextParagraph(False)

            view_cursor = self.doc.getCurrentController().getViewCursor()
            view_cursor.gotoRange(cursor, False)
        except:
            display_exception()


class StructureCheckDialog(unohelper.Base, XActionListener):
    '''
    Dialog to display the result ot a structure check
    '''
    def __init__(self, ctx, doc, errors, continue_action = None, continue_label = u'Continuer'):
        self.ctx = ctx
        self.continue_action = continue_action
        self.continue_label = continue_label
        self.doc = doc
        self.errors = errors

    def show(self):
        smgr = self.ctx.ServiceManager

        # create the dialog model and set the properties
        dialogModel = smgr.createInstanceWithContext(
                'com.sun.star.awt.UnoControlDialogModel', self.ctx)

        dialogModel.PositionX = 100
        dialogModel.PositionY = 200
        dialogModel.Width = 210
        dialogModel.Height = 130
        dialogModel.Title = u'Analyse du document'

        listModel = addWidget(dialogModel, 'errorList', 'ListBox', 5, 5, 200, 70)

        listErrors = []
        for i, error in enumerate(self.errors):
            if error.type == 'word-copy-paste-horizontal-line':
                listErrors.append(u'Page %s: %s' % (error.page_no, error.get_short()))
            else:
                listErrors.append(u'Page %s: %s: %s' % (error.page_no,
                        error.para_style.replace('Heading', 'Titre'), error.get_short()))

        listModel.StringItemList = tuple(listErrors)

        text = addWidget(dialogModel, 'detail', 'FixedText', 5, 80, 200, 22)
        text.MultiLine = True
        text.Label = u''

        if self.continue_action:
            # continue button
            button = addWidget(dialogModel, 'continueButton', 'Button', 155, 110, 50, 14)
            button.TabIndex = 0
            button.Label = u'Continuer'

            button = addWidget(dialogModel, 'closeButton', 'Button', 95, 110, 50, 14)
            button.TabIndex = 1
            button.Label = u'Annuler'
        else:
            button = addWidget(dialogModel, 'closeButton', 'Button', 155, 110, 50, 14)
            button.TabIndex = 0
            button.Label = u'Fermer'

        # create the dialog control and set the model
        controlContainer = smgr.createInstanceWithContext(
                'com.sun.star.awt.UnoControlDialog', self.ctx)
        controlContainer.setModel(dialogModel)

        if self.continue_action:
            controlContainer.getControl('continueButton').setActionCommand('continue')
            controlContainer.getControl('continueButton').addActionListener(self)
        controlContainer.getControl('closeButton').addActionListener(self)

        controlContainer.getControl('errorList').addItemListener(
                StructureCheckListListener(controlContainer, self.doc, self.errors))
        self.dialog = controlContainer

        # create a peer
        toolkit = smgr.createInstanceWithContext(
                'com.sun.star.awt.ExtToolkit', self.ctx)

        controlContainer.setVisible(False)
        controlContainer.createPeer(toolkit, None)
        controlContainer.setVisible(True)

    def actionPerformed(self, actionEvent):
        self.dialog.setVisible(False)
        self.dialog.dispose()

        if actionEvent.ActionCommand == 'continue':
            self.continue_action(self.doc)


def check_structure(doc):
    '''Check structure of the document, returns a list of errors'''
    errors = []

    text = doc.Text
    view_cursor = doc.getCurrentController().getViewCursor()
    cursor = text.createTextCursor()
    cursor.gotoStart(False)

    lastStyle = None
    lastLegisticStyle = None
    seenContent = False
    seenLegiContent = False
    topLevel = 0
    topLegisticLevel = 0

    styles = ['Partie', 'Chap', 'Sec 1', 'Sec 1.1', 'Sec 1.1.1', 'Sec 1.1.1.1', 'Ss-Titre']
    legistic_styles = ['Lpart', 'LLivre', 'Ltitre', 'Lchapitre', 'Lsection', 'Lsoussection']

    paragraph_index = 0
    seen_preface = False
    while True:
        error = None
        currentStyle = cursor.ParaStyleName
        if currentStyle in styles:
            if lastStyle:
                last_level = styles.index(lastStyle)
                new_level = styles.index(currentStyle)
                if new_level == last_level:
                    if not seenContent:
                        debug_print('lastStyle:', lastStyle)
                        debug_print('currentStyle:', currentStyle)
                        # 'same level without content' check has been disabled
                        # http://bugzilla.entrouvert.org/show_bug.cgi?id=56
                        #error = StructureError('same level without content')
                    seenContent = False
                elif new_level == last_level+1:
                    # ok
                    seenContent = False
                elif new_level > last_level+1:
                    debug_print('skipped some level', lastStyle, currentStyle)
                    error = StructureError('skipped some level', lastStyle, currentStyle)
                    seenContent = False
                elif new_level < topLevel:
                    error = StructureError('went below top level')
                    seenContent = False
            lastStyle = currentStyle
            lastLegisticStyle = None
            topLegisticLevel = None
        elif currentStyle in legistic_styles:
            if lastLegisticStyle:
                last_legistic_level = legistic_styles.index(lastLegisticStyle)
                new_legistic_level = legistic_styles.index(currentStyle)
                if new_legistic_level == last_legistic_level:
                    if not seenLegiContent:
                        error = StructureError('same level without content')
                    seenLegiContent = False
                elif new_legistic_level == last_legistic_level+1:
                    # ok
                    seenLegiContent = False
                elif new_legistic_level > last_legistic_level+1:
                    debug_print('skipped some level', lastStyle, currentStyle)
                    error = StructureError('skipped some level', lastStyle, currentStyle)
                    seenLegiContent = False
                elif new_legistic_level < topLegisticLevel:
                    error = StructureError('went below top level')
                    seenLegiContent = False
            else:
                # first time in a legistic section
                new_legistic_level = legistic_styles.index(currentStyle)
                if new_legistic_level > legistic_styles.index('Lchapitre'):
                    error = StructureError('started legistic section too low')
                topLegisticLevel =  new_legistic_level
            lastLegisticStyle = currentStyle
        elif currentStyle == 'TitrePreface':
            # preface can only happen <book> or <part>
            if lastStyle:
                last_level = styles.index(lastStyle)
                if last_level > 0:
                    error = StructureError('preface in wrong place')
            if seen_preface:
                error = StructureError('two prefaces')
            seen_preface = True
        if currentStyle == 'Horizontal Line':
            # this is added when copy/pasting footnotes from Microsoft Word
            error = StructureError('word-copy-paste-horizontal-line')
        else:
            if currentStyle not in ('Standard', 'Larttitre'):
                debug_print ('unknown style:', currentStyle)
            seenContent = True
            seenLegiContent = True

        cursor.gotoEndOfParagraph(True)
        s = cursor.String.replace(' ', '').replace('\t', '').replace(u'\xa0', '')
        if cursor.String and s == '': # paragraph filled with nothing but spaces
            #error = StructureError('paragraph-filled-with-nothing-but-spaces')
            # do not mark this as an error, they will be ignored automatically
            # by odf2legi
            pass

        if error:
            view_cursor.gotoRange(cursor, False)
            error.paragraph_index = paragraph_index
            error.page_no = view_cursor.getPage()
            error.para_style = cursor.ParaStyleName
            errors.append(error)
        paragraph_index += 1
        if not cursor.gotoNextParagraph(False):
            break

    return errors


class StructureCheck(unohelper.Base, XJobExecutor):
    '''
    Job to perform a structure check
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        try:
            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                    "com.sun.star.frame.Desktop", self.ctx )
            self.doc = desktop.getCurrentComponent()
            self.errors = check_structure(self.doc)

            if self.errors:
                text = self.doc.Text
                cursor = text.createTextCursor()
                view_cursor = self.doc.getCurrentController().getViewCursor()
                cursor.gotoStart(False)
                for i in range(self.errors[0].paragraph_index):
                    cursor.gotoNextParagraph(False)
                view_cursor.gotoRange(cursor, False)

                dialog = StructureCheckDialog(self.ctx, self.doc, self.errors)
                dialog.show()
            else:
                parentwin = self.doc.CurrentController.Frame.ContainerWindow
                return MessageBox(parentwin,
                        u'Vérification de la structure terminée',
                        'Tabellio', 'infobox')
        except:
            display_exception(self.ctx)


class SpeakerDialog(unohelper.Base, XJobExecutor):
    '''
    Job to display an insert speaker dialog
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        '''
        Arg should be one of deputes, ministres, new, commissions
        '''
        ctx = self.ctx
        desktop = self.ctx.ServiceManager.createInstanceWithContext(
                'com.sun.star.frame.Desktop', self.ctx)
        doc = desktop.getCurrentComponent()

        if not args in ('deputes', 'ministres', 'new', 'commissions'):
            parentwin = doc.CurrentController.Frame.ContainerWindow
            return MessageBox(parentwin, 'Liste inconnue', 'Alerte', "infobox")

        try:
            if args not in ('deputes', 'ministres'):
                parentwin = doc.CurrentController.Frame.ContainerWindow
                return MessageBox(parentwin, 'Liste inconnue', 'Alerte', "infobox")

            smgr = ctx.ServiceManager

            # create the dialog model and set the properties
            dialogModel = smgr.createInstanceWithContext(
                    'com.sun.star.awt.UnoControlDialogModel', ctx)

            dialogModel.PositionX = 100
            dialogModel.PositionY = 200
            dialogModel.Width = 155
            dialogModel.Height = 50

            if args == 'ministres':
                dialogModel.Title = u"Insertion Ministres et Présidents"
            elif args == 'deputes':
                dialogModel.Title = u"Insertion Députés"

            # create the button model and set the properties
            button = addWidget(dialogModel, 'insertButton', 'Button', 50, 30, 50, 14)
            button.TabIndex = 0
            button.Label = u"Insérer"

            text = addWidget(dialogModel, 'label0', 'FixedText', 10, 10, 100, 14)
            text.Label = u'Orateur :'

            combobox = addWidget(dialogModel, 'insertWidget', 'ComboBox', 50, 7, 100, 14)
            combobox.Dropdown = True

            if args == 'deputes':
                values = Deputy.values(ctx)
            elif args == 'ministres':
                values = get_min_pres_menu_items(ctx)

            combobox.StringItemList = tuple([x.get_name() for x in values])

            # create the dialog control and set the model
            controlContainer = smgr.createInstanceWithContext(
                    'com.sun.star.awt.UnoControlDialog', ctx)
            controlContainer.setModel(dialogModel)

            # add the action listener
            controlContainer.getControl('insertButton').addActionListener(
                InsertSpeakerDlgListener(self.ctx, doc, combobox, controlContainer, values))

            toolkit = smgr.createInstanceWithContext('com.sun.star.awt.ExtToolkit', ctx)
            controlContainer.setVisible(False)
            controlContainer.createPeer(toolkit, None)
            controlContainer.execute()
            controlContainer.dispose()
        except:
            display_exception(self.ctx)


class InsertManualSpeakerDlgListener(unohelper.Base, XActionListener):
    '''
    Listener for the manual speaker insert dialog
    '''
    def __init__(self, ctx, doc, entry, dialog):
        self.ctx = ctx
        self.entry = entry
        self.doc = doc
        self.dialog = dialog

    def actionPerformed(self, actionEvent):
        try:
            cursor = self.doc.getCurrentController().getViewCursor()
            string = self.entry.Text

            annotation = self.doc.createInstance('com.sun.star.text.TextField.Annotation')
            annotation.setPropertyValue('Author', '')
            text = 'type: TABELLIO\nclassname: SPEAKER'
            annotation.setPropertyValue('Content', text)
            self.doc.Text.insertTextContent(cursor, annotation, False)
            cursor.setPropertyValue('CharWeight', BOLD)
            self.doc.Text.insertString(cursor, string.replace(' ', u' '), 0)
            self.doc.Text.insertString(cursor, u'. – ', 0)
            cursor.setPropertyValue('CharWeight', NORMAL)
            self.dialog.endExecute()
        except:
            display_exception(self.ctx)

class ManualSpeakerDialog(unohelper.Base, XJobExecutor):
    '''
    Dialog for manual speaker insert
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        ctx = self.ctx
        desktop = self.ctx.ServiceManager.createInstanceWithContext(
                'com.sun.star.frame.Desktop', self.ctx)
        doc = desktop.getCurrentComponent()

        try:
            smgr = ctx.ServiceManager

            # create the dialog model and set the properties
            dialogModel = smgr.createInstanceWithContext(
                    'com.sun.star.awt.UnoControlDialogModel', ctx)

            dialogModel.PositionX = 100
            dialogModel.PositionY = 200
            dialogModel.Width = 155
            dialogModel.Height = 50
            dialogModel.Title = u'Insertion d\'un orateur manuel'

            # create the button model and set the properties
            button = addWidget(dialogModel, 'insertButton', 'Button', 50, 30, 50, 14)
            button.TabIndex = 0
            button.Label = u"Insérer"

            text = addWidget(dialogModel, 'label0', 'FixedText', 10, 10, 100, 14)
            text.Label = u'Orateur :'

            entry = addWidget(dialogModel, 'insertWidget', 'Edit', 50, 7, 100, 14)
            entry.MultiLine = False

            # create the dialog control and set the model
            controlContainer = smgr.createInstanceWithContext(
                'com.sun.star.awt.UnoControlDialog', ctx)
            controlContainer.setModel(dialogModel)

            # add the action listener
            controlContainer.getControl('insertButton').addActionListener(
                InsertManualSpeakerDlgListener(self.ctx, doc, entry, controlContainer))

            toolkit = smgr.createInstanceWithContext('com.sun.star.awt.ExtToolkit', ctx)
            controlContainer.setVisible(False)
            controlContainer.createPeer(toolkit, None)

            controlContainer.execute()
            controlContainer.dispose()
        except:
            display_exception(self.ctx)


def getConfigAccess(ctx, cNodePath, bWriteAccess=False):
    oConfigProvider = ctx.ServiceManager.createInstance(
            'com.sun.star.configuration.ConfigurationProvider')

    if bWriteAccess:
        cServiceName = 'com.sun.star.configuration.ConfigurationUpdateAccess'
    else:
        cServiceName = 'com.sun.star.configuration.ConfigurationAccess'

    oConfigAccess = oConfigProvider.createInstanceWithArguments( cServiceName,
            ( makePropertyValue( 'nodepath', cNodePath ),))

    return oConfigAccess


def set_toolbar_visibility(ctx, visibility):
    oConfigAccess = getConfigAccess(ctx, '/org.openoffice.Office.Addons/AddonUI')
    oElement = oConfigAccess.getByName('OfficeToolBar')
    toolbar_names = oElement.getElementNames()

    desktop = ctx.ServiceManager.createInstanceWithContext(
            'com.sun.star.frame.Desktop', ctx)
    doc = desktop.getCurrentComponent()
    if not doc:
        components = desktop.getComponents().createEnumeration()
        while components.hasMoreElements():
            try:
                doc = components.nextElement()
            except NoSuchElementException:
                break 
            break

    layout_manager = doc.CurrentController.Frame.LayoutManager

    if visibility is False:
        for toolbar in toolbar_names:
            layout_manager.hideElement('private:resource/toolbar/addon_%s' % toolbar)
    else:
        for toolbar in toolbar_names:
            layout_manager.showElement('private:resource/toolbar/addon_%s' % toolbar)
        try:
            President.get_nodes(ctx)
        except DownloadError, e:
            parentwin = doc.CurrentController.Frame.ContainerWindow
            MessageBox(parentwin,
                    u'Les listes d\'orateurs n\'ont pu être chargées (problème de réseau?)',
                    'Tabellio', 'errorbox')


class ShowHideToolbars(unohelper.Base, XJobExecutor):
    '''
    Job to display or hide toolbars
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        ctx = self.ctx
        try:
            set_toolbar_visibility(ctx, args != 'hide')
        except:
            display_exception(self.ctx)


class AboutDialog(unohelper.Base, XJobExecutor):
    '''
    Job to display an about Tabellio dialog
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def getLogoUrl(self):
        oConfigAccess = getConfigAccess(self.ctx,
                '/org.entrouvert.openoffice.tabellio.FileLocations', False)
        exp = self.ctx.getValueByName('/singletons/com.sun.star.util.theMacroExpander')
        path = oConfigAccess.getByName('ImageLogo')
        url = exp.expandMacros(path)
        return url


    def trigger(self, args):
        ctx = self.ctx
        try:
            smgr = self.ctx.ServiceManager
            dialogModel = smgr.createInstanceWithContext(
                    'com.sun.star.awt.UnoControlDialogModel', self.ctx)
            dialogModel.Width = 210
            dialogModel.Height = 100
            dialogModel.Title = u'À propos de Tabellio'

            logo = addWidget(dialogModel, 'logo', 'ImageControl', 5, 5, 155/2, 155/2)
            logo.ImageURL = self.getLogoUrl()

            text = addWidget(dialogModel, 'title', 'FixedText', 5 + 155/2 + 5, 5, 100, 25)
            text.Label = u'Tabellio³'
            fdesc = FontDescriptor()
            fdesc.Weight = 200
            fdesc.Height = 16
            text.FontDescriptor = fdesc

            # close button
            button = addWidget(dialogModel, 'closeButton', 'Button', 155, 80, 50, 14)
            button.TabIndex = 0
            button.Label = u'Fermer'

            # create the dialog control and set the model
            controlContainer = smgr.createInstanceWithContext(
                    'com.sun.star.awt.UnoControlDialog', self.ctx)
            controlContainer.setModel(dialogModel)

            controlContainer.getControl('closeButton').addActionListener(
                    CloseListener(controlContainer))

            # create a peer
            toolkit = smgr.createInstanceWithContext(
                    'com.sun.star.awt.ExtToolkit', ctx)

            controlContainer.setVisible(False)
            controlContainer.createPeer(toolkit, None)

            # execute it
            controlContainer.execute()

            # dispose the dialog
            controlContainer.dispose()

        except:
            display_exception(self.ctx)

def addWidget(dialog, name, type, x, y, w, h):
    '''Utility function to add a widget to a dialog'''
    widget = dialog.createInstance('com.sun.star.awt.UnoControl%sModel' % type)
    widget.Name = name
    widget.PositionX = x
    widget.PositionY = y
    widget.Width = w
    widget.Height = h
    dialog.insertByName(name, widget)
    return widget


class ConfigurationDlgListener(unohelper.Base, XActionListener):
    '''
    Listener for the configuration dialog
    '''
    def __init__(self, ui):
        self.ui = ui

    def actionPerformed(self, actionEvent):
        try:
            oConfigAccess = getConfigAccess(self.ui.ctx,
                    '/org.entrouvert.openoffice.tabellio.Configuration', True)

            oConfigAccess.PreviewServerURL = self.ui.preview_server_url.Text
            oConfigAccess.InsertsRootURL = self.ui.inserts_root_url.Text
            oConfigAccess.ProxyServerURL = self.ui.proxy_server_url.Text
            oConfigAccess.Mode = self.ui.mode.Text
            oConfigAccess.commitChanges()

            self.ui.dialog.endExecute()

        except:
            display_exception(self.ui.ctx)

class ConfigurationDialog(unohelper.Base, XJobExecutor):
    '''
    Configuration dialog
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        ctx = self.ctx
        try:
            smgr = self.ctx.ServiceManager

            desktop = smgr.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx )
            self.doc = doc = desktop.getCurrentComponent()

            dialogModel = smgr.createInstanceWithContext(
                    'com.sun.star.awt.UnoControlDialogModel', self.ctx)
            dialogModel.Width = 140
            dialogModel.Height = 140
            dialogModel.Title = u'Configuration'

            label = addWidget(dialogModel, 'previewServerUrlLabel', 'FixedText', 5, 5, 100, 10)
            label.Label = u'URL du serveur de prévisualisation'
            self.preview_server_url = addWidget(dialogModel, 
                    'preview_server_url', 'Edit', 10, 15, 125, 14)

            label = addWidget(dialogModel, 'insertsRootUrlLabel', 'FixedText', 5, 35, 100, 10)
            label.Label = u'URL racine pour les insertions'
            self.inserts_root_url = addWidget(dialogModel, 
                    'inserts_root_url', 'Edit', 10, 45, 125, 14)

            label = addWidget(dialogModel, 'proxyServerUrlLabel', 'FixedText', 5, 65, 100, 10)
            label.Label = u'URL du proxy'
            self.proxy_server_url = addWidget(dialogModel, 
                    'proxy_server_url', 'Edit', 10, 75, 125, 14)

            label = addWidget(dialogModel, 'modeLabel', 'FixedText', 5, 95, 100, 10)
            label.Label = u'Mode'
            self.mode = addWidget(dialogModel,
                    'mode', 'ComboBox', 10, 105, 125, 14)
            self.mode.Dropdown = True
            self.mode.StringItemList = ((u'PCF', u'PFB'))
            self.mode.Text = self.mode.StringItemList[0]

            oConfigAccess = getConfigAccess(self.ctx,
                    '/org.entrouvert.openoffice.tabellio.Configuration', False)

            self.preview_server_url.Text = oConfigAccess.getByName('PreviewServerURL')
            self.inserts_root_url.Text = oConfigAccess.getByName('InsertsRootURL')
            self.proxy_server_url.Text = oConfigAccess.getByName('ProxyServerURL')
            self.mode.Text = oConfigAccess.getByName('Mode')

            # save button
            saveButton = addWidget(dialogModel, 'saveButton', 'Button', 85, 125, 50, 14)
            saveButton.Label = u'Enregistrer'

            # cancel button
            cancelButton = addWidget(dialogModel, 'cancelButton', 'Button', 30, 125, 50, 14)
            cancelButton.Label = u'Annuler'

            # create the dialog control and set the model
            self.dialog = controlContainer = smgr.createInstanceWithContext(
                'com.sun.star.awt.UnoControlDialog', self.ctx)
            controlContainer.setModel(dialogModel)

            controlContainer.getControl('cancelButton').addActionListener(
                    CloseListener(controlContainer))
            controlContainer.getControl('saveButton').addActionListener(
                    ConfigurationDlgListener(self))

            # create a peer
            toolkit = smgr.createInstanceWithContext(
                    'com.sun.star.awt.ExtToolkit', ctx)

            controlContainer.setVisible(False)
            controlContainer.createPeer(toolkit, None)

            # execute it
            controlContainer.execute()

            # dispose the dialog
            controlContainer.dispose()

        except:
            display_exception(self.ctx)


class InsertVote(unohelper.Base, XJobExecutor):
    '''
    Job to insert the result of a vote
    '''

    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        try:
            smgr = self.ctx.ServiceManager
            desktop = smgr.createInstanceWithContext('com.sun.star.frame.Desktop', self.ctx)
            doc = desktop.getCurrentComponent()

            filepicker = smgr.createInstance('com.sun.star.ui.dialogs.FilePicker')
            filepicker.appendFilter('*.txt', '*.txt')
            if filepicker.execute() == 0: # cancel
                return

            filenames = filepicker.getFiles()
            if not filenames:
                return

            # OOo returns the filenames as RFC 1738 URLs
            filename = normalize_filename(filenames[0])
            self.insert_vote_result(filename, doc)
        except:
            display_exception(self.ctx)

    def format_list_of_deputes(self, deputes):
        '''Format a list of deputies, to have them grouped by gender and
           prefixed by the appropriate titles. (e.g. MM. for a group of
           mens)'''
        deputes.sort(lambda x,y: cmp(x.name.lower(), y.name.lower()))
        deputes.reverse()

        r = []
        if len(deputes) == 2:
            # special case, join them with 'et'
            depute1 = deputes[0]
            depute2 = deputes[1]
            if depute1.sexe == depute2.sexe:
                if depute1.sexe == 'M':
                    r.append('MM. ')
                else:
                    r.append('Mmes ')
                r.append('%s %s' % (depute1.name, depute1.firstname))
                r.append(' et ')
                r.append('%s %s' % (depute2.name, depute2.firstname))
            else:
                if depute1.sexe == 'M':
                    title = 'M.'
                else:
                    title = 'Mme'
                r.append('%s %s %s' % (title, depute1.name, depute1.firstname))

                r.append(' et ')

                if depute2.sexe == 'M':
                    title = 'M.'
                else:
                    title = 'Mme'
                r.append('%s %s %s' % (title, depute2.name, depute2.firstname))
            r.append('.')

            return ''.join(r)

        while deputes:
            depute = deputes.pop()
            if r:
                r.append(', ')
            if len(deputes) == 0 or deputes[-1].sexe != depute.sexe:
                if depute.sexe == 'M':
                    title = 'M.'
                else:
                    title = 'Mme'
                r.append('%s %s %s' % (title, depute.name, depute.firstname))
            else:
                if depute.sexe == 'M':
                    r.append('MM. ')
                else:
                    r.append('Mmes ')
                r.append('%s %s' % (depute.name, depute.firstname))
                while deputes and deputes[-1].sexe == depute.sexe:
                    depute = deputes.pop()
                    r.append(', %s %s' % (depute.name, depute.firstname))

        r.append('.')

        return ''.join(r)

    def insert_vote_result(self, filename, doc):
        '''Insert the result of a vote (taken from a text file) in the current
           document.'''
        cursor = doc.getCurrentController().getViewCursor()

        deputies_dict = {}
        for d in Deputy.values(self.ctx):
            deputies_dict[d.id] = d

        votes_by_deputy = {}
        missing_deputies = []

        for line_no, vote in enumerate(csv.reader(file(filename), delimiter = '\t')):
            parti = vote[3]
            lastname = unicode(vote[4], 'iso-8859-1')
            firstname = unicode(vote[5], 'iso-8859-1')
            type = vote[6] # 0, +, - or AB

            dep_id = Deputy.get_deputy_id(firstname, lastname, ctx=self.ctx)
            if dep_id is None:
                # it was not possible to find this person, keep track of this,
                # it will be used to display a dialog at the end.
                missing_deputies.append((line_no+1, '%s %s' % (firstname, lastname)))
                continue
            votes_by_deputy[dep_id] = type

        # count the different kind of votes
        nb_yes = len([x for x in votes_by_deputy if votes_by_deputy.get(x) == '+'])
        nb_no = len([x for x in votes_by_deputy if votes_by_deputy.get(x) == '-'])
        nb_abst = len([x for x in votes_by_deputy if votes_by_deputy.get(x) == 'AB'])
        nb_null = len([x for x in votes_by_deputy if votes_by_deputy.get(x) == '0'])

        if nb_yes == 0:
            str_yes = u'Aucun membre n\'a répondu oui.'
        elif nb_yes == 1:
            str_yes = u'1 membre a répondu oui.'
        else:
            str_yes = u'%s membres ont répondu oui.' % nb_yes

        if nb_no == 0:
            str_no = u'Aucun membre n\'a répondu non.'
        elif nb_no == 1:
            str_no = u'1 membre a répondu non.'
        else:
            str_no = u'%s membres ont répondu non.' % nb_no

        if nb_abst == 0:
            str_abst = u'Aucun membre ne s\'est abstenu.'
        elif nb_abst == 1:
            str_abst = u'1 membre s\'est abstenu.'
        else:
            str_abst = u'%s membres se sont abstenus.' % nb_abst

        def insert_string(str):
            cursor.Text.insertString(cursor, str, False)

        insert_string(u'%s membres ont pris part au vote.' % sum((nb_yes, nb_no, nb_abst)))
        cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)


        if nb_no + nb_abst + nb_null == 0:
            # -> everyone agree, the text is adopted
            insert_string(u'Tous ont répondu oui.')
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            insert_string(u'''En conséquence, [mettre ici l'objet du vote] est adopté. Il sera soumis à la sanction du Gouvernement de la Communauté française. / est adopté et l'article est modifié.''')
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            deputes = [deputies_dict[x] for x in votes_by_deputy]
            insert_string(u'Ont pris part au vote:')
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            insert_string(self.format_list_of_deputes(deputes))
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

        elif nb_yes > nb_no:
            # -> the text is adopted
            insert_string(str_yes)
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            if nb_no:
                insert_string(str_no)
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            if nb_abst:
                insert_string(str_abst)
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            insert_string(u'''En conséquence, [mettre ici l'objet du vote] est adopté. Il sera soumis à la sanction du Gouvernement de la Communauté française. / est adopté et l'article est modifié.''')
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            deputes = [deputies_dict[x] for x in votes_by_deputy if votes_by_deputy.get(x) == '+']
            insert_string(u'Ont répondu oui:')
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            insert_string(self.format_list_of_deputes(deputes))
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            deputes = [deputies_dict[x] for x in votes_by_deputy if votes_by_deputy.get(x) == '-']
            if deputes:
                insert_string(u'Ont répondu non:')
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
                insert_string(self.format_list_of_deputes(deputes))
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            deputes = [deputies_dict[x] for x in votes_by_deputy if votes_by_deputy.get(x) == 'AB']
            if deputes:
                insert_string(u'Se sont abstenus:')
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
                insert_string(self.format_list_of_deputes(deputes))
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

        elif nb_no > nb_yes:
            # -> the text is rejected
            insert_string(str_no)
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            if nb_yes:
                insert_string(str_yes)
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            if nb_abst:
                insert_string(str_abst)
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            insert_string(u'''En conséquence, [mettre ici l'objet du vote] est rejeté. / est rejeté. L'article est adopté.''')
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            deputes = [deputies_dict[x] for x in votes_by_deputy if votes_by_deputy.get(x) == '-']
            insert_string(u'Ont répondu non:')
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            insert_string(self.format_list_of_deputes(deputes))
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            deputes = [deputies_dict[x] for x in votes_by_deputy if votes_by_deputy.get(x) == '+']
            if deputes:
                insert_string(u'Ont répondu oui:')
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
                insert_string(self.format_list_of_deputes(deputes))
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

            deputes = [deputies_dict[x] for x in votes_by_deputy if votes_by_deputy.get(x) == 'AB']
            if deputes:
                insert_string(u'Se sont abstenus:')
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
                insert_string(self.format_list_of_deputes(deputes))
                cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)

        if missing_deputies:
            # there were persons that could not be found, notify the user now
            parentwin = doc.CurrentController.Frame.ContainerWindow
            s = '\n'.join('%s (%s)' % x for x in missing_deputies)
            MessageBox(parentwin,
                u'''Attention, des lignes n'ont pu être insérées:\n%s''' % s,
                'Tabellio', 'infobox')


class InsertStandardTextDialog(unohelper.Base, XActionListener):
    '''
    Dialog to insert a standard snippet of text
    '''
    def __init__(self, ctx, doc):
        self.ctx = ctx
        self.doc = doc

    def show(self):
        smgr = self.ctx.ServiceManager

        dialogModel = smgr.createInstanceWithContext(
                'com.sun.star.awt.UnoControlDialogModel', self.ctx)
        dialogModel.Width = 210
        dialogModel.Height = 200
        dialogModel.Title = u"Insertion de texte standard"

        self.snippets = SnippetDoc.values(self.ctx)
        self.listbox = addWidget(dialogModel, 'listbox', 'ListBox', 5, 5, 200, 170)
        self.listbox.StringItemList = tuple([x.title for x in self.snippets])

        button = addWidget(dialogModel, 'insertButton', 'Button', 155, 180, 50, 14)
        button.TabIndex = 0
        button.DefaultButton = True
        button.Label = u'Insérer'

        button = addWidget(dialogModel, 'closeButton', 'Button', 95, 180, 50, 14)
        button.TabIndex = 1
        button.Label = u'Annuler'

        # create the dialog control and set the model
        controlContainer = smgr.createInstanceWithContext(
                'com.sun.star.awt.UnoControlDialog', self.ctx)
        controlContainer.setModel(dialogModel)
        self.dialog = controlContainer

        controlContainer.getControl('insertButton').setActionCommand('insert')
        controlContainer.getControl('insertButton').addActionListener(self)
        controlContainer.getControl('closeButton').addActionListener(self)

        toolkit = smgr.createInstanceWithContext('com.sun.star.awt.ExtToolkit', self.ctx)
        controlContainer.setVisible(False)
        controlContainer.createPeer(toolkit, None)
        controlContainer.execute()
        controlContainer.dispose()

    def actionPerformed(self, actionEvent):
        try:
            if actionEvent.ActionCommand == 'insert':
                snippets = SnippetDoc.values(self.ctx)
                if self.listbox.SelectedItems:
                    snippet = self.snippets[self.listbox.SelectedItems[0]]
                    text = self.doc.Text
                    cursor = text.createTextCursorByRange(
                            self.doc.getCurrentController().getViewCursor().getStart())

                    oConfigAccess = getConfigAccess(self.ctx,
                            '/org.entrouvert.openoffice.tabellio.Configuration', False)
                    inserts_base_url = oConfigAccess.getByName('InsertsRootURL')
                    if type(snippet.filename) is unicode:
                        snippet.filename = snippet.filename.encode('utf-8')
                    document_url = inserts_base_url + urllib2.quote(snippet.filename)

                    filename = download(self.ctx, document_url)
                    url = unohelper.systemPathToFileUrl(os.path.abspath(filename))

                    cursor.insertDocumentFromURL(url, ())
            self.dialog.endExecute()
        except:
            display_exception(self.ctx)


class InsertStandardText(unohelper.Base, XJobExecutor):
    '''
    Job to display the dialog to insert a standard snippet of text
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        try:
            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                    "com.sun.star.frame.Desktop", self.ctx )
            doc = desktop.getCurrentComponent()
            dialog = InsertStandardTextDialog(self.ctx, doc)
            dialog.show()
        except:
            display_exception(self.ctx)


class ImportDalet(unohelper.Base, XJobExecutor):
    '''
    Job to insert a serie of documents from dalet
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        try:
            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx)
            doc = desktop.getCurrentComponent()

            filepicker = self.ctx.ServiceManager.createInstance(
                    'com.sun.star.ui.dialogs.FilePicker')
            filepicker.appendFilter('*.xml', '*.xml')
            if filepicker.execute() == 0: # cancel
                return

            filenames = filepicker.getFiles()
            if not filenames:
                return

            filename = normalize_filename(filenames[0])

            doc = desktop.getCurrentComponent()
            self.import_dalet(filename, doc)

        except:
            display_exception(self.ctx)

    def import_dalet(self, filename, doc):
        dalet = xml.dom.minidom.parseString(open(filename).read())

        paths = []
        for transcription in dalet.getElementsByTagName('FrenchTranscription'):
            fullpath = transcription.getElementsByTagName('FullPath')[0]
            clipcaption = get_text_node_content(
                            transcription.parentNode.getElementsByTagName('ClipCaption')[0])
            document_url = get_text_node_content(fullpath)
            if document_url.startswith('\\\\'):
                # on a windows share
                document_url = 'file://' + document_url[2:].replace('\\', '/')
            paths.append((clipcaption, document_url))

        paths.sort()

        cursor = doc.Text.createTextCursor()
        for caption, document_url in paths:
            cursor.gotoEnd(False)
            cursor.insertDocumentFromURL(document_url, ())
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
            # insert the caption as a review note
            cursor.setPropertyValue('CharBackColor', 16776960)
            cursor.Text.insertString(cursor, '--- %s ---' % caption, False)
            cursor.setPropertyValue('CharBackColor', -1)
            cursor.Text.insertControlCharacter(cursor, APPEND_PARAGRAPH, False)
        cursor.gotoEnd(False)


class Preview(unohelper.Base, XJobExecutor):
    '''
    Job to perform a preview of the current document
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        ctx = self.ctx
        try:
            smgr = self.ctx.ServiceManager

            desktop = smgr.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx )
            doc = desktop.getCurrentComponent()

            errors = check_structure(doc)
            if errors:
                dialog = StructureCheckDialog(self.ctx, doc, errors,
                        continue_action = self.preview, continue_label = 'Plop')
                dialog.show()
            else:
                self.preview(doc)

        except:
            display_exception(self.ctx)

    def preview(self, doc):
        parentwin = doc.CurrentController.Frame.ContainerWindow

        # saving odt to local file
        temp_file = os.path.join(tempfile.gettempdir(), 'preview.odt')
        doc.storeToURL(unohelper.systemPathToFileUrl(temp_file), ())

        oConfigAccess = getConfigAccess(self.ctx,
                '/org.entrouvert.openoffice.tabellio.Configuration', False)
        href = oConfigAccess.getByName('PreviewServerURL')

        try:
            url = get_url_opener(self.ctx).open(href, data = file(temp_file, 'rb').read())
            s = url.read()
        except socket.timeout, e:
            parentwin = doc.CurrentController.Frame.ContainerWindow
            return MessageBox(parentwin, 'Timeout sur le serveur', 'Alerte', 'infobox')

        if url.headers['Content-type'] == 'text/plain':
            # error
            return MessageBox(parentwin, s, 'Alerte', 'infobox')

        temp_pdf_file = os.path.join(tempfile.gettempdir(),
                        'tabellio-preview-%s.pdf' % random.randint(100000, 999999))
        file(temp_pdf_file, 'wb').write(s)

        if sys.platform.startswith('win'):
            os.system('start file://%s' % temp_pdf_file)
        else:
            os.system('xdg-open %s &' % temp_pdf_file)


class ExportAsLegi(unohelper.Base, XJobExecutor):
    '''
    Job to export current document as legi
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        ctx = self.ctx
        try:
            smgr = self.ctx.ServiceManager

            desktop = smgr.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx )
            doc = desktop.getCurrentComponent()

            errors = check_structure(doc)
            if errors:
                dialog = StructureCheckDialog(self.ctx, doc, errors,
                        continue_action = self.do_export, continue_label = 'Plop')
                dialog.show()
            else:
                self.do_export(doc)

        except:
            display_exception(self.ctx)

    def do_export(self, doc):
        parentwin = doc.CurrentController.Frame.ContainerWindow

        smgr = self.ctx.ServiceManager
        filepicker = smgr.createInstance('com.sun.star.ui.dialogs.FilePicker')
        filepicker.initialize((FILESAVE_SIMPLE,))
        filepicker.appendFilter('*.legi', '*.legi')
        if filepicker.execute() == 0: # cancel
            return
        filenames = filepicker.getFiles()

        if not filenames:
            return
        filename = normalize_filename(filenames[0])

        # saving odt to local file
        temp_file = os.path.join(tempfile.gettempdir(), 'preview.odt')
        doc.storeToURL(unohelper.systemPathToFileUrl(temp_file), ())

        oConfigAccess = getConfigAccess(self.ctx,
                '/org.entrouvert.openoffice.tabellio.Configuration', False)
        href = oConfigAccess.getByName('PreviewServerURL') + 'legi'

        try:
            url = get_url_opener(self.ctx).open(href, data = file(temp_file, 'rb').read())
            s = url.read()
        except socket.timeout, e:
            parentwin = doc.CurrentController.Frame.ContainerWindow
            return MessageBox(parentwin, 'Timeout sur le serveur', 'Alerte', 'infobox')

        if url.headers['Content-type'] == 'text/plain':
            # error
            return MessageBox(parentwin, s, 'Alerte', 'infobox')

        file(filename, 'wb').write(s)


class PcfLogonDlg(unohelper.Base, XActionListener):
    '''
    Dialog to log on PCF document store
    '''
    def __init__(self, ctx, doc, logon_callback):
        self.ctx = ctx
        self.doc = doc
        self.logon_callback = logon_callback

    def display(self):
        try:
            smgr = self.ctx.ServiceManager

            desktop = smgr.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx)
            self.doc = doc = desktop.getCurrentComponent()

            self.dialog = dialogModel = smgr.createInstanceWithContext(
                    'com.sun.star.awt.UnoControlDialogModel', self.ctx)
            dialogModel.Width = 130
            dialogModel.Height = 55
            dialogModel.Title = u'Authentification'

            label = addWidget(dialogModel, 'usernameLabel', 'FixedText', 5, 5, 40, 15)
            label.Label = u'Identifiant'
            self.username = addWidget(dialogModel, 'username', 'Edit', 55, 2, 70, 14)

            label = addWidget(dialogModel, 'passwordLabel', 'FixedText', 5, 20, 40, 15)
            label.Label = u'Mot de passe'
            self.password = addWidget(dialogModel, 'password', 'Edit', 55, 17, 70, 14)
            self.password.EchoChar = 42

            # login button
            saveButton = addWidget(dialogModel, 'loginButton', 'Button', 75, 35, 50, 14)
            saveButton.Label = u"S'identifer"
            saveButton.DefaultButton = True

            # cancel button
            cancelButton = addWidget(dialogModel, 'cancelButton', 'Button', 20, 35, 50, 14)
            cancelButton.Label = u'Annuler'

            # create the dialog control and set the model
            self.dialog = controlContainer = smgr.createInstanceWithContext(
                'com.sun.star.awt.UnoControlDialog', self.ctx)
            controlContainer.setModel(dialogModel)

            controlContainer.getControl('cancelButton').addActionListener(
                    CloseListener(controlContainer))
            controlContainer.getControl('loginButton').addActionListener(self)

            # create a peer
            toolkit = smgr.createInstanceWithContext(
                    'com.sun.star.awt.ExtToolkit', self.ctx)

            controlContainer.setVisible(False)
            controlContainer.createPeer(toolkit, None)

            # execute it
            controlContainer.execute()

            # dispose the dialog
            controlContainer.dispose()

        except:
            display_exception(self.ctx)

    def actionPerformed(self, actionEvent):

        try:
            self.dialog.endExecute()

            cursor = self.doc.getCurrentController().getViewCursor()

            # this was captured once between the Word Add-in and the
            # tabellio procedure server
            dauth = {'username': self.username.Text,
                     'password': self.password.Text}
            data = 'verb=DispatchClassMethodXML&className=SERVICE&'\
                   'methodName=Logon&xmlRqst=%s' % urllib2.quote(
                      '<MLogon>'\
                         '<cvers>1,+0,+1,+0</cvers>'\
                         '<pwd>%(password)s</pwd>'\
                         '<usr>%(username)s</usr>'\
                      '</MLogon>' % dauth)

            props = self.doc.DocumentProperties.getUserDefinedProperties()
            post_url = props.getPropertyValue('documentUrl')
            if not post_url:
                parentwin = self.doc.CurrentController.Frame.ContainerWindow
                error = u"Métadonnée documentUrl absente du document, enregistrement impossible"
                return MessageBox(parentwin, error, 'Alerte', 'errorbox')

            href = urlparse.urlunsplit(urlparse.urlsplit(post_url)[:2] + (
                                       '/xmldispatcher', '', ''))

            try:
                get_url_opener(self.ctx).open(href, data=data)
            except (urllib2.HTTPError, urllib2.URLError), e:
                if hasattr(e, 'code') and e.code == 510:
                    s = e.fp.read()
                    if s.startswith('<error>'):
                        dom = xml.dom.minidom.parseString(s)
                        error = get_text_node_content(
                                        dom.childNodes[0].getElementsByTagName('description')[0])
                elif hasattr(e, 'code'):
                    error = u"Erreur %s à l'authentification" % e.code
                elif hasattr(e, 'reason'):
                    error = u"Erreur à l'authentification (%s)" % e.reason
                else:
                    error = u"Erreur à l'authentification"
                parentwin = self.doc.CurrentController.Frame.ContainerWindow
                return MessageBox(parentwin, error, 'Alerte', 'errorbox')

            self.logon_callback(self.doc)
        except:
            display_exception(self.ctx)


class UploadLegi(unohelper.Base, XJobExecutor):
    '''
    Job to upload current file as legi to the server
    '''

    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        try:
            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx)
            doc = desktop.getCurrentComponent()

            try:
                props = doc.DocumentProperties.getUserDefinedProperties()
                post_url = props.getPropertyValue('documentUrl')
            except: # com.sun.star.beans.UnknownPropertyException
                parentwin = doc.CurrentController.Frame.ContainerWindow
                return MessageBox(parentwin, "Pas d'adresse pour ce document.",
                                  'Alerte', 'errorbox')

            errors = check_structure(doc)
            if errors:
                dialog = StructureCheckDialog(self.ctx, doc, errors,
                        continue_action=self.do_check_auth, continue_label='Continuer')
                dialog.show()
            else:
                self.do_check_auth(doc)
        except:
            display_exception(self.ctx)

    def do_check_auth(self, doc):
        if get_mode(self.ctx) == 'PCF':
            dlg = PcfLogonDlg(self.ctx, doc, self.do_upload)
            dlg.display()
        else:
            self.do_upload(doc)

    def do_upload(self, doc):
        parentwin = doc.CurrentController.Frame.ContainerWindow

        try:
            # saving odt to local file
            temp_file = os.path.join(tempfile.gettempdir(), 'preview.odt')
            doc.storeToURL(unohelper.systemPathToFileUrl(temp_file), ()) 

            oConfigAccess = getConfigAccess(self.ctx,
                    '/org.entrouvert.openoffice.tabellio.Configuration', False)
            href = oConfigAccess.getByName('PreviewServerURL') + 'legi'

            try:
                url = get_url_opener(self.ctx).open(href, data = file(temp_file, 'rb').read())
                as_legi_string = url.read()
            except socket.timeout, e:
                return MessageBox(parentwin, 'Timeout sur le serveur', 'Alerte', 'errorbox')

            if url.headers['Content-type'] == 'text/plain':
                # error
                return MessageBox(parentwin, as_legi_string, 'Alerte', 'errorbox')

            props = doc.DocumentProperties.getUserDefinedProperties()
            post_url = props.getPropertyValue('documentUrl')

            urlopener = get_url_opener(self.ctx)
            request = urllib2.Request(post_url, data=as_legi_string)
            request.add_header('Content-Type', 'text/pcf-legi')
            request.get_method = lambda: 'PUT'
            try:
                url = urlopener.open(request)
                s = url.read()
            except socket.timeout, e:
                return MessageBox(parentwin, 'Timeout sur le serveur', 'Alerte', 'errorbox')
            except urllib2.URLError, e:
                href = urlparse.urlsplit(post_url)[1]
                error = u"L'authentification sur le serveur (%s) a échoué." % href
                return MessageBox(parentwin, error, 'Alerte', 'errorbox')

            return MessageBox(parentwin,
                            u'Document sauvegardé sur le serveur',
                            'Tabellio', 'infobox')
        except:
            display_exception(self.ctx)

class NewTabellioDocument(unohelper.Base, XJobExecutor):
    '''
    Job to create a new document based on the tabellio model
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        ctx = self.ctx
        try:
            smgr = self.ctx.ServiceManager
            desktop = smgr.createInstanceWithContext('com.sun.star.frame.Desktop', self.ctx)

            oConfigAccess = getConfigAccess(self.ctx,
                    '/org.entrouvert.openoffice.tabellio.Configuration', False)
            inserts_base_url = oConfigAccess.getByName('InsertsRootURL')
            document_url = inserts_base_url + 'tabellio.ott'
            filename = download(ctx, document_url)
            url = unohelper.systemPathToFileUrl(os.path.abspath(filename))
            desktop.loadComponentFromURL(url, '_default', 0, () )
        except:
            display_exception(self.ctx)


class SwitchReviewNoteMode(unohelper.Base, XJobExecutor):
    '''
    Job to switch back and to review mode (a simple highlighting)
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        desktop = self.ctx.ServiceManager.createInstanceWithContext(
                'com.sun.star.frame.Desktop', self.ctx)
        doc = desktop.getCurrentComponent()
        cursor = doc.getCurrentController().getViewCursor()
        current_bg = cursor.getPropertyValue('CharBackColor')
        if current_bg == -1:
            # 16776960 is yellow (red:255 << 16 | green:255 << 8 | blue:0)
            cursor.setPropertyValue('CharBackColor', 16776960)
        else:
            cursor.setPropertyValue('CharBackColor', -1)


class InsertFootnote(unohelper.Base, XJobExecutor):
    '''
    Job to insert a footnote
    '''
    def __init__(self, ctx):
        self.ctx = ctx

    def trigger(self, args):
        try:
            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                'com.sun.star.frame.Desktop', self.ctx)
            doc = desktop.getCurrentComponent()
            dispatchHelper = self.ctx.ServiceManager.createInstanceWithContext(
                    'com.sun.star.frame.DispatchHelper', self.ctx)
            dispatchHelper.executeDispatch(
                            doc.getCurrentController().getFrame(),
                            '.uno:InsertFootnote',
                            '', 0, ())
        except:
            display_exception(self.ctx)


class DoNothing(unohelper.Base, XJobExecutor):
    def __init__(self, ctx):
        pass

    def trigger(self, args):
        pass


class Dispatcher(unohelper.Base, XDispatch, XControlNotificationListener, XJobExecutor,
            XActionListener):
    '''
    Dispatcher necessary to handle complex controls in toolbars
    '''
    def __init__(self, ctx):
        self.listeners = []
        self.ctx = ctx

    def addStatusListener(self, control, url):
        if control not in self.listeners:
            # keep track of listeners, for proper removal afterwards
            self.listeners.append(control)


    def removeStatusListener(self, control, url):
        if control in self.listeners:
            i = self.listeners.index(control)
            del self.listeners[i]

    def dispatch(self, url, args):
        try:
            debug_print('received dispatch instruction on', url.Path)
            debug_print('  args:', args)
            pass
        except:
            display_exception()

    def controlEvent(self, event):
        pass

    def sendCommand(self, control, aUrl, command = None, args = None, enable = True):
        try:
            aEvent = FeatureStateEvent()
            aEvent.FeatureURL = aUrl
            aEvent.Source = self
            aEvent.IsEnabled = enable
            aEvent.Requery = False
            if command:
                aCtrlCmd = ControlCommand()
                aCtrlCmd.Command = command
                aCtrlCmd.Arguments = args
                aEvent.State = aCtrlCmd
            control.statusChanged(aEvent)
        except:
            display_exception()


class InsertDispatcher(Dispatcher):
    '''
    Dispatcher handling the insert deputy/minitre/... dropdowns in toolbars
    '''
    def __init__(self, ctx):
        Dispatcher.__init__(self, ctx)
        self.has_just_changed = False
        self.lists = {}
        self.deputy_letters = {}
        self.controls = {}
        self.ctx = ctx

    def addStatusListener(self, control, url):
        Dispatcher.addStatusListener(self, control, url)
        try:
            self.controls[url.Path] = control
            if url.Path.startswith('depute'):
                PARTS = 4 # four dropdown boxes
                part = url.Path.split('_')[1]
                parls = Deputy.values(self.ctx)
                if not parls:
                    return
                list_content = [x.get_name() for x in parls]
                list_content.sort(lambda x, y: cmp(x.lower(), y.lower()))

                naive_part_len = len(list_content) / PARTS
                for i in range(PARTS):
                    if i == 0:
                        start_letter = 'a'
                    else:
                        start_letter = string.lowercase[
                            string.lowercase.index(list_content[i*naive_part_len][0].lower())+1]
                        start_letter = chr(ord(start_letter)-1)
                    if i == (PARTS-1):
                        end_letter = 'z'
                    else:
                        end_letter = string.lowercase[
                            string.lowercase.index(list_content[(i+1)*naive_part_len][0].lower())]
                        end_letter = chr(ord(end_letter)-1)
                    if int(part) == i+1:
                        break

                list_content = [x for x in list_content if x[0].lower() >= start_letter and
                        x[0].lower() <= end_letter]

                self.deputy_letters[part] = '%s->%s' % (start_letter, end_letter)
                values = makeNamedList(tuple(list_content))
                self.sendCommand(control, url, 'SetList', values)

                self.set_dropdown_label(control, self.deputy_letters[part])

            elif url.Path == 'ministres':
                minsts = get_min_pres_menu_items(self.ctx)
                list_content = [x.get_name() for x in minsts]
                values = makeNamedList(tuple(list_content))
                self.sendCommand(control, url, 'SetList', values)
                self.set_dropdown_label(control, u'Ministres et Présidents')

            elif url.Path == 'commissions':
                elems = Commission.values(self.ctx)
                list_content = [x.get_name() for x in elems]
                values = makeNamedList(tuple(list_content))
                self.sendCommand(control, url, 'SetList', values)
                self.set_dropdown_label(control, u'Commissions')
            else:
                debug_print('Unknown Path:', url.Path)
        except:
            display_exception()

    def set_dropdown_label(self, control, str):
        aEvent = FeatureStateEvent()
        aEvent.Source = self
        aEvent.IsEnabled = True
        aEvent.State = str
        control.statusChanged(aEvent)

    def dispatch(self, url, args):
        try:
            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                    "com.sun.star.frame.Desktop", self.ctx )
            doc = desktop.getCurrentComponent()
            cursor = doc.getCurrentController().getViewCursor()
            string = args[1].Value

            if url.Path.startswith('depute') or url.Path == 'ministres':
                text = doc.Text
                text_cursor = text.createTextCursor()
                text_cursor.gotoRange(cursor, False)
                is_start_of_paragraph = text_cursor.isStartOfParagraph()

            if url.Path.startswith('depute'):
                parls = Deputy.values(self.ctx)
                t = [x for x in parls if x.get_name() == string]
                if not t:
                    raise Exception('Unknown Deputy')
                parl = t[0]
                if is_start_of_paragraph:
                    parl.insert_as_speaker(self.ctx, doc, cursor)
                else:
                    parl.insert(self.ctx, doc, cursor)

            elif url.Path == 'ministres':
                minsts = get_min_pres_menu_items(self.ctx)
                t = [x for x in minsts if x.get_name() == string]
                if not t:
                    raise Exception('Unknown Ministres')
                minist = t[0]
                if is_start_of_paragraph:
                    minist.insert_as_speaker(self.ctx, doc, cursor)
                else:
                    minist.insert(self.ctx, doc, cursor)

            elif url.Path == 'commissions':
                commissions = Commission.values(self.ctx)
                t = [x for x in commissions if x.get_name() == string]
                if not t:
                    raise Exception('Unknown Commission')
                commis = t[0]
                commis.insert(self.ctx, cursor)
                self.set_dropdown_label(self.controls[url.Path], u'Commissions')
        except:
            display_exception()
        finally:
            if url.Path.startswith('depute'):
                part = url.Path.split('_')[1]
                self.set_dropdown_label(self.controls[url.Path], self.deputy_letters[part])
            elif url.Path == 'ministres':
                self.set_dropdown_label(self.controls[url.Path], u'Ministres et Présidents')
            elif url.Path == 'commissions':
                self.set_dropdown_label(self.controls[url.Path], u'Commissions')
            # set focus to document
            current_frame = desktop.getCurrentFrame()
            current_frame.getComponentWindow().setFocus()


class Inserts(unohelper.Base, XJobExecutor, XDispatchProvider, XActionListener):
    def __init__(self, ctx):
        self.ctx = ctx

    def queryDispatch(self, url, target, flags):
        return InsertDispatcher(self.ctx)


class OnLoadEvent(unohelper.Base, XJob):
    '''
    Job executed when a document is loaded
    '''
    def __init__ (self, ctx):
        self.ctx = ctx

    def execute(self, args):
        try:
            desktop = self.ctx.ServiceManager.createInstanceWithContext(
                    'com.sun.star.frame.Desktop', self.ctx)
            document = desktop.getCurrentComponent()
            if document and document.supportsService('com.sun.star.text.TextDocument'):
                layout_manager = document.CurrentController.Frame.LayoutManager
                if 'Tabellio' in (document.DocumentInfo.Keywords or ''):
                    set_toolbar_visibility(self.ctx, True)
                else:
                    set_toolbar_visibility(self.ctx, False)
            else:
                set_toolbar_visibility(self.ctx, False)
        except:
            display_exception(self.ctx)


# register everything to OpenOffice.org
g_ImplementationHelper = unohelper.ImplementationHelper()

g_ImplementationHelper.addImplementation(
    SpeakerDialog,
    "org.entrouvert.openoffice.SpeakerDialog",
    ("com.sun.star.task.Job",),)

g_ImplementationHelper.addImplementation(
    ManualSpeakerDialog,
    "org.entrouvert.openoffice.ManualSpeakerDialog",
    ("com.sun.star.task.Job",),)

g_ImplementationHelper.addImplementation(
    StyleApply,
    "org.entrouvert.openoffice.StyleApply",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    ListStyleApply,
    "org.entrouvert.openoffice.ListStyleApply",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    StructureCheck,
    "org.entrouvert.openoffice.StructureCheck",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    ShowHideToolbars,
    "org.entrouvert.openoffice.ShowHideToolbars",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    AboutDialog,
    "org.entrouvert.openoffice.AboutDialog",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    ConfigurationDialog,
    "org.entrouvert.openoffice.ConfigurationDialog",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    Preview,
    "org.entrouvert.openoffice.Preview",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    ExportAsLegi,
    "org.entrouvert.openoffice.ExportAsLegi",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    UploadLegi,
    "org.entrouvert.openoffice.UploadLegi",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    NewTabellioDocument,
    "org.entrouvert.openoffice.NewTabellioDocument",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    InsertVote,
    "org.entrouvert.openoffice.InsertVote",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    InsertStandardText,
    "org.entrouvert.openoffice.InsertStandardText",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    ImportDalet,
    "org.entrouvert.openoffice.ImportDalet",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    SwitchReviewNoteMode,
    "org.entrouvert.openoffice.SwitchReviewNoteMode",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    InsertFootnote,
    "org.entrouvert.openoffice.InsertFootnote",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    DoNothing,
    "org.entrouvert.openoffice.DoNothing",
    ("com.sun.star.task.Job",))

g_ImplementationHelper.addImplementation(
    Inserts,
    'org.entrouvert.openoffice.Inserts',
    ('com.sun.star.frame.DispatchProvider', 'com.sun.star.task.Job',))

g_ImplementationHelper.addImplementation(
    OnLoadEvent,
    'org.entrouvert.openoffice.OnLoadEvent',
    ('com.sun.star.task.Job',))

