From 83553f76a3a9a26bf760b249eb0d6e19f4297feb Mon Sep 17 00:00:00 2001
From: Octavian <octavsly@octavsly.net>
Date: Wed, 22 Dec 2021 14:59:14 +0300
Subject: Add Qt5 port


diff --git a/clients/Makefile.in b/clients/Makefile.in
index 9c2a3c1..6a3a6aa 100644
--- a/clients/Makefile.in
+++ b/clients/Makefile.in
@@ -10,13 +10,13 @@ DEBUG :=
 CXX := @CXX@ -Wall
 SED := sed
 DEFS :=
-MOC := @QT4_MOC@
-QT4_INC := @QT4_INC@
-QT4_LIB := @QT4_LIB@
-QT4_INC_NET := @QT4_INC_NET@
-QT4_LIB_NET := @QT4_LIB_NET@
-QT4_CLIENT_DEPS := ../libyateqt4.so
-QT4_CLIENT_LIBS := -lyateqt4
+MOC := @QT5_MOC@
+QT5_INC := @QT5_INC@
+QT5_LIB := @QT5_LIB@
+QT5_INC_NET := @QT5_INC_NET@
+QT5_LIB_NET := @QT5_LIB_NET@
+QT5_CLIENT_DEPS := ../libyateqt5.so
+QT5_CLIENT_LIBS := -lyateqt5
 LIBTHR:= @THREAD_LIB@
 INCLUDES := -I.. -I@top_srcdir@
 CFLAGS := @CFLAGS@ @MODULE_CPPFLAGS@ @INLINE_FLAGS@
@@ -31,21 +31,21 @@ LIBS :=
 MENUFILES :=
 DESKFILES :=
 
-ifneq (@HAVE_QT4@,no)
-SUBDIRS := $(SUBDIRS) qt4
-PROGS := $(PROGS) yate-qt4
-MENUFILES := $(MENUFILES) yate-qt4.menu
-DESKFILES := $(DESKFILES) yate-qt4.desktop
+ifneq (@HAVE_QT5@,no)
+SUBDIRS := $(SUBDIRS) qt5
+PROGS := $(PROGS) yate-qt5
+MENUFILES := $(MENUFILES) yate-qt5.menu
+DESKFILES := $(DESKFILES) yate-qt5.desktop
 ICONFILES := $(ICONFILES) null_team-16.png null_team-32.png null_team-48.png null_team-64.png null_team-128.png
 
-ifneq (@QT4_STATIC_MODULES@,no)
-ifeq (@QT4_STATIC_MODULES@,yes)
-QT4_CLIENT_LIBS := customtable customtext customtree widgetlist clientarchive
+ifneq (@QT5_STATIC_MODULES@,no)
+ifeq (@QT5_STATIC_MODULES@,yes)
+QT5_CLIENT_LIBS := customtable customtext customtree widgetlist clientarchive
 else
-QT4_CLIENT_LIBS := $(strip @QT4_STATIC_MODULES@)
+QT5_CLIENT_LIBS := $(strip @QT5_STATIC_MODULES@)
 endif
-QT4_CLIENT_LIBS := $(foreach mod,$(QT4_CLIENT_LIBS),../modules/qt4/$(mod).o) qt4/qt4client.a
-QT4_CLIENT_DEPS := $(QT4_CLIENT_LIBS)
+QT5_CLIENT_LIBS := $(foreach mod,$(QT5_CLIENT_LIBS),../modules/qt5/$(mod).o) qt5/qt5client.a
+QT5_CLIENT_DEPS := $(QT5_CLIENT_LIBS)
 endif
 
 endif
@@ -57,8 +57,8 @@ EXTERNLIBS =
 COMPILE = $(CXX) $(DEFS) $(DEBUG) $(INCLUDES) $(CFLAGS)
 LINK = $(CXX) $(LDFLAGS)
 
-ifneq (x@QT4_VER@,x)
-DEFS := $(DEFS) -DQT4_VER=@QT4_VER@
+ifneq (x@QT5_VER@,x)
+DEFS := $(DEFS) -DQT5_VER=@QT5_VER@
 endif
 
 prefix = @prefix@
@@ -135,17 +135,17 @@ uninstall: do-uninstall
 	rmdir "$(DESTDIR)$(icondir)" \
 	)
 
-../modules/qt4/%.o: @top_srcdir@/modules/qt4/%.cpp
-	$(MAKE) -C ../modules qt4/$(notdir $@)
+../modules/qt5/%.o: @top_srcdir@/modules/qt5/%.cpp
+	$(MAKE) -C ../modules qt5/$(notdir $@)
 
 %.o: @srcdir@/%.cpp $(MKDEPS) $(INCFILES)
 	$(COMPILE) -c $<
 
 %.moc.o: %.moc $(INCFILES)
-	$(COMPILE) $(QT4_INC) -o $@ -c -x c++ $<
+	$(COMPILE) $(QT5_INC) -o $@ -c -x c++ $<
 
 %.moc: @srcdir@/%.h
-	$(MOC) $(DEFS) $(INCLUDES) $(QT4_INC) -o $@ $<
+	$(MOC) $(DEFS) $(INCLUDES) $(QT5_INC) -o $@ $<
 
 do-all do-strip do-clean do-install do-uninstall:
 	$(if $(SUBDIRS),\
@@ -160,12 +160,12 @@ do-all do-strip do-clean do-install do-uninstall:
 Makefile: @srcdir@/Makefile.in $(MKDEPS)
 	cd .. && ./config.status
 
-yate-qt4: $(QT4_CLIENT_DEPS)
-yate-qt4: EXTERNFLAGS = $(QT4_INC)
-yate-qt4: EXTERNLIBS = $(QT4_CLIENT_LIBS) $(QT4_LIB)
+yate-qt5: $(QT5_CLIENT_DEPS)
+yate-qt5: EXTERNFLAGS = $(QT5_INC)
+yate-qt5: EXTERNLIBS = $(QT5_CLIENT_LIBS) $(QT5_LIB)
 
-qt4/qt4client.a: @srcdir@/qt4/qt4client.h @srcdir@/qt4/qt4client.cpp
-	$(MAKE) -C qt4 $(notdir $@)
+qt5/qt5client.a: @srcdir@/qt5/qt5client.h @srcdir@/qt5/qt5client.cpp
+	$(MAKE) -C qt5 $(notdir $@)
 
 yate-%: @srcdir@/main-%.cpp $(MKDEPS) ../libyate.so $(INCFILES)
 	$(COMPILE) -o $@ $(LOCALFLAGS) $(EXTERNFLAGS) $< $(LDFLAGS) $(LIBTHR) $(LOCALLIBS) $(YATELIBS) $(EXTERNLIBS)
diff --git a/clients/main-qt5.cpp b/clients/main-qt5.cpp
new file mode 100644
index 0000000..996cc0d
--- /dev/null
+++ b/clients/main-qt5.cpp
@@ -0,0 +1,89 @@
+/**
+ * main-qt5.cpp
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * A Qt-5 based universal telephony client
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#include <yatephone.h>
+#include "qt5/qt5client.h"
+
+#define WAIT_ENGINE 10000       //wait 10 seconds for engine to halt
+
+using namespace TelEngine;
+
+class EngineThread;
+
+static QtDriver qtdriver(false);
+static EngineThread* s_engineThread = 0;
+
+class EngineThread : public Thread
+{
+public:
+    inline EngineThread()
+      : Thread("Engine")
+      { }
+    virtual void run();
+    virtual void cleanup();
+};
+
+void EngineThread::run()
+{
+    Engine::self()->run();
+    Debug(DebugAll,"Engine stopped running");
+}
+
+void EngineThread::cleanup()
+{
+    Debug(DebugAll,"EngineThread::cleanup() [%p]",this);
+    if (QtClient::self())
+	QtClient::self()->quit();
+    s_engineThread = 0;
+}
+
+static int mainLoop()
+{
+    // create engine from this thread
+    Engine::self();
+    s_engineThread = new EngineThread;
+    if (!s_engineThread->startup())
+	return EINVAL;
+
+    // build client if the driver didn't
+    if (!QtClient::self())
+	QtClient::setSelf(new QtClient());
+
+    // run the client
+    if (!Engine::exiting())
+	QtClient::self()->run();
+    // the client finished running, do cleanup
+    QtClient::self()->cleanup();
+
+    Engine::halt(0);
+    unsigned long count = WAIT_ENGINE / Thread::idleMsec();
+    while (s_engineThread && count--)
+	Thread::idle();
+
+    return 0;
+}
+
+extern "C" int main(int argc, const char** argv, const char** envp)
+{
+    TelEngine::Engine::extraPath("qt5");
+    return TelEngine::Engine::main(argc,argv,envp,TelEngine::Engine::Client,&mainLoop);
+}
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/clients/qt5/Makefile.in b/clients/qt5/Makefile.in
new file mode 100644
index 0000000..a77b9f0
--- /dev/null
+++ b/clients/qt5/Makefile.in
@@ -0,0 +1,118 @@
+# Makefile
+# This file holds the make rules for the Qt5 client support
+
+# override DEBUG at compile time to enable full debug or remove it all
+DEBUG :=
+
+CXX := @CXX@ -Wall
+AR  := ar
+MOC := @QT5_MOC@
+QT5_INC := @QT5_INC@
+QT5_LIB := @QT5_LIB@
+QT5_INC_NET := @QT5_INC_NET@
+QT5_LIB_NET := @QT5_LIB_NET@
+DEFS:=
+
+INCLUDES:=-I. -I@srcdir@ -I@top_srcdir@ $(QT5_INC)
+CFLAGS := @CFLAGS@ @MODULE_CPPFLAGS@ @INLINE_FLAGS@
+LDFLAGS:= @LDFLAGS@
+SONAME_OPT := @SONAME_OPT@
+YATELIBS := -L../.. -lyate @LIBS@
+INCFILES := @top_srcdir@/yateclass.h @top_srcdir@/yatecbase.h @srcdir@/qt5client.h
+
+PROGS=
+LIBS = qt5client.a
+SOURCES = qt5client.cpp
+OBJS = $(SOURCES:.cpp=.o) qt5client.moc.o
+INST:=
+LIBD_DEV:= libyateqt5.so
+LIBD_VER:= $(LIBD_DEV).@PACKAGE_VERSION@
+ifeq (@QT5_STATIC_MODULES@,no)
+LIBD:= ../../$(LIBD_VER) ../../$(LIBD_DEV)
+INST:= $(LIBD_VER) $(LIBD_DEV)
+endif
+
+LOCALFLAGS =
+LOCALLIBS =
+COMPILE = $(CXX) $(DEFS) $(DEBUG) $(INCLUDES) $(CFLAGS)
+LINK = $(CXX) $(LDFLAGS)
+
+ifneq (x@QT5_VER@,x)
+DEFS := $(DEFS) -DQT5_VER=@QT5_VER@
+endif
+
+prefix = @prefix@
+exec_prefix = @exec_prefix@
+datarootdir = @datarootdir@
+
+bindir = @bindir@
+libdir = @libdir@
+incdir = @includedir@/yate
+
+# include optional local make rules
+-include YateLocal.mak
+
+.PHONY: all debug ddebug xdebug
+all: $(LIBS) $(LIBD) $(PROGS)
+
+debug:
+	$(MAKE) all DEBUG=-g3 MODSTRIP=
+
+ddebug:
+	$(MAKE) all DEBUG='-g3 -DDEBUG' MODSTRIP=
+
+xdebug:
+	$(MAKE) all DEBUG='-g3 -DXDEBUG' MODSTRIP=
+
+.PHONY: strip
+strip: all
+	strip --strip-debug --discard-locals $(PROGS)
+
+.PHONY: clean
+clean:
+	@-$(RM) $(PROGS) $(LIBS) $(LIBD) $(OBJS) core 2>/dev/null
+
+.PHONY: install uninstall
+install: all
+	$(if $(INST),\
+	@mkdir -p "$(DESTDIR)$(libdir)" && \
+	for i in $(INST) ; do \
+	    if [ -h "../../$$i" ]; then \
+		f=`readlink "../../$$i"` ; \
+		ln -sf "$$f" "$(DESTDIR)$(libdir)/$$i" ; \
+	    else \
+		install @INSTALL_L@ ../../$$i "$(DESTDIR)$(libdir)/" ; \
+	    fi \
+	done; \
+	mkdir -p "$(DESTDIR)$(incdir)" && \
+	install -m 0644 @srcdir@/qt5client.h "$(DESTDIR)$(incdir)/" \
+	)
+
+uninstall:
+	$(if $(INST),\
+	@-for i in $(INST) ; do \
+	    rm "$(DESTDIR)$(libdir)/$$i" ; \
+	done; \
+	rm "$(DESTDIR)$(incdir)/qt5client.h" && rmdir "$(DESTDIR)$(libdir)" \
+	)
+
+%.o: @srcdir@/%.cpp $(INCFILES)
+	$(COMPILE) -c $<
+
+%.moc.o: %.moc $(INCFILES)
+	$(COMPILE) -o $@ -c -x c++ $<
+
+%.moc: @srcdir@/%.h
+	$(MOC) $(DEFS) $(INCLUDES) -o $@ $<
+
+Makefile: @srcdir@/Makefile.in ../../config.status
+	cd ../.. && ./config.status
+
+../../$(LIBD_VER): $(OBJS)
+	$(LINK) -o $@ $(SONAME_OPT)$(LIBD_VER) $^ $(YATELIBS) $(QT5_LIB)
+
+../../$(LIBD_DEV): ../../$(LIBD_VER)
+	cd ../.. && ln -sf $(LIBD_VER) $(LIBD_DEV)
+
+$(LIBS): $(OBJS)
+	$(AR) rcs $@ $^
diff --git a/clients/qt5/qt5client.cpp b/clients/qt5/qt5client.cpp
new file mode 100644
index 0000000..13c4c77
--- /dev/null
+++ b/clients/qt5/qt5client.cpp
@@ -0,0 +1,5846 @@
+/**
+ * qt5client.cpp
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * A Qt-5 based universal telephony client
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#include "qt5client.h"
+#include <QtUiTools>
+
+#ifdef _WINDOWS
+#define DEFAULT_DEVICE "dsound/*"
+#define PLATFORM_LOWERCASE_NAME "windows"
+#elif defined(__APPLE__)
+#define DEFAULT_DEVICE "coreaudio/*"
+#define PLATFORM_LOWERCASE_NAME "apple"
+#elif defined(__linux__)
+#define DEFAULT_DEVICE "alsa/default"
+#define PLATFORM_LOWERCASE_NAME "linux"
+#else
+#define DEFAULT_DEVICE "oss//dev/dsp"
+#define PLATFORM_LOWERCASE_NAME "unknown"
+#endif
+
+namespace TelEngine {
+
+static unsigned int s_allHiddenQuit = 0; // Quit on all hidden notification if this counter is 0
+
+// Factory used to create objects in client's thread
+class Qt5ClientFactory : public UIFactory
+{
+public:
+    Qt5ClientFactory(const char* name = "Qt5ClientFactory");
+    virtual void* create(const String& type, const char* name, NamedList* params = 0);
+};
+
+// Class used for temporary operations on QT widgets
+// Keeps a pointer to a widget and its type
+// NOTE: The methods of this class don't check the widget pointer
+class QtWidget
+{
+public:
+    enum Type {
+	PushButton     = 0,
+	CheckBox       = 1,
+	Table          = 2,
+	ListBox        = 3,
+	ComboBox       = 4,
+	Tab            = 5,
+	StackWidget    = 6,
+	TextEdit       = 7,
+	Label          = 8,
+	LineEdit       = 9,
+	AbstractButton = 10,
+	Slider         = 11,
+	ProgressBar    = 12,
+	SpinBox        = 13,
+	Calendar       = 14,
+	Splitter       = 15,
+	TextBrowser    = 16,
+	Unknown,                         // Unknown type
+	Action,                          // QAction descendant
+	CustomTable,                     // QtTable descendant
+	CustomTree,                      // QtTree descendant
+	CustomWidget,                    // QtCustomWidget descendant
+	CustomObject,                    // QtCustomObject descendant
+	Missing                          // Invalid pointer
+    };
+    // Set widget from object
+    inline QtWidget(QObject* w)
+	: m_widget(0), m_action(0), m_object(0), m_type(Missing) {
+	    if (!w)
+		return;
+	    if (w->inherits("QWidget"))
+		m_widget = static_cast<QWidget*>(w);
+	    else if (w->inherits("QAction"))
+		m_action = static_cast<QAction*>(w);
+	    m_type = getType();
+	}
+    // Set widget from object and type
+    inline QtWidget(QWidget* w, int t)
+	: m_widget(w), m_action(0), m_object(0), m_type(t) {
+	    if (!m_widget)
+		m_type = Missing;
+	}
+    // Set widget/action from object and name
+    inline QtWidget(QObject* parent, const String& name)
+	: m_widget(0), m_action(0), m_object(0), m_type(Missing) {
+	    QString what = QtClient::setUtf8(name);
+	    m_widget = parent->findChild<QWidget*>(what);
+	    if (!m_widget) {
+		m_action = parent->findChild<QAction*>(what);
+		if (!m_action)
+		    m_object = parent->findChild<QObject*>(what);
+	    }
+	    m_type = getType();
+	}
+    inline bool valid() const
+	{ return type() != Missing; }
+    inline bool invalid() const
+	{ return type() == Missing; }
+    inline int type() const
+	{ return m_type; }
+    inline operator QWidget*()
+	{ return m_widget; }
+    inline bool inherits(const char* classname)
+	{ return m_widget && m_widget->inherits(classname); }
+    inline bool inherits(Type t)
+	{ return inherits(s_types[t]); }
+    inline QWidget* widget()
+	{ return m_widget; }
+    inline QWidget* operator ->()
+	{ return m_widget; }
+    // Static cast methods
+    inline QPushButton* button()
+	{ return static_cast<QPushButton*>(m_widget); }
+    inline QCheckBox* check()
+	{ return static_cast<QCheckBox*>(m_widget); }
+    inline QTableWidget* table()
+	{ return static_cast<QTableWidget*>(m_widget); }
+    inline QListWidget* list()
+	{ return static_cast<QListWidget*>(m_widget); }
+    inline QComboBox* combo()
+	{ return static_cast<QComboBox*>(m_widget); }
+    inline QTabWidget* tab()
+	{ return static_cast<QTabWidget*>(m_widget); }
+    inline QStackedWidget* stackWidget()
+	{ return static_cast<QStackedWidget*>(m_widget); }
+    inline QTextEdit* textEdit()
+	{ return static_cast<QTextEdit*>(m_widget); }
+    inline QLabel* label()
+	{ return static_cast<QLabel*>(m_widget); }
+    inline QLineEdit* lineEdit()
+	{ return static_cast<QLineEdit*>(m_widget); }
+    inline QAbstractButton* abstractButton()
+	{ return static_cast<QAbstractButton*>(m_widget); }
+    inline QSlider* slider()
+	{ return static_cast<QSlider*>(m_widget); }
+    inline QProgressBar* progressBar()
+	{ return static_cast<QProgressBar*>(m_widget); }
+    inline QSpinBox* spinBox()
+	{ return static_cast<QSpinBox*>(m_widget); }
+    inline QCalendarWidget* calendar()
+	{ return static_cast<QCalendarWidget*>(m_widget); }
+    inline QSplitter* splitter()
+	{ return static_cast<QSplitter*>(m_widget); }
+    inline QtTable* customTable()
+	{ return qobject_cast<QtTable*>(m_widget); }
+    inline QtTree* customTree()
+	{ return qobject_cast<QtTree*>(m_widget); }
+    inline QtCustomWidget* customWidget()
+	{ return qobject_cast<QtCustomWidget*>(m_widget); }
+    inline QtCustomObject* customObject()
+	{ return qobject_cast<QtCustomObject*>(m_object); }
+    inline UIWidget* uiWidget() {
+	    switch (type()) {
+		case CustomTable:
+		    return static_cast<UIWidget*>(customTable());
+		case CustomWidget:
+		    return static_cast<UIWidget*>(customWidget());
+		case CustomObject:
+		    return static_cast<UIWidget*>(customObject());
+		case CustomTree:
+		    return static_cast<UIWidget*>(customTree());
+	    }
+	    return 0;
+	}
+
+    inline QAction* action()
+	{ return m_action; }
+
+    // Find a combo box item
+    inline int findComboItem(const String& item) {
+	    QComboBox* c = combo();
+	    return c ? c->findText(QtClient::setUtf8(item)) : -1;
+	}
+    // Add an item to a combo box
+    inline bool addComboItem(const String& item, bool atStart) {
+	    QComboBox* c = combo();
+	    if (!c)
+		return false;
+	    QString it(QtClient::setUtf8(item));
+	    if (atStart)
+		c->insertItem(0,it);
+	    else
+		c->addItem(it);
+	    return true;
+	}
+    // Find a list box item
+    inline int findListItem(const String& item) {
+	    QListWidget* l = list();
+	    if (!l)
+		return -1;
+	    QString it(QtClient::setUtf8(item));
+	    for (int i = l->count(); i >= 0 ; i--) {
+		QListWidgetItem* tmp = l->item(i);
+		if (tmp && it == tmp->text())
+		    return i;
+	    }
+	    return -1;
+	}
+    // Add an item to a list box
+    inline bool addListItem(const String& item, bool atStart) {
+	    QListWidget* l = list();
+	    if (!l)
+		return false;
+	    QString it(QtClient::setUtf8(item));
+	    if (atStart)
+		l->insertItem(0,it);
+	    else
+		l->addItem(it);
+	    return true;
+	}
+
+    int getType() {
+	    if (m_widget) {
+		String cls = m_widget->metaObject()->className();
+		for (int i = 0; i < Unknown; i++)
+		    if (s_types[i] == cls)
+			return i;
+		if (customTable())
+		    return CustomTable;
+		if (customWidget())
+		    return CustomWidget;
+		if (customTree())
+		    return CustomTree;
+		return Unknown;
+	    }
+	    if (m_action && m_action->inherits("QAction"))
+		return Action;
+	    if (customObject())
+		return CustomObject;
+	    return Missing;
+	}
+    static String s_types[Unknown];
+protected:
+    QWidget* m_widget;
+    QAction* m_action;
+    QObject* m_object;
+    int m_type;
+private:
+    QtWidget() {}
+};
+
+// Class used for temporary operations on QTableWidget objects
+// NOTE: The methods of this class don't check the table pointer
+class TableWidget : public GenObject
+{
+public:
+    TableWidget(QTableWidget* table, bool tmp = true);
+    TableWidget(QWidget* wid, const String& name, bool tmp = true);
+    TableWidget(QtWidget& table, bool tmp = true);
+    ~TableWidget();
+    inline QTableWidget* table()
+	{ return m_table; }
+    inline bool valid()
+	{ return m_table != 0; }
+    inline QtTable* customTable()
+	{ return (valid()) ? qobject_cast<QtTable*>(m_table) : 0; }
+    inline const String& name()
+	{ return m_name; }
+    inline int rowCount()
+	{ return m_table->rowCount(); }
+    inline int columnCount()
+	{ return m_table->columnCount(); }
+    inline void setHeaderText(int col, const char* text) {
+	    if (col < columnCount())
+		m_table->setHorizontalHeaderItem(col,
+		    new QTableWidgetItem(QtClient::setUtf8(text)));
+	}
+    inline bool getHeaderText(int col, String& dest, bool lower = true) {
+    	    QTableWidgetItem* item = m_table->horizontalHeaderItem(col);
+	    if (item) {
+		QtClient::getUtf8(dest,item->text());
+		if (lower)
+		    dest.toLower();
+	    }
+	    return item != 0;
+	}
+    // Get the current selection's row
+    inline int crtRow() {
+	    QList<QTableWidgetItem*> items = m_table->selectedItems();
+	    if (items.size())
+		return items[0]->row();
+	    return -1;
+	}
+    inline void repaint()
+	{ m_table->repaint(); }
+    inline void addRow(int index)
+	{ m_table->insertRow(index); }
+    inline void delRow(int index) {
+	    if (index >= 0)
+		m_table->removeRow(index);
+	}
+    inline void addColumn(int index, int width = -1, const char* name = 0) {
+	    m_table->insertColumn(index);
+	    if (width >= 0)
+		m_table->setColumnWidth(index,width);
+	    setHeaderText(index,name);
+	}
+    inline void setImage(int row, int col, const String& image) {
+	    QTableWidgetItem* item = m_table->item(row,col);
+	    if (item)
+		item->setIcon(QIcon(QtClient::setUtf8(image)));
+	}
+    inline void addCell(int row, int col, const String& value) {
+	    QTableWidgetItem* item = new QTableWidgetItem(QtClient::setUtf8(value));
+	    m_table->setItem(row,col,item);
+	}
+    inline void setCell(int row, int col, const String& value, bool addNew = true) {
+	    QTableWidgetItem* item = m_table->item(row,col);
+	    if (item)
+		item->setText(QtClient::setUtf8(value));
+	    else if (addNew)
+		addCell(row,col,value);
+	}
+    inline bool getCell(int row, int col, String& dest, bool lower = false) {
+    	    QTableWidgetItem* item = m_table->item(row,col);
+	    if (item) {
+		QtClient::getUtf8(dest,item->text());
+		if (lower)
+		    dest.toLower();
+		return true;
+	    }
+	    return false;
+	}
+    inline void setID(int row, const String& value)
+	{ setCell(row,0,value); }
+    // Add or set a row
+    void updateRow(const String& item, const NamedList* data, bool atStart);
+    // Update a row from a list of parameters
+    void updateRow(int row, const NamedList& data);
+    // Find a row by the first's column value. Return -1 if not found
+    int getRow(const String& item);
+    // Find a column by its label. Return -1 if not found
+    int getColumn(const String& name, bool caseInsentive = true);
+protected:
+    void init(bool tmp);
+private:
+    QTableWidget* m_table;               // The table
+    String m_name;                       // Table's name
+    int m_sortControl;                   // Flag used to set/reset sorting attribute of the table
+};
+
+// Store an UI loaded from file to avoid loading it again
+class UIBuffer : public String
+{
+public:
+    inline UIBuffer(const String& name, QByteArray* buf)
+	: String(name), m_buffer(buf)
+	{ s_uiCache.append(this); }
+    inline QByteArray* buffer()
+	{ return m_buffer; }
+    // Remove from list. Release memory
+    virtual void destruct();
+    // Return an already loaded UI. Load from file if not found.
+    // Add URLs paths when missing
+    static UIBuffer* build(const String& name);
+    // Find a buffer
+    static UIBuffer* find(const String& name);
+    // Buffer cache
+    static ObjList s_uiCache;
+private:
+    QByteArray* m_buffer;                // The buffer
+};
+
+}; // namespace TelEngine
+
+using namespace TelEngine;
+
+// Dynamic properies
+static const String s_propsSave = "_yate_save_props"; // Save properties property name
+static const String s_propColWidths = "_yate_col_widths"; // Column widths
+static const String s_propSorting = "_yate_sorting";  // Table/List sorting
+static const String s_propSizes = "_yate_sizes";      // Size int array
+static const String s_propShowWndWhenActive = "_yate_showwnd_onactive"; // Show another window when a window become active
+static String s_propHHeader = "dynamicHHeader";       // Tables: show/hide the horizontal header
+static String s_propAction = "dynamicAction";         // Prefix for properties that would trigger some action
+static String s_propWindowFlags = "_yate_windowflags"; // Window flags
+static const String s_propContextMenu = "_yate_context_menu"; // Context menu name
+static String s_propHideInactive = "dynamicHideOnInactive"; // Hide inactive window
+static const String s_yatePropPrefix = "_yate_";      // Yate dynamic properties prefix
+static NamedList s_qtStyles("");                      // Qt styles classname -> internal name
+//
+static Qt5ClientFactory s_qt5Factory;
+static Configuration s_cfg;
+static Configuration s_save;
+ObjList UIBuffer::s_uiCache;
+
+// Values used to configure window title bar and border
+static TokenDict s_windowFlags[] = {
+    // Window type
+    {"popup",              Qt::Popup},
+    {"tool",               Qt::Tool},
+    {"subwindow",          Qt::SubWindow},
+#ifdef _WINDOWS
+    {"notificationtype",   Qt::Tool},
+#else
+    {"notificationtype",   Qt::SubWindow},
+#endif
+    // Window flags
+    {"title",              Qt::WindowTitleHint},
+    {"sysmenu",            Qt::WindowSystemMenuHint},
+    {"maximize",           Qt::WindowMaximizeButtonHint},
+    {"minimize",           Qt::WindowMinimizeButtonHint},
+    {"help",               Qt::WindowContextHelpButtonHint},
+    {"stayontop",          Qt::WindowStaysOnTopHint},
+    {"frameless",          Qt::FramelessWindowHint},
+#if QT_VERSION >= 0x040500
+    {"close",              Qt::WindowCloseButtonHint},
+#endif
+    {0,0}
+};
+
+// Widget attribute names
+static const TokenDict s_widgetAttributes[] = {
+    {"macshowfocusrect",   Qt::WA_MacShowFocusRect},
+    {0,0}
+};
+
+String QtWidget::s_types[QtWidget::Unknown] = {
+    "QPushButton",
+    "QCheckBox",
+    "QTableWidget",
+    "QListWidget",
+    "QComboBox",
+    "QTabWidget",
+    "QStackedWidget",
+    "QTextEdit",
+    "QLabel",
+    "QLineEdit",
+    "QAbstractButton",
+    "QSlider",
+    "QProgressBar",
+    "QSpinBox",
+    "QCalendarWidget",
+    "QSplitter",
+    "QTextBrowser",
+};
+
+// QVariant type translation dictionary
+static const TokenDict s_qVarType[] = {
+    {"string",       QVariant::String},
+    {"bool",         QVariant::Bool},
+    {"int",          QVariant::Int},
+    {"uint",         QVariant::UInt},
+    {"stringlist",   QVariant::StringList},
+    {"icon",         QVariant::Icon},
+    {"pixmap",       QVariant::Pixmap},
+    {"double",       QVariant::Double},
+    {"keysequence",  QVariant::KeySequence},
+    {0,0}
+};
+
+// Qt alignment flags translation
+static const TokenDict s_qAlign[] = {
+    {"left",      Qt::AlignLeft},
+    {"right",     Qt::AlignRight},
+    {"hcenter",   Qt::AlignHCenter},
+    {"justify",   Qt::AlignJustify},
+    {"top",       Qt::AlignTop},
+    {"bottom",    Qt::AlignBottom},
+    {"vcenter",   Qt::AlignVCenter},
+    {"center",    Qt::AlignCenter},
+    {"absolute",  Qt::AlignAbsolute},
+    {0,0}
+};
+
+// Qt alignment flags translation
+static const TokenDict s_qEditTriggers[] = {
+    {"currentchanged", QAbstractItemView::CurrentChanged},
+    {"doubleclick",    QAbstractItemView::DoubleClicked},
+    {"selclick",       QAbstractItemView::SelectedClicked},
+    {"editkeypress",   QAbstractItemView::EditKeyPressed},
+    {"anykeypress",    QAbstractItemView::AnyKeyPressed},
+    {"all",            QAbstractItemView::AllEditTriggers},
+    {0,0}
+};
+
+// QtClientSort name
+static const TokenDict s_sorting[] = {
+    {"ascending",      QtClient::SortAsc},
+    {"descending",     QtClient::SortDesc},
+    {"none",           QtClient::SortNone},
+    {0,0}
+};
+
+// Handler for QT library messages
+static void qtMsgHandler(QtMsgType type, const QMessageLogContext& context, const QString& text)
+{
+    int dbg = DebugAll;
+    switch (type) {
+	case QtDebugMsg:
+	case QtInfoMsg:
+	    dbg = DebugInfo;
+	    break;
+	case QtWarningMsg:
+	    dbg = DebugWarn;
+	    break;
+	case QtCriticalMsg:
+	    dbg = DebugCrit;
+	    break;
+	case QtFatalMsg:
+	    dbg = DebugFail;
+	    break;
+    }
+    QByteArray local(text.toLocal8Bit());
+    Debug("QT",dbg,"%s",local.data());
+}
+
+// Build a list of parameters from a string
+// Return the number of parameters found
+static unsigned int str2Params(NamedList& params, const String& buf, char sep = '|')
+{
+    ObjList* list = 0;
+    // Check if we have another separator
+    if (buf.startsWith("separator=")) {
+	sep = buf.at(10);
+	list = buf.substr(11).split(sep,false);
+    }
+    else
+	list = buf.split(sep,false);
+    unsigned int n = 0;
+    for (ObjList* o = list->skipNull(); o; o = o->skipNext()) {
+	String* s = static_cast<String*>(o->get());
+	int pos = s->find('=');
+	if (pos < 1)
+	    continue;
+	params.addParam(s->substr(0,pos),s->substr(pos + 1));
+	n++;
+    }
+    TelEngine::destruct(list);
+    return n;
+}
+
+// Utility: fix QT path separator on Windows
+// (display paths using only one separator to the user)
+static inline QString fixPathSep(QString str)
+{
+#ifdef _WINDOWS
+    QString tmp = str;
+    tmp.replace(QChar('/'),QtClient::setUtf8(Engine::pathSeparator()));
+    return tmp;
+#else
+    return str;
+#endif
+}
+
+// Utility: find a stacked widget's page with the given name
+static int findStackedWidget(QStackedWidget& w, const String& name)
+{
+    QString n(QtClient::setUtf8(name));
+    for (int i = 0; i < w.count(); i++) {
+	QWidget* page = w.widget(i);
+	if (page && n == page->objectName())
+	    return i;
+    }
+    return -1;
+}
+
+// Utility function used to get the name of a control
+// The name of the control indicates actions, toggles ...
+// The action name alias can contain parameters
+static bool translateName(QtWidget& w, String& name, NamedList** params = 0)
+{
+    if (w.invalid())
+	return false;
+    if (w.type() != QtWidget::Action)
+	QtClient::getIdentity(w.widget(),name);
+    else
+	QtClient::getIdentity(w.action(),name);
+    if (!name)
+	return true;
+    // Check params
+    int pos = name.find('|');
+    if (pos < 1)
+	return true;
+    if (params) {
+	*params = new NamedList("");
+	if (!str2Params(**params,name.substr(pos + 1)))
+	    TelEngine::destruct(*params);
+    }
+    name = name.substr(0,pos);
+    return true;
+}
+
+// Utility: raise a select event if a list is empty
+static inline void raiseSelectIfEmpty(int count, Window* wnd, const String& name)
+{
+    if (!Client::exiting() && count <= 0 && Client::self())
+	Client::self()->select(wnd,name,String::empty());
+}
+
+// Add dynamic properties from a list of parameters
+// Parameter format:
+// property_name:property_type=property_value
+static void addDynamicProps(QObject* obj, NamedList& props)
+{
+    static String typeString = "string";
+    static String typeBool = "bool";
+    static String typeInt = "int";
+
+    if (!obj)
+	return;
+    unsigned int n = props.length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* ns = props.getParam(i);
+	if (!(ns && ns->name()))
+	    continue;
+	int pos = ns->name().find(':');
+	if (pos < 1)
+	    continue;
+
+	String prop = ns->name().substr(0,pos);
+	String type = ns->name().substr(pos + 1);
+	QVariant var;
+	if (type == typeString)
+	    var.setValue(QString(ns->c_str()));
+	else if (type == typeBool)
+	    var.setValue(ns->toBoolean());
+	else if (type == typeInt)
+	    var.setValue(ns->toInteger());
+
+	if (var.type() != QVariant::Invalid) {
+	    obj->setProperty(prop,var);
+	    DDebug(ClientDriver::self(),DebugAll,
+		"Object '%s': added dynamic property %s='%s' type=%s",
+		YQT_OBJECT_NAME(obj),prop.c_str(),ns->c_str(),var.typeName());
+	}
+	else
+	    Debug(ClientDriver::self(),DebugStub,
+		"Object '%s': dynamic property '%s' type '%s' is not supported",
+		YQT_OBJECT_NAME(obj),prop.c_str(),type.c_str());
+    }
+}
+
+// Find a QSystemTrayIcon child of an object
+static inline QSystemTrayIcon* findSysTrayIcon(QObject* obj, const char* name)
+{
+    return obj->findChild<QSystemTrayIcon*>(QtClient::setUtf8(name));
+}
+
+// Utility used to create an object's property if not found
+// Add it to a list of strings
+// Return true if the list changed
+static bool createProperty(QObject* obj, const char* name, QVariant::Type t,
+    QtWindow* wnd, QStringList* list)
+{
+    if (!obj || TelEngine::null(name))
+	return false;
+    QVariant var = obj->property(name);
+    if (var.type() == QVariant::Invalid)
+	obj->setProperty(name,QVariant(t));
+    else if (var.type() != t) {
+	if (wnd)
+	    Debug(QtDriver::self(),DebugNote,
+		"Window(%s) child '%s' already has a %s property '%s' [%p]",
+		wnd->toString().c_str(),YQT_OBJECT_NAME(obj),var.typeName(),name,wnd);
+	return false;
+    }
+    if (!list)
+	return false;
+    QString s = QtClient::setUtf8(name);
+    if (list->contains(s))
+	return false;
+    *list << s;
+    return true;
+}
+
+// Replace file path in URLs in a character array
+static void addFilePathUrl(QByteArray& a, const String& file)
+{
+    if (!file)
+	return;
+    QString path = QDir::fromNativeSeparators(QtClient::setUtf8(file));
+    // Truncate after last path separator (lastIndexOf() returns -1 if not found)
+    path.truncate(path.lastIndexOf(QString("/")) + 1);
+    if (!path.size())
+	return;
+    int start = 0;
+    int end = -1;
+    while ((start = a.indexOf("url(",end + 1)) > 0) {
+	start += 4;
+	end = a.indexOf(")",start);
+	if (end <= start)
+	    break;
+	// Add
+	int len = end - start;
+	QByteArray tmp = a.mid(start,len);
+	if (tmp.indexOf('/') != -1)
+	    continue;
+	tmp.insert(0,path.toUtf8());
+	a.replace(start,len,tmp);
+    }
+}
+
+// Read data from file and append it to a string buffer
+// Optionally append suffix characters to file name
+static bool appendStyleSheet(QString& buf, const char* file,
+    const char* suffix1 = 0, const char* suffix2 = 0)
+{
+    if (TelEngine::null(file))
+	return false;
+    String shf = file;
+    const char* oper = 0;
+    int pos = shf.rfind('/');
+    if (pos < 0)
+	pos = shf.rfind('\\');
+    if (pos < 0)
+	shf = Client::s_skinPath + shf;
+    int level = DebugNote;
+    if (!(TelEngine::null(suffix1) && TelEngine::null(suffix2))) {
+	level = DebugAll;
+	int dotPos = shf.rfind('.');
+	if (dotPos > pos) {
+	    String tmp = shf.substr(0,dotPos);
+	    tmp.append(suffix1,"_");
+	    tmp.append(suffix2,"_");
+	    shf = tmp + shf.substr(dotPos);
+	}
+    }
+    DDebug(ClientDriver::self(),DebugAll,"Loading stylesheet file '%s'",shf.c_str());
+    QFile f(QtClient::setUtf8(shf));
+    if (f.open(QIODevice::ReadOnly)) {
+	QByteArray a = f.readAll();
+	if (a.size()) {
+	    addFilePathUrl(a,shf);
+	    buf += QString::fromUtf8(a.constData());
+	}
+	else if (f.error() != QFile::NoError)
+	    oper = "read";
+    }
+    else
+	oper = "open";
+    if (!oper)
+	return true;
+    Debug(ClientDriver::self(),level,"Failed to %s stylesheet file '%s': %d '%s'",
+	oper,shf.c_str(),f.error(),f.errorString().toUtf8().constData());
+    return false;
+}
+
+// Split an integer string list
+// Result list length can be set by indicating a length
+static QList<int> buildIntList(const String& buf, int len = 0)
+{
+    QList<int> ret;
+    ObjList* list = buf.split(',');
+    int pos = 0;
+    ObjList* o = list;
+    while (o || pos < len) {
+	int val = 0;
+	if (o) {
+	    if (o->get())
+		val = o->get()->toString().toInteger();
+	    o = o->next();
+	}
+	ret.append(val);
+	pos++;
+	if (pos == len)
+	    break;
+    }
+    TelEngine::destruct(list);
+    return ret;
+}
+
+// Retrieve an object's property
+// Check platform dependent value
+static inline bool getPropPlatform(QObject* obj, const String& name, String& val)
+{
+    if (!(obj && name))
+	return false;
+    if (QtClient::getProperty(obj,name,val))
+	return true;
+    return QtClient::getProperty(obj,name + "_os" + PLATFORM_LOWERCASE_NAME,val);
+}
+
+
+/**
+ * Qt5ClientFactory
+ */
+Qt5ClientFactory::Qt5ClientFactory(const char* name)
+    : UIFactory(name)
+{
+    m_types.append(new String("QSound"));
+}
+
+// Build QSound
+void* Qt5ClientFactory::create(const String& type, const char* name, NamedList* params)
+{
+    if (type == YSTRING("QSound"))
+	return new QSound(QtClient::setUtf8(name));
+    return 0;
+}
+
+
+/**
+ * TableWidget
+ */
+TableWidget::TableWidget(QTableWidget* table, bool tmp)
+    : m_table(table), m_sortControl(-1)
+{
+    if (!m_table)
+	return;
+    init(tmp);
+}
+
+TableWidget::TableWidget(QWidget* wid, const String& name, bool tmp)
+    : m_table(0), m_sortControl(-1)
+{
+    if (wid)
+	m_table = wid->findChild<QTableWidget*>(QtClient::setUtf8(name));
+    if (!m_table)
+	return;
+    init(tmp);
+}
+
+TableWidget::TableWidget(QtWidget& table, bool tmp)
+    : m_table(static_cast<QTableWidget*>((QWidget*)table)), m_sortControl(-1)
+{
+    if (m_table)
+	init(tmp);
+}
+
+TableWidget::~TableWidget()
+{
+    if (!m_table)
+	return;
+    if (m_sortControl >= 0)
+	m_table->setSortingEnabled((bool)m_sortControl);
+    m_table->repaint();
+}
+
+// Add or set a row
+void TableWidget::updateRow(const String& item, const NamedList* data, bool atStart)
+{
+    int row = getRow(item);
+    // Add a new one ?
+    if (row < 0) {
+	row = atStart ? 0 : rowCount();
+	addRow(row);
+	setID(row,item);
+    }
+    // Update
+    if (data)
+	updateRow(row,*data);
+}
+
+// Update a row from a list of parameters
+void TableWidget::updateRow(int row, const NamedList& data)
+{
+    int ncol = columnCount();
+    for (int i = 0; i < ncol; i++) {
+	String header;
+	if (!getHeaderText(i,header))
+	    continue;
+	NamedString* tmp = data.getParam(header);
+	if (tmp)
+	    setCell(row,i,*tmp);
+	// Set image
+	tmp = data.getParam(header + "_image");
+	if (tmp)
+	    setImage(row,i,*tmp);
+    }
+    // Init vertical header
+    String* rowText = data.getParam(YSTRING("row_text"));
+    String* rowImg = data.getParam(YSTRING("row_image"));
+    if (rowText || rowImg) {
+	QTableWidgetItem* item = m_table->verticalHeaderItem(row);
+	if (!item) {
+	    item = new QTableWidgetItem;
+	    m_table->setVerticalHeaderItem(row,item);
+	}
+	if (rowText)
+	    item->setText(QtClient::setUtf8(*rowText));
+	if (rowImg)
+	    item->setIcon(QIcon(QtClient::setUtf8(*rowImg)));
+    }
+}
+
+// Find a row by the first's column value. Return -1 if not found
+int TableWidget::getRow(const String& item)
+{
+    int n = rowCount();
+    for (int i = 0; i < n; i++) {
+	String val;
+	if (getCell(i,0,val) && item == val)
+	    return i;
+    }
+    return -1;
+}
+
+// Find a column by its label. Return -1 if not found
+int TableWidget::getColumn(const String& name, bool caseInsensitive)
+{
+    int n = columnCount();
+    for (int i = 0; i < n; i++) {
+	String val;
+	if (!getHeaderText(i,val,false))
+	    continue;
+	if ((caseInsensitive && (name &= val)) || (!caseInsensitive && name == val))
+	    return i;
+    }
+    return -1;
+}
+
+void TableWidget::init(bool tmp)
+{
+    QtClient::getUtf8(m_name,m_table->objectName());
+    if (tmp) {
+	m_sortControl = m_table->isSortingEnabled() ? 1 : 0;
+	if (m_sortControl)
+	    m_table->setSortingEnabled(false);
+    }
+}
+
+/**
+ * UIBuffer
+ */
+// Remove from list. Release memory
+void UIBuffer::destruct()
+{
+    s_uiCache.remove(this,false);
+    if (m_buffer) {
+	delete m_buffer;
+	m_buffer = 0;
+    }
+    String::destruct();
+}
+
+// Return an already loaded UI. Load from file if not found.
+// Add URLs paths when missing
+UIBuffer* UIBuffer::build(const String& name)
+{
+    // Check if already loaded from the same location
+    UIBuffer* buf = find(name);
+    if (buf)
+	return buf;
+
+    // Load
+    QFile file(QtClient::setUtf8(name));
+    file.open(QIODevice::ReadOnly);
+    QByteArray* qArray = new QByteArray;
+    *qArray = file.readAll();
+    file.close();
+    if (!qArray->size()) {
+	delete qArray;
+	return 0;
+    }
+    // Add URLs path when missing
+    addFilePathUrl(*qArray,name);
+    return new UIBuffer(name,qArray);
+}
+
+// Find a buffer
+UIBuffer* UIBuffer::find(const String& name)
+{
+    ObjList* o = s_uiCache.find(name);
+    return o ? static_cast<UIBuffer*>(o->get()) : 0;
+}
+
+
+/**
+ * QtWindow
+ */
+QtWindow::QtWindow()
+    : m_x(0), m_y(0), m_width(0), m_height(0),
+    m_maximized(false), m_mainWindow(false), m_moving(0)
+{
+}
+
+QtWindow::QtWindow(const char* name, const char* description, const char* alias, QtWindow* parent)
+    : QWidget(parent, Qt::Window),
+    Window(alias ? alias : name), m_description(description), m_oldId(name),
+    m_x(0), m_y(0), m_width(0), m_height(0),
+    m_maximized(false), m_mainWindow(false), m_moving(0)
+{
+    setObjectName(QtClient::setUtf8(m_id));
+}
+
+QtWindow::~QtWindow()
+{
+    // Update all hidden counter for tray icons owned by this window
+    QList<QSystemTrayIcon*> trayIcons = findChildren<QSystemTrayIcon*>();
+    if (trayIcons.size() > 0) {
+	if (s_allHiddenQuit >= (unsigned int)trayIcons.size())
+	    s_allHiddenQuit -= trayIcons.size();
+	else {
+	    Debug(QtDriver::self(),DebugFail,
+		"QtWindow(%s) destroyed with all hidden counter %u greater then tray icons %d [%p]",
+		m_id.c_str(),s_allHiddenQuit,trayIcons.size(),this);
+	    s_allHiddenQuit = 0;
+	}
+    }
+
+    // Save settings
+    if (m_saveOnClose) {
+	m_maximized = isMaximized();
+	s_save.setValue(m_id,"maximized",String::boolText(m_maximized));
+	// Don't save position if maximized: keep the old one
+	if (!m_maximized) {
+	    s_save.setValue(m_id,"x",m_x);
+	    s_save.setValue(m_id,"y",m_y);
+	    s_save.setValue(m_id,"width",m_width);
+	    s_save.setValue(m_id,"height",m_height);
+	}
+	s_save.setValue(m_id,"visible",m_visible);
+	// Set dynamic properties to be saved for native QT objects
+	QList<QTableWidget*> tables = findChildren<QTableWidget*>();
+	for (int i = 0; i < tables.size(); i++) {
+	    if (qobject_cast<QtTable*>(tables[i]))
+		continue;
+	    // Column widths
+	    unsigned int n = tables[i]->columnCount();
+	    String widths;
+	    for (unsigned int j = 0; j < n; j++)
+		widths.append(String(tables[i]->columnWidth(j)),",",true);
+	    tables[i]->setProperty(s_propColWidths,QVariant(QtClient::setUtf8(widths)));
+	    // Sorting
+	    String sorting;
+	    if (tables[i]->isSortingEnabled()) {
+		QHeaderView* h = tables[i]->horizontalHeader();
+		int col = h ? h->sortIndicatorSection() : -1;
+		if (col >= 0)
+		    sorting << col << "," <<
+			String::boolText(Qt::AscendingOrder == h->sortIndicatorOrder());
+	    }
+	    tables[i]->setProperty(s_propSorting,QVariant(QtClient::setUtf8(sorting)));
+	}
+	QList<QSplitter*> spl = findChildren<QSplitter*>();
+	for (int i = 0; i < spl.size(); i++) {
+	    String sizes;
+	    QtClient::intList2str(sizes,spl[i]->sizes());
+	    QtClient::setProperty(spl[i],s_propSizes,sizes);
+	}
+	// Save child objects properties
+	QList<QObject*> child = findChildren<QObject*>();
+	for (int i = 0; i < child.size(); i++) {
+	    NamedList props("");
+	    if (!QtClient::getProperty(child[i],s_propsSave,props))
+		continue;
+	    unsigned int n = props.length();
+	    for (unsigned int j = 0; j < n; j++) {
+		NamedString* ns = props.getParam(j);
+		if (ns && ns->name())
+		    QtClient::saveProperty(child[i],ns->name(),this);
+	    }
+	}
+    }
+}
+
+// Set windows title
+void QtWindow::title(const String& text)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::title(%s) [%p]",text.c_str(),this);
+    Window::title(text);
+    QWidget::setWindowTitle(QtClient::setUtf8(text));
+}
+
+void QtWindow::context(const String& text)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::context(%s) [%p]",text.c_str(),this);
+    m_context = text;
+}
+
+bool QtWindow::setParams(const NamedList& params)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::setParams() [%p]",this);
+
+    setUpdatesEnabled(false);
+    // Check for custom widget params
+    if (params == YSTRING("customwidget")) {
+	// Each parameter is a list of parameters for a custom widget
+	// Parameter name is the widget's name
+	unsigned int n = params.length();
+	bool ok = true;
+	for (unsigned int i = 0; i < n; i++) {
+	    NamedString* ns = params.getParam(i);
+	    NamedList* nl = static_cast<NamedList*>(ns ? ns->getObject(YATOM("NamedList")) : 0);
+	    if (!(nl && ns->name()))
+		continue;
+	    // Find the widget and set its params
+	    QtWidget w(this,ns->name());
+	    if (w.type() == QtWidget::CustomTable)
+		ok = w.customTable()->setParams(*nl) && ok;
+	    else if (w.type() == QtWidget::CustomWidget)
+		ok = w.customWidget()->setParams(*nl) && ok;
+	    else if (w.type() == QtWidget::CustomObject)
+		ok = w.customObject()->setParams(*nl) && ok;
+	    else
+		ok = false;
+	}
+	setUpdatesEnabled(true);
+	return ok;
+    }
+    // Check for system tray icon params
+    if (params == YSTRING("systemtrayicon")) {
+	// Each parameter is a list of parameters for a system tray icon
+	// Parameter name is the widget's name
+	// Parameter value indicates delete/create/set an existing one
+	unsigned int n = params.length();
+	bool ok = false;
+	for (unsigned int i = 0; i < n; i++) {
+	    NamedString* ns = params.getParam(i);
+	    if (!(ns && ns->name()))
+		continue;
+	    QSystemTrayIcon* trayIcon = findSysTrayIcon(this,ns->name());
+	    // Delete
+	    if (ns->null()) {
+		if (trayIcon) {
+		    // Reactivate program termination when the last window was hidden
+		    if (s_allHiddenQuit)
+			s_allHiddenQuit--;
+		    else
+			Debug(QtDriver::self(),DebugFail,
+			    "QtWindow(%s) all hidden counter is 0 while deleting '%s' tray icon [%p]",
+			    m_id.c_str(),YQT_OBJECT_NAME(trayIcon),this);
+		    QtClient::deleteLater(trayIcon);
+		}
+		continue;
+	    }
+	    NamedList* nl = YOBJECT(NamedList,ns);
+	    if (!nl)
+		continue;
+	    // Create a new one if needed
+	    if (!trayIcon) {
+		if (!ns->toBoolean())
+		    continue;
+		trayIcon = new QSystemTrayIcon(this);
+		trayIcon->setObjectName(QtClient::setUtf8(ns->name()));
+		QtClient::connectObjects(trayIcon,SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
+		    this,SLOT(sysTrayIconAction(QSystemTrayIcon::ActivationReason)));
+		// Deactivate program termination when the last window was hidden
+		s_allHiddenQuit++;
+	    }
+	    ok = true;
+	    // Add dynamic properties
+	    // TODO: track the properties, clear the old ones if needed
+	    addDynamicProps(trayIcon,*nl);
+	    // Set icon and tooltip
+	    NamedString* tmp = nl->getParam(YSTRING("icon"));
+	    if (tmp && *tmp)
+		trayIcon->setIcon(QIcon(QtClient::setUtf8(*tmp)));
+	    tmp = nl->getParam(YSTRING("tooltip"));
+	    if (tmp && *tmp)
+		trayIcon->setToolTip(QtClient::setUtf8(*tmp));
+	    // Check context menu
+	    NamedString* menu = nl->getParam(YSTRING("menu"));
+	    if (menu) {
+		QMenu* oldMenu = trayIcon->contextMenu();
+		NamedList* nlMenu = YOBJECT(NamedList,menu);
+		trayIcon->setContextMenu(nlMenu ? QtClient::buildMenu(*nlMenu,*menu,this,
+		    SLOT(action()),SLOT(toggled(bool)),this) : 0);
+		delete oldMenu;
+		// Momentarily hide the system tray icon (if visible) because
+		// the change of menu does not take effect until the icon is
+		// made visible again, at least with the dbus system tray icon
+		// backend in Qt 5.15.2
+		trayIcon->setVisible(false);
+	    }
+	    if (nl->getBoolValue(YSTRING("show"),true))
+		trayIcon->setVisible(true);
+	}
+	setUpdatesEnabled(true);
+	return ok;
+    }
+    // Parameters for the widget whose name is the list name
+    if(params) {
+	QtWidget w(this, params);
+	// Handle UIWidget descendants
+	UIWidget* uiw = w.uiWidget();
+	if (uiw) {
+	    bool ok = uiw->setParams(params);
+	    setUpdatesEnabled(true);
+	    return ok;
+	}
+	if (w.type() == QtWidget::Calendar) {
+	    int year = params.getIntValue(YSTRING("year"));
+	    int month = params.getIntValue(YSTRING("month"));
+	    int day = params.getIntValue(YSTRING("day"));
+	    w.calendar()->setCurrentPage(year, month);
+	    w.calendar()->setSelectedDate(QDate(year, month, day));
+	    setUpdatesEnabled(true);
+	    return true;
+	}
+    }
+
+    // Window or other parameters
+    if (params.getBoolValue(YSTRING("modal"))) {
+	if (parentWindow())
+	    setWindowModality(Qt::WindowModal);
+	else
+	    setWindowModality(Qt::ApplicationModal);
+    }
+    if (params.getBoolValue(YSTRING("minimized")))
+	QWidget::setWindowState(Qt::WindowMinimized);
+    bool ok = Window::setParams(params);
+    setUpdatesEnabled(true);
+    return ok;
+}
+
+void QtWindow::setOver(const Window* parent)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::setOver(%p) [%p]",parent,this);
+    QWidget::raise();
+}
+
+bool QtWindow::hasElement(const String& name)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::hasElement(%s) [%p]",name.c_str(),this);
+    QtWidget w(this,name);
+    return w.valid();
+}
+
+bool QtWindow::setActive(const String& name, bool active)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::setActive(%s,%s) [%p]",
+	name.c_str(),String::boolText(active),this);
+    bool ok = (name == m_id);
+    if (ok) {
+	if (QWidget::isMinimized())
+	    QWidget::showNormal();
+	QWidget::activateWindow();
+	QWidget::raise();
+    }
+    QtWidget w(this,name);
+    if (w.invalid())
+	return ok;
+    if (w.type() != QtWidget::Action)
+	w->setEnabled(active);
+    else
+	w.action()->setEnabled(active);
+    return true;
+}
+
+bool QtWindow::setFocus(const String& name, bool select)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::setFocus(%s,%s) [%p]",
+	name.c_str(),String::boolText(select),this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    w->setFocus();
+    switch (w.type()) {
+	case  QtWidget::ComboBox:
+	    if (w.combo()->isEditable() && select)
+		w.combo()->lineEdit()->selectAll();
+	    break;
+    }
+    return true;
+}
+
+bool QtWindow::setShow(const String& name, bool visible)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::setShow(%s,%s) [%p]",
+	name.c_str(),String::boolText(visible),this);
+    // Check system tray icons
+    QSystemTrayIcon* trayIcon = findSysTrayIcon(this,name);
+    if (trayIcon) {
+	trayIcon->setVisible(visible);
+	return true;
+    }
+    // Widgets
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    setUpdatesEnabled(false);
+    if (w.type() != QtWidget::Action)
+	w->setVisible(visible);
+    else
+	w.action()->setVisible(visible);
+    setUpdatesEnabled(true);
+    return true;
+}
+
+bool QtWindow::setText(const String& name, const String& text,
+	bool richText)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) setText(%s,%s) [%p]",
+	m_id.c_str(),name.c_str(),text.c_str(),this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->setText(text,richText);
+    switch (w.type()) {
+	case QtWidget::CheckBox:
+	    w.check()->setText(QtClient::setUtf8(text));
+	    return true;
+	case QtWidget::LineEdit:
+	    w.lineEdit()->setText(QtClient::setUtf8(text));
+	    return true;
+	case QtWidget::TextBrowser:
+	case QtWidget::TextEdit:
+	    if (richText) {
+		w.textEdit()->clear();
+		w.textEdit()->insertHtml(QtClient::setUtf8(text));
+	    }
+	    else
+		w.textEdit()->setText(QtClient::setUtf8(text));
+	    {
+		QScrollBar* bar = w.textEdit()->verticalScrollBar();
+		if (bar)
+		    bar->setSliderPosition(bar->maximum());
+	    }
+	    return true;
+	case QtWidget::Label:
+	    w.label()->setText(QtClient::setUtf8(text));
+	    return true;
+	case QtWidget::ComboBox:
+	    if (w.combo()->lineEdit())
+		w.combo()->lineEdit()->setText(QtClient::setUtf8(text));
+	    else
+		setSelect(name,text);
+	    return true;
+	case QtWidget::Action:
+	    w.action()->setText(QtClient::setUtf8(text));
+	    return true;
+	case QtWidget::SpinBox:
+	    w.spinBox()->setValue(text.toInteger());
+	    return true;
+    }
+
+    // Handle some known base classes having a setText() method
+    if (w.inherits(QtWidget::AbstractButton))
+	w.abstractButton()->setText(QtClient::setUtf8(text));
+    else
+	return false;
+    return true;
+}
+
+bool QtWindow::setCheck(const String& name, bool checked)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::setCheck(%s,%s) [%p]",
+	name.c_str(),String::boolText(checked),this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    if (w.inherits(QtWidget::AbstractButton))
+	w.abstractButton()->setChecked(checked);
+    else if (w.type() == QtWidget::Action)
+	w.action()->setChecked(checked);
+    else
+	return false;
+    return true;
+}
+
+bool QtWindow::setSelect(const String& name, const String& item)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::setSelect(%s,%s) [%p]",
+	name.c_str(),item.c_str(),this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->setSelect(item);
+
+    int d = 0;
+    switch (w.type()) {
+	case QtWidget::Table:
+	    {
+		TableWidget t(w);
+		int row = t.getRow(item);
+		if (row < 0)
+		    return false;
+		t.table()->setCurrentCell(row,0);
+		return true;
+	    }
+	case QtWidget::ComboBox:
+	    if (item) {
+		d = w.findComboItem(item);
+		if (d < 0)
+		    return false;
+	        w.combo()->setCurrentIndex(d);
+	    }
+	    else if (w.combo()->lineEdit())
+		w.combo()->lineEdit()->setText("");
+	    else
+		return false;
+	    return true;
+	case QtWidget::ListBox:
+	    d = w.findListItem(item);
+	    if (d >= 0)
+		w.list()->setCurrentRow(d);
+	    return d >= 0;
+	case QtWidget::Slider:
+	    w.slider()->setValue(item.toInteger());
+	    return true;
+	case QtWidget::StackWidget:
+	    d = item.toInteger(-1);
+	    while (d < 0) {
+		d = findStackedWidget(*(w.stackWidget()),item);
+		if (d >= 0)
+		    break;
+		// Check for a default widget
+		String def = YQT_OBJECT_NAME(w.stackWidget());
+		def << "_default";
+		d = findStackedWidget(*(w.stackWidget()),def);
+		break;
+	    }
+	    if (d >= 0 && d < w.stackWidget()->count()) {
+		w.stackWidget()->setCurrentIndex(d);
+		return true;
+	    }
+	    return false;
+	case QtWidget::ProgressBar:
+	    d = item.toInteger();
+	    if (d >= w.progressBar()->minimum() && d <= w.progressBar()->maximum())
+		w.progressBar()->setValue(d);
+	    else if (d < w.progressBar()->minimum())
+		w.progressBar()->setValue(w.progressBar()->minimum());
+	    else
+		w.progressBar()->setValue(w.progressBar()->maximum());
+	    return true;
+	case QtWidget::Tab:
+	    d = w.tab()->count() - 1;
+	    for (QString tmp = QtClient::setUtf8(item); d >= 0; d--) {
+		QWidget* wid = w.tab()->widget(d);
+		if (wid && wid->objectName() == tmp)
+		    break;
+	    }
+	    if (d >= 0 && d < w.tab()->count()) {
+		w.tab()->setCurrentIndex(d);
+		return true;
+	    }
+	    return false;
+
+    }
+    return false;
+}
+
+bool QtWindow::setUrgent(const String& name, bool urgent)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::setUrgent(%s,%s) [%p]",
+	name.c_str(),String::boolText(urgent),this);
+
+    if (name == m_id) {
+	QApplication::alert(this,0);
+	return true;
+    }
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    w->raise();
+    return true;
+}
+
+bool QtWindow::hasOption(const String& name, const String& item)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::hasOption(%s,%s) [%p]",
+	name.c_str(),item.c_str(),this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    switch (w.type()) {
+	case QtWidget::ComboBox:
+	    return -1 != w.findComboItem(item);
+	case QtWidget::Table:
+	    return getTableRow(name,item);
+	case QtWidget::ListBox:
+	    return -1 != w.findListItem(item);
+    }
+    return false;
+}
+
+bool QtWindow::addOption(const String& name, const String& item, bool atStart,
+	const String& text)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) addOption(%s,%s,%s,%s) [%p]",
+	m_id.c_str(),name.c_str(),item.c_str(),
+	String::boolText(atStart),text.c_str(),this);
+
+    QtWidget w(this,name);
+    switch (w.type()) {
+	case QtWidget::ComboBox:
+	    w.addComboItem(item,atStart);
+	    if (atStart && w.combo()->lineEdit())
+		w.combo()->lineEdit()->setText(w.combo()->itemText(0));
+	    return true;
+	case QtWidget::Table:
+	    return addTableRow(name,item,0,atStart);
+	case QtWidget::ListBox:
+	    return w.addListItem(item,atStart);
+    }
+    return false;
+}
+
+bool QtWindow::delOption(const String& name, const String& item)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) delOption(%s,%s) [%p]",
+	m_id.c_str(),name.c_str(),item.c_str(),this);
+    return delTableRow(name,item);
+}
+
+bool QtWindow::getOptions(const String& name, NamedList* items)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) getOptions(%s,%p) [%p]",
+	m_id.c_str(),name.c_str(),items,this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    if (!items)
+	return true;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->getOptions(*items);
+
+    switch (w.type()) {
+	case QtWidget::ComboBox:
+	    for (int i = 0; i < w.combo()->count(); i++)
+		QtClient::getUtf8(*items,"",w.combo()->itemText(i),false);
+	    break;
+	case QtWidget::Table:
+	    {
+		TableWidget t(w.table(),false);
+		for (int i = 0; i < t.rowCount(); i++) {
+		    String item;
+		    if (t.getCell(i,0,item) && item)
+			items->addParam(item,"");
+		}
+	    }
+	    break;
+	case QtWidget::ListBox:
+	    for (int i = 0; i < w.list()->count(); i++) {
+		QListWidgetItem* tmp = w.list()->item(i);
+		if (tmp)
+		    QtClient::getUtf8(*items,"",tmp->text(),false);
+	    }
+	    break;
+    }
+    return true;
+}
+
+// Append or insert text lines to a widget
+bool QtWindow::addLines(const String& name, const NamedList* lines, unsigned int max,
+	bool atStart)
+{
+    DDebug(ClientDriver::self(),DebugAll,"QtWindow(%s) addLines('%s',%p,%u,%s) [%p]",
+	m_id.c_str(),name.c_str(),lines,max,String::boolText(atStart),this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    if (!lines)
+	return true;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->addLines(*lines,max,atStart);
+    unsigned int count = lines->length();
+    if (!count)
+	return true;
+
+    switch (w.type()) {
+	case QtWidget::TextBrowser:
+	case QtWidget::TextEdit:
+	    // Limit the maximum number of paragraphs
+	    if (max) {
+		QTextDocument* doc = w.textEdit()->document();
+		if (!doc)
+		    return false;
+		doc->setMaximumBlockCount((int)max);
+	    }
+	    {
+		// FIXME: delete lines from begining if appending and the number
+		//  of lines exceeds the maximum allowed
+		QString s = w.textEdit()->toPlainText();
+		int pos = atStart ? 0 : s.length();
+		for (unsigned int i = 0; i < count; i++) {
+		    NamedString* ns = lines->getParam(i);
+		    if (!ns)
+			continue;
+		    if (ns->name().endsWith("\n"))
+			s.insert(pos,QtClient::setUtf8(ns->name()));
+		    else {
+			String tmp = ns->name() + "\n";
+			s.insert(pos,QtClient::setUtf8(tmp));
+			pos++;
+		    }
+		    pos += (int)ns->name().length();
+		}
+		w.textEdit()->setText(s);
+		// Scroll down if added at end
+		if (!atStart) {
+		    QScrollBar* bar = w.textEdit()->verticalScrollBar();
+		    if (bar)
+			bar->setSliderPosition(bar->maximum());
+		}
+	    }
+	    return true;
+	case QtWidget::Table:
+	    // TODO: implement
+	    break;
+	case QtWidget::ComboBox:
+	    if (atStart) {
+		for (; count; count--) {
+		    NamedString* ns = lines->getParam(count - 1);
+		    if (ns)
+			w.combo()->insertItem(0,QtClient::setUtf8(ns->name()));
+		}
+		if (w.combo()->lineEdit())
+		    w.combo()->lineEdit()->setText(w.combo()->itemText(0));
+	    }
+	    else {
+		for (unsigned int i = 0; i < count; i++) {
+		    NamedString* ns = lines->getParam(i);
+		    if (ns)
+			w.combo()->addItem(QtClient::setUtf8(ns->name()));
+		}
+	    }
+	    return true;
+	case QtWidget::ListBox:
+	    // TODO: implement
+	    break;
+    }
+    return false;
+}
+
+bool QtWindow::addTableRow(const String& name, const String& item,
+	const NamedList* data, bool atStart)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) addTableRow(%s,%s,%p,%s) [%p]",
+	m_id.c_str(),name.c_str(),item.c_str(),data,String::boolText(atStart),this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->addTableRow(item,data,atStart);
+    // Handle basic QTableWidget
+    if (w.type() != QtWidget::Table)
+	return false;
+    TableWidget tbl(w.table());
+    int row = atStart ? 0 : tbl.rowCount();
+    tbl.addRow(row);
+    // Set item (the first column) and the rest of data
+    tbl.setID(row,item);
+    if (data)
+	tbl.updateRow(row,*data);
+    return true;
+}
+
+// Insert or update multiple rows in a single operation
+bool QtWindow::setMultipleRows(const String& name, const NamedList& data, const String& prefix)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) setMultipleRows('%s',%p,'%s') [%p]",
+	m_id.c_str(),name.c_str(),&data,prefix.c_str(),this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    return uiw && uiw->setMultipleRows(data,prefix);
+}
+
+// Insert a row into a table owned by this window
+bool QtWindow::insertTableRow(const String& name, const String& item,
+    const String& before, const NamedList* data)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) insertTableRow(%s,%s,%s,%p) [%p]",
+	m_id.c_str(),name.c_str(),item.c_str(),before.c_str(),data,this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->insertTableRow(item,before,data);
+    if (w.type() != QtWidget::Table)
+	return false;
+    TableWidget tbl(w.table());
+    int row = tbl.getRow(before);
+    if (row == -1)
+	row = tbl.rowCount();
+    tbl.addRow(row);
+    // Set item (the first column) and the rest of data
+    tbl.setID(row,item);
+    if (data)
+	tbl.updateRow(row,*data);
+    return true;
+}
+
+bool QtWindow::delTableRow(const String& name, const String& item)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::delTableRow(%s,%s) [%p]",
+	name.c_str(),item.c_str(),this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    int row = -1;
+    int n = 0;
+    switch (w.type()) {
+	case QtWidget::Table:
+	case QtWidget::CustomTable:
+	    {
+		TableWidget tbl(w.table());
+		QtTable* custom = tbl.customTable();
+		if (custom) {
+		    if (custom->delTableRow(item))
+			row = 0;
+		}
+		else {
+		    row = tbl.getRow(item);
+		    if (row >= 0)
+			tbl.delRow(row);
+		}
+		n = tbl.rowCount();
+	    }
+	    break;
+	case QtWidget::ComboBox:
+	    row = w.findComboItem(item);
+	    if (row >= 0) {
+		w.combo()->removeItem(row);
+		n = w.combo()->count();
+	    }
+	    break;
+	case QtWidget::ListBox:
+	    row = w.findListItem(item);
+	    if (row >= 0) {
+		QStringListModel* model = (QStringListModel*)w.list()->model();
+		if (!(model && model->removeRow(row)))
+		    row = -1;
+		n = w.list()->count();
+	    }
+	    break;
+	default:
+	    UIWidget* uiw = w.uiWidget();
+	    if (uiw && uiw->delTableRow(item)) {
+		row = 0;
+		// Don't notify empty: we don't know it
+		n = 1;
+	    }
+    }
+    if (row < 0)
+	return false;
+    if (!n)
+	raiseSelectIfEmpty(0,this,name);
+    return true;
+}
+
+bool QtWindow::setTableRow(const String& name, const String& item, const NamedList* data)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) setTableRow(%s,%s,%p) [%p]",
+	m_id.c_str(),name.c_str(),item.c_str(),data,this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->setTableRow(item,data);
+    if (w.type() != QtWidget::Table)
+	return false;
+    TableWidget tbl(w.table());
+    int row = tbl.getRow(item);
+    if (row < 0)
+	return false;
+    if (data)
+	tbl.updateRow(row,*data);
+    return true;
+}
+
+bool QtWindow::getTableRow(const String& name, const String& item, NamedList* data)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::getTableRow(%s,%s,%p) [%p]",
+	name.c_str(),item.c_str(),data,this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->getTableRow(item,data);
+    if (w.type() != QtWidget::Table)
+	return false;
+    TableWidget tbl(w.table(),false);
+    int row = tbl.getRow(item);
+    if (row < 0)
+	return false;
+    if (!data)
+	return true;
+    int n = tbl.columnCount();
+    for (int i = 0; i < n; i++) {
+	String name;
+	if (!tbl.getHeaderText(i,name))
+	    continue;
+	String value;
+	if (tbl.getCell(row,i,value))
+	    data->setParam(name,value);
+    }
+    return true;
+}
+
+// Set a table row or add a new one if not found
+bool QtWindow::updateTableRow(const String& name, const String& item,
+    const NamedList* data, bool atStart)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) updateTableRow('%s','%s',%p,%s) [%p]",
+	m_id.c_str(),name.c_str(),item.c_str(),data,String::boolText(atStart),this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    switch (w.type()) {
+	case QtWidget::Table:
+	case QtWidget::CustomTable:
+	    {
+		TableWidget tbl(w.table());
+		QtTable* custom = tbl.customTable();
+		if (custom) {
+		    if (custom->getTableRow(item))
+			return custom->setTableRow(item,data);
+		    return custom->addTableRow(item,data,atStart);
+		}
+		tbl.updateRow(item,data,atStart);
+		return true;
+	    }
+	case QtWidget::CustomTree:
+	    {
+		QtTree* custom = w.customTree();
+		if (custom) {
+		    if (custom->getTableRow(item))
+			return custom->setTableRow(item,data);
+		    return custom->addTableRow(item,data,atStart);
+		}
+		return false;
+	    }
+	case QtWidget::ComboBox:
+	    return w.findComboItem(item) >= 0 || w.addComboItem(item,atStart);
+	case QtWidget::ListBox:
+	    return w.findListItem(item) >= 0 || w.addListItem(item,atStart);
+    }
+    return false;
+}
+
+// Add or set one or more table row(s). Screen update is locked while changing the table.
+// Each data list element is a NamedPointer carrying a NamedList with item parameters.
+// The name of an element is the item to update.
+// Element's value not empty: update the item
+// Else: delete it
+bool QtWindow::updateTableRows(const String& name, const NamedList* data, bool atStart)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) updateTableRows('%s',%p,%s) [%p]",
+	m_id.c_str(),name.c_str(),data,String::boolText(atStart),this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw) {
+	bool ok = uiw->updateTableRows(data,atStart);
+	QtTable* ct = w.customTable();
+	if (ct)
+	    raiseSelectIfEmpty(ct->rowCount(),this,name);
+	return ok;
+    }
+    if (w.type() != QtWidget::Table)
+	return false;
+    if (!data)
+	return true;
+    TableWidget tbl(w.table());
+    bool ok = true;
+    tbl.table()->setUpdatesEnabled(false);
+    ObjList add;
+    unsigned int n = data->length();
+    for (unsigned int i = 0; i < n; i++) {
+	if (Client::exiting())
+	    break;
+	// Get item and the list of parameters
+	NamedString* ns = data->getParam(i);
+	if (!ns)
+	    continue;
+	// Delete ?
+	if (ns->null()) {
+	    int row = tbl.getRow(ns->name());
+	    if (row >= 0)
+		tbl.delRow(row);
+	    else
+		ok = false;
+	    continue;
+	}
+	// Set existing row or postpone add
+	int row = tbl.getRow(ns->name());
+	if (row >= 0) {
+	    const NamedList* params = YOBJECT(NamedList,ns);
+	    if (params)
+		tbl.updateRow(row,*params);
+	}
+	else if (ns->toBoolean())
+	    add.append(ns)->setDelete(false);
+	else
+	    ok = false;
+    }
+    n = add.count();
+    if (n) {
+	int row = tbl.rowCount();
+	if (row < 0)
+	    row = 0;
+	// Append if not requested to insert at start or table is empty
+	if (!(atStart && row))
+	    tbl.table()->setRowCount(row + n);
+	else {
+	    for (unsigned int i = 0; i < n; i++)
+		tbl.table()->insertRow(0);
+	}
+	for (ObjList* o = add.skipNull(); o; row++, o = o->skipNext()) {
+	    NamedString* ns = static_cast<NamedString*>(o->get());
+	    tbl.setID(row,ns->name());
+	    const NamedList* params = YOBJECT(NamedList,ns);
+	    if (params)
+		tbl.updateRow(row,*params);
+	}
+    }
+    tbl.table()->setUpdatesEnabled(true);
+    raiseSelectIfEmpty(tbl.rowCount(),this,name);
+    return ok;
+}
+
+bool QtWindow::clearTable(const String& name)
+{
+    DDebug(QtDriver::self(),DebugAll,"QtWindow::clearTable(%s) [%p]",name.c_str(),this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->clearTable();
+    bool ok = true;
+    if (w.widget())
+	w->setUpdatesEnabled(false);
+    switch (w.type()) {
+	case QtWidget::Table:
+	    w.table()->setRowCount(0);
+	    break;
+	case QtWidget::TextBrowser:
+	case QtWidget::TextEdit:
+	    w.textEdit()->clear();
+	    break;
+	case QtWidget::ListBox:
+	    w.list()->clear();
+	    break;
+	case QtWidget::ComboBox:
+	    w.combo()->clear();
+	    break;
+	default:
+	    ok = false;
+    }
+    if (w.widget())
+	w->setUpdatesEnabled(true);
+    return ok;
+}
+
+// Show or hide control busy state
+bool QtWindow::setBusy(const String& name, bool on)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) setBusy(%s,%u) [%p]",
+	m_id.c_str(),name.c_str(),on,this);
+    if (name == m_id)
+	return QtBusyWidget::showBusyChild(this,on);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->setBusy(on);
+    if (w.widget())
+	return QtBusyWidget::showBusyChild(w.widget(),on);
+    return false;
+}
+
+bool QtWindow::getText(const String& name, String& text, bool richText)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) getText(%s) [%p]",
+	m_id.c_str(),name.c_str(),this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->getText(text,richText);
+    switch (w.type()) {
+	case QtWidget::ComboBox:
+	    QtClient::getUtf8(text,w.combo()->currentText());
+	    return true;
+	case QtWidget::LineEdit:
+	    QtClient::getUtf8(text,w.lineEdit()->text());
+	    return true;
+	case QtWidget::TextBrowser:
+	case QtWidget::TextEdit:
+	    if (!richText)
+		QtClient::getUtf8(text,w.textEdit()->toPlainText());
+	    else
+		QtClient::getUtf8(text,w.textEdit()->toHtml());
+	    return true;
+	case QtWidget::Label:
+	    QtClient::getUtf8(text,w.label()->text());
+	    return true;
+	case QtWidget::Action:
+	    QtClient::getUtf8(text,w.action()->text());
+	    return true;
+	case QtWidget::SpinBox:
+	    text = w.spinBox()->value();
+	    return true;
+	default:
+	    if (w.inherits(QtWidget::AbstractButton)) {
+		QtClient::getUtf8(text,w.abstractButton()->text());
+		return true;
+	    }
+     }
+     return false;
+}
+
+bool QtWindow::getCheck(const String& name, bool& checked)
+{
+    DDebug(QtDriver::self(),DebugAll,"QtWindow::getCheck(%s) [%p]",name.c_str(),this);
+
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    if (w.inherits(QtWidget::AbstractButton))
+	checked = w.abstractButton()->isChecked();
+    else if (w.type() == QtWidget::Action)
+	checked = w.action()->isChecked();
+    else
+	return false;
+    return true;
+}
+
+bool QtWindow::getSelect(const String& name, String& item)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::getSelect(%s) [%p]",name.c_str(),this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->getSelect(item);
+    switch (w.type()) {
+	case QtWidget::ComboBox:
+	    if (w.combo()->lineEdit() && w.combo()->lineEdit()->selectedText().isEmpty())
+		return false;
+	    QtClient::getUtf8(item,w.combo()->currentText());
+	    return true;
+	case QtWidget::Table:
+	    {
+		TableWidget t(w);
+		int row = t.crtRow();
+		return row >= 0 ? t.getCell(row,0,item) : false;
+	    }
+	case QtWidget::ListBox:
+	    {
+		QListWidgetItem* crt = w.list()->currentItem();
+		if (!crt)
+		    return false;
+		QtClient::getUtf8(item,crt->text());
+	    }
+	    return true;
+	case QtWidget::Slider:
+	    item = w.slider()->value();
+	    return true;
+	case QtWidget::ProgressBar:
+	    item = w.progressBar()->value();
+	    return true;
+	case QtWidget::Tab:
+	    {
+		item = "";
+		QWidget* wid = w.tab()->currentWidget();
+		if (wid)
+		    QtClient::getUtf8(item,wid->objectName());
+	    }
+	    return true;
+	case QtWidget::StackWidget:
+	    {
+		item = "";
+		QWidget* wid = w.stackWidget()->currentWidget();
+		if (wid)
+		    QtClient::getUtf8(item,wid->objectName());
+	    }
+	    return true;
+    }
+    return false;
+}
+
+// Retrieve an element's multiple selection
+bool QtWindow::getSelect(const String& name, NamedList& items)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow::getSelect(%p) [%p]",&items,this);
+    QtWidget w(this,name);
+    if (w.invalid())
+	return false;
+    UIWidget* uiw = w.uiWidget();
+    if (uiw)
+	return uiw->getSelect(items);
+    switch (w.type()) {
+	case QtWidget::ComboBox:
+	case QtWidget::Table:
+	case QtWidget::ListBox:
+	case QtWidget::Slider:
+	case QtWidget::ProgressBar:
+	case QtWidget::Tab:
+	case QtWidget::StackWidget:
+	    DDebug(QtDriver::self(),DebugStub,"QtWindow::getSelect(%p) not implemented for '%s; [%p]",
+		&items,w.widget()->metaObject()->className(),this);
+    }
+    return false;
+}
+
+// Build a menu from a list of parameters
+bool QtWindow::buildMenu(const NamedList& params)
+{
+    QWidget* parent = this;
+    // Retrieve the owner
+    const String& owner = params[YSTRING("owner")];
+    if (owner && owner != m_id) {
+	parent = findChild<QWidget*>(QtClient::setUtf8(owner));
+	if (!parent) {
+	    DDebug(QtDriver::self(),DebugNote,
+		"QtWindow(%s) buildMenu(%s) owner '%s' not found [%p]",
+		m_id.c_str(),params.c_str(),owner.c_str(),this);
+	    return false;
+	}
+    }
+    QWidget* target = parent;
+    const String& t = params[YSTRING("target")];
+    if (t) {
+	target = findChild<QWidget*>(QtClient::setUtf8(t));
+	if (!target) {
+	    DDebug(QtDriver::self(),DebugNote,
+		"QtWindow(%s) buildMenu(%s) target '%s' not found [%p]",
+		m_id.c_str(),params.c_str(),t.c_str(),this);
+	    return false;
+	}
+    }
+    // Remove existing menu
+    removeMenu(params);
+    QMenu* menu = QtClient::buildMenu(params,params.getValue(YSTRING("title"),params),this,
+	SLOT(action()),SLOT(toggled(bool)),parent);
+    if (!menu) {
+	DDebug(QtDriver::self(),DebugNote,
+	    "QtWindow(%s) failed to build menu '%s' target='%s' [%p]",
+	    m_id.c_str(),params.c_str(),YQT_OBJECT_NAME(target),this);
+	return false;
+    }
+    DDebug(QtDriver::self(),DebugAll,"QtWindow(%s) built menu '%s' target='%s' [%p]",
+	m_id.c_str(),params.c_str(),YQT_OBJECT_NAME(target),this);
+    QMenuBar* mbOwner = qobject_cast<QMenuBar*>(target);
+    QMenu* mOwner = !mbOwner ? qobject_cast<QMenu*>(target) : 0;
+    if (mbOwner || mOwner) {
+	QAction* before = 0;
+	const String& bef = params[YSTRING("before")];
+	// Retrieve the action to insert before
+	if (bef) {
+	    QString cmp = QtClient::setUtf8(bef);
+	    QList<QAction*> list = target->actions();
+	    for (int i = 0; !before && i < list.size(); i++) {
+		// Check action name or menu name if the action is associated with a menu
+		if (list[i]->objectName() == cmp)
+		    before = list[i];
+		else if (list[i]->menu() && list[i]->menu()->objectName() == cmp)
+		    before = list[i]->menu()->menuAction();
+		if (before && i && list[i - 1]->isSeparator() &&
+		    params.getBoolValue(YSTRING("before_separator"),true))
+		    before = list[i - 1];
+	    }
+	}
+	// Insert the menu
+	if (mbOwner)
+	    mbOwner->insertMenu(before,menu);
+	else
+	    mOwner->insertMenu(before,menu);
+    }
+    else {
+	QToolButton* tb = qobject_cast<QToolButton*>(target);
+	if (tb)
+	    tb->setMenu(menu);
+	else {
+	    QPushButton* pb = qobject_cast<QPushButton*>(target);
+	    if (pb)
+		pb->setMenu(menu);
+	    else if (!QtClient::setProperty(target,s_propContextMenu,params))
+		target->addAction(menu->menuAction());
+	}
+    }
+    return true;
+}
+
+// Remove a menu
+bool QtWindow::removeMenu(const NamedList& params)
+{
+    QWidget* parent = this;
+    // Retrieve the owner
+    const String& owner = params[YSTRING("owner")];
+    if (owner && owner != m_id) {
+	parent = findChild<QWidget*>(QtClient::setUtf8(owner));
+	if (!parent)
+	    return false;
+    }
+    QMenu* menu = parent->findChild<QMenu*>(QtClient::setUtf8(params));
+    if (!menu)
+	return false;
+    QtClient::deleteLater(menu);
+    return true;
+}
+
+// Set an element's image
+bool QtWindow::setImage(const String& name, const String& image, bool fit)
+{
+    if (!name)
+	return false;
+    if (name == m_id)
+	return QtClient::setImage(this,image);
+    QObject* obj = findChild<QObject*>(QtClient::setUtf8(name));
+    return obj && QtClient::setImage(obj,image,fit);
+}
+
+// Set a property for this window or for a widget owned by it
+bool QtWindow::setProperty(const String& name, const String& item, const String& value)
+{
+    if (name == m_id)
+	return QtClient::setProperty(wndWidget(),item,value);
+    QObject* obj = findChild<QObject*>(QtClient::setUtf8(name));
+    return obj ? QtClient::setProperty(obj,item,value) : false;
+}
+
+// Get a property from this window or from a widget owned by it
+bool QtWindow::getProperty(const String& name, const String& item, String& value)
+{
+    if (name == m_id)
+	return QtClient::getProperty(wndWidget(),item,value);
+    QObject* obj = findChild<QObject*>(QtClient::setUtf8(name));
+    return obj ? QtClient::getProperty(obj,item,value) : false;
+}
+
+void QtWindow::moveEvent(QMoveEvent* event)
+{
+    QWidget::moveEvent(event);
+    // Don't update pos if not shown normal
+    if (!isShownNormal())
+	return;
+    m_x = pos().x();
+    m_y = pos().y();
+    DDebug(QtDriver::self(),DebugAll,"QtWindow(%s) moved x=%d y=%d [%p]",
+	m_id.c_str(),m_x,m_y,this);
+}
+
+void QtWindow::resizeEvent(QResizeEvent* event)
+{
+    QWidget::resizeEvent(event);
+    // Don't update size if not shown normal
+    if (!isShownNormal())
+	return;
+    m_width = width();
+    m_height = height();
+    DDebug(QtDriver::self(),DebugAll,"QtWindow(%s) resized width=%d height=%d [%p]",
+	m_id.c_str(),m_width,m_height,this);
+}
+
+bool QtWindow::event(QEvent* ev)
+{
+    static const String s_activeChg("window_active_changed");
+    if (ev->type() == QEvent::WindowDeactivate) {
+	String hideProp;
+	QtClient::getProperty(wndWidget(),s_propHideInactive,hideProp);
+	if (hideProp && hideProp.toBoolean())
+	    setVisible(false);
+	m_active = false;
+	Client::self()->toggle(this,s_activeChg,false);
+    }
+    else if (ev->type() == QEvent::WindowActivate) {
+	m_active = true;
+	Client::self()->toggle(this,s_activeChg,true);
+	String wName;
+	if (getPropPlatform(wndWidget(),s_propShowWndWhenActive,wName) && wName)
+	    Client::setVisible(wName);
+    }
+    else if (ev->type() == QEvent::ApplicationDeactivate) {
+	if (m_active) {
+	    m_active = false;
+	    Client::self()->toggle(this,s_activeChg,true);
+	}
+    }
+    return QWidget::event(ev);
+}
+
+void QtWindow::closeEvent(QCloseEvent* event)
+{
+    // NOTE: Don't access window's data after calling hide():
+    //  some logics might destroy the window when hidden
+
+    // Notify window closed
+    String tmp;
+    if (Client::self() &&
+	QtClient::getProperty(wndWidget(),"_yate_windowclosedaction",tmp))
+	Client::self()->action(this,tmp);
+
+    // Hide the window when requested
+    if (QtClient::getBoolProperty(wndWidget(),"_yate_hideonclose")) {
+	event->ignore();
+	hide();
+	return;
+    }
+
+    QWidget::closeEvent(event);
+    if (m_mainWindow && Client::self()) {
+	Client::self()->quit();
+	return;
+    }
+    if (QtClient::getBoolProperty(wndWidget(),"_yate_destroyonclose")) {
+	XDebug(QtDriver::self(),DebugAll,
+	    "Window(%s) closeEvent() set delete later [%p]",m_id.c_str(),this);
+	QObject::deleteLater();
+	// Safe to call hide(): the window will be deleted when control returns
+	//  to the main loop
+    }
+    hide();
+}
+
+void QtWindow::changeEvent(QEvent* event)
+{
+    if (event->type() == QEvent::WindowStateChange)
+	m_maximized = isMaximized();
+    QWidget::changeEvent(event);
+}
+
+void QtWindow::action()
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) action() sender=%s [%p]",
+	m_id.c_str(),YQT_OBJECT_NAME(sender()),this);
+    if (!QtClient::self() || QtClient::changing())
+	return;
+    String name;
+    NamedList* params = 0;
+    if (!QtClient::getBoolProperty(sender(),"_yate_translateidentity"))
+	QtClient::getIdentity(sender(),name);
+    else {
+	QtWidget w(sender());
+	translateName(w,name,&params);
+    }
+    if (name)
+	QtClient::self()->action(this,name,params);
+    TelEngine::destruct(params);
+}
+
+// Toggled actions
+void QtWindow::toggled(bool on)
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) toggled=%s sender=%s [%p]",
+	m_id.c_str(),String::boolText(on),YQT_OBJECT_NAME(sender()),this);
+    QtClient::updateToggleImage(sender());
+    if (!QtClient::self() || QtClient::changing())
+	return;
+    QtWidget w(sender());
+    String name;
+    if (translateName(w,name))
+	QtClient::self()->toggle(this,name,on);
+}
+
+// System tray actions
+void QtWindow::sysTrayIconAction(QSystemTrayIcon::ActivationReason reason)
+{
+    String action;
+    switch (reason) {
+	case QSystemTrayIcon::Context:
+	    QtClient::getProperty(sender(),s_propAction + "Context",action);
+	    break;
+	case QSystemTrayIcon::DoubleClick:
+	    QtClient::getProperty(sender(),s_propAction + "DoubleClick",action);
+	    break;
+	case QSystemTrayIcon::Trigger:
+	    QtClient::getProperty(sender(),s_propAction + "Trigger",action);
+	    break;
+	case QSystemTrayIcon::MiddleClick:
+	    QtClient::getProperty(sender(),s_propAction + "MiddleClick",action);
+	    break;
+	default:
+	    return;
+    }
+    if (action)
+	Client::self()->action(this,action);
+}
+
+// Choose file window was accepted
+void QtWindow::chooseFileAccepted()
+{
+    QFileDialog* dlg = qobject_cast<QFileDialog*>(sender());
+    if (!dlg)
+	return;
+    String action;
+    QtClient::getUtf8(action,dlg->objectName());
+    if (!action)
+	return;
+    NamedList params("");
+    QDir dir = dlg->directory();
+    if (dir.absolutePath().length())
+	QtClient::getUtf8(params,"dir",fixPathSep(dir.absolutePath()));
+    QStringList files = dlg->selectedFiles();
+    for (int i = 0; i < files.size(); i++)
+	QtClient::getUtf8(params,"file",fixPathSep(files[i]));
+    if (dlg->fileMode() != QFileDialog::DirectoryOnly &&
+	dlg->fileMode() != QFileDialog::Directory) {
+	QString filter = dlg->selectedNameFilter();
+	if (filter.length())
+	    QtClient::getUtf8(params,"filter",filter);
+    }
+    Client::self()->action(this,action,&params);
+}
+
+// Choose file window was cancelled
+void QtWindow::chooseFileRejected()
+{
+    QFileDialog* dlg = qobject_cast<QFileDialog*>(sender());
+    if (!dlg)
+	return;
+    String action;
+    QtClient::getUtf8(action,dlg->objectName());
+    if (!action)
+	return;
+    Client::self()->action(this,action,0);
+}
+
+void QtWindow::openUrl(const QString& link)
+{
+    QDesktopServices::openUrl(QUrl(link));
+}
+
+void QtWindow::doubleClick()
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) doubleClick() sender=%s [%p]",
+	m_id.c_str(),YQT_OBJECT_NAME(sender()),this);
+    if (QtClient::self() && sender())
+	Client::self()->action(this,YQT_OBJECT_NAME(sender()));
+}
+
+// A widget's selection changed
+void QtWindow::selectionChanged()
+{
+    XDebug(QtDriver::self(),DebugAll,"QtWindow(%s) selectionChanged() sender=%s [%p]",
+	m_id.c_str(),YQT_OBJECT_NAME(sender()),this);
+    if (!(QtClient::self() && sender()))
+	return;
+    String name = YQT_OBJECT_NAME(sender());
+    QtWidget w(sender());
+    if (w.type() != QtWidget::Calendar) {
+	String item;
+	getSelect(name,item);
+	Client::self()->select(this,name,item);
+    }
+    else {
+	NamedList p("");
+	QDate d = w.calendar()->selectedDate();
+	p.addParam("year",String(d.year()));
+	p.addParam("month",String(d.month()));
+	p.addParam("day",String(d.day()));
+	Client::self()->action(this,name,&p);
+    }
+}
+
+// Connect an object's text changed signal to window's slot
+bool QtWindow::connectTextChanged(QObject* obj)
+{
+    if (!(obj && QtClient::getBoolProperty(obj,"_yate_textchangednotify")))
+	return false;
+    QComboBox* combo = qobject_cast<QComboBox*>(obj);
+    if (combo)
+	return QtClient::connectObjects(combo,SIGNAL(editTextChanged(const QString&)),
+	    this,SLOT(textChanged(const QString&)));
+    QLineEdit* lineEdit = qobject_cast<QLineEdit*>(obj);
+    if (lineEdit)
+	return QtClient::connectObjects(lineEdit,SIGNAL(textChanged(const QString&)),
+	    this,SLOT(textChanged(const QString&)));
+    QTextEdit* textEdit = qobject_cast<QTextEdit*>(obj);
+    if (textEdit)
+	return QtClient::connectObjects(textEdit,SIGNAL(textChanged()),
+	    this,SLOT(textChanged()));
+    const QMetaObject* meta = obj->metaObject();
+    Debug(DebugStub,"connectTextChanged() not implemented for class '%s'",
+	meta ? meta->className() :  "");
+    return false;
+}
+
+// Notify text changed to the client
+void QtWindow::notifyTextChanged(QObject* obj, const QString& text)
+{
+    if (!(obj && QtClient::getBoolProperty(obj,"_yate_textchangednotify")))
+	return;
+    // Detect QtUIWidget item. Get its container identity if found
+    String item;
+    QtUIWidget::getListItemProp(obj,item);
+    QtUIWidget* uiw = item ? QtUIWidget::container(obj) : 0;
+    String name;
+    if (!uiw)
+	QtClient::getIdentity(obj,name);
+    else
+	uiw->getIdentity(obj,name);
+    if (!name)
+	return;
+    NamedList p("");
+    p.addParam("sender",name);
+    if (text.size())
+	QtClient::getUtf8(p,"text",text);
+    Client::self()->action(this,YSTRING("textchanged"),&p);
+}
+
+// Load a widget from file
+QWidget* QtWindow::loadUI(const char* fileName, QWidget* parent,
+	const char* uiName, const char* path)
+{
+    if (Client::exiting())
+	return 0;
+    if (!(fileName && *fileName && parent))
+	return 0;
+
+    if (!(path && *path))
+	path = Client::s_skinPath.c_str();
+    UIBuffer* buf = UIBuffer::build(fileName);
+    const char* err = 0;
+    if (buf && buf->buffer()) {
+	QBuffer b(buf->buffer());
+	QUiLoader loader;
+        loader.setWorkingDirectory(QDir(QtClient::setUtf8(path)));
+	QWidget* w = loader.load(&b,parent);
+	if (w)
+	    return w;
+	err = "loader failed";
+    }
+    else
+	err = buf ? "file is empty" : "file not found";
+    // Error
+    TelEngine::destruct(buf);
+    Debug(DebugWarn,"Failed to load widget '%s' file='%s' path='%s': %s",
+        uiName,fileName,path,err);
+    return 0;
+}
+
+// Clear the UI cache
+void QtWindow::clearUICache(const char* fileName)
+{
+    if (!fileName)
+	UIBuffer::s_uiCache.clear();
+    else
+	TelEngine::destruct(UIBuffer::s_uiCache.find(fileName));
+}
+
+// Filter events
+bool QtWindow::eventFilter(QObject* obj, QEvent* event)
+{
+    if (!obj)
+	return false;
+    // Apply dynamic properties changes
+    if (event->type() == QEvent::DynamicPropertyChange) {
+	String name = YQT_OBJECT_NAME(obj);
+	QDynamicPropertyChangeEvent* ev = static_cast<QDynamicPropertyChangeEvent*>(event);
+	String prop = ev->propertyName().constData();
+	// Handle only yate dynamic properties
+	if (!prop.startsWith(s_yatePropPrefix,false))
+	    return QWidget::eventFilter(obj,event);
+	XDebug(QtDriver::self(),DebugAll,"Window(%s) eventFilter(%s) prop=%s [%p]",
+	    m_id.c_str(),YQT_OBJECT_NAME(obj),prop.c_str(),this);
+	// Return false for now on: it's our property
+	QtWidget w(obj);
+	if (w.invalid())
+	    return false;
+	String value;
+	if (!QtClient::getProperty(obj,prop,value))
+	    return false;
+	bool ok = true;
+	bool handled = true;
+	if (prop == s_propColWidths) {
+	    if (w.type() == QtWidget::Table) {
+		QHeaderView* hdr = w.table()->horizontalHeader();
+		bool skipLast = hdr && hdr->stretchLastSection();
+		ObjList* list = value.split(',',false);
+		int col = 0;
+		for (ObjList* o = list->skipNull(); o; o = o->skipNext(), col++) {
+		    if (skipLast && col == w.table()->columnCount() - 1)
+			break;
+		    int width = (static_cast<String*>(o->get()))->toInteger(-1);
+		    if (width >= 0)
+			w.table()->setColumnWidth(col,width);
+		}
+		TelEngine::destruct(list);
+	    }
+	}
+	else if (prop == s_propSorting) {
+	    if (w.type() == QtWidget::Table) {
+		ObjList* list = value.split(',',false);
+		String* tmp = static_cast<String*>((*list)[0]);
+		int col = tmp ? tmp->toInteger(-1) : -1;
+		if (col >= 0) {
+		    tmp = static_cast<String*>((*list)[1]);
+		    bool asc = tmp ? tmp->toBoolean(true) : true;
+		    w.table()->sortItems(col,asc ? Qt::AscendingOrder : Qt::DescendingOrder);
+		}
+		TelEngine::destruct(list);
+	    }
+	}
+	else if (prop == s_propSizes) {
+	    if (w.type() == QtWidget::Splitter) {
+		QList<int> list = QtClient::str2IntList(value);
+		w.splitter()->setSizes(list);
+	    }
+	}
+	else if (prop == s_propWindowFlags) {
+	    QWidget* wid = (name == m_id || name == m_oldId) ? this : w.widget();
+	    QtClient::applyWindowFlags(wid,value);
+	}
+	else if (prop == s_propHHeader) {
+	    // Show/hide the horizontal header
+	    ok = ((w.type() == QtWidget::Table || w.type() == QtWidget::CustomTable) &&
+		value.isBoolean() && w.table()->horizontalHeader());
+	    if (ok)
+		w.table()->horizontalHeader()->setVisible(value.toBoolean());
+	}
+	else
+	    ok = handled = false;
+	if (ok)
+	    DDebug(ClientDriver::self(),DebugAll,
+		"Applied dynamic property %s='%s' for object='%s'",
+		prop.c_str(),value.c_str(),name.c_str());
+	else if (handled)
+	    Debug(ClientDriver::self(),DebugMild,
+		"Failed to apply dynamic property %s='%s' for object='%s'",
+		prop.c_str(),value.c_str(),name.c_str());
+	return false;
+    }
+    if (event->type() == QEvent::KeyPress) {
+	String action;
+	bool filter = false;
+	if (!QtClient::filterKeyEvent(obj,static_cast<QKeyEvent*>(event),
+	    action,filter,this))
+	    return QWidget::eventFilter(obj,event);
+	if (action && Client::self())
+	    Client::self()->action(this,action);
+	return filter;
+    }
+    if (event->type() == QEvent::ContextMenu) {
+	if (handleContextMenuEvent(static_cast<QContextMenuEvent*>(event),obj))
+	    return false;
+    }
+    if (event->type() == QEvent::Enter) {
+	QtClient::updateImageFromMouse(obj,true,true);
+	return QWidget::eventFilter(obj,event);
+    }
+    if (event->type() == QEvent::Leave) {
+	QtClient::updateImageFromMouse(obj,true,false);
+	return QWidget::eventFilter(obj,event);
+    }
+    if (event->type() == QEvent::MouseButtonPress) {
+	QtClient::updateImageFromMouse(obj,false,true);
+	return QWidget::eventFilter(obj,event);
+    }
+    if (event->type() == QEvent::MouseButtonRelease) {
+	QtClient::updateImageFromMouse(obj,false,false);
+	return QWidget::eventFilter(obj,event);
+    }
+    return QWidget::eventFilter(obj,event);
+}
+
+// Handle key pressed events
+void QtWindow::keyPressEvent(QKeyEvent* event)
+{
+    if (!(Client::self() && event)) {
+	QWidget::keyPressEvent(event);
+	return;
+    }
+    QVariant var = this->property("_yate_keypress_redirect");
+    QString child = var.toString();
+    if (child.size() > 0 && QtClient::sendEvent(*event,this,child)) {
+	QWidget* wid = findChild<QWidget*>(child);
+	if (wid)
+	    wid->setFocus();
+	return;
+    }
+    if (event->key() == Qt::Key_Backspace)
+	Client::self()->backspace(m_id,this);
+    QWidget::keyPressEvent(event);
+}
+
+// Show hide window. Notify the client
+void QtWindow::setVisible(bool visible)
+{
+    // Override position for notification windows
+    if (visible && isShownNormal() &&
+	QtClient::getBoolProperty(wndWidget(),"_yate_notificationwindow")) {
+	// Don't move
+	m_moving = -1;
+#ifndef Q_WS_MAC
+	// Detect unavailable screen space position and move the window in the apropriate position
+	// bottom/right/none: move it in the right/bottom corner.
+	// top: move it in the right/top corner.
+	// left: move it in the left/bottom corner.
+	int pos = QtClient::PosNone;
+	if (QtClient::getScreenUnavailPos(this,pos)) {
+	    if (0 != (pos & (QtClient::PosBottom | QtClient::PosRight)) || pos == QtClient::PosNone)
+		QtClient::moveWindow(this,QtClient::CornerBottomRight);
+	    else if (0 != (pos & QtClient::PosTop))
+		QtClient::moveWindow(this,QtClient::CornerTopRight);
+	    else
+		QtClient::moveWindow(this,QtClient::CornerBottomLeft);
+	}
+#else
+	QtClient::moveWindow(this,QtClient::CornerTopRight);
+#endif
+    }
+    if (visible && isMinimized())
+	showNormal();
+    else
+	QWidget::setVisible(visible);
+    // Notify the client on window visibility changes
+    bool changed = (m_visible != visible);
+    m_visible = visible;
+    if (changed && Client::self()) {
+	QVariant var;
+	if (this)
+	    var = this->property("dynamicUiActionVisibleChanged");
+	if (!var.toBool())
+	    Client::self()->toggle(this,YSTRING("window_visible_changed"),m_visible);
+	else {
+	    Message* m = new Message("ui.action");
+	    m->addParam("action","window_visible_changed");
+	    m->addParam("visible",String::boolText(m_visible));
+	    m->addParam("window",m_id);
+	    Engine::enqueue(m);
+	}
+    }
+    if (!m_visible && QtClient::getBoolProperty(wndWidget(),"_yate_destroyonhide")) {
+	DDebug(QtDriver::self(),DebugAll,
+	    "Window(%s) setVisible(false) set delete later [%p]",m_id.c_str(),this);
+	QObject::deleteLater();
+    }
+    // Destroy owned dialogs
+    if (!m_visible) {
+	QList<QDialog*> d = findChildren<QDialog*>();
+	for (int i = 0; i < d.size(); i++)
+	    d[i]->deleteLater();
+    }
+}
+
+// Show the window
+void QtWindow::show()
+{
+    setVisible(true);
+    m_maximized = m_maximized || isMaximized();
+    if (m_maximized)
+	setWindowState(Qt::WindowMaximized);
+}
+
+// Hide the window
+void QtWindow::hide()
+{
+    setVisible(false);
+}
+
+void QtWindow::size(int width, int height)
+{
+    Debug(QtDriver::self(),DebugStub,"QtWindow(%s)::size(%d,%d) [%p]",m_id.c_str(),width,height,this);
+}
+
+void QtWindow::move(int x, int y)
+{
+    DDebug(QtDriver::self(),DebugAll,"QtWindow(%s)::move(%d,%d) [%p]",m_id.c_str(),x,y,this);
+    QWidget::move(x,y);
+}
+
+void QtWindow::moveRel(int dx, int dy)
+{
+    DDebug(QtDriver::self(),DebugAll,"QtWindow::moveRel(%d,%d) [%p]",dx,dy,this);
+}
+
+bool QtWindow::related(const Window* wnd) const
+{
+    DDebug(QtDriver::self(),DebugAll,"QtWindow::related(%p) [%p]",wnd,this);
+    return false;
+}
+
+void QtWindow::menu(int x, int y)
+{
+    DDebug(QtDriver::self(),DebugAll,"QtWindow::menu(%d,%d) [%p]",x,y,this);
+}
+
+// Create a modal dialog
+bool QtWindow::createDialog(const String& name, const String& title, const String& alias,
+    const NamedList* params)
+{
+    QtDialog* d = new QtDialog(this);
+    if (d->show(name,title,alias,params))
+	return true;
+    d->deleteLater();
+    return false;
+}
+
+// Destroy a modal dialog
+bool QtWindow::closeDialog(const String& name)
+{
+    QDialog* d = findChild<QDialog*>(QtClient::setUtf8(name));
+    if (!d)
+	return false;
+    d->deleteLater();
+    return true;
+}
+
+// Load UI file and setup the window
+void QtWindow::doPopulate()
+{
+    Debug(QtDriver::self(),DebugAll,"Populating window '%s' [%p]",m_id.c_str(),this);
+    QWidget* formWidget = loadUI(m_description,this,m_id);
+    if (!formWidget)
+	return;
+    // Set window title decoration flags to avoid pos/size troubles with late decoration
+    QVariant var = formWidget->property(s_propWindowFlags);
+    if (var.type() == QVariant::Invalid) {
+	String flgs = "title,sysmenu,minimize,close";
+	// Add maximize only if allowed
+	if (formWidget->maximumWidth() == QWIDGETSIZE_MAX ||
+	    formWidget->maximumHeight() == QWIDGETSIZE_MAX)
+	    flgs.append("maximize",",");
+	formWidget->setProperty(s_propWindowFlags,QVariant(QtClient::setUtf8(flgs)));
+    }
+    setMinimumSize(formWidget->minimumSize().width(),formWidget->minimumSize().height());
+    setMaximumSize(formWidget->maximumSize().width(),formWidget->maximumSize().height());
+    m_x = formWidget->pos().x();
+    m_y = formWidget->pos().y();
+    m_width = formWidget->width();
+    m_height = formWidget->height();
+    move(m_x,m_y);
+    QWidget::resize(m_width,m_height);
+    QtClient::setWidget(this,formWidget);
+    m_widget = YQT_OBJECT_NAME(formWidget);
+    String wTitle;
+    QtClient::getUtf8(wTitle,formWidget->windowTitle());
+    title(wTitle);
+    setWindowIcon(formWidget->windowIcon());
+    setStyleSheet(formWidget->styleSheet());
+}
+
+// Initialize window
+void QtWindow::doInit()
+{
+    DDebug(QtDriver::self(),DebugAll,"Initializing window '%s' [%p]",
+	m_id.c_str(),this);
+
+    // Create window's dynamic properties from config
+    Configuration cfg(Engine::configFile(m_oldId),false);
+    NamedList* sectGeneral = cfg.getSection("general");
+    if (sectGeneral)
+	addDynamicProps(this,*sectGeneral);
+
+    // Load window data
+    m_mainWindow = s_cfg.getBoolValue(m_oldId,"mainwindow");
+    m_saveOnClose = s_cfg.getBoolValue(m_oldId,"save",true);
+    if (m_id != m_oldId)
+	m_saveOnClose = s_cfg.getBoolValue(m_oldId,"savealias",m_saveOnClose);
+    NamedList* sect = s_save.getSection(m_id);
+    if (sect) {
+	m_maximized = sect->getBoolValue("maximized");
+	m_x = sect->getIntValue("x",m_x);
+	m_y = sect->getIntValue("y",m_y);
+	m_width = sect->getIntValue("width",m_width);
+	m_height = sect->getIntValue("height",m_height);
+	m_visible = sect->getBoolValue("visible");
+    }
+    else {
+	if (m_saveOnClose)
+	    Debug(QtDriver::self(),DebugNote,"Window(%s) not found in config [%p]",
+		m_id.c_str(),this);
+	m_visible = s_cfg.getBoolValue(m_oldId,"visible");
+	// Make sure the window is shown in the available geometry
+	QDesktopWidget* d = QApplication::desktop();
+	if (d) {
+	    QRect r = d->availableGeometry(this);
+	    m_x = r.x();
+	    m_y = r.y();
+	}
+    }
+    m_visible = m_mainWindow || m_visible;
+    if (!m_width)
+	m_width = this->width();
+    if (!m_height)
+	m_height = this->height();
+    move(m_x,m_y);
+    QWidget::resize(m_width,m_height);
+
+    // Build custom UI widgets from frames owned by this widget
+    QtClient::buildFrameUiWidgets(this);
+
+    // Create custom widgets from
+    // _yate_identity=customwidget|[separator=sep|] sep widgetclass sep widgetname [sep param=value]
+    QList<QFrame*> frm = findChildren<QFrame*>();
+    for (int i = 0; i < frm.size(); i++) {
+	String create;
+	QtClient::getProperty(frm[i],"_yate_identity",create);
+	if (!create.startSkip("customwidget|",false))
+	    continue;
+	char sep = '|';
+	// Check if we have another separator
+	if (create.startSkip("separator=",false)) {
+	    if (create.length() < 2)
+		continue;
+	    sep = create.at(0);
+	    create = create.substr(2);
+	}
+	ObjList* list = create.split(sep,false);
+	String type;
+	String name;
+	NamedList params("");
+	int what = 0;
+	for (ObjList* o = list->skipNull(); o; o = o->skipNext(), what++) {
+	    GenObject* p = o->get();
+	    if (what == 0)
+		type = p->toString();
+	    else if (what == 1)
+		name = p->toString();
+	    else {
+		// Decode param
+		int pos = p->toString().find('=');
+		if (pos != -1)
+		    params.addParam(p->toString().substr(0,pos),p->toString().substr(pos + 1));
+	    }
+	}
+	TelEngine::destruct(list);
+	params.addParam("parentwindow",m_id);
+	NamedString* pw = new NamedString("parentwidget");
+	QtClient::getUtf8(*pw,frm[i]->objectName());
+	params.addParam(pw);
+	QObject* obj = (QObject*)UIFactory::build(type,name,&params);
+	if (!obj)
+	    continue;
+	QWidget* wid = qobject_cast<QWidget*>(obj);
+	if (wid)
+	    QtClient::setWidget(frm[i],wid);
+	else {
+	    obj->setParent(frm[i]);
+	    QtCustomObject* customObj = qobject_cast<QtCustomObject*>(obj);
+	    if (customObj)
+		customObj->parentChanged();
+	}
+    }
+
+    // Add the first menubar to layout
+    QList<QMenuBar*> menuBars = findChildren<QMenuBar*>();
+    if (menuBars.size() && layout()) {
+	layout()->setMenuBar(menuBars[0]);
+	// Decrease minimum size policy to make sure the layout is made properly
+	if (wndWidget()) {
+	    int h = menuBars[0]->height();
+	    int min = wndWidget()->minimumHeight();
+	    if (min > h)
+		wndWidget()->setMinimumHeight(min - h);
+	    else
+		wndWidget()->setMinimumHeight(0);
+	}
+#ifdef Q_WS_MAC
+	if (m_mainWindow) {
+	    // Create a parentless menu bar to be set as the default application menu by copying it from the main window menu
+	    DDebug(QtDriver::self(),DebugAll,"Setting as default menu bar the menu bar of window '%s' [%p]",
+	    m_id.c_str(),this);
+	    QMenuBar* mainMenu = menuBars[0];
+	    QMenuBar* defaultMenu = new QMenuBar(0);
+	    QList<QAction*> topActions = mainMenu->actions();
+	    for (int i = 0; i < topActions.count(); i++) {
+		QMenu* menu = topActions[i]->menu();
+		if (menu) {
+		    QMenu* m = new QMenu(menu->title(),defaultMenu);
+		    String tmp;
+		    QtClient::getProperty(menu,YSTRING("_yate_menuNoCopy"),tmp);
+		    if (tmp.toBoolean())
+			continue;
+		    defaultMenu->addMenu(m);
+		    QList<QAction*> actions = menu->actions();
+		    for (int j = 0; j < actions.count(); j++) {
+			QAction* act = actions[j];
+			tmp.clear();
+			QtClient::getProperty(act,YSTRING("_yate_menuNoCopy"),tmp);
+			if (tmp.toBoolean())
+			    continue;
+			m->addAction(act);
+		    }
+		}
+	    }
+	}
+#endif
+    }
+
+    // Create window's children dynamic properties from config
+    unsigned int n = cfg.sections();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedList* sect = cfg.getSection(i);
+	if (sect && *sect && *sect != "general")
+	    addDynamicProps(findChild<QObject*>(sect->c_str()),*sect);
+    }
+
+    // Process "_yate_setaction" property for our children
+    QtClient::setAction(this);
+
+    // Connect actions' signal
+    QList<QAction*> actions = findChildren<QAction*>();
+    for (int i = 0; i < actions.size(); i++) {
+	String addToWidget;
+	QtClient::getProperty(actions[i],"dynamicAddToParent",addToWidget);
+	if (addToWidget && addToWidget.toBoolean())
+	    QWidget::addAction(actions[i]);
+	if (actions[i]->isCheckable())
+	    QtClient::connectObjects(actions[i],SIGNAL(toggled(bool)),this,SLOT(toggled(bool)));
+	else
+	    QtClient::connectObjects(actions[i],SIGNAL(triggered()),this,SLOT(action()));
+    }
+
+    // Connect combo boxes signals
+    QList<QComboBox*> combos = findChildren<QComboBox*>();
+    for (int i = 0; i < combos.size(); i++) {
+	QtClient::connectObjects(combos[i],SIGNAL(activated(int)),this,SLOT(selectionChanged()));
+    	connectTextChanged(combos[i]);
+    }
+
+    // Connect abstract buttons (check boxes and radio/push/tool buttons) signals
+    QList<QAbstractButton*> buttons = findChildren<QAbstractButton*>();
+    for(int i = 0; i < buttons.size(); i++)
+	if (QtClient::autoConnect(buttons[i]))
+	    connectButton(buttons[i]);
+
+    // Connect group boxes signals
+    QList<QGroupBox*> grp = findChildren<QGroupBox*>();
+    for(int i = 0; i < grp.size(); i++)
+	if (grp[i]->isCheckable())
+	    QtClient::connectObjects(grp[i],SIGNAL(toggled(bool)),this,SLOT(toggled(bool)));
+
+    // Connect sliders signals
+    QList<QSlider*> sliders = findChildren<QSlider*>();
+    for (int i = 0; i < sliders.size(); i++)
+	QtClient::connectObjects(sliders[i],SIGNAL(valueChanged(int)),this,SLOT(selectionChanged()));
+
+    // Connect calendar widget signals
+    QList<QCalendarWidget*> cals = findChildren<QCalendarWidget*>();
+    for (int i = 0; i < cals.size(); i++)
+	QtClient::connectObjects(cals[i],SIGNAL(selectionChanged()),this,SLOT(selectionChanged()));
+
+    // Connect list boxes signals
+    QList<QListWidget*> lists = findChildren<QListWidget*>();
+    for (int i = 0; i < lists.size(); i++) {
+	QtClient::connectObjects(lists[i],SIGNAL(itemDoubleClicked(QListWidgetItem*)),
+	    this,SLOT(doubleClick()));
+	QtClient::connectObjects(lists[i],SIGNAL(itemActivated(QListWidgetItem*)),
+	    this,SLOT(doubleClick()));
+	QtClient::connectObjects(lists[i],SIGNAL(currentRowChanged(int)),
+	    this,SLOT(selectionChanged()));
+    }
+
+    // Connect tab widget signals
+    QList<QTabWidget*> tabs = findChildren<QTabWidget*>();
+    for (int i = 0; i < tabs.size(); i++)
+	QtClient::connectObjects(tabs[i],SIGNAL(currentChanged(int)),this,SLOT(selectionChanged()));
+
+    // Connect stacked widget signals
+    QList<QStackedWidget*> sw = findChildren<QStackedWidget*>();
+    for (int i = 0; i < sw.size(); i++)
+	QtClient::connectObjects(sw[i],SIGNAL(currentChanged(int)),this,SLOT(selectionChanged()));
+
+    // Connect line edit signals
+    QList<QLineEdit*> le = findChildren<QLineEdit*>();
+    for (int i = 0; i < le.size(); i++)
+	connectTextChanged(le[i]);
+
+    // Connect text edit signals
+    QList<QTextEdit*> te = findChildren<QTextEdit*>();
+    for (int i = 0; i < te.size(); i++)
+	connectTextChanged(te[i]);
+
+    // Process tables:
+    // Insert a column and connect signals
+    // Hide columns starting with "hidden:"
+    QList<QTableWidget*> tables = findChildren<QTableWidget*>();
+    for (int i = 0; i < tables.size(); i++) {
+	bool nonCustom = (0 == qobject_cast<QtTable*>(tables[i]));
+	// Horizontal header
+	QHeaderView* hdr = tables[i]->horizontalHeader();
+	// Stretch last column
+	bool b = QtClient::getBoolProperty(tables[i],"_yate_horizontalstretch",true);
+	hdr->setStretchLastSection(b);
+	String tmp;
+	QtClient::getProperty(tables[i],"_yate_horizontalheader_align",tmp);
+	if (tmp) {
+	    int def = hdr->defaultAlignment();
+	    hdr->setDefaultAlignment((Qt::Alignment)QtClient::str2align(tmp,def));
+	}
+	if (!QtClient::getBoolProperty(tables[i],"_yate_horizontalheader",true))
+	    hdr->hide();
+	// Vertical header
+	hdr = tables[i]->verticalHeader();
+	int itemH = QtClient::getIntProperty(tables[i],"_yate_rowheight");
+	if (itemH > 0)
+	    hdr->setDefaultSectionSize(itemH);
+	if (!QtClient::getBoolProperty(tables[i],"_yate_verticalheader"))
+	    hdr->hide();
+	else {
+	    int width = QtClient::getIntProperty(tables[i],"_yate_verticalheaderwidth");
+	    if (width > 0)
+		hdr->setFixedWidth(width);
+	    if (!QtClient::getBoolProperty(tables[i],"_yate_allowvheaderresize"))
+		hdr->setSectionResizeMode(QHeaderView::Fixed);
+	}
+	if (nonCustom) {
+	    // Set _yate_save_props
+	    QVariant var = tables[i]->property(s_propsSave);
+	    if (var.type() != QVariant::StringList) {
+		// Create the property if not found, ignore it if not a string list
+		if (var.type() == QVariant::Invalid)
+		    var = QVariant(QVariant::StringList);
+		else
+		    Debug(QtDriver::self(),DebugNote,
+			"Window(%s) table '%s' already has a non string list property %s [%p]",
+			m_id.c_str(),YQT_OBJECT_NAME(tables[i]),s_propsSave.c_str(),this);
+	    }
+	    if (var.type() == QVariant::StringList) {
+		// Make sure saved properties exists to allow them to be restored
+		QStringList sl = var.toStringList();
+		bool changed = createProperty(tables[i],s_propColWidths,QVariant::String,this,&sl);
+		changed = createProperty(tables[i],s_propSorting,QVariant::String,this,&sl) || changed;
+		if (changed)
+		    tables[i]->setProperty(s_propsSave,QVariant(sl));
+	    }
+	}
+	TableWidget t(tables[i]);
+	// Insert the column containing the ID
+	t.addColumn(0,0,"hidden:id");
+	// Hide columns
+	for (int i = 0; i < t.columnCount(); i++) {
+	    String name;
+	    t.getHeaderText(i,name,false);
+	    if (name.startsWith("hidden:"))
+		t.table()->setColumnHidden(i,true);
+	}
+	// Connect signals
+	QtClient::connectObjects(t.table(),SIGNAL(cellDoubleClicked(int,int)),
+	    this,SLOT(doubleClick()));
+#if 0
+	// This would generate action() twice since QT will signal both cell and
+	// table item double click
+	QtClient::connectObjects(t.table(),SIGNAL(itemDoubleClicked(QTableWidgetItem*)),
+	    this,SLOT(doubleClick()));
+#endif
+	String noSel;
+	getProperty(t.name(),"dynamicNoItemSelChanged",noSel);
+	if (!noSel.toBoolean())
+	    QtClient::connectObjects(t.table(),SIGNAL(itemSelectionChanged()),
+		this,SLOT(selectionChanged()));
+	// Optionally connect cell clicked
+	// This is done when we want to generate a select() or action() from cell clicked
+	String cellClicked;
+	getProperty(t.name(),"dynamicCellClicked",cellClicked);
+	if (cellClicked) {
+	    if (cellClicked == "selectionChanged")
+		QtClient::connectObjects(t.table(),SIGNAL(cellClicked(int,int)),
+		    this,SLOT(selectionChanged()));
+	    else if (cellClicked == "doubleClick")
+		QtClient::connectObjects(t.table(),SIGNAL(cellClicked(int,int)),
+		    this,SLOT(doubleClick()));
+	}
+    }
+
+    // Restore saved children properties
+    if (sect) {
+	unsigned int n = sect->length();
+	for (unsigned int i = 0; i < n; i++) {
+	    NamedString* ns = sect->getParam(i);
+	    if (!ns)
+		continue;
+	    String prop(ns->name());
+	    if (!prop.startSkip("property:",false))
+		continue;
+	    int pos = prop.find(":");
+	    if (pos > 0) {
+		String wName = prop.substr(0,pos);
+		String pName = prop.substr(pos + 1);
+		DDebug(QtDriver::self(),DebugAll,
+		    "Window(%s) restoring property %s=%s for child '%s' [%p]",
+		    m_id.c_str(),pName.c_str(),ns->c_str(),wName.c_str(),this);
+		setProperty(wName,pName,*ns);
+	    }
+	}
+    }
+
+    // Install event filter and apply dynamic properties
+    QList<QObject*> w = findChildren<QObject*>();
+    w.append(this);
+    for (int i = 0; i < w.size(); i++) {
+	QList<QByteArray> props = w[i]->dynamicPropertyNames();
+	// Check for our dynamic properties
+	int j = 0;
+	for (j = 0; j < props.size(); j++)
+	    if (props[j].startsWith(s_yatePropPrefix))
+		break;
+	if (j == props.size())
+	    continue;
+	// Add event hook to be used when a dynamic property changes
+	w[i]->installEventFilter(this);
+	// Fake dynamic property change to apply them
+	for (j = 0; j < props.size(); j++) {
+	    if (!props[j].startsWith(s_yatePropPrefix))
+		continue;
+	    QDynamicPropertyChangeEvent ev(props[j]);
+	    eventFilter(w[i],&ev);
+	}
+    }
+
+    qRegisterMetaType<QModelIndex>("QModelIndex");
+    qRegisterMetaType<QTextCursor>("QTextCursor");
+
+    // Force window visibility change notification by changing the visibility flag
+    // Some controls might need to be updated
+    m_visible = !m_visible;
+    if (m_visible) {
+	// Disable _yate_destroyonhide property: avoid destroying the window now
+	String tmp;
+	getProperty(m_id,"_yate_destroyonhide",tmp);
+	if (tmp)
+	    setProperty(m_id,"_yate_destroyonhide",String::boolText(false));
+	hide();
+	if (tmp)
+	    setProperty(m_id,"_yate_destroyonhide",tmp);
+    }
+    else
+	show();
+}
+
+// Mouse button pressed notification
+void QtWindow::mousePressEvent(QMouseEvent* event)
+{
+    if (m_moving >= 0 && Qt::LeftButton == event->button() && isShownNormal()) {
+	m_movePos = event->globalPos();
+	m_moving = 1;
+    }
+}
+
+// Mouse button release notification
+void QtWindow::mouseReleaseEvent(QMouseEvent* event)
+{
+    if (m_moving >= 0 && Qt::LeftButton == event->button())
+	m_moving = 0;
+}
+
+// Move the window if the moving flag is set
+void QtWindow::mouseMoveEvent(QMouseEvent* event)
+{
+    if (m_moving <= 0 || Qt::LeftButton != event->buttons() || !isShownNormal())
+	return;
+    int cx = event->globalPos().x() - m_movePos.x();
+    int cy = event->globalPos().y() - m_movePos.y();
+    if (cx || cy) {
+	m_movePos = event->globalPos();
+	QWidget::move(x() + cx,y() + cy);
+    }
+}
+
+// Handle context menu events. Return true if handled
+bool QtWindow::handleContextMenuEvent(QContextMenuEvent* event, QObject* obj)
+{
+    if (!(event && obj))
+	return false;
+    String mname;
+    QtClient::getProperty(obj,s_propContextMenu,mname);
+    XDebug(ClientDriver::self(),DebugAll,
+	"Window(%s) handleContextMenuEvent() obj=%s menu=%s [%p]",
+	m_id.c_str(),YQT_OBJECT_NAME(obj),mname.c_str(),this);
+    QMenu* m = mname ? findChild<QMenu*>(QtClient::setUtf8(mname)) : 0;
+    if (m)
+	m->exec(event->globalPos());
+    return m != 0;
+}
+
+
+/*
+ * QtDialog
+ */
+// Destructor. Notify the client if not exiting
+QtDialog::~QtDialog()
+{
+    QtWindow* w = parentWindow();
+    if (w && m_notifyOnClose && Client::valid())
+	QtClient::self()->action(w,buildActionName(m_notifyOnClose,m_notifyOnClose));
+    DDebug(QtDriver::self(),DebugAll,"QtWindow(%s) QtDialog(%s) destroyed [%p]",
+	w ? w->id().c_str() : "",YQT_OBJECT_NAME(this),w);
+}
+
+// Initialize dialog. Load the widget.
+// Connect non checkable actions to own slot.
+// Connect checkable actions/buttons to parent window's slot
+// Display the dialog on success
+bool QtDialog::show(const String& name, const String& title, const String& alias,
+    const NamedList* params)
+{
+    QtWindow* w = parentWindow();
+    if (!w)
+	return false;
+    QWidget* widget = QtWindow::loadUI(Client::s_skinPath + s_cfg.getValue(name,"description"),this,name);
+    if (!widget)
+	return false;
+    QtClient::getProperty(widget,"_yate_notifyonclose",m_notifyOnClose);
+    setObjectName(QtClient::setUtf8(alias ? alias : name));
+    setMinimumSize(widget->minimumSize().width(),widget->minimumSize().height());
+    setMaximumSize(widget->maximumSize().width(),widget->maximumSize().height());
+    resize(widget->width(),widget->height());
+    QtClient::setWidget(this,widget);
+    if (title)
+	setWindowTitle(QtClient::setUtf8(title));
+    else if (widget->windowTitle().length())
+	setWindowTitle(widget->windowTitle());
+    else
+	setWindowTitle(w->windowTitle());
+    // Connect abstract buttons (check boxes and radio/push/tool buttons) signals
+    QList<QAbstractButton*> buttons = widget->findChildren<QAbstractButton*>();
+    for(int i = 0; i < buttons.size(); i++) {
+	if (!QtClient::autoConnect(buttons[i]))
+	    continue;
+	if (!buttons[i]->isCheckable())
+	    QtClient::connectObjects(buttons[i],SIGNAL(clicked()),this,SLOT(action()));
+	else
+	    QtClient::connectObjects(buttons[i],SIGNAL(toggled(bool)),w,SLOT(toggled(bool)));
+    }
+    // Connect actions' signal
+    QList<QAction*> actions = widget->findChildren<QAction*>();
+    for (int i = 0; i < actions.size(); i++) {
+	if (!QtClient::autoConnect(actions[i]))
+	    continue;
+	if (!actions[i]->isCheckable())
+	    QtClient::connectObjects(actions[i],SIGNAL(triggered()),this,SLOT(action()));
+	else
+	    QtClient::connectObjects(actions[i],SIGNAL(toggled(bool)),w,SLOT(toggled(bool)));
+    }
+    String* flags = 0;
+    String tmp;
+    QtClient::getProperty(widget,s_propWindowFlags,tmp);
+    if (tmp)
+	flags = &tmp;
+    if (params) {
+	if (!flags)
+	    flags = params->getParam(s_propWindowFlags);
+	m_closable = params->getBoolValue(YSTRING("closable"),"true");
+	w->setParams(*params);
+    }
+    if (flags)
+	QtClient::applyWindowFlags(this,*flags);
+    setWindowModality(Qt::WindowModal);
+    QDialog::show();
+    return true;
+}
+
+// Notify client
+void QtDialog::action()
+{
+    QtWindow* w = parentWindow();
+    if (!w)
+	return;
+    DDebug(QtDriver::self(),DebugAll,"QtWindow(%s) dialog action '%s' [%p]",
+	w->id().c_str(),YQT_OBJECT_NAME(sender()),w);
+    if (!QtClient::self() || QtClient::changing())
+	return;
+    String name;
+    QtClient::getIdentity(sender(),name);
+    if (name && QtClient::self()->action(w,buildActionName(name,name)))
+	deleteLater();
+}
+
+// Delete the dialog
+void QtDialog::closeEvent(QCloseEvent* event)
+{
+    if (m_closable) {
+	QDialog::closeEvent(event);
+	deleteLater();
+    }
+    else
+	event->ignore();
+}
+
+// Destroy the dialog
+void QtDialog::reject()
+{
+    if (!m_closable)
+	return;
+    QDialog::reject();
+    deleteLater();
+}
+
+
+/**
+ * QtClient
+ */
+QtClient::QtClient()
+    : Client("Qt Client")
+{
+    m_oneThread = Engine::config().getBoolValue("client","onethread",true);
+
+    s_save = Engine::configFile("qt5client",true);
+    s_save.load();
+    // Fill QT styles
+    s_qtStyles.addParam("IaOraKde","iaorakde");
+    s_qtStyles.addParam("QWindowsStyle","windows");
+    s_qtStyles.addParam("QMacStyle","mac");
+    s_qtStyles.addParam("QMotifStyle","motif");
+    s_qtStyles.addParam("QCDEStyle","cde");
+    s_qtStyles.addParam("QWindowsXPStyle","windowsxp");
+    s_qtStyles.addParam("QCleanlooksStyle","cleanlooks");
+    s_qtStyles.addParam("QPlastiqueStyle","plastique");
+    s_qtStyles.addParam("QGtkStyle","gtk");
+    s_qtStyles.addParam("IaOraQt","iaoraqt");
+    s_qtStyles.addParam("OxygenStyle","oxygen");
+    s_qtStyles.addParam("PhaseStyle","phase");
+}
+
+QtClient::~QtClient()
+{
+}
+
+void QtClient::cleanup()
+{
+    Client::cleanup();
+    m_events.clear();
+    Client::save(s_save);
+    QtWindow::clearUICache();
+    m_app->quit();
+    if (!m_app->startingUp())
+	delete m_app;
+}
+
+void QtClient::run()
+{
+    const char* style = Engine::config().getValue("client","style");
+    if (style && !QApplication::setStyle(QString::fromUtf8(style)))
+	Debug(ClientDriver::self(),DebugWarn,"Could not set Qt style '%s'",style);
+    int argc = 0;
+    char* argv =  0;
+    m_app = new QApplication(argc,&argv);
+    m_app->setQuitOnLastWindowClosed(false);
+    updateAppStyleSheet();
+    String imgRead;
+    QList<QByteArray> imgs = QImageReader::supportedImageFormats();
+    for (int i = 0; i < imgs.size(); i++)
+	imgRead.append(imgs[i].constData(),",");
+    imgRead = "read image formats '" + imgRead + "'";
+    Debug(ClientDriver::self(),DebugInfo,"QT client start running (version=%s) %s",
+	qVersion(),imgRead.c_str());
+    if (!QAudioDeviceInfo::availableDevices(QAudio::AudioOutput).isEmpty())
+	Debug(ClientDriver::self(),DebugWarn,"QT sounds are not available");
+    // Create events proxy
+    m_events.append(new QtEventProxy(QtEventProxy::Timer));
+    m_events.append(new QtEventProxy(QtEventProxy::AllHidden,m_app));
+    if (Engine::exiting())
+	return;
+    Client::run();
+}
+
+void QtClient::main()
+{
+    if (!Engine::exiting())
+	m_app->exec();
+}
+
+void QtClient::lock()
+{}
+
+void QtClient::unlock()
+{}
+
+void QtClient::allHidden()
+{
+    Debug(QtDriver::self(),DebugInfo,"QtClient::allHiden() counter=%d",s_allHiddenQuit);
+    if (s_allHiddenQuit > 0)
+	return;
+    quit();
+}
+
+bool QtClient::createWindow(const String& name, const String& alias)
+{
+    String parent = s_cfg.getValue(name,"parent");
+    QtWindow* parentWnd = 0;
+    if (!TelEngine::null(parent)) {
+	ObjList* o = m_windows.find(parent);
+	if (o)
+	    parentWnd = YOBJECT(QtWindow,o->get());
+    }
+    QtWindow* w = new QtWindow(name,s_skinPath + s_cfg.getValue(name,"description"),alias,parentWnd);
+    if (w) {
+	Debug(QtDriver::self(),DebugAll,"Created window name=%s alias=%s with parent=(%s [%p]) (%p)",
+	    name.c_str(),alias.c_str(),parent.c_str(),parentWnd,w);
+	// Remove the old window
+	ObjList* o = m_windows.find(w->id());
+	if (o)
+	    Client::self()->closeWindow(w->id(),false);
+	w->populate();
+	m_windows.append(w);
+	return true;
+    }
+    else
+	Debug(QtDriver::self(),DebugCrit,"Could not create window name=%s alias=%s",
+	    name.c_str(),alias.c_str());
+    return false;
+}
+
+void QtClient::loadWindows(const char* file)
+{
+    if (!file)
+	s_cfg = s_skinPath + "qt5client.rc";
+    else
+	s_cfg = String(file);
+    s_cfg.load();
+    Debug(QtDriver::self(),DebugInfo,"Loading Windows");
+    unsigned int n = s_cfg.sections();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedList* l = s_cfg.getSection(i);
+	if (l && l->getBoolValue(YSTRING("enabled"),true))
+	    createWindow(*l);
+    }
+}
+
+bool QtClient::isUIThread()
+{
+    return (QApplication::instance() && QApplication::instance()->thread() == QThread::currentThread());
+}
+
+// Open a file open dialog window
+// Parameters that can be specified include 'caption',
+//  'dir', 'filter', 'selectedfilter', 'confirmoverwrite', 'choosedir'
+bool QtClient::chooseFile(Window* parent, NamedList& params)
+{
+    QtWindow* wnd = static_cast<QtWindow*>(parent);
+    QFileDialog* dlg = new QFileDialog(wnd,setUtf8(params.getValue(YSTRING("caption"))),
+	setUtf8(params.getValue(YSTRING("dir"))));
+
+    if (wnd)
+	dlg->setWindowIcon(wnd->windowIcon());
+
+    // Connect signals
+    String* action = params.getParam(YSTRING("action"));
+    if (wnd && !null(action)) {
+	dlg->setObjectName(setUtf8(*action));
+	QtClient::connectObjects(dlg,SIGNAL(accepted()),wnd,SLOT(chooseFileAccepted()));
+	QtClient::connectObjects(dlg,SIGNAL(rejected()),wnd,SLOT(chooseFileRejected()));
+    }
+
+    // Destroy it when closed
+    dlg->setAttribute(Qt::WA_DeleteOnClose);
+    // This dialog should always stay on top
+    dlg->setWindowFlags(dlg->windowFlags() | Qt::WindowStaysOnTopHint);
+
+    if (params.getBoolValue(YSTRING("modal"),true))
+	dlg->setWindowModality(Qt::WindowModal);
+
+    // Filters
+    NamedString* f = params.getParam(YSTRING("filters"));
+    if (f) {
+	QStringList filters;
+	ObjList* obj = f->split('|',false);
+	for (ObjList* o = obj->skipNull(); o; o = o->skipNext())
+	    filters.append(QtClient::setUtf8(o->get()->toString()));
+	TelEngine::destruct(obj);
+	dlg->setNameFilters(filters);
+    }
+    QString flt = QtClient::setUtf8(params.getValue(YSTRING("selectedfilter")));
+    if (flt.length())
+	dlg->selectNameFilter(flt);
+
+    if (params.getBoolValue(YSTRING("save")))
+	dlg->setAcceptMode(QFileDialog::AcceptSave);
+    else
+	dlg->setAcceptMode(QFileDialog::AcceptOpen);
+
+    // Choose options
+    if (params.getBoolValue(YSTRING("choosefile"),true)) {
+	if (params.getBoolValue(YSTRING("chooseanyfile")))
+	    dlg->setFileMode(QFileDialog::AnyFile);
+	else if (params.getBoolValue(YSTRING("multiplefiles")))
+	    dlg->setFileMode(QFileDialog::ExistingFiles);
+	else
+	    dlg->setFileMode(QFileDialog::ExistingFile);
+    }
+    else
+	dlg->setFileMode(QFileDialog::DirectoryOnly);
+
+    dlg->selectFile(QtClient::setUtf8(params.getValue(YSTRING("selectedfile"))));
+
+    dlg->setVisible(true);
+    return true;
+}
+
+bool QtClient::action(Window* wnd, const String& name, NamedList* params)
+{
+    String tmp = name;
+    if (tmp.startSkip("openurl:",false))
+	return openUrl(tmp);
+    return Client::action(wnd,name,params);
+}
+
+// Create a sound object. Append it to the global list
+bool QtClient::createSound(const char* name, const char* file, const char* device)
+{
+    if (!(QAudioDeviceInfo::availableDevices(QAudio::AudioOutput).isEmpty() &&
+	  name && *name && file && *file))
+	return false;
+    Lock lock(ClientSound::s_soundsMutex);
+    if (ClientSound::s_sounds.find(name))
+	return false;
+    ClientSound::s_sounds.append(new QtSound(name,file,device));
+    DDebug(ClientDriver::self(),DebugAll,"Added sound=%s file=%s device=%s",
+	name,file,device);
+    return true;
+}
+
+// Build a date/time string from UTC time
+bool QtClient::formatDateTime(String& dest, unsigned int secs,
+    const char* format, bool utc)
+{
+    if (!(format && *format))
+	return false;
+    QtClient::getUtf8(dest,formatDateTime(secs,format,utc));
+    return true;
+}
+
+// Build a date/time QT string from UTC time
+QString QtClient::formatDateTime(unsigned int secs, const char* format, bool utc)
+{
+    QDateTime time;
+    if (utc)
+	time.setTimeSpec(Qt::UTC);
+    time.setTime_t(secs);
+    return time.toString(format);
+}
+
+// Retrieve an object's QtWindow parent
+QtWindow* QtClient::parentWindow(QObject* obj)
+{
+    for (; obj; obj = obj->parent()) {
+	QtWindow* w = qobject_cast<QtWindow*>(obj);
+	if (w)
+	    return w;
+    }
+    return 0;
+}
+
+// Save an object's property into parent window's section. Clear it on failure
+bool QtClient::saveProperty(QObject* obj, const String& prop, QtWindow* owner)
+{
+    if (!obj)
+	return false;
+    if (!owner)
+	owner = parentWindow(obj);
+    if (!owner)
+	return false;
+    String value;
+    bool ok = getProperty(obj,prop,value);
+    String pName;
+    pName << "property:" << YQT_OBJECT_NAME(obj) << ":" << prop;
+    if (ok)
+	s_save.setValue(owner->id(),pName,value);
+    else
+	s_save.clearKey(owner->id(),pName);
+    return ok;
+}
+
+// Set or an object's property
+bool QtClient::setProperty(QObject* obj, const char* name, const String& value)
+{
+    if (!(obj && name && *name))
+	return false;
+    QVariant var = obj->property(name);
+    const char* err = 0;
+    bool ok = false;
+    switch (var.type()) {
+	case QVariant::String:
+	    ok = obj->setProperty(name,QVariant(QtClient::setUtf8(value)));
+	    break;
+	case QVariant::Bool:
+	    ok = obj->setProperty(name,QVariant(value.toBoolean()));
+	    break;
+	case QVariant::Int:
+	    ok = obj->setProperty(name,QVariant(value.toInteger()));
+	    break;
+	case QVariant::UInt:
+	    ok = obj->setProperty(name,QVariant((unsigned int)value.toInteger()));
+	    break;
+	case QVariant::Icon:
+	    ok = obj->setProperty(name,QVariant(QIcon(QtClient::setUtf8(value))));
+	    break;
+	case QVariant::Pixmap:
+	    ok = obj->setProperty(name,QVariant(QPixmap(QtClient::setUtf8(value))));
+	    break;
+	case QVariant::Double:
+	    ok = obj->setProperty(name,QVariant(value.toDouble()));
+	    break;
+	case QVariant::KeySequence:
+	    ok = obj->setProperty(name,QVariant(QtClient::setUtf8(value)));
+	    break;
+	case QVariant::StringList:
+	    {
+		QStringList qList;
+		if (value)
+		    qList.append(setUtf8(value));
+		ok = obj->setProperty(name,QVariant(qList));
+	    }
+	    break;
+	case QVariant::Invalid:
+	    err = "no such property";
+	    break;
+	default:
+	    err = "unsupported type";
+    }
+    YIGNORE(err);
+    if (ok)
+	DDebug(ClientDriver::self(),DebugAll,"Set property %s=%s for object '%s'",
+	    name,value.c_str(),YQT_OBJECT_NAME(obj));
+    else
+	DDebug(ClientDriver::self(),DebugNote,
+	    "Failed to set %s=%s (type=%s) for object '%s': %s",
+	    name,value.c_str(),var.typeName(),YQT_OBJECT_NAME(obj),err);
+    return ok;
+}
+
+// Get an object's property
+bool QtClient::getProperty(QObject* obj, const char* name, String& value)
+{
+    if (!(obj && name && *name))
+	return false;
+    QVariant var = obj->property(name);
+    if (var.type() == QVariant::StringList) {
+	NamedList* l = static_cast<NamedList*>(value.getObject(YATOM("NamedList")));
+	if (l)
+	    copyParams(*l,var.toStringList());
+	else
+	    getUtf8(value,var.toStringList().join(","));
+	DDebug(ClientDriver::self(),DebugAll,"Got list property %s for object '%s'",
+	    name,YQT_OBJECT_NAME(obj));
+	return true;
+    }
+    if (var.canConvert(QVariant::String)) {
+	QtClient::getUtf8(value,var.toString());
+	DDebug(ClientDriver::self(),DebugAll,"Got property %s=%s for object '%s'",
+	    name,value.c_str(),YQT_OBJECT_NAME(obj));
+	return true;
+    }
+    DDebug(ClientDriver::self(),DebugNote,
+	"Failed to get property '%s' (type=%s) for object '%s': %s",
+	name,var.typeName(),YQT_OBJECT_NAME(obj),
+	((var.type() == QVariant::Invalid) ? "no such property" : "unsupported type"));
+    return false;
+}
+
+// Copy a string list to a list of parameters
+void QtClient::copyParams(NamedList& dest, const QStringList& src)
+{
+    for (int i = 0; i < src.size(); i++) {
+	if (!src[i].length())
+	    continue;
+	int pos = src[i].indexOf('=');
+	String name;
+	if (pos >= 0) {
+	    getUtf8(name,src[i].left(pos));
+	    getUtf8(dest,name,src[i].right(src[i].length() - pos - 1));
+	}
+	else {
+	    getUtf8(name,src[i]);
+	    dest.addParam(name,"");
+	}
+    }
+}
+
+// Copy a list of parameters to string list
+void QtClient::copyParams(QStringList& dest, const NamedList& src)
+{
+    unsigned int n = src.length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* ns = src.getParam(i);
+	if (ns)
+	    dest.append(setUtf8(ns->name() + "=" + *ns));
+    }
+}
+
+// Build QObject properties from list
+void QtClient::buildProps(QObject* obj, const String& props)
+{
+    if (!(obj && props))
+	return;
+    ObjList* list = props.split(',',false);
+    for (ObjList* o = list->skipNull(); o; o = o->skipNext()) {
+	String* s = static_cast<String*>(o->get());
+	int pos = s->find('=');
+	if (pos < 1)
+	    continue;
+	String ptype = s->substr(pos + 1);
+	QVariant::Type t = (QVariant::Type)lookup(ptype,s_qVarType,QVariant::Invalid);
+	if (t == QVariant::Invalid) {
+	    Debug(ClientDriver::self(),DebugStub,
+		"QtClient::buildProps() unhandled type '%s'",ptype.c_str());
+	    continue;
+	}
+	String pname = s->substr(0,pos);
+	QVariant existing = obj->property(pname);
+	if (existing.type() == QVariant::Invalid) {
+	    obj->setProperty(pname,QVariant(t));
+	    continue;
+	}
+	Debug(ClientDriver::self(),DebugNote,
+	    "Can't create property '%s' type=%s for object (%p,%s): already exists",
+	    pname.c_str(),ptype.c_str(),obj,YQT_OBJECT_NAME(obj));
+    }
+    TelEngine::destruct(list);
+}
+
+// Build custom UI widgets from frames owned by a widget
+void QtClient::buildFrameUiWidgets(QWidget* parent)
+{
+    if (!parent)
+	return;
+    QList<QFrame*> frm = parent->findChildren<QFrame*>();
+    for (int i = 0; i < frm.size(); i++) {
+	if (!getBoolProperty(frm[i],"_yate_uiwidget"))
+	    continue;
+	String name;
+	String type;
+	getProperty(frm[i],"_yate_uiwidget_name",name);
+	getProperty(frm[i],"_yate_uiwidget_class",type);
+	if (!(name && type))
+	    continue;
+	NamedList params("");
+	getProperty(frm[i],"_yate_uiwidget_params",params);
+	QtWindow* w = static_cast<QtWindow*>(parent->window());
+	if (w)
+	    params.setParam("parentwindow",w->id());
+	getUtf8(params,"parentwidget",frm[i]->objectName(),true);
+	QObject* obj = (QObject*)UIFactory::build(type,name,&params);
+	if (!obj)
+	    continue;
+	QWidget* wid = qobject_cast<QWidget*>(obj);
+	if (wid)
+	    QtClient::setWidget(frm[i],wid);
+	else {
+	    obj->setParent(frm[i]);
+	    QtCustomObject* customObj = qobject_cast<QtCustomObject*>(obj);
+	    if (customObj)
+		customObj->parentChanged();
+	}
+    }
+}
+
+// Associate actions to buttons with '_yate_setaction' property set
+void QtClient::setAction(QWidget* parent)
+{
+    if (!parent)
+	return;
+    QList<QToolButton*> tb = parent->findChildren<QToolButton*>();
+    for (int i = 0; i < tb.size(); i++) {
+	QVariant var = tb[i]->property("_yate_setaction");
+	if (var.toString().isEmpty())
+	    continue;
+	QAction* a = parent->findChild<QAction*>(var.toString());
+	if (a)
+	    tb[i]->setDefaultAction(a);
+    }
+}
+
+// Build a menu object from a list of parameters
+QMenu* QtClient::buildMenu(const NamedList& params, const char* text, QObject* receiver,
+	 const char* triggerSlot, const char* toggleSlot, QWidget* parent,
+	 const char* aboutToShowSlot)
+{
+    QMenu* menu = 0;
+    unsigned int n = params.length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* param = params.getParam(i);
+	if (!(param && param->name().startsWith("item:")))
+	    continue;
+
+	if (!menu)
+	    menu = new QMenu(setUtf8(text),parent);
+
+	NamedList* p = YOBJECT(NamedList,param);
+	if (p)  {
+	    QMenu* subMenu = buildMenu(*p,*param ? param->c_str() : p->getValue(YSTRING("title"),*p),
+		receiver,triggerSlot,toggleSlot,menu);
+	    if (subMenu)
+		menu->addMenu(subMenu);
+	    continue;
+	}
+	String name = param->name().substr(5);
+	if (*param) {
+	    QAction* a = menu->addAction(QtClient::setUtf8(*param));
+	    a->setObjectName(QtClient::setUtf8(name));
+	    a->setParent(menu);
+	    setImage(a,params["image:" + name]);
+	}
+	else if (!name)
+	    menu->addSeparator()->setParent(menu);
+	else {
+	    // Check if the action is already there
+	    QAction* a = 0;
+	    if (parent && parent->window())
+		a = parent->window()->findChild<QAction*>(QtClient::setUtf8(name));
+	    if (a)
+		menu->addAction(a);
+	    else
+		Debug(ClientDriver::self(),DebugNote,
+		    "buildMenu(%s) action '%s' not found",params.c_str(),name.c_str());
+	}
+    }
+
+    if (!menu)
+	return 0;
+
+    // Set name
+    menu->setObjectName(setUtf8(params));
+    setImage(menu,params["image:" + params]);
+    // Apply properties
+    // Format: property:object_name:property_name=value
+    if (parent)
+	for (unsigned int i = 0; i < n; i++) {
+	    NamedString* param = params.getParam(i);
+	    if (!(param && param->name().startsWith("property:")))
+		continue;
+	    int pos = param->name().find(':',9);
+	    if (pos < 9)
+		continue;
+	    QObject* obj = parent->findChild<QObject*>(setUtf8(param->name().substr(9,pos - 9)));
+	    if (obj)
+		setProperty(obj,param->name().substr(pos + 1),*param);
+	}
+    // Connect signals (direct children only: actions from sub-menus are already connected)
+    QList<QAction*> list = menu->findChildren<QAction*>();
+    for (int i = 0; i < list.size(); i++) {
+	if (list[i]->isSeparator() || list[i]->parent() != menu)
+	    continue;
+	if (list[i]->isCheckable())
+	    QtClient::connectObjects(list[i],SIGNAL(toggled(bool)),receiver,toggleSlot);
+	else
+	    QtClient::connectObjects(list[i],SIGNAL(triggered()),receiver,triggerSlot);
+    }
+    if (!TelEngine::null(aboutToShowSlot))
+	QtClient::connectObjects(menu,SIGNAL(aboutToShow()),receiver,aboutToShowSlot);
+
+    return menu;
+}
+
+// Wrapper for QObject::connect() used to put a debug mesage on failure
+bool QtClient::connectObjects(QObject* sender, const char* signal,
+    QObject* receiver, const char* slot)
+{
+    if (!(sender && signal && *signal && receiver && slot && *slot))
+	return false;
+    bool ok = QObject::connect(sender,signal,receiver,slot);
+    if (ok)
+	DDebug(QtDriver::self(),DebugAll,
+	    "Connected sender=%s signal=%s to receiver=%s slot=%s",
+	    YQT_OBJECT_NAME(sender),signal,YQT_OBJECT_NAME(receiver),slot);
+    else
+	Debug(QtDriver::self(),DebugWarn,
+	    "Failed to connect sender=%s signal=%s to receiver=%s slot=%s",
+	    YQT_OBJECT_NAME(sender),signal,YQT_OBJECT_NAME(receiver),slot);
+    return ok;
+}
+
+// Insert a widget into another one replacing any existing children
+bool QtClient::setWidget(QWidget* parent, QWidget* child)
+{
+    if (!(parent && child))
+	return false;
+    QVBoxLayout* layout = new QVBoxLayout;
+    layout->setSpacing(0);
+    String margins;
+    QtClient::getProperty(parent,"_yate_layout_margins",margins);
+    if (!margins)
+	layout->setContentsMargins(0,0,0,0);
+    else {
+	QList<int> m = buildIntList(margins,4);
+	layout->setContentsMargins(m[0],m[1],m[2],m[3]);
+    }
+    layout->addWidget(child);
+    QLayout* l = parent->layout();
+    if (l)
+	delete l;
+    parent->setLayout(layout);
+    return true;
+}
+
+// Set an object's image property from image file
+bool QtClient::setImage(QObject* obj, const String& img, bool fit)
+{
+    if (!obj)
+	return false;
+    QPixmap pixmap(setUtf8(img));
+    return setImage(obj,pixmap,fit);
+}
+
+// Set an object's image property from raw data.
+bool QtClient::setImage(QObject* obj, const DataBlock& data, const String& format, bool fit)
+{
+    if (!obj)
+	return false;
+    QPixmap pixmap;
+    String f = format;
+    f.startSkip("image/",false);
+    if (!pixmap.loadFromData((const uchar*)data.data(),data.length(),f))
+	return false;
+    return setImage(obj,pixmap,fit);
+}
+
+// Set an object's image property from QPixmap
+bool QtClient::setImage(QObject* obj, const QPixmap& img, bool fit)
+{
+    if (!obj)
+	return false;
+    if (obj->isWidgetType()) {
+	QLabel* l = qobject_cast<QLabel*>(obj);
+	if (l) {
+	    if (fit && !l->hasScaledContents() &&
+		(img.width() > l->width() || img.height() > l->height())) {
+		QPixmap tmp;
+		if (l->width() <= l->height())
+		    tmp = img.scaledToWidth(l->width());
+		else
+		    tmp = img.scaledToHeight(l->height());
+		l->setPixmap(tmp);
+	    }
+	    else
+		l->setPixmap(img);
+	}
+	else {
+	    QAbstractButton* b = qobject_cast<QAbstractButton*>(obj);
+	    if (b)
+		b->setIcon(img);
+	    else {
+		QMenu* m = qobject_cast<QMenu*>(obj);
+		if (m)
+		    m->setIcon(img);
+		else
+		    return false;
+	    }
+	}
+	return true;
+    }
+    QAction* a = qobject_cast<QAction*>(obj);
+    if (a) {
+	a->setIcon(img);
+	return true;
+    }
+    return false;
+}
+
+// Update a toggable object's image from properties
+void QtClient::updateToggleImage(QObject* obj)
+{
+    QtWidget w(obj);
+    QAbstractButton* b = 0;
+    if (w.inherits(QtWidget::AbstractButton))
+	b = w.abstractButton();
+    if (!(b && b->isCheckable()))
+	return;
+    String icon;
+    bool set = false;
+    if (b->isChecked())
+	set = QtClient::getProperty(w,"_yate_pressed_icon",icon);
+    else
+	set = QtClient::getProperty(w,"_yate_normal_icon",icon);
+    if (set)
+        QtClient::setImage(obj,Client::s_skinPath + icon);
+}
+
+// Update an object's image from properties on mouse events
+void QtClient::updateImageFromMouse(QObject* obj, bool inOut, bool on)
+{
+    QtWidget w(obj);
+    QAbstractButton* b = 0;
+    if (w.inherits(QtWidget::AbstractButton))
+	b = w.abstractButton();
+    if (!b)
+	return;
+    if (!b->isEnabled())
+	return;
+    String icon;
+    bool set = false;
+    if (inOut) {
+	if (on)
+	    set = QtClient::getProperty(obj,"_yate_hover_icon",icon);
+	else {
+	    if (b->isCheckable() && b->isChecked())
+		set = QtClient::getProperty(obj,"_yate_pressed_icon",icon);
+	    set = set || QtClient::getProperty(obj,"_yate_normal_icon",icon);
+	}
+    }
+    else {
+	if (on) {
+	    if (!b->isCheckable())
+		set = QtClient::getProperty(obj,"_yate_pressed_icon",icon);
+	}
+	else {
+	    set = QtClient::getProperty(obj,"_yate_hover_icon",icon);
+	    if (!set && b->isCheckable() && b->isChecked())
+		set = QtClient::getProperty(obj,"_yate_pressed_icon",icon);
+	    set = set || QtClient::getProperty(obj,"_yate_normal_icon",icon);
+	}
+    }
+    if (set)
+	QtClient::setImage(obj,Client::s_skinPath + icon);
+}
+
+// Process a key press event. Retrieve an action associated with the key
+bool QtClient::filterKeyEvent(QObject* obj, QKeyEvent* event, String& action,
+    bool& filter, QObject* parent)
+{
+    static const Qt::KeyboardModifiers::Int mask = Qt::SHIFT | Qt::CTRL |
+						   Qt::ALT;
+    if (!(obj && event))
+	return false;
+    // Try to match key and modifiers
+    QKeySequence ks(event->key());
+    String prop;
+    getUtf8(prop,ks.toString());
+    prop = "dynamicAction" + prop;
+    // Get modifiers from property and check them against event
+    QVariant v = obj->property(prop + "Modifiers");
+    Qt::KeyboardModifiers::Int tmp = 0;
+    if (v.type() == QVariant::String) {
+	QKeySequence ks(v.toString());
+	for (int i = 0; i < ks.count(); i++)
+	    tmp |= ks[i];
+    }
+    if (tmp != (mask & event->modifiers()))
+	return false;
+    // We matched the key and modifiers
+    // Set filter flag
+    filter = getBoolProperty(obj,prop + "Filter");
+    // Retrieve the action
+    getProperty(obj,prop,action);
+    if (!action)
+	return true;
+    if (!parent)
+	return true;
+    parent = parent->findChild<QObject*>(setUtf8(action));
+    if (!parent)
+	return true;
+    // Avoid notifying a disabled action
+    bool ok = true;
+    if (parent->isWidgetType())
+	ok = (qobject_cast<QWidget*>(parent))->isEnabled();
+    else {
+	QAction* a = qobject_cast<QAction*>(parent);
+	ok = !a || a->isEnabled();
+    }
+    if (!ok)
+	action.clear();
+    return true;
+}
+
+// Safely delete a QObject (reset its parent, calls it's deleteLater() method)
+void QtClient::deleteLater(QObject* obj)
+{
+    if (!obj)
+	return;
+    obj->disconnect();
+    if (obj->isWidgetType())
+	(static_cast<QWidget*>(obj))->setParent(0);
+    else
+	obj->setParent(0);
+    obj->deleteLater();
+}
+
+// Retrieve unavailable space position (if any) in the screen containing a given widget.
+QDesktopWidget* QtClient::getScreenUnavailPos(QWidget* w, int& pos)
+{
+    if (!w)
+	return 0;
+    QDesktopWidget* d = QApplication::desktop();
+    if (!d)
+	return 0;
+    pos = PosNone;
+    QRect rScreen = d->screenGeometry(w);
+    QRect rClient = d->availableGeometry(w);
+    int dx = rClient.x() - rScreen.x();
+    if (dx > 0)
+	pos |= PosLeft;
+    int dy = rClient.y() - rScreen.y();
+    if (dy > 0)
+	pos |= PosTop;
+    int dw = rScreen.width() - rClient.width();
+    if (dw > 0 && (!dx || (dx > 0 && dw > dx)))
+	pos |= PosRight;
+    int dh = rScreen.height() - rClient.height();
+    if (dh > 0 && (!dy || (dy > 0 && dh > dy)))
+	pos |= PosBottom;
+    return d;
+}
+
+// Move a window to a specified position
+void QtClient::moveWindow(QtWindow* w, int pos)
+{
+    if (!w)
+	return;
+    QDesktopWidget* d = QApplication::desktop();
+    if (!d)
+	return;
+    QRect r = d->availableGeometry(w);
+    int x = r.x();
+    int y = r.y();
+    QSize sz = w->frameSize();
+    if (pos == CornerBottomRight) {
+        if (r.width() > sz.width())
+	    x += r.width() - sz.width();
+	if (r.height() > sz.height())
+	    y += r.height() - sz.height();
+    }
+    else if (pos == CornerTopRight) {
+        if (r.width() > sz.width())
+	    x += r.width() - sz.width();
+    }
+    else if (pos == CornerBottomLeft) {
+	if (r.height() > sz.height())
+	    y += r.height() - sz.height();
+    }
+    else if (pos != CornerTopLeft)
+	return;
+    w->move(x,y);
+}
+
+// Build a QStringList from a list of strings
+QStringList QtClient::str2list(const String& str, char sep, bool emptyOk)
+{
+    QStringList l;
+    if (!str)
+	return l;
+    ObjList* list = str.split(sep,emptyOk);
+    for (ObjList* o = list->skipNull(); o; o = o->skipNext())
+	l.append(setUtf8(static_cast<String*>(o->get())->c_str()));
+    TelEngine::destruct(list);
+    return l;
+}
+
+// Split a string. Returns a list of int values
+QList<int> QtClient::str2IntList(const String& str, int defVal, bool emptyOk)
+{
+    QList<int> list;
+    ObjList* l = str.split(',',emptyOk);
+    for (ObjList* o = l->skipNull(); o; o = o->skipNext())
+	list.append(o->get()->toString().toInteger(defVal));
+    TelEngine::destruct(l);
+    return list;
+}
+
+// Build a comma separated list of integers
+void QtClient::intList2str(String& str, QList<int> list)
+{
+    for (int i = 0; i < list.size(); i++)
+	str.append(String(list[i]),",");
+}
+
+// Get sorting from string
+int QtClient::str2sort(const String& str, int defVal)
+{
+    return lookup(str,s_sorting,defVal);
+}
+
+// Apply a comma separated list of window flags to a widget
+void QtClient::applyWindowFlags(QWidget* w, const String& value)
+{
+    if (!w)
+	return;
+    // Set window flags from enclosed widget:
+    //  custom window title/border/sysmenu config
+    ObjList* f = value.split(',',false);
+    int flags = Qt::CustomizeWindowHint | w->windowFlags();
+    // Clear settable flags
+    TokenDict* dict = s_windowFlags;
+    for (int i = 0; dict[i].token; i++)
+	flags &= ~dict[i].value;
+    // Set flags
+    for (ObjList* o = f->skipNull(); o; o = o->skipNext())
+	flags |= lookup(o->get()->toString(),s_windowFlags,0);
+    TelEngine::destruct(f);
+    w->setWindowFlags((Qt::WindowFlags)flags);
+}
+
+// Build a QT Alignment mask from a comma separated list of flags
+int QtClient::str2align(const String& flags, int initVal)
+{
+    ObjList* list = flags.split(',',false);
+    for (ObjList* o = list->skipNull(); o; o = o->skipNext()) {
+	int val = ::lookup((static_cast<String*>(o->get()))->c_str(),s_qAlign);
+	if (0 != (val & Qt::AlignHorizontal_Mask))
+	    initVal &= ~Qt::AlignHorizontal_Mask;
+	if (0 != (val & Qt::AlignVertical_Mask))
+	    initVal &= ~Qt::AlignVertical_Mask;
+	initVal |= val;
+    }
+    TelEngine::destruct(list);
+    return initVal;
+}
+
+// Retrieve QT selection mode from a string value
+QAbstractItemView::SelectionMode QtClient::str2selmode(const String& value,
+    QAbstractItemView::SelectionMode defVal)
+{
+    if (!value)
+	return defVal;
+    if (value == YSTRING("none"))
+	return QAbstractItemView::NoSelection;
+    if (value == YSTRING("single"))
+	return QAbstractItemView::SingleSelection;
+    if (value == YSTRING("multi"))
+	return QAbstractItemView::MultiSelection;
+    if (value == YSTRING("extended"))
+	return QAbstractItemView::ExtendedSelection;
+    if (value == YSTRING("contiguous"))
+	return QAbstractItemView::ContiguousSelection;
+    return defVal;
+}
+
+// Retrieve QT edit triggers from a string value
+QAbstractItemView::EditTriggers QtClient::str2editTriggers(const String& value,
+    QAbstractItemView::EditTrigger defVal)
+{
+    return (QAbstractItemView::EditTriggers)Client::decodeFlags(s_qEditTriggers,value,defVal);
+}
+
+// Send an event to an object's child
+bool QtClient::sendEvent(QEvent& e, QObject* parent, const QString& name)
+{
+    if (!(parent && e.isAccepted()))
+	return false;
+    QObject* child = parent->findChild<QObject*>(name);
+    if (!child)
+	return false;
+    e.setAccepted(false);
+    bool ok = QCoreApplication::sendEvent(child,&e);
+    if (!ok)
+	e.setAccepted(true);
+    return ok;
+}
+
+// Retrieve a pixmap from global application cache.
+// Load and add it to the cache if not found
+bool QtClient::getPixmapFromCache(QPixmap& pixmap, const QString& file)
+{
+    if (file.isEmpty())
+	return false;
+    if (QPixmapCache::find(file,&pixmap)) {
+	return true;
+    }
+    if (!pixmap.load(file))
+	return false;
+#ifdef XDEBUG
+    String f;
+    getUtf8(f,file);
+    Debug(ClientDriver::self(),DebugAll,"Loaded '%s' in pixmap cache",f.c_str());
+#endif
+    QPixmapCache::insert(file,pixmap);
+    return true;
+}
+
+// Update application style sheet from config
+// Build style sheet from files:
+// stylesheet.css
+// stylesheet_stylename.css
+// stylesheet_osname.css
+// stylesheet_osname_stylename.css
+void QtClient::updateAppStyleSheet()
+{
+    if (!qApp) {
+	Debug(ClientDriver::self(),DebugWarn,"Update app stylesheet called without app");
+	return;
+    }
+    String shf = Engine::config().getValue("client","stylesheet_file","stylesheet.css");
+    if (!shf)
+	return;
+    QString sh;
+    if (!appendStyleSheet(sh,shf))
+	return;
+    String styleName;
+    QStyle* style = qApp->style();
+    const QMetaObject* meta = style ? style->metaObject() : 0;
+    if (meta) {
+	styleName = s_qtStyles.getValue(meta->className());
+	if (!styleName)
+	    styleName = meta->className();
+    }
+    if (styleName)
+	appendStyleSheet(sh,shf,styleName);
+    String osname;
+    osname << "os" << PLATFORM_LOWERCASE_NAME;
+    appendStyleSheet(sh,shf,osname);
+    if (styleName)
+	appendStyleSheet(sh,shf,osname,styleName);
+    qApp->setStyleSheet(sh);
+}
+
+// Set widget attributes from list
+void QtClient::setWidgetAttributes(QWidget* w, const String& attrs)
+{
+    if (!(w && attrs))
+	return;
+    ObjList* list = attrs.split(',',false);
+    for (ObjList* o = list->skipNull(); o; o = o->skipNext()) {
+	const String& attr = *static_cast<String*>(o->get());
+	bool on = (attr[0] != '!');
+	const char* name = attr.c_str();
+	int val = lookup(on ? name : name + 1,s_widgetAttributes);
+	if (val)
+	    w->setAttribute((Qt::WidgetAttribute)val,on);
+    }
+    TelEngine::destruct(list);
+}
+
+// Adjust widget height
+void QtClient::setWidgetHeight(QWidget* w, const String& height)
+{
+    if (!w)
+	return;
+    int h = 0;
+    if (height.isBoolean()) {
+	h = QtClient::getIntProperty(w,"_yate_height_delta",-1);
+	if (h > 0) {
+	    if (height.toBoolean())
+		h += w->height();
+	    else if (h < w->height())
+		h = w->height() - h;
+	    else
+		h = 0;
+	}
+    }
+    else
+	h = height.toInteger();
+    if (h < 0)
+	return;
+    QSizePolicy sp = w->sizePolicy();
+    sp.setVerticalPolicy(QSizePolicy::Fixed);
+    w->setSizePolicy(sp);
+    w->setMinimumHeight(h);
+    w->setMaximumHeight(h);
+}
+
+// Build a busy widget child for a given widget
+QWidget* QtClient::buildBusy(QWidget* parent, QWidget* target, const String& ui,
+    const NamedList& params)
+{
+    QtBusyWidget* w = new QtBusyWidget(parent);
+    w->init(ui,params,target);
+    return w;
+}
+
+// Load a movie
+QMovie* QtClient::loadMovie(const char* file, QObject* parent, const char* path)
+{
+    static NamedList s_failed("");
+
+    if (TelEngine::null(file))
+	return 0;
+    String tmp = path;
+    if (!path)
+	tmp = Client::s_skinPath;
+    else if (tmp && !tmp.endsWith(Engine::pathSeparator()))
+	tmp << Engine::pathSeparator();
+    tmp << file;
+    QMovie* m = new QMovie(setUtf8(tmp),QByteArray(),parent);
+    NamedString* ns = s_failed.getParam(tmp);
+    if (m->isValid()) {
+	if (ns)
+	    s_failed.clearParam(ns);
+	return m;
+    }
+    if (!ns) {
+	s_failed.addParam(tmp,"");
+	String error;
+	error << "Failed to load movie '" << tmp << "'";
+	Debug(QtDriver::self(),DebugNote,"%s",error.c_str());
+	if (self())
+	    self()->addToLog(error);
+    }
+    delete m;
+    return 0;
+}
+
+// Fill a list from URL parameters
+void QtClient::fillUrlParams(const QUrl& url, NamedList& list, QString* path,
+    bool pathToList)
+{
+    safeGetUtf8(list,"protocol",url.scheme());
+    safeGetUtf8(list,"host",url.host());
+    if (url.port() >= 0)
+	list.addParam("port",String(url.port()));
+    safeGetUtf8(list,"username",url.userName());
+    safeGetUtf8(list,"password",url.password());
+    QString tmp;
+    if (!path) {
+	tmp = url.path();
+	path = &tmp;
+    }
+    if (pathToList)
+	list.assign(path->toUtf8().constData());
+    else
+	safeGetUtf8(list,"path",*path);
+    QUrlQuery query( url );
+    QList<QPair<QString, QString> > items = query.queryItems();
+    for (int i = 0; i < items.size(); i++)
+	list.addParam(items[i].first.toUtf8().constData(),items[i].second.toUtf8().constData());
+}
+
+// Dump MIME data for debug purposes
+void QtClient::dumpMime(String& buf, const QMimeData* m)
+{
+    static const char* indent = "\r\n    ";
+    if (!m)
+	return;
+    QStringList fmts = m->formats();
+    if (fmts.size() > 0) {
+	buf.append("FORMATS:","\r\n") << indent;
+	QString s = fmts.join(indent);
+	buf << s.toUtf8().constData();
+    }
+    if (m->html().length() > 0)
+	buf.append("HTML: ","\r\n") << m->html().toUtf8().constData();
+    if (m->text().length() > 0)
+	buf.append("TEXT: ","\r\n") << m->text().toUtf8().constData();
+    QList<QUrl> urls = m->urls();
+    if (urls.size() > 0) {
+	buf.append("URLS:","\r\n");
+	for (int i = 0; i < urls.size(); i++)
+	    buf << indent << urls[i].toString().toUtf8().constData();
+    }
+}
+
+
+/**
+ * QtDriver
+ */
+QtDriver::QtDriver(bool buildClientThread)
+    : m_init(false), m_clientThread(buildClientThread)
+{
+    qInstallMessageHandler(qtMsgHandler);
+}
+
+QtDriver::~QtDriver()
+{
+    qInstallMessageHandler(0);
+}
+
+void QtDriver::initialize()
+{
+    Output("Initializing module Qt5 client");
+    s_device = Engine::config().getValue("client","device",DEFAULT_DEVICE);
+    if (!QtClient::self()) {
+	debugCopy();
+	QtClient::setSelf(new QtClient);
+	if (m_clientThread)
+	    QtClient::self()->startup();
+    }
+    if (!m_init) {
+	m_init = true;
+	setup();
+    }
+}
+
+/**
+ * QtEventProxy
+ */
+QtEventProxy::QtEventProxy(Type type, QApplication* app)
+{
+#define SET_NAME(n) { m_name = n; setObjectName(QtClient::setUtf8(m_name)); }
+    switch (type) {
+	case Timer:
+	    SET_NAME("qtClientTimerProxy");
+	    {
+		QTimer* timer = new QTimer(this);
+		timer->setObjectName("qtClientIdleTimer");
+		QtClient::connectObjects(timer,SIGNAL(timeout()),this,SLOT(timerTick()));
+		timer->start(0);
+	    }
+	    break;
+	case AllHidden:
+	    SET_NAME("qtClientAllHidden");
+	    if (app)
+		QtClient::connectObjects(app,SIGNAL(lastWindowClosed()),this,SLOT(allHidden()));
+	    break;
+	default:
+	    return;
+    }
+#undef SET_NAME
+}
+
+void QtEventProxy::timerTick()
+{
+    if (Client::self())
+	Client::self()->idleActions();
+    Thread::idle();
+}
+
+void QtEventProxy::allHidden()
+{
+    if (Client::self())
+	Client::self()->allHidden();
+}
+
+
+//
+// QtUrlBuilder
+//
+QtUrlBuilder::QtUrlBuilder(QObject* parent, const String& format,
+    const String& queryParams)
+    : QObject(parent),
+    m_format(format),
+    m_queryParams(0)
+{
+    if (queryParams) {
+	m_queryParams = queryParams.split(',',false);
+	if (!m_queryParams->skipNull())
+	    TelEngine::destruct(m_queryParams);
+    }
+}
+
+QtUrlBuilder::~QtUrlBuilder()
+{
+    TelEngine::destruct(m_queryParams);
+}
+
+// Build URL
+QUrl QtUrlBuilder::build(const NamedList& params) const
+{
+    String tmp;
+    if (m_format) {
+	tmp = m_format;
+	params.replaceParams(tmp);
+    }
+    QUrl url(QtClient::setUtf8(tmp));
+    if (m_queryParams) {
+	QUrlQuery urlQuery(url);
+	NamedIterator iter(params);
+	for (const NamedString* ns = 0; 0 != (ns = iter.get());)
+	    if (m_queryParams->find(ns->name()))
+		urlQuery.addQueryItem(QtClient::setUtf8(ns->name()),QtClient::setUtf8(*ns));
+	url.setQuery(urlQuery);
+    }
+    return url;
+}
+
+
+/*
+ * QtUIWidget
+ */
+// Retrieve item type definition from [type:]value. Create it if not found
+QtUIWidgetItemProps* QtUIWidget::getItemProps(QString& in, String& value)
+{
+    String type;
+    int pos = in.indexOf(':');
+    if (pos >= 0) {
+	QtClient::getUtf8(type,in.left(pos));
+	QtClient::getUtf8(value,in.right(in.length() - pos - 1));
+    }
+    else
+	QtClient::getUtf8(value,in);
+    QtUIWidgetItemProps* p = QtUIWidget::getItemProps(type);
+    if (!p) {
+	p = new QtUIWidgetItemProps(type);
+	m_itemProps.append(p);
+    }
+    DDebug(ClientDriver::self(),DebugAll,"QtUIWidget(%s) getItemProps(%s,%s) got (%p) ui=%s [%p]",
+	name().c_str(),in.toUtf8().constData(),value.c_str(),p,p->m_ui.c_str(),this);
+    return p;
+}
+
+// Set widget's parameters.
+// Handle an 'applyall' parameter carrying a NamedList to apply to all items
+bool QtUIWidget::setParams(const NamedList& params)
+{
+    bool ok = false;
+    NamedIterator iter(params);
+    for (const NamedString* ns = 0; 0 != (ns = iter.get());) {
+	if (ns->name() == YSTRING("applyall")) {
+	    const NamedList* list = YOBJECT(NamedList,ns);
+	    if (list) {
+		ok = true;
+		applyAllParams(*list);
+	    }
+	}
+	else if (ns->name().startsWith("beginedit:"))
+	    beginEdit(ns->name().substr(10),ns);
+    }
+    return ok;
+}
+
+// Apply a list of parameters to all container items
+void QtUIWidget::applyAllParams(const NamedList& params)
+{
+    QList<QObject*> list = getContainerItems();
+    for (int i = 0; i < list.size(); i++)
+	setParams(list[i],params);
+}
+
+// Find an item widget by id
+QWidget* QtUIWidget::findItem(const String& id)
+{
+    QString item = QtClient::setUtf8(id);
+    QList<QObject*> list = getContainerItems();
+    for (int i = 0; i < list.size(); i++) {
+	if (!list[i]->isWidgetType())
+	    continue;
+	String item;
+	getListItemIdProp(list[i],item);
+	if (id == item)
+	    return static_cast<QWidget*>(list[i]);
+    }
+    return 0;
+}
+
+// Retrieve the object identity from '_yate_identity' property or name
+// Retrieve the object item from '_yate_widgetlistitem' property.
+// Set 'identity' to object_identity[:item_name]
+void QtUIWidget::getIdentity(QObject* obj, String& identity)
+{
+    if (!obj)
+	return;
+    String ident;
+    QtClient::getIdentity(obj,ident);
+    if (!ident)
+	return;
+    String item;
+    getListItemProp(obj,item);
+    identity.append(ident,":");
+    identity.append(item,":");
+}
+
+// Update a widget and children from a list a parameters
+bool QtUIWidget::setParams(QObject* parent, const NamedList& params)
+{
+    static const String s_property = "property";
+    static const String s_active = "active";
+    static const String s_image = "image";
+    static const String s_show = "show";
+    static const String s_display = "display";
+    static const String s_check = "check";
+    static const String s_select = "select";
+    static const String s_addlines = "addlines";
+    static const String s_setrichtext = "setrichtext";
+    static const String s_updatetablerows = "updatetablerows";
+    static const String s_cleartable = "cleartable";
+    static const String s_rawimage = "rawimage";
+    static const String s_setparams = "setparams";
+    static const String s_setmenu = "setmenu";
+    static const String s_height = "height";
+
+    if (!parent)
+	return false;
+    QtWindow* wnd = QtClient::parentWindow(parent);
+    if (!wnd)
+	return false;
+#ifdef DEBUG
+    String tmp;
+    params.dump(tmp," ");
+    Debug(ClientDriver::self(),DebugAll,"QtUIWidget(%s)::setParams(%p,%s) %s",
+	name().c_str(),parent,YQT_OBJECT_NAME(parent),tmp.c_str());
+#endif
+    String pName(YQT_OBJECT_NAME(parent));
+    bool ok = true;
+    unsigned int n = params.length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* ns = params.getParam(i);
+	if (!ns)
+	    continue;
+	XDebug(ClientDriver::self(),DebugInfo,"QtUIWidget(%s)::setParams() %s=%s",
+	    name().c_str(),ns->name().c_str(),ns->c_str());
+	String buf;
+	int pos = ns->name().find(':');
+	if (pos < 0) {
+	    if (ns->name() != s_setmenu)
+		ok = wnd->setText(buildChildName(buf,pName,ns->name()),*ns,false) && ok;
+	    else
+		buildWidgetItemMenu(qobject_cast<QWidget*>(parent),YOBJECT(NamedList,ns));
+	    continue;
+	}
+	String n(ns->name().substr(0,pos));
+	String cName = ns->name().substr(pos + 1);
+	if (n == s_property) {
+	    // Handle property[:child]:property_name
+	    int pos = cName.find(':');
+	    if (pos >= 0) {
+		QString tmp = buildQChildName(pName,cName.substr(0,pos));
+		QObject* c = parent->findChild<QObject*>(tmp);
+		ok = c && QtClient::setProperty(c,cName.substr(pos + 1),*ns) && ok;
+	    }
+	    else
+		ok = QtClient::setProperty(parent,cName,*ns) && ok;
+	}
+	else if (n == s_active)
+	    ok = wnd->setActive(buildChildName(buf,pName,cName),ns->toBoolean()) && ok;
+	else if (n == s_image)
+	    ok = wnd->setImage(buildChildName(buf,pName,cName),*ns) && ok;
+	else if (n == s_show || n == s_display)
+	    ok = wnd->setShow(buildChildName(buf,pName,cName),ns->toBoolean()) && ok;
+	else if (n == s_check)
+	    ok = wnd->setCheck(buildChildName(buf,pName,cName),ns->toBoolean()) && ok;
+	else if (n == s_select)
+	    ok = wnd->setSelect(buildChildName(buf,pName,cName),*ns) && ok;
+	if (n == s_setparams) {
+	    NamedList* p = YOBJECT(NamedList,ns);
+	    if (!p)
+		continue;
+	    QtWidget w(parent,buildChildName(buf,pName,cName));
+	    UIWidget* uiw = w.uiWidget();
+	    ok = uiw && uiw->setParams(*p) && ok;
+	}
+	else if (n == s_addlines) {
+	    NamedList* p = YOBJECT(NamedList,ns);
+	    if (p)
+		ok = wnd->addLines(buildChildName(buf,pName,cName),p,0,ns->toBoolean()) && ok;
+	}
+	else if (n == s_setrichtext)
+	    ok = wnd->setText(buildChildName(buf,pName,cName),*ns,true) && ok;
+	else if (n == s_updatetablerows) {
+	    NamedList* p = YOBJECT(NamedList,ns);
+	    if (p)
+		ok = wnd->updateTableRows(buildChildName(buf,pName,cName),p,ns->toBoolean()) && ok;
+	}
+	else if (n == s_cleartable)
+	    ok = wnd->clearTable(buildChildName(buf,pName,cName)) && ok;
+	else if (n == s_rawimage) {
+	    DataBlock* data = YOBJECT(DataBlock,ns);
+	    if (data) {
+		QString tmp = buildQChildName(pName,cName.substr(0,pos));
+		QObject* c = parent->findChild<QObject*>(tmp);
+		ok = c && QtClient::setImage(c,*data,*ns) && ok;
+	    }
+	}
+	else if (n == s_setmenu)
+	    buildWidgetItemMenu(qobject_cast<QWidget*>(parent),YOBJECT(NamedList,ns),cName);
+	else if (n == s_height) {
+	    QString tmp = buildQChildName(pName,cName);
+	    QWidget* w = qobject_cast<QWidget*>(parent)->findChild<QWidget*>(tmp);
+	    QtClient::setWidgetHeight(w,*ns);
+	}
+	else
+	    ok = wnd->setText(buildChildName(buf,pName,ns->name()),*ns,false) && ok;
+    }
+    // Set item parameters
+    NamedString* yparams = params.getParam(YSTRING("_yate_itemparams"));
+    if (!TelEngine::null(yparams)) {
+	QVariant var = parent->property(yparams->name().c_str());
+	if (var.type() == QVariant::Invalid || var.type() == QVariant::StringList) {
+	    QStringList list;
+	    if (var.type() == QVariant::StringList)
+		list = var.toStringList();
+	    NamedList tmp("");
+	    tmp.copyParams(params,*yparams);
+	    QtClient::copyParams(list,tmp);
+	    parent->setProperty(yparams->name().c_str(),QVariant(list));
+	}
+	else
+	    ok = false;
+    }
+    return ok;
+}
+
+// Get an item object's parameters
+bool QtUIWidget::getParams(QObject* parent, NamedList& params)
+{
+    static const String s_property = "property";
+    static const String s_getcheck = "getcheck";
+    static const String s_getselect = "getselect";
+    static const String s_getrichtext = "getrichtext";
+
+    if (!parent)
+	return false;
+    QtWindow* wnd = QtClient::parentWindow(parent);
+    if (!wnd)
+	return false;
+    DDebug(ClientDriver::self(),DebugAll,"QtUIWidget(%s)::getParams(%p,%s)",
+	name().c_str(),parent,YQT_OBJECT_NAME(parent));
+    String pName;
+    QtClient::getUtf8(pName,parent->objectName());
+    bool ok = true;
+    unsigned int n = params.length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* ns = params.getParam(i);
+	if (!ns)
+	    continue;
+	String buf;
+	int pos = ns->name().find(':');
+	if (pos < 0) {
+	    ok = wnd->getText(buildChildName(buf,pName,ns->name()),*ns,false) && ok;
+	    continue;
+	}
+	String n(ns->name().substr(0,pos));
+	String cName = ns->name().substr(pos + 1);
+	if (n == s_property) {
+	    // Handle property[:child]:property_name
+	    int pos = cName.find(':');
+	    if (pos >= 0) {
+		QString tmp = buildQChildName(pName,cName.substr(0,pos));
+		QObject* c = parent->findChild<QObject*>(tmp);
+		ok = c && QtClient::getProperty(c,cName.substr(pos + 1),*ns) && ok;
+	    }
+	    else
+		ok = QtClient::getProperty(parent,cName,*ns) && ok;
+	}
+	else if (n == s_getselect)
+	    ok = wnd->getSelect(buildChildName(buf,pName,cName),*ns) && ok;
+	else if (n == s_getcheck) {
+	    bool on = false;
+	    ok = wnd->getCheck(buildChildName(buf,pName,cName),on) && ok;
+	    *ns = String::boolText(on);
+	}
+	else if (n == s_getrichtext)
+	    ok = wnd->getText(buildChildName(buf,pName,cName),*ns,true) && ok;
+	else
+	    ok = wnd->getText(buildChildName(buf,pName,ns->name()),*ns,false) && ok;
+    }
+    // Get item parameters
+    QtClient::getProperty(parent,"_yate_itemparams",params);
+    return ok;
+}
+
+// Show or hide control busy state
+bool QtUIWidget::setBusy(bool on)
+{
+    QObject* o = getQObject();
+    QWidget* w = (o && o->isWidgetType()) ? static_cast<QWidget*>(o) : 0;
+    return w && QtBusyWidget::showBusyChild(w,on);
+}
+
+// Apply properties for QAbstractItemView descendents
+void QtUIWidget::applyItemViewProps(const NamedList& params)
+{
+    static const String s_selMode = "_yate_selection_mode";
+    static const String s_editTriggers = "_yate_edit_triggers";
+
+    QObject* obj = getQObject();
+    QAbstractItemView* av = qobject_cast<QAbstractItemView*>(obj);
+    if (!av)
+	return;
+    NamedIterator iter(params);
+    for (const NamedString* ns = 0; 0 != (ns = iter.get());) {
+	if (ns->name() == s_selMode)
+	    av->setSelectionMode(QtClient::str2selmode(*ns));
+	else if (ns->name() == s_editTriggers)
+	    av->setEditTriggers(QtClient::str2editTriggers(*ns));
+    }
+}
+
+// Begin item edit. The default behaviour start edit for QAbstractItemView descendants
+bool QtUIWidget::beginEdit(const String& item, const String* what)
+{
+    QObject* obj = getQObject();
+    QAbstractItemView* av = qobject_cast<QAbstractItemView*>(obj);
+    if (!av)
+	return false;
+    QModelIndex idx = modelIndex(item,what);
+    if (!idx.isValid())
+	return false;
+    av->setCurrentIndex(idx);
+    av->edit(idx);
+    return true;
+}
+
+// Build item widget menu
+QMenu* QtUIWidget::buildWidgetItemMenu(QWidget* w, const NamedList* params,
+    const String& child, bool set)
+{
+    if (!(w && params))
+	return 0;
+    QWidget* parent = w;
+    // Retrieve the item owner
+    QWidget* pItem = 0;
+    String item;
+    getListItemIdProp(w,item);
+    if (item)
+	pItem = findItem(item);
+    else {
+	getListItemProp(w,item);
+	pItem = item ? findItem(item) : 0;
+    }
+    XDebug(ClientDriver::self(),DebugAll,
+	"QtUIWidget(%s)::buildMenu() widget=%s item=%s [%p]",
+	this->name().c_str(),YQT_OBJECT_NAME(w),item.c_str(),this);
+    String pName(YQT_OBJECT_NAME(w));
+    const String& owner = (*params)[YSTRING("owner")];
+    if (owner && owner != item) {
+	QString tmp = buildQChildName(pName,owner);
+	parent = w->findChild<QWidget*>(tmp);
+	if (!parent) {
+	    Debug(QtDriver::self(),DebugNote,
+		"QtUIWidget(%s) buildMenu() owner '%s' not found [%p]",
+		name().c_str(),owner.c_str(),this);
+	    return 0;
+	}
+    }
+    QWidget* target = parent;
+    String t = child ? child : (*params)[YSTRING("target")];
+    if (t) {
+	QString tmp = buildQChildName(pName,t);
+	target = w->findChild<QWidget*>(tmp);
+	if (!target) {
+	    Debug(QtDriver::self(),DebugNote,
+		"QtUIWidget(%s) buildMenu() target '%s' not found [%p]",
+		name().c_str(),t.c_str(),this);
+	    return 0;
+	}
+    }
+    QString menuName = buildQChildName(pName,t + "_menu");
+    // Remove existing menu
+    QMenu* menu = parent->findChild<QMenu*>(menuName);
+    if (menu) {
+	delete menu;
+	menu = 0;
+    }
+    // Build the menu
+    QObject* thisObj = getQObject();
+    if (!thisObj)
+	return 0;
+    String actionSlot;
+    String toggleSlot;
+    String selectSlot;
+    getSlots(actionSlot,toggleSlot,selectSlot);
+    if (!(actionSlot || toggleSlot))
+	return 0;
+    bool addActions = set && target->contextMenuPolicy() == Qt::ActionsContextMenu;
+    unsigned int n = params->length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* param = params->getParam(i);
+	if (!(param && param->name().startsWith("item:")))
+	    continue;
+	if (!menu)
+	    menu = new QMenu(QtClient::setUtf8(params->getValue(YSTRING("title"))),parent);
+	NamedList* p = YOBJECT(NamedList,param);
+	if (p)  {
+	    QMenu* subMenu = QtClient::buildMenu(*p,
+		*param ? param->c_str() : p->getValue(YSTRING("title"),*p),
+		thisObj,actionSlot,toggleSlot,menu);
+	    if (subMenu) {
+		menu->addMenu(subMenu);
+		if (addActions)
+		    target->addAction(subMenu->menuAction());
+	    }
+	    continue;
+	}
+	QAction* a = 0;
+	String name = param->name().substr(5);
+	if (*param) {
+	    a = menu->addAction(QtClient::setUtf8(*param));
+	    a->setObjectName(buildQChildName(pName,name));
+	    a->setParent(menu);
+	    QtClient::setImage(a,(*params)["image:" + name]);
+	}
+	else if (!name) {
+	    a = menu->addSeparator();
+	    a->setParent(menu);
+	}
+	else if (pItem) {
+	    // Check if the action is already there
+	    QString aName = buildQChildName(pItem->objectName(),QtClient::setUtf8(name));
+	    a = pItem->findChild<QAction*>(aName);
+	    if (a)
+		menu->addAction(a);
+	}
+	if (a) {
+	    if (addActions)
+		target->addAction(a);
+	}
+	else
+	    Debug(ClientDriver::self(),DebugNote,
+		"QtUIWidget(%s)::buildMenu() action '%s' not found for item=%s [%p]",
+		this->name().c_str(),name.c_str(),item.c_str(),this);
+    }
+    if (!menu)
+	return 0;
+    // Set name
+    menu->setObjectName(menuName);
+    // Apply properties
+    // Format: property:object_name:property_name=value
+    if (parent)
+	for (unsigned int i = 0; i < n; i++) {
+	    NamedString* param = params->getParam(i);
+	    if (!(param && param->name().startsWith("property:")))
+		continue;
+	    int pos = param->name().find(':',9);
+	    if (pos < 9)
+		continue;
+	    QString n = buildQChildName(pName,param->name().substr(9,pos - 9));
+	    QObject* obj = parent->findChild<QObject*>(n);
+	    if (obj)
+		QtClient::setProperty(obj,param->name().substr(pos + 1),*param);
+	}
+    // Connect signals (direct children only: actions from sub-menus are already connected)
+    QList<QAction*> list = menu->findChildren<QAction*>();
+    for (int i = 0; i < list.size(); i++) {
+	if (list[i]->isSeparator() || list[i]->parent() != menu)
+	    continue;
+	if (list[i]->isCheckable())
+	    QtClient::connectObjects(list[i],SIGNAL(toggled(bool)),thisObj,toggleSlot);
+	else
+	    QtClient::connectObjects(list[i],SIGNAL(triggered()),thisObj,actionSlot);
+    }
+    if (addActions)
+	return menu;
+    QMenu* mOwner = qobject_cast<QMenu*>(target);
+    if (mOwner)
+	mOwner->insertMenu(0,menu);
+    else {
+	QToolButton* tb = qobject_cast<QToolButton*>(target);
+	if (tb)
+	    tb->setMenu(menu);
+	else {
+	    QPushButton* pb = qobject_cast<QPushButton*>(target);
+	    if (pb)
+		pb->setMenu(menu);
+	    else if (!QtClient::setProperty(target,s_propContextMenu,params))
+		target->addAction(menu->menuAction());
+	}
+    }
+    return menu;
+}
+
+// Build a container child name from parent property
+bool QtUIWidget::buildQChildNameProp(QString& dest, QObject* parent, const char* prop)
+{
+    if (!(parent && prop))
+	return false;
+    QVariant var = parent->property(prop);
+    if (!var.isValid() || var.toString().size() <= 0)
+	return false;
+    dest = buildQChildName(parent->objectName(),var.toString());
+    return true;
+}
+
+// Retrieve the top level QtUIWidget container parent of an object
+QtUIWidget* QtUIWidget::container(QObject* obj)
+{
+    if (!obj)
+	return 0;
+    QtUIWidget* uiw = 0;
+    while (0 != (obj = obj->parent())) {
+	QtWidget w(obj);
+	UIWidget* u = w.uiWidget();
+	if (u)
+	    uiw = static_cast<QtUIWidget*>(u);
+    }
+    return uiw;
+}
+
+// Utility used in QtUIWidget::initNavigation
+static bool initNavAction(QObject* obj, const String& name, const String& actionSlot)
+{
+    if (!(obj && name))
+	return false;
+    QtWindow* wnd = QtClient::parentWindow(obj);
+    QObject* child = wnd->findChild<QObject*>(QtClient::setUtf8(name));
+    if (!child)
+	return false;
+    QAbstractButton* b = 0;
+    QAction* a = 0;
+    if (child->isWidgetType())
+	b = qobject_cast<QAbstractButton*>(child);
+    else
+	a = qobject_cast<QAction*>(child);
+    if (b || a) {
+	if (b)
+	    QtClient::connectObjects(b,SIGNAL(clicked()),obj,actionSlot);
+	else
+	    QtClient::connectObjects(a,SIGNAL(triggered()),obj,actionSlot);
+    }
+    return b || a;
+}
+
+// Initialize navigation controls
+void QtUIWidget::initNavigation(const NamedList& params)
+{
+    String actionSlot;
+    String toggleSlot;
+    String selectSlot;
+    getSlots(actionSlot,toggleSlot,selectSlot);
+    QObject* qObj = getQObject();
+    if (qObj && actionSlot) {
+	m_prev = params.getValue(YSTRING("navigate_prev"));
+	if (!initNavAction(qObj,m_prev,actionSlot))
+	    m_prev = "";
+	m_next = params.getValue(YSTRING("navigate_next"));
+	if (!initNavAction(qObj,m_next,actionSlot))
+	    m_next = "";
+    }
+    m_info = params.getValue(YSTRING("navigate_info"));
+    m_infoFormat = params.getValue(YSTRING("navigate_info_format"));
+    m_title = params.getValue(YSTRING("navigate_title"));
+    updateNavigation();
+}
+
+// Update navigation controls
+void QtUIWidget::updateNavigation()
+{
+    if (!(m_prev || m_next || m_info || m_title))
+	return;
+    QtWindow* wnd = QtClient::parentWindow(getQObject());
+    if (!wnd)
+	return;
+    NamedList p("");
+    int crt = currentItemIndex();
+    if (crt < 0)
+	crt = 0;
+    else
+	crt++;
+    int n = itemCount();
+    if (n < crt)
+	n = crt;
+    if (m_prev || m_next) {
+	if (m_prev)
+	    p.addParam("active:" + m_prev,String::boolText(crt > 1));
+	if (m_next)
+	    p.addParam("active:" + m_next,String::boolText(crt < n));
+    }
+    if (m_info) {
+	String tmp = m_infoFormat;
+	NamedList pp("");
+	pp.addParam("index",String(crt));
+	pp.addParam("count",String(n));
+	pp.replaceParams(tmp);
+	p.addParam(m_info,tmp);
+    }
+    if (m_title) {
+	String crt;
+	getSelect(crt);
+	NamedList pp("");
+	if (crt)
+	    getTableRow(crt,&pp);
+	p.addParam(m_title,pp[YSTRING("title")]);
+    }
+    wnd->setParams(p);
+}
+
+// Trigger a custom action from an item
+bool QtUIWidget::triggerAction(const String& item, const String& action, QObject* sender,
+    NamedList* params)
+{
+    if (!(Client::self() && action))
+	return false;
+    if (!sender)
+	sender = getQObject();
+    String s;
+    getIdentity(sender,s);
+    if (!s)
+	return false;
+    NamedList p("");
+    if (!params)
+	params = &p;
+    params->addParam("item",item,false);
+    params->addParam("widget",s);
+    return QtClient::self()->action(QtClient::parentWindow(sender),action,params);
+}
+
+// Trigger a custom action from already built list params
+bool QtUIWidget::triggerAction(const String& action, NamedList& params, QObject* sender)
+{
+    if (!(Client::self() && action))
+	return false;
+    if (!sender)
+	sender = getQObject();
+    String s;
+    getIdentity(sender,s);
+    if (!s)
+	return false;
+    params.setParam("widget",s);
+    return QtClient::self()->action(QtClient::parentWindow(sender),action,&params);
+}
+
+// Handle a child's action
+void QtUIWidget::onAction(QObject* sender)
+{
+    if (!Client::self())
+	return;
+    String s;
+    getIdentity(sender,s);
+    if (!s)
+	return;
+    int dir = 0;
+    if (s == m_next)
+	dir = 1;
+    else if (s == m_prev)
+	dir = -1;
+    if (dir) {
+	int crt = currentItemIndex();
+	if (crt >= 0)
+	    setSelectIndex(crt + dir);
+	return;
+    }
+    DDebug(ClientDriver::self(),DebugAll,"QtUIWidget(%s) raising action %s",
+	name().c_str(),s.c_str());
+    Client::self()->action(QtClient::parentWindow(sender),s);
+}
+
+// Handle a child's toggle notification
+void QtUIWidget::onToggle(QObject* sender, bool on)
+{
+    if (!Client::self())
+	return;
+    QtClient::updateToggleImage(sender);
+    String s;
+    getIdentity(sender,s);
+    if (!s)
+	return;
+    DDebug(ClientDriver::self(),DebugAll,"QtUIWidget(%s) raising toggle %s",
+	name().c_str(),s.c_str());
+    Client::self()->toggle(QtClient::parentWindow(sender),s,on);
+}
+
+// Handle a child's selection change
+void QtUIWidget::onSelect(QObject* sender, const String* item)
+{
+    if (!Client::self())
+	return;
+    String s;
+    getIdentity(sender,s);
+    if (!s)
+	return;
+    QtWindow* wnd = QtClient::parentWindow(sender);
+    String tmp;
+    if (!item) {
+	item = &tmp;
+	if (wnd)
+	    wnd->getSelect(YQT_OBJECT_NAME(sender),tmp);
+    }
+    DDebug(ClientDriver::self(),DebugAll,"QtUIWidget(%s) raising select %s",
+	name().c_str(),s.c_str());
+    Client::self()->select(wnd,s,*item);
+}
+
+// Handle a child's multiple selection change
+void QtUIWidget::onSelectMultiple(QObject* sender, const NamedList* items)
+{
+    if (!Client::self())
+	return;
+    String s;
+    getIdentity(sender,s);
+    if (!s)
+	return;
+    QtWindow* wnd = QtClient::parentWindow(sender);
+    DDebug(ClientDriver::self(),DebugAll,"QtUIWidget(%s) raising select multiple",
+	name().c_str());
+    if (items) {
+	Client::self()->select(wnd,s,*items);
+	return;
+    }
+    NamedList tmp("");
+    if (wnd)
+	wnd->getSelect(YQT_OBJECT_NAME(sender),tmp);
+    Client::self()->select(wnd,s,tmp);
+}
+
+// Filter wathed events for children.
+// Handle child image changing on mouse events
+bool QtUIWidget::onChildEvent(QObject* watched, QEvent* event)
+{
+    if (event->type() == QEvent::Enter)
+	QtClient::updateImageFromMouse(watched,true,true);
+    else if (event->type() == QEvent::Leave)
+	QtClient::updateImageFromMouse(watched,true,false);
+    else if (event->type() == QEvent::MouseButtonPress)
+	QtClient::updateImageFromMouse(watched,false,true);
+    else if (event->type() == QEvent::MouseButtonRelease)
+	QtClient::updateImageFromMouse(watched,false,false);
+    return false;
+}
+
+// Load an item's widget. Rename children. Connect actions
+QWidget* QtUIWidget::loadWidget(QWidget* parent, const String& name, const String& ui)
+{
+    // Build a new widget name to make sure there are no duplicates:
+    //   Some containers (like QTreeWidget) calls deleteLater() for widget's
+    //   set to items which might lead to wrong widget update
+    // Make sure the widget name contains only 'standard' characters
+    //   to avoid errors when replaced in style sheets
+    MD5 md5(name);
+    String wName;
+    buildChildName(wName,md5.hexDigest());
+    wName << "_" << (unsigned int)Time::now();
+    QWidget* w = QtWindow::loadUI(Client::s_skinPath + ui,parent,ui);
+    DDebug(ClientDriver::self(),w ? DebugAll : DebugNote,
+	"QtUIWidget(%s)::loadWidget(%p,%s,%s) widget=%p",
+	this->name().c_str(),parent,wName.c_str(),ui.c_str(),w);
+    if (!w)
+	return 0;
+    QObject* qObj = getQObject();
+    QtWindow* wnd = getWindow();
+    // Install event filter in parent window
+    if (!m_wndEvHooked && wnd && qObj) {
+	QVariant var = w->property("_yate_keypress_redirect");
+	if (var.isValid()) {
+	    m_wndEvHooked = true;
+	    wnd->installEventFilter(qObj);
+	}
+    }
+    String actionSlot;
+    String toggleSlot;
+    String selectSlot;
+    getSlots(actionSlot,toggleSlot,selectSlot);
+    QString wListItem = QtClient::setUtf8(name);
+    w->setObjectName(QtClient::setUtf8(wName));
+    setListItemIdProp(w,wListItem);
+    // Build custom UI widgets
+    QtClient::buildFrameUiWidgets(w);
+    // Process "_yate_setaction" property before changing names
+    QtClient::setAction(w);
+    // Process children
+    QList<QObject*> c = w->findChildren<QObject*>();
+    for (int i = 0; i < c.size(); i++) {
+	// Set object item owner name
+	setListItemProp(c[i],wListItem);
+	// Rename child
+	String n;
+	QtClient::getUtf8(n,c[i]->objectName());
+	c[i]->setObjectName(buildQChildName(wName,n));
+	// Install event filters
+	if (qObj && QtClient::getBoolProperty(c[i],"_yate_filterevents"))
+	    c[i]->installEventFilter(qObj);
+	// Connect text changed to window's slot
+	bool connect = QtClient::autoConnect(c[i]);
+	if (wnd && connect)
+	    wnd->connectTextChanged(c[i]);
+	// Connect signals
+	if (!(qObj && connect && (actionSlot || toggleSlot || selectSlot)))
+	    continue;
+	// Use isWidgetType() (faster then qobject_cast)
+	if (c[i]->isWidgetType()) {
+	    // Connect abstract buttons (check boxes and radio/push/tool buttons) signals
+	    QAbstractButton* b = qobject_cast<QAbstractButton*>(c[i]);
+	    if (b) {
+		if (!b->isCheckable())
+		    QtClient::connectObjects(b,SIGNAL(clicked()),qObj,actionSlot);
+		else
+		    QtClient::connectObjects(b,SIGNAL(toggled(bool)),qObj,toggleSlot);
+		continue;
+	    }
+	    // Connect group boxes
+	    QGroupBox* gb = qobject_cast<QGroupBox*>(c[i]);
+	    if (gb) {
+		if (gb->isCheckable())
+		    QtClient::connectObjects(gb,SIGNAL(toggled(bool)),qObj,toggleSlot);
+		continue;
+	    }
+	    // Connect combo boxes
+	    QComboBox* combo = qobject_cast<QComboBox*>(c[i]);
+	    if (combo) {
+		QtClient::connectObjects(combo,SIGNAL(activated(int)),qObj,selectSlot);
+		continue;
+	    }
+	    // Connect list boxes
+	    QListWidget* lst = qobject_cast<QListWidget*>(c[i]);
+	    if (lst) {
+		QtClient::connectObjects(lst,SIGNAL(currentRowChanged(int)),qObj,selectSlot);
+		continue;
+	    }
+	    // Connect sliders
+	    QSlider* sld = qobject_cast<QSlider*>(c[i]);
+	    if (sld) {
+		QtClient::connectObjects(sld,SIGNAL(valueChanged(int)),qObj,selectSlot);
+		continue;
+	    }
+	    continue;
+	}
+	// Connect actions signals
+	QAction* a = qobject_cast<QAction*>(c[i]);
+	if (a) {
+	    if (!a->isCheckable())
+		QtClient::connectObjects(a,SIGNAL(triggered()),qObj,actionSlot);
+	    else
+		QtClient::connectObjects(a,SIGNAL(toggled(bool)),qObj,toggleSlot);
+	    continue;
+	}
+    }
+    return w;
+}
+
+// Apply a QWidget style sheet. Replace ${name} with widget name in style
+void QtUIWidget::applyWidgetStyle(QWidget* w, const String& style)
+{
+    if (!(w && style))
+	return;
+    QString s = QtClient::setUtf8(style);
+    s.replace("${name}",w->objectName());
+    w->setStyleSheet(s);
+}
+
+// Filter key press events. Retrieve an action associated with the key.
+// Check if the object is allowed to process the key.
+// Raise the action
+bool QtUIWidget::filterKeyEvent(QObject* watched, QKeyEvent* event, bool& filter)
+{
+    String action;
+    if (!QtClient::filterKeyEvent(watched,event,action,filter))
+	return false;
+    if (!action)
+	return true;
+    String item;
+    getListItemProp(watched,item);
+    // Avoid raising a disabled actions
+    if (item) {
+	bool ok = true;
+	QWidget* w = findItem(item);
+	if (w) {
+	    QString n = buildQChildName(w->objectName(),QtClient::setUtf8(action));
+	    QObject* act = w->findChild<QObject*>(n);
+	    if (act) {
+		if (act->isWidgetType())
+		    ok = (qobject_cast<QWidget*>(act))->isEnabled();
+		else {
+		    QAction* a = qobject_cast<QAction*>(act);
+		    ok = !a || a->isEnabled();
+		}
+	    }
+	}
+	if (!ok)
+	    return true;
+	// Append container item to action
+	action.append(item,":");
+    }
+    Client::self()->action(QtClient::parentWindow(getQObject()),action);
+    return true;
+}
+
+
+/**
+ * QtSound
+ */
+bool QtSound::doStart()
+{
+    doStop();
+    if (Client::self())
+	Client::self()->createObject((void**)&m_sound,"QSound",m_file);
+    if (m_sound)
+	DDebug(ClientDriver::self(),DebugAll,"Sound(%s) started file=%s",
+	    c_str(),m_file.c_str());
+    else {
+	Debug(ClientDriver::self(),DebugNote,"Sound(%s) failed to start file=%s",
+	    c_str(),m_file.c_str());
+	return false;
+    }
+    m_sound->setLoops(m_repeat ? m_repeat : -1);
+    m_sound->play();
+    return true;
+}
+
+void QtSound::doStop()
+{
+    if (!m_sound)
+	return;
+    m_sound->stop();
+    delete m_sound;
+    DDebug(ClientDriver::self(),DebugAll,"Sound(%s) stopped",c_str());
+    m_sound = 0;
+}
+
+
+//
+// QtDragAndDrop
+//
+// Reset data
+void QtDragAndDrop::reset()
+{
+    m_started = false;
+}
+
+// Check a string value for 'drag', 'drop', 'both'
+void QtDragAndDrop::checkEnable(const String& s, bool& drag, bool& drop)
+{
+    drag = (s == YSTRING("drag"));
+    drop = !drag && (s == YSTRING("drop"));
+    if (!(drag || drop))
+	drag = drop = (s == YSTRING("both"));
+}
+
+//
+// QtDrop
+//
+const String QtDrop::s_askClientAcceptDrop = "_yate_event_drop_accept";
+const String QtDrop::s_notifyClientDrop = "_yate_event_drop";
+const QString QtDrop::s_fileScheme = "file";
+
+const TokenDict QtDrop::s_acceptDropName[] = {
+    {"always", Always},
+    {"ask", Ask},
+    {"none", 0},
+    {0,0}
+};
+
+QtDrop::QtDrop(QObject* parent, const NamedList* params)
+    : QtDragAndDrop(parent),
+    m_dropParams(""),
+    m_acceptFiles(false),
+    m_acceptDirs(false)
+{
+    if (!params)
+	return;
+    NamedIterator iter(*params);
+    for (const NamedString* ns = 0; 0 != (ns = iter.get());) {
+	if (ns->name() == YSTRING("_yate_accept_drop_schemes"))
+	    QtClient::addStrListUnique(m_schemes,QtClient::str2list(*ns));
+	else if (ns->name() == YSTRING("_yate_accept_drop_file"))
+	    m_acceptFiles = ns->toBoolean();
+	else if (ns->name() == YSTRING("_yate_accept_drop_dir"))
+	    m_acceptDirs = ns->toBoolean();
+    }
+}
+
+// Update parameters from drag enter event
+bool QtDrop::start(QDragEnterEvent& e)
+{
+    static const String s_prefix = "drop:";
+
+    reset();
+    const QMimeData* m = e.mimeData();
+    if (!(m && m->hasUrls()))
+	return false;
+    int nUrls = m->urls().size();
+    unsigned int nItems = 0;
+    for (int i = 0; i < nUrls; i++) {
+	QString scheme = m->urls()[i].scheme();
+	if (m_schemes.size() > 0 && !m_schemes.contains(scheme)) {
+	    reset();
+	    return false;
+	}
+	QString path = m->urls()[i].path();
+	String what = scheme.toUtf8().constData();
+	if (scheme == s_fileScheme) {
+#ifdef _WINDOWS
+	    path = path.mid(1);
+#endif
+	    path = QDir::toNativeSeparators(path);
+	    QFileInfo fi(path);
+	    if (fi.isDir()) {
+		if (!m_acceptDirs) {
+		    reset();
+		    return false;
+		}
+		what = "directory";
+	    }
+	    else if (fi.isFile() && !m_acceptFiles) {
+		reset();
+		return false;
+	    }
+	}
+	nItems++;
+	NamedList* nl = new NamedList("");
+	QtClient::fillUrlParams(m->urls()[i],*nl,&path);
+	m_dropParams.addParam(new NamedPointer(s_prefix + what,nl,*nl));
+    }
+    if (!nItems) {
+	reset();
+	return false;
+    }
+    if (e.source()) {
+	QtWindow* wnd = QtClient::parentWindow(e.source());
+	if (wnd) {
+	    m_dropParams.addParam("source_window",wnd->toString());
+	    QtClient::getUtf8(m_dropParams,"source",e.source()->objectName());
+	}
+    }
+    m_started = true;
+    return true;
+}
+
+// Reset data
+void QtDrop::reset()
+{
+    m_dropParams.clearParams();
+    QtDragAndDrop::reset();
+}
+
+
+//
+// QtListDrop
+//
+QtListDrop::QtListDrop(QObject* parent, const NamedList* params)
+    : QtDrop(parent,params),
+    m_acceptOnEmpty(None)
+{
+}
+
+// Update accept
+void QtListDrop::updateAcceptType(const String list, int type)
+{
+    if (!list)
+	return;
+    ObjList* l = list.split(',',false);
+    for (ObjList* o = l->skipNull(); o; o = o->skipNext()) {
+	NamedInt* ni = new NamedInt(*static_cast<String*>(o->get()),type);
+	NamedInt::addToListUniqueName(m_acceptItemTypes,ni);
+    }
+    TelEngine::destruct(l);
+}
+
+// Update accept from parameters list
+void QtListDrop::updateAccept(const NamedList& params)
+{
+    NamedIterator iter(params);
+    for (const NamedString* ns = 0; 0 != (ns = iter.get());) {
+	if (ns->name() == YSTRING("_yate_accept_drop_onempty"))
+	    m_acceptOnEmpty = this->acceptDropType(*ns,None);
+	else if (ns->name() == YSTRING("_yate_accept_drop_item_type_always"))
+	    updateAcceptType(*ns,Always);
+	else if (ns->name() == YSTRING("_yate_accept_drop_item_type_none"))
+	    updateAcceptType(*ns,None);
+	else if (ns->name() == YSTRING("_yate_accept_drop_item_type_ask"))
+	    updateAcceptType(*ns,Ask);
+    }
+}
+
+// Reset data
+void QtListDrop::reset()
+{
+    m_acceptItemTypes.clear();
+    QtDrop::reset();
+}
+
+
+//
+// QtBusyWidget
+//
+const QString QtBusyWidget::s_busySuffix("_yate_busy_widget_generated");
+
+// Constructor
+QtBusyWidget::QtBusyWidget(QWidget* parent)
+    : QtCustomWidget(0,parent),
+    m_target(0), m_shown(false), m_delayMs(0), m_delayTimer(0),
+    m_movieLabel(0)
+{
+    if (parent)
+	setObjectName(parent->objectName() + s_busySuffix);
+    QWidget::hide();
+}
+
+// Initialize
+void QtBusyWidget::init(const String& ui, const NamedList& params, QWidget* target)
+{
+    hideBusy();
+    m_target = target;
+    m_movieLabel = 0;
+    unsigned int delay = 0;
+    QWidget* w = ui ? loadWidget(this,"",ui) : 0;
+    if (w) {
+	QtClient::setWidget(this,w);
+	int tmp = QtClient::getIntProperty(w,"_yate_busywidget_delay");
+	if (tmp > 0)
+	    delay = tmp;
+	QList<QWidget*> c = w->findChildren<QWidget*>();
+	for (int i = 0; i < c.size(); i++) {
+	    QLabel* l = qobject_cast<QLabel*>(c[i]);
+	    if (l) {
+		if (!m_movieLabel) {
+		    String file;
+		    QtClient::getProperty(l,"_yate_movie_file",file);
+		    if (file) {
+			l->setMovie(QtClient::loadMovie(file,l));
+			if (l->movie())
+			    m_movieLabel = l;
+		    }
+		}
+	    }
+	}
+    }
+    m_delayMs = params.getIntValue(YSTRING("_yate_busywidget_delay"),delay,0);
+}
+
+// Show the widget
+void QtBusyWidget::showBusy()
+{
+    if (m_shown)
+	return;
+    m_shown = true;
+    if (m_delayMs)
+	m_delayTimer = startTimer(m_delayMs);
+    if (!m_delayTimer)
+	internalShow();
+}
+
+// Hide the widget
+void QtBusyWidget::hideBusy()
+{
+    if (!m_shown)
+	return;
+    m_shown = false;
+    stopDelayTimer();
+    if (m_target)
+	m_target->removeEventFilter(this);
+    setContent(false);
+    lower();
+    hide();
+}
+
+// Filter wathed events
+bool QtBusyWidget::onChildEvent(QObject* watched, QEvent* event)
+{
+    if (m_target && m_target == watched) {
+	if (event->type() == QEvent::Resize)
+	    resize(m_target->size());
+    }
+    return false;
+}
+
+void QtBusyWidget::timerEvent(QTimerEvent* ev)
+{
+    if (m_delayTimer && ev->timerId() == m_delayTimer) {
+	stopDelayTimer();
+	internalShow();
+	return;
+    }
+    QtCustomWidget::timerEvent(ev);
+}
+
+// Show/hide busy content
+void QtBusyWidget::setContent(bool on)
+{
+    QMovie* movie = m_movieLabel ? m_movieLabel->movie() : 0;
+    if (!movie)
+	return;
+    if (on)
+	movie->start();
+    else
+	movie->stop();
+}
+
+void QtBusyWidget::internalShow()
+{
+    if (m_target) {
+	resize(m_target->size());
+	m_target->installEventFilter(this);
+    }
+    setContent(true);
+    raise();
+    show();
+}
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/clients/qt5/qt5client.h b/clients/qt5/qt5client.h
new file mode 100644
index 0000000..0e9f889
--- /dev/null
+++ b/clients/qt5/qt5client.h
@@ -0,0 +1,2115 @@
+/**
+ * qt5client.h
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * A Qt-5 based universal telephony client
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#ifndef __QT5CLIENT_H
+#define __QT5CLIENT_H
+
+#include <yatecbase.h>
+
+#ifdef _WINDOWS
+
+#ifdef LIBYQT5_EXPORTS
+#define YQT5_API __declspec(dllexport)
+#else
+#ifndef LIBYQT5_STATIC
+#define YQT5_API __declspec(dllimport)
+#endif
+#endif
+
+#endif /* _WINDOWS */
+
+#ifndef YQT5_API
+#define YQT5_API
+#endif
+
+#undef open
+#undef read
+#undef close
+#undef write
+#undef mkdir
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#define QT_NO_DEBUG
+#define QT_DLL
+#define QT_GUI_LIB
+#define QT_CORE_LIB
+#define QT_THREAD_SUPPORT
+
+#include <QtWidgets>
+#include <QSound>
+#include <QAudioDeviceInfo>
+
+namespace TelEngine {
+
+class QtRefObjectHolder;                 // A QObject holding a RefPointer
+class QtEventProxy;                      // Proxy to global QT events
+class QtUrlBuilder;                      // QUrl builder
+class QtClient;                          // The QT based client
+class QtDriver;                          // The QT based telephony driver
+class QtWindow;                          // A QT window
+class QtDialog;                          // A custom modal dialog
+class QtUIWidgetItemProps;               // Widget container item properties
+class QtUIWidget;                        // A widget container
+class QtCustomObject;                    // A custom QT object
+class QtCustomWidget;                    // A custom QT widget
+class QtTable;                           // A custom QT table widget
+class QtSound;                           // A QT client sound
+class QtDragAndDrop;                     // Base class for Drag&Drop operations
+class QtDrop;                            // Drop data holder
+class QtListDrop;                        // Drop data holder for widget list items
+class QtBusyWidget;                      // Busy widget to show over controls
+
+// Macro used to get a QT object's name
+// Can't use an inline function: the QByteArray object returned by toUtf8()
+//  would be destroyed on exit
+#define YQT_OBJECT_NAME(qobject) ((qobject) ? (qobject)->objectName().toUtf8().constData() : "")
+
+
+/**
+ * A QObject holding a RefPointer. Suitable to be set in QVariant
+ * @short A QObject holding a RefPointer
+ */
+class YQT5_API QtRefObjectHolder : public QObject
+{
+    Q_CLASSINFO("QtRefObjectHolder","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     */
+    inline QtRefObjectHolder()
+	{}
+
+    /**
+     * Constructor
+     * @param obj Object to set
+     */
+    inline QtRefObjectHolder(RefObject* obj)
+	: m_refObj(obj)
+	{}
+
+    /**
+     * Copy constructor
+     * @param other Source object
+     */
+    inline QtRefObjectHolder(const QtRefObjectHolder& other)
+	: m_refObj((RefObject*)other.m_refObj)
+	{}
+
+    /**
+     * Build a variant from RefObject
+     * @param obj Object to build from
+     * @param force True to build empty variant, false (default) to fail if obj is 0
+     * @return QVariant
+     */
+    static inline QVariant setVariant(RefObject* obj, bool force = false) {
+	    QtRefObjectHolder data(obj);
+	    if (data.m_refObj)
+		return QVariant::fromValue(data);
+	    return QVariant();
+	}
+
+    RefPointer<RefObject> m_refObj;
+};
+
+/**
+ * Proxy to global QT events
+ * @short A QT proxy class
+ */
+class YQT5_API QtEventProxy : public QObject, public GenObject
+{
+    YCLASS(QtEventProxy,GenObject)
+    Q_CLASSINFO("QtEventProxy","Yate")
+    Q_OBJECT
+
+public:
+    enum Type {
+	Timer,
+	AllHidden,
+    };
+
+    /**
+     * Constructor
+     * @param Event type
+     * @param pointer to QT application when needed
+     */
+    QtEventProxy(Type type, QApplication* app = 0);
+
+    /**
+     * Get a string representation of this object
+     * @return Object's name
+     */
+    virtual const String& toString() const
+	{ return m_name; }
+
+private slots:
+    void timerTick();                    // Idle timer
+    void allHidden();                    // All windows closed notification
+
+private:
+    String m_name;                       // Object name
+};
+
+/**
+ * This class holds data used to build an url
+ * @short QUrl builder
+ */
+class YQT5_API QtUrlBuilder : public QObject, public GenObject
+{
+    YCLASS(QtUrlBuilder,GenObject)
+    Q_CLASSINFO("QtUrlBuilder","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param parent Object parent
+     * @param format Format to use when building base URL
+     * @param queryParams Query params to add to URL
+     */
+    QtUrlBuilder(QObject* parent, const String& format, const String& queryParams);
+
+    /**
+     * Destructor
+     */
+    ~QtUrlBuilder();
+
+    /**
+     * Build URL
+     * @param params URL params
+     * @return QUrl object
+     */
+    virtual QUrl build(const NamedList& params) const;
+
+protected:
+    String m_format;
+    ObjList* m_queryParams;
+};
+
+class YQT5_API QtClient : public Client
+{
+    friend class QtWindow;
+public:
+    /**
+     * Generic position flags
+     */
+    enum QtClientPos {
+	PosNone   = 0,
+	PosLeft   = 0x01,
+	PosRight  = 0x02,
+	PosTop    = 0x04,
+	PosBottom = 0x08,
+	// Corners
+	CornerTopLeft = PosTop | PosLeft,
+	CornerTopRight = PosTop | PosRight,
+	CornerBottomLeft = PosBottom | PosLeft,
+	CornerBottomRight = PosBottom | PosRight,
+    };
+
+    /**
+     * Sorting
+     */
+    enum Sort {
+	SortNone = 0,
+	SortAsc,
+	SortDesc,
+    };
+
+    QtClient();
+    virtual ~QtClient();
+    virtual void run();
+    virtual void cleanup();
+    virtual void main();
+    virtual void lock();
+    virtual void unlock();
+    virtual void allHidden();
+    virtual bool createWindow(const String& name,
+	const String& alias = String::empty());
+    virtual bool action(Window* wnd, const String& name, NamedList* params = 0);
+    virtual void quit() {
+	    if (m_app)
+		m_app->quit();
+	    Engine::halt(0);
+	}
+
+    /**
+     * Open an URL (link)
+     * @param url The URL to open
+     * @return True on success
+     */
+    virtual bool openUrl(const String& url)
+	{ return QDesktopServices::openUrl(QUrl(setUtf8(url))); }
+
+    /**
+     * Show a file save/open dialog window. If the list of parameters contains an 'action'
+     *  parameter, an action will be raised when the dialog will be closed. The action's
+     *  parameter list pointer will be non 0 if the dialog was accepted and 0 if cancelled.
+     *  The list will contain one or more 'file' parameter(s) with selected file(s)
+     * @param parent Dialog window's parent
+     * @param params Dialog window's params. Parameters that can be specified include 'caption',
+     *  'dir', 'filters', 'selectedfilter', 'choosefile'
+     * @return True on success (the dialog was opened)
+     */
+    virtual bool chooseFile(Window* parent, NamedList& params);
+
+    /**
+     * Create a sound object. Append it to the global list
+     * @param name The name of sound object
+     * @param file The file to play (should contain the whole path and the file name)
+     * @param device Optional device used to play the file. Set to 0 to use the default one
+     * @return True on success, false if a sound with the given name already exists
+     */
+    virtual bool createSound(const char* name, const char* file, const char* device = 0);
+
+    /**
+     * Build a date/time string from UTC time
+     * @param dest Destination string
+     * @param secs Seconds since EPOCH
+     * @param format Format string used to build the destination
+     * @param utc True to build UTC time instead of local time
+     * @return True on success
+     */
+    virtual bool formatDateTime(String& dest, unsigned int secs, const char* format,
+	bool utc = false);
+
+    /**
+     * Build a date/time QT string from UTC time
+     * @param secs Seconds since EPOCH
+     * @param format Format string
+     * @param utc True to build UTC time instead of local time
+     * @return The formated string
+     */
+    static QString formatDateTime(unsigned int secs, const char* format,
+	bool utc = false);
+
+    /**
+     * Get an UTF8 representation of a QT string
+     * @param dest Destination string
+     * @param src Source QT string
+     */
+    static inline void getUtf8(String& dest, const QString& src)
+	{ dest = src.toUtf8().constData(); }
+
+    /**
+     * Get an UTF8 representation of a QT string and add it to a list of parameters
+     * @param dest Destination list
+     * @param param Parameter name/value
+     * @param src Source QT string
+     * @param setValue True to set the QT string as parameter value, false to set it
+     *  as parameter name
+     */
+    static inline void getUtf8(NamedList& dest, const char* param,
+	const QString& src, bool setValue = true) {
+	    if (setValue)
+		dest.addParam(param,src.toUtf8().constData());
+	    else
+		dest.addParam(src.toUtf8().constData(),param);
+	}
+
+    /**
+     * Get an UTF8 representation of a QT string and add it to a list of parameters if not empty
+     * @param dest Destination list
+     * @param param Parameter name/value
+     * @param src Source QT string
+     * @param setValue True to set the QT string as parameter value, false to set it
+     *  as parameter name
+     */
+    static inline void safeGetUtf8(NamedList& dest, const char* param,
+	const QString& src, bool setValue = true) {
+	    if (src.length() > 0)
+		getUtf8(dest,param,src,setValue);
+	}
+
+    /**
+     * Set a QT string from an UTF8 char buffer
+     * @param str The buffer
+     * @return A QT string filled with the buffer
+     */
+    static inline QString setUtf8(const char* str)
+	{ return QString::fromUtf8(TelEngine::c_safe(str)); }
+
+    /**
+     * Retrieve an object's QtWindow parent
+     * @param obj The object
+     * @return QtWindow pointer or 0
+     */
+    static QtWindow* parentWindow(QObject* obj);
+
+    /**
+     * Set an object's property into parent window's section. Clear it on failure
+     * @param obj The object
+     * @param prop Property to save
+     * @param owner Optional window owning the object
+     * @return True on success
+     */
+    static bool saveProperty(QObject* obj, const String& prop, QtWindow* owner = 0);
+
+    /**
+     * Set or an object's property
+     * @param obj The object
+     * @param name Property's name
+     * @param value Property's value
+     * @return False if the property doesn't exist or has a type not supported by String
+     */
+    static bool setProperty(QObject* obj, const char* name, const String& value);
+
+    /**
+     * Get an object's property
+     * @param obj The object
+     * @param name Property's name
+     * @param value Property's value
+     * @return False if the property doesn't exist or has a type not supported by String
+     */
+    static bool getProperty(QObject* obj, const char* name, String& value);
+
+    /**
+     * Get an object's property and return its boolean conversion
+     * @param obj The object
+     * @param name Property name
+     * @param defVal Default value to return if the property is not found or has
+     *  invalid boolean value
+     * @return The boolean conversion of the property or given default value
+     */
+    static inline bool getBoolProperty(QObject* obj, const char* name,
+	bool defVal = false) {
+	    String tmp;
+	    if (!getProperty(obj,name,tmp))
+		return defVal;
+	    return tmp.toBoolean(defVal);
+	}
+
+    /**
+     * Get an object's property and return its integer conversion
+     * @param obj The object
+     * @param name Property name
+     * @param defVal Default value to return if the property is not found or has
+     *  invalid integer value
+     * @return The integer conversion of the property or given default value
+     */
+    static inline int getIntProperty(QObject* obj, const char* name,
+	int defVal = 0) {
+	    String tmp;
+	    if (!getProperty(obj,name,tmp))
+		return defVal;
+	    return tmp.toInteger(defVal);
+	}
+
+    /**
+     * Associate actions to buttons with '_yate_setaction' property set
+     * @param parent Parent widget
+     */
+    static void setAction(QWidget* parent);
+
+    /**
+     * Check if an object has '_yate_noautoconnect' boolean property set to true
+     * @param obj The object
+     * @return True if the object don't have the property or its value is not a boolean 'true'
+     */
+    static inline bool autoConnect(QObject* obj)
+	{ return !getBoolProperty(obj,"_yate_noautoconnect"); }
+
+    /**
+     * Retrieve an object's identity from '_yate_identity' property or object name
+     * @param obj The object
+     * @param ident String to be filled with object identity
+     */
+    static inline void getIdentity(QObject* obj, String& ident) {
+	    if (obj && !(getProperty(obj,"_yate_identity",ident) && ident))
+		getUtf8(ident,obj->objectName());
+	}
+
+    /**
+     * Copy a string list to a list of parameters
+     * @param dest Destination list
+     * @param src Source string list
+     */
+    static void copyParams(NamedList& dest, const QStringList& src);
+
+    /**
+     * Copy a list of parameters to string list
+     * @param dest Destination list
+     * @param src Source list
+     */
+    static void copyParams(QStringList& dest, const NamedList& src);
+
+    /**
+     * Build QObject properties from list
+     * @param obj The object
+     * @param props Comma separated list of properties. Format: name=type
+     */
+    static void buildProps(QObject* obj, const String& props);
+
+    /**
+     * Build custom UI widgets from frames owned by a widget
+     * @param parent Parent widget
+     */
+    static void buildFrameUiWidgets(QWidget* parent);
+
+    /**
+     * Build a menu object from a list of parameters.
+     * Each menu item is indicated by a parameter starting with 'item:".
+     * item:menu_name=Menu Text will create a menu item named 'menu_name' with
+     *  'Menu Text' as display name.
+     * If the item parameter is a NamedPointer a submenu will be created.
+     * Menu actions properties can be set from parameters with format:
+     *  property:object_name:property_name=value
+     * @param params The menu parameters. The list name is the object name
+     * @param text The menu display text
+     * @param receiver Object receiving menu actions
+     * @param actionSlot The receiver's slot for menu signal triggered()
+     * @param toggleSlot The receiver's slot for menu signal toggled()
+     * @param aboutToShowSlot The receiver's slot for menu signal aboutToShow()
+     * @param parent Optional widget parent
+     * @return QMenu pointer or 0 if failed to build it
+     */
+    static QMenu* buildMenu(const NamedList& params, const char* text, QObject* receiver,
+	 const char* actionSlot, const char* toggleSlot, QWidget* parent = 0,
+	 const char* aboutToShowSlot = 0);
+
+    /**
+     * Insert a widget into another one replacing any existing children
+     * @param parent Parent widget
+     * @param child Widget to insert into parent
+     * @return True on success
+     */
+    static bool setWidget(QWidget* parent, QWidget* child);
+
+    /**
+     * Set an object's image property from image file
+     * @param obj The object
+     * @param img Image file to load
+     * @param fit True to adjust the image to target size if applicable (like
+     *  a QLabel without scaled contents)
+     * @return True on success
+     */
+    static bool setImage(QObject* obj, const String& img, bool fit = true);
+
+    /**
+     * Set an object's image property from raw data
+     * @param obj The object
+     * @param data The image data
+     * @param format Image format if known
+     * @param fit True to adjust the image to target size if applicable (like
+     *  a QLabel without scaled contents)
+     * @return True on success
+     */
+    static bool setImage(QObject* obj, const DataBlock& data,
+	const String& format = String::empty(), bool fit = true);
+
+    /**
+     * Set an object's image property from QPixmap
+     * @param obj The object
+     * @param img The image
+     * @param fit True to adjust the image to target size if applicable (like
+     *  a QLabel without scaled contents)
+     * @return True on success
+     */
+    static bool setImage(QObject* obj, const QPixmap& img, bool fit = true);
+
+    /**
+     * Update a toggable object's image from properties
+     * @param obj The object
+     */
+    static void updateToggleImage(QObject* obj);
+
+    /**
+     * Update an object's image from properties on mouse events
+     * @param obj The object
+     * @param inOut True for mouse enter/leave, false for mouse press/release events
+     * @param on True for mouse enter/press, false for mouse leave/release
+     */
+    static void updateImageFromMouse(QObject* obj, bool inOut, bool on);
+
+    /**
+     * Filter key press events. Retrieve an action associated with the key.
+     * Check if the object is allowed to process the key
+     * @param obj The object
+     * @param event QKeyEvent event to process
+     * @param action Found action name
+     * @param filter Filter key or let the object process it
+     * @param parent Optional parent to look for the action and check its state
+     * @return True if key and modifiers were matched against object properties
+     *  (the action parameter may be empty if true is returned and the action is disabled)
+     */
+    static bool filterKeyEvent(QObject* obj, QKeyEvent* event, String& action,
+	bool& filter, QObject* parent = 0);
+
+    /**
+     * Wrapper for QObject::connect() used to put a debug mesage on failure
+     */
+    static bool connectObjects(QObject* sender, const char* signal,
+	 QObject* receiver, const char* slot);
+
+    /**
+     * Safely delete a QObject. Disconnect it, reset its parent, calls its deleteLater() method
+     * @param obj The object to delete
+     */
+    static void deleteLater(QObject* obj);
+
+    /**
+     * Retrieve unavailable space position (if any) in the screen containing a given widget.
+     * The positions are set using the difference between screen geometry and available geometry
+     * @param w The widget
+     * @param pos Unavailable screen space if any (QtClientPos combination)
+     * @return Valid pointer to global desktop widget on success
+     */
+    static QDesktopWidget* getScreenUnavailPos(QWidget* w, int& pos);
+
+    /**
+     * Move a window to a specified position
+     * @param w The window to move
+     * @param pos A corner position
+     */
+    static void moveWindow(QtWindow* w, int pos);
+
+    /**
+     * Append a non empty string to a list if not already there
+     * @param list Destination list
+     * @param str The string to append
+     */
+    static inline void addStrUnique(QStringList& list, QString str) {
+	    if (str.length() > 0 && !list.contains(str))
+		list.append(str);
+	}
+
+    /**
+     * Append non empty strings to a list if not already there
+     * @param list Destination list
+     * @param strs Source list
+     */
+    static void addStrListUnique(QStringList& list, QStringList src) {
+	    for (int i = 0; i < src.size(); i++)
+		addStrUnique(list,src[i]);
+	}
+
+    /**
+     * Build a QStringList from a list of strings
+     * @param str The string
+     * @param sep The separator
+     * @param emptyOk True to process empty string items
+     * @return QStringList
+     */
+    static QStringList str2list(const String& str, char sep = ',', bool emptyOk = true);
+
+    /**
+     * Split an integer string list
+     * @param str The string
+     * @param defVal Default value for failed items
+     * @param emptyOk True to process empty string items
+     * @return A list of integers
+     */
+    static QList<int> str2IntList(const String& str, int defVal = 0, bool emptyOk = true);
+
+    /**
+     * Build a comma separated list of integers
+     * @param str The destination string
+     * @param list The source integer list
+     */
+    static void intList2str(String& str, QList<int> list);
+
+    /**
+     * Get sorting from string
+     * @param str Sorting name
+     * @param defVal Default value to return if invalid
+     * @return Sorting as QtClientSort enumeration
+     */
+    static int str2sort(const String& str, int defVal = SortNone);
+
+    /**
+     * Apply a comma separated list of window flags to a widget
+     * @param wid The widget
+     * @param str The list of flags
+     */
+    static void applyWindowFlags(QWidget* w, const String& value);
+
+    /**
+     * Build a QT Alignment mask from a comma separated list of flags
+     * @param flags The flags list
+     * @param initVal Initial value for the returned mask
+     * @return QT Alignment mask
+     */
+    static int str2align(const String& flags, int initVal = 0);
+
+    /**
+     * Retrieve QT selection mode from a string value
+     * @param value String value
+     * @param defVal Default value to return if invalid
+     * @return QAbstractItemView selection mode
+     */
+    static QAbstractItemView::SelectionMode str2selmode(const String& value,
+	QAbstractItemView::SelectionMode defVal = QAbstractItemView::SingleSelection);
+
+    /**
+     * Retrieve QT edit triggers from a string value
+     * @param value String value
+     * @param defVal Default value to set if invalid
+     * @return QAbstractItemView edit triggers mask
+     */
+    static QAbstractItemView::EditTriggers str2editTriggers(const String& value,
+	QAbstractItemView::EditTrigger defVal = QAbstractItemView::NoEditTriggers);
+
+    /**
+     * Send an event to an object's child. The event must be already accepted
+     * The event's accepted flag is set to false before sending it and restored on failure
+     *  to avoid looping in event filters
+     * @param e The event to send
+     * @param parent The parent object
+     * @param name Child name
+     * @return True if the event was accepted by the target
+     */
+    static bool sendEvent(QEvent& e, QObject* parent, const QString& name);
+
+    /**
+     * Retrieve a pixmap from global application cache.
+     * Load and add it to the cache if not found
+     * @param pixmap Destination pixmap to set
+     * @param file File name to retrieve or load
+     * @return True on success, false if failed to load
+     */
+    static bool getPixmapFromCache(QPixmap& pixmap, const QString& file);
+
+    /**
+     * Retrieve a pixmap from global application cache. Add skin path to file name
+     * Load and add it to the cache if not found
+     * @param pixmap Destination pixmap to set
+     * @param file File name to retrieve or load
+     * @return True on success, false if failed to load
+     */
+    static inline bool getSkinPathPixmapFromCache(QPixmap& pixmap, const String& file) {
+	    if (!file)
+		return false;
+	    return getPixmapFromCache(pixmap,setUtf8(s_skinPath + file));
+	}
+
+    /**
+     * Update application style sheet from config
+     */
+    static void updateAppStyleSheet();
+
+    /**
+     * Set widget attributes from list
+     * @param w The widget
+     * @param attrs Comma separated list of attributes.
+     *  To reset an attribute an item must start with '!'
+     */
+    static void setWidgetAttributes(QWidget* w, const String& attrs);
+
+    /**
+     * Set a widget's height
+     * @param w The widget
+     * @param height Height value. If boolean, Increase(true)/decrease(false) widget
+     *  height from _yate_height_delta property. Set widget height otherwise
+     */
+    static void setWidgetHeight(QWidget* w, const String& height);
+
+    /**
+     * Build a busy widget child for a given widget
+     * @param parent Busy widget parent
+     * @param target Busy widget target
+     * @param ui UI file
+     * @param params Busy widget parameters
+     * @return Busy widget pointer or 0 on failure
+     */
+    static QWidget* buildBusy(QWidget* parent, QWidget* target, const String& ui,
+	const NamedList& params);
+
+    /**
+     * Load a movie
+     * @param file Movie file
+     * @param parent Movie parent
+     * @param path File path, 0 to use client skin path
+     * @return QMovie pointer or 0
+     */
+    static QMovie* loadMovie(const char* file, QObject* parent, const char* path = 0);
+
+    /**
+     * Fill a list from URL parameters
+     * @param url URL to fill
+     * @param list Destination list
+     * @param path Optional URL path, user provided URL's path if 0
+     * @param pathToList True to set path in list name, false to set as parameter
+     */
+    static void fillUrlParams(const QUrl& url, NamedList& list, QString* path = 0,
+	bool pathToList = true);
+
+    /**
+     * Dump MIME data for debug purposes
+     * @param buf Destination buffer
+     * @param m MIME data to dump
+     */
+    static void dumpMime(String& buf, const QMimeData* m);
+
+protected:
+    virtual void loadWindows(const char* file = 0);
+    virtual bool isUIThread();
+private:
+    QApplication* m_app;
+    ObjList m_events;                    // Proxy events objects
+};
+
+class YQT5_API QtDriver : public ClientDriver
+{
+public:
+    QtDriver(bool buildClientThread = true);
+    virtual ~QtDriver();
+    virtual void initialize();
+private:
+    bool m_init;                         // Already initialized flag
+    bool m_clientThread;                 // does the client need a thread to run on?
+};
+
+class YQT5_API QtWindow : public QWidget, public Window
+{
+    YCLASS(QtWindow, Window)
+    Q_CLASSINFO("QtWindow", "Yate")
+    Q_OBJECT
+
+    friend class QtClient;
+public:
+    QtWindow();
+    QtWindow(const char* name, const char* description, const char* alias, QtWindow* parent = 0);
+    virtual ~QtWindow();
+
+    virtual void title(const String& text);
+    virtual void context(const String& text);
+    virtual bool setParams(const NamedList& params);
+    virtual void setOver(const Window* parent);
+    virtual bool hasElement(const String& name);
+    virtual bool setActive(const String& name, bool active);
+    virtual bool setFocus(const String& name, bool select = false);
+    virtual bool setShow(const String& name, bool visible);
+
+    /**
+     * Set the displayed text of an element in the window
+     * @param name Name of the element
+     * @param text Text value to set in the element
+     * @param richText True if the text contains format data
+     * @return True if the operation was successfull
+     */
+    virtual bool setText(const String& name, const String& text,
+	bool richText = false);
+
+    virtual bool setCheck(const String& name, bool checked);
+    virtual bool setSelect(const String& name, const String& item);
+    virtual bool setUrgent(const String& name, bool urgent);
+
+    virtual bool hasOption(const String& name, const String& item);
+    virtual bool addOption(const String& name, const String& item, bool atStart = false, const String& text = String::empty());
+    virtual bool delOption(const String& name, const String& item);
+    virtual bool getOptions(const String& name, NamedList* items);
+
+    /**
+     * Append or insert text lines to a widget
+     * @param name The name of the widget
+     * @param lines List containing the lines
+     * @param max The maximum number of lines allowed to be displayed. Set to 0 to ignore
+     * @param atStart True to insert, false to append
+     * @return True on success
+     */
+    virtual bool addLines(const String& name, const NamedList* lines, unsigned int max,
+	bool atStart = false);
+
+    virtual bool addTableRow(const String& name, const String& item, const NamedList* data = 0, bool atStart = false);
+
+    virtual bool setMultipleRows(const String& name, const NamedList& data, const String& prefix);
+
+    /**
+     * Insert a row into a table owned by this window
+     * @param name Name of the element
+     * @param item Name of the item to insert
+     * @param before Name of the item to insert before
+     * @param data Table's columns to set
+     * @return True if the operation was successfull
+     */
+    virtual bool insertTableRow(const String& name, const String& item,
+	const String& before, const NamedList* data = 0);
+
+    virtual bool delTableRow(const String& name, const String& item);
+    virtual bool setTableRow(const String& name, const String& item, const NamedList* data);
+    virtual bool getTableRow(const String& name, const String& item, NamedList* data = 0);
+    virtual bool clearTable(const String& name);
+
+    /**
+     * Set a table row or add a new one if not found
+     * @param name Name of the element
+     * @param item Table item to set/add
+     * @param data Optional list of parameters used to set row data
+     * @param atStart True to add item at start, false to add them to the end
+     * @return True if the operation was successfull
+     */
+    virtual bool updateTableRow(const String& name, const String& item,
+	const NamedList* data = 0, bool atStart = false);
+
+    /**
+     * Add or set one or more table row(s). Screen update is locked while changing the table.
+     * Each data list element is a NamedPointer carrying a NamedList with item parameters.
+     * The name of an element is the item to update.
+     * Set element's value to boolean value 'true' to add a new item if not found
+     *  or 'false' to set an existing one. Set it to empty string to delete the item
+     * @param name Name of the table
+     * @param data The list of items to add/set/delete
+     * @param atStart True to add new items at start, false to add them to the end
+     * @return True if the operation was successfull
+     */
+    virtual bool updateTableRows(const String& name, const NamedList* data,
+	bool atStart = false);
+
+    /**
+     * Show or hide control busy state
+     * @param name Name of the element
+     * @param on True to show, false to hide
+     * @return True if all the operations were successfull
+     */
+    bool setBusy(const String& name, bool on);
+
+    /**
+     * Get an element's text
+     * @param name Name of the element
+     * @param text The destination string
+     * @param richText True to get the element's roch text if supported.
+     * @return True if the operation was successfull
+     */
+    virtual bool getText(const String& name, String& text, bool richText = false);
+
+    virtual bool getCheck(const String& name, bool& checked);
+    virtual bool getSelect(const String& name, String& item);
+
+    /**
+     * Retrieve an element's multiple selection
+     * @param name Name of the element
+     * @param items List to be to filled with selection's contents
+     * @return True if the operation was successfull
+     */
+    virtual bool getSelect(const String& name, NamedList& items);
+
+    /**
+     * Build a menu from a list of parameters.
+     * See Client::buildMenu() for more info
+     * @param params Menu build parameters
+     * @return True on success
+     */
+    virtual bool buildMenu(const NamedList& params);
+
+    /**
+     * Remove a menu from UI and memory
+     * See Client::removeMenu() for more info
+     * @param params Menu remove parameters
+     * @return True on success
+     */
+    virtual bool removeMenu(const NamedList& params);
+
+    /**
+     * Set an element's image
+     * @param name Name of the element
+     * @param image Image to set
+     * @param fit Fit image in element (defaults to false)
+     * @return True on success
+     */
+    virtual bool setImage(const String& name, const String& image, bool fit = false);
+
+    /**
+     * Set a property for this window or for a widget owned by it
+     * @param name Name of the element
+     * @param item Property's name
+     * @param value Property's value
+     * @return False if the property doesn't exist or has a type not supported by String
+     */
+    virtual bool setProperty(const String& name, const String& item, const String& value);
+
+    /**
+     * Get a property from this window or from a widget owned by it
+     * @param name Name of the element
+     * @param item Property's name
+     * @param value Property's value
+     * @return False if the property doesn't exist or has a type not supported by String
+     */
+    virtual bool getProperty(const String& name, const String& item, String& value);
+
+    virtual void show();
+    virtual void hide();
+    virtual void size(int width, int height);
+    virtual void move(int x, int y);
+    virtual void moveRel(int dx, int dy);
+    virtual bool related(const Window* wnd) const;
+    virtual void menu(int x, int y) ;
+
+    /**
+     * Create a modal dialog
+     * @param name Dialog name (resource config section)
+     * @param title Dialog title
+     * @param alias Optional dialog alias (used as dialog object name)
+     * @param params Optional dialog parameters
+     * @return True on success
+     */
+    virtual bool createDialog(const String& name, const String& title,
+	const String& alias = String::empty(), const NamedList* params = 0);
+
+    /**
+     * Destroy a modal dialog
+     * @param name Dialog name
+     * @return True on success
+     */
+    virtual bool closeDialog(const String& name);
+
+    /**
+     * Connect an abstract button to window slots
+     * @param b The button to connect
+     * @return True on success
+     */
+    inline bool connectButton(QAbstractButton* b) {
+	    if (!b)
+		return false;
+	    if (!b->isCheckable())
+		return QtClient::connectObjects(b,SIGNAL(clicked()),this,SLOT(action()));
+	    return QtClient::connectObjects(b,SIGNAL(toggled(bool)),this,SLOT(toggled(bool)));
+	}
+
+    /**
+     * Connect an object's text changed signal to window's slot
+     * @param obj The object to connect
+     * @return True on success
+     */
+    bool connectTextChanged(QObject* obj);
+
+    /**
+     * Notify text changed to the client
+     * @param obj The object sending the notification
+     * @param text Optional object text
+     */
+    void notifyTextChanged(QObject* obj, const QString& text = QString());
+
+    /**
+     * Load a widget from file
+     * @param fileName UI filename to load
+     * @param parent The widget holding the loaded widget's contents
+     * @param uiName The loaded widget's name (used for debug)
+     * @param path Optional fileName path. Set to 0 to use the default one
+     * @return QWidget pointer or 0 on failure
+     */
+    static QWidget* loadUI(const char* fileName, QWidget* parent,
+	const char* uiName, const char* path = 0);
+
+    /**
+     * Clear the UI cache
+     * @param fileName Optional UI filename to clear. Clear all if 0
+     */
+    static void clearUICache(const char* fileName = 0);
+
+    /**
+     * Retrieve the parent window
+     * @return QtWindow pointer or 0
+     */
+    inline QtWindow* parentWindow() const
+	{ return qobject_cast<QtWindow*>(parentWidget() ? parentWidget()->window() : 0); }
+
+    /**
+     * Check if this window is shown normal (not maximixed, minimized or full screen)
+     * @return True if the window is not maximixed, minimized or full screen
+     */
+    inline bool isShownNormal() const
+	{ return !(isMaximized() || isMinimized() || isFullScreen()); }
+
+protected:
+    // Notify client on selection changes
+    inline bool select(const String& name, const String& item,
+	const String& text = String::empty()) {
+	    if (!QtClient::self() || QtClient::changing())
+		return false;
+	    return QtClient::self()->select(this,name,item,text);
+	}
+
+    // Filter events to apply dynamic properties changes
+    bool eventFilter(QObject* watched, QEvent* event);
+    // Handle key pressed events
+    void keyPressEvent(QKeyEvent* event);
+
+public slots:
+    void setVisible(bool visible);
+    // A widget was double clicked
+    void doubleClick();
+    // A widget's selection changed
+    void selectionChanged();
+    // Clicked actions
+    void action();
+    // Toggled actions
+    void toggled(bool);
+    // System tray actions
+    void sysTrayIconAction(QSystemTrayIcon::ActivationReason reason);
+    // Choose file window was accepted
+    void chooseFileAccepted();
+    // Choose file window was cancelled
+    void chooseFileRejected();
+    // Text changed slot. Notify the client
+    void textChanged(const QString& text)
+	{ notifyTextChanged(sender(),text); }
+    void textChanged()
+	{ notifyTextChanged(sender()); }
+
+private slots:
+    void openUrl(const QString& link);
+
+protected:
+    virtual void doPopulate();
+    virtual void doInit();
+    // Methods inherited from QWidget
+    virtual void moveEvent(QMoveEvent* event);
+    virtual void resizeEvent(QResizeEvent* event);
+    virtual bool event(QEvent* ev);
+    virtual void mousePressEvent(QMouseEvent* event);
+    virtual void mouseReleaseEvent(QMouseEvent* event);
+    virtual void mouseMoveEvent(QMouseEvent* event);
+    virtual void closeEvent(QCloseEvent* event);
+    virtual void changeEvent(QEvent* event);
+    virtual void contextMenuEvent(QContextMenuEvent* ev) {
+	    if (handleContextMenuEvent(ev,wndWidget()))
+		ev->accept();
+	}
+    // Get the widget with this window's content
+    inline QWidget* wndWidget()
+	{ return findChild<QWidget*>(m_widget); }
+    // Handle context menu events. Return true if handled
+    bool handleContextMenuEvent(QContextMenuEvent* event, QObject* obj);
+
+    String m_description;
+    String m_oldId;                      // Old id used to retreive the config section in .rc
+    int m_x;
+    int m_y;
+    int m_width;                         // Client area width
+    int m_height;                        // Client area height
+    bool m_maximized;
+    bool m_mainWindow;                   // Main window flag: close app when this window is closed
+    QString m_widget;                    // The widget with window's content
+    int m_moving;                        // Flag used to move the window on mouse move event
+    QPoint m_movePos;                    // Old position used when moving the window
+};
+
+/**
+ * This class encapsulates a custom modal dialog window.
+ * A dialog context can be set in '_yate_context' property
+ * Actions triggered by dialogs have the following format: dialog:dialog_name:action_name.
+ * The dialog will delete itself if an action is handled
+ * @short A custom modal dialog
+ */
+class YQT5_API QtDialog : public QDialog
+{
+    Q_CLASSINFO("QtDialog","Yate")
+    Q_OBJECT
+    Q_PROPERTY(QString _yate_context READ context WRITE setContext(QString))
+public:
+    /**
+     * Constructor
+     * @param parent Parent widget
+     */
+    inline QtDialog(QWidget* parent)
+	: QDialog(parent), m_closable(true)
+	{}
+
+    /**
+     * Destructor. Notify the client if not exiting
+     */
+    virtual ~QtDialog();
+
+    /**
+     * Retrieve the parent window
+     * @return QtWindow pointer or 0
+     */
+    inline QtWindow* parentWindow() const
+	{ return qobject_cast<QtWindow*>(parentWidget() ? parentWidget()->window() : 0); }
+
+    /**
+     * Initialize dialog. Load the widget.
+     * Connect non checkable actions to own slot.
+     * Connect checkable actions/buttons to parent window's slot
+     * Display the dialog on success
+     * @param name Object and config section name
+     * @param title Window title
+     * @param alias Object name to set if not empty
+     * @param params Optional parent window parameters
+     * @return True on success
+     */
+    bool show(const String& name, const String& title, const String& alias,
+	const NamedList* params);
+
+    /**
+     * Retrieve the context property
+     * @return The dialog context
+     */
+    QString context()
+	{ return m_context; }
+
+    /**
+     * Set the dialog context
+     * @param c The new dialog context
+     */
+    void setContext(QString c)
+	{ m_context = c; }
+
+    /**
+     * Build an action's name
+     * @param buf Destination buffer
+     * @param action Action name
+     * @return The destination string
+     */
+    inline String& buildActionName(String& buf, const String& action) {
+	    buf = String("dialog:") + YQT_OBJECT_NAME(this) + ":" + action;
+	    return buf;
+	}
+
+protected slots:
+    // Notify client
+    void action();
+
+protected:
+    // Destroy the dialog
+    virtual void closeEvent(QCloseEvent* event);
+    // Destroy the dialog
+    virtual void reject();
+
+    String m_notifyOnClose;              // Action to notify when closed
+    QString m_context;                   // Dialog context
+    bool m_closable;                     // Allow the dialog to be closed by the user
+};
+
+/**
+ * This class holds data about a widget container item
+ * @short Widget container item properties
+ */
+class QtUIWidgetItemProps : public String
+{
+public:
+    /**
+     * Constructor
+     * @param type Item type
+     */
+    explicit inline QtUIWidgetItemProps(const String& type)
+	: String(type), m_acceptDrop(0)
+	{}
+
+    String m_ui;                         // Item UI file
+    String m_styleSheet;                 // Item style sheet when not selected
+    String m_selStyleSheet;              // Item selected style
+    int m_acceptDrop;                    // Accept drop
+};
+
+/**
+ * This class holds a basic widget container with functions to rename children
+ * @short A widget container
+ */
+class YQT5_API QtUIWidget : public UIWidget
+{
+    YCLASS(QtUIWidget,UIWidget)
+public:
+    /**
+     * Constructor
+     * @param name Object name
+     * @param params Object parameters
+     * @param parent Optional parent
+     */
+    inline QtUIWidget(const char* name)
+	: UIWidget(name),
+	m_wndEvHooked(false)
+	{}
+
+    /**
+     * Build a child name from this one
+     * @param buf Destination buffer
+     * @param item Child name
+     * @return The destination buffer
+     */
+    inline String& buildChildName(String& buf, const String& item)
+	{ return buildChildName(buf,name(),item); }
+
+    /**
+     * Build a container QString child name
+     * @param item Child name
+     * @return QString child name
+     */
+    inline QString buildQChildName(const String& item)
+	{ return buildQChildName(name(),item); }
+
+    /**
+     * Retrieve item type definition
+     * @param type Item type name
+     * @return QtUIWidgetItemProps pointer or 0
+     */
+    inline QtUIWidgetItemProps* getItemProps(const String& type) const {
+	    ObjList* o = m_itemProps.find(type);
+	    return o ? static_cast<QtUIWidgetItemProps*>(o->get()) : 0;
+	}
+
+    /**
+     * Retrieve item type definition from [type:]value. Create it if not found
+     * @param in Input string
+     * @param value Item property value
+     * @return QtUIWidgetItemProps pointer or 0
+     */
+    virtual QtUIWidgetItemProps* getItemProps(QString& in, String& value);
+
+    /**
+     * Retrieve the list of properties to save
+     * @return The list of properties to save
+     */
+    QStringList saveProps()
+	{ return m_saveProps; }
+
+    /**
+     * Set the list of properties to save
+     * @param list The new list of properties to save
+     */
+    void setSaveProps(QStringList list) {
+	    if (list.size() != 1)
+		m_saveProps = list;
+	    else
+		m_saveProps = list[0].split(QChar(','),Qt::SkipEmptyParts);
+	}
+
+    /**
+     * Retrieve a QObject descendent of this object
+     * @return QObject pointer or 0
+     */
+    virtual QObject* getQObject()
+	{ return 0; }
+
+    /**
+     * Retrieve the window owning this object
+     * @return QtWindow pointer or 0
+     */
+    virtual QtWindow* getWindow()
+	{ return QtClient::parentWindow(getQObject()); }
+
+    /**
+     * Set widget's parameters.
+     * Handle an 'applyall' parameter carrying a NamedList to apply to all items
+     * @param params List of parameters
+     * @return True if all parameters could be set
+     */
+    virtual bool setParams(const NamedList& params);
+
+    /**
+     * Retrieve a QObject list containing container items
+     * @return The list of container items
+     */
+    virtual QList<QObject*> getContainerItems()
+	{ return QList<QObject*>(); }
+
+    /**
+     * Find an item widget by id
+     * @param id Item id
+     * @return QWidget pointer or 0
+     */
+    virtual QWidget* findItem(const String& id);
+
+    /**
+     * Apply a list of parameters to all container items
+     * @return The list of parameters to apply
+     */
+    virtual void applyAllParams(const NamedList& params);
+
+    /**
+     * Retrieve the object identity from '_yate_identity' property or name
+     * Retrieve the object item from '_yate_widgetlistitem' property.
+     * Set 'identity' to object_identity[:item_name]
+     * @param obj The object
+     * @param identiy Destination buffer
+     */
+    virtual void getIdentity(QObject* obj, String& identity);
+
+    /**
+     * Update an item object and children from a list a parameters
+     * @param parent Parent object
+     * @param params The list of parameters
+     * @return True on success
+     */
+    virtual bool setParams(QObject* parent, const NamedList& params);
+
+    /**
+     * Get an item object's parameters
+     * @param parent The object
+     * @param params Parameter list
+     * @return True on success
+     */
+    virtual bool getParams(QObject* parent, NamedList& params);
+
+    /**
+     * Show or hide control busy state
+     * @param on True to show, false to hide
+     * @return True if all the operations were successfull
+     */
+    virtual bool setBusy(bool on);
+
+    /**
+     * Retrieve object slots
+     * @param actionSlot Action (triggerred) slot
+     * @param toggleSlot Toggled slot
+     * @param selectSlot Selection change slot
+     */
+    virtual void getSlots(String& actionSlot, String& toggleSlot, String& selectSlot) {
+	    actionSlot = SLOT(itemChildAction());
+	    toggleSlot = SLOT(itemChildToggle(bool));
+	    selectSlot = SLOT(itemChildSelect());
+	}
+
+    /**
+     * Select an item by its index
+     * @param index Item index to select
+     * @return True on success
+     */
+    virtual bool setSelectIndex(int index)
+	{ return false; }
+
+    /**
+     * Retrieve the 0 based index of the current item
+     * @return The index of the current item (-1 on error or container empty)
+     */
+    virtual int currentItemIndex()
+	{ return -1; }
+
+    /**
+     * Retrieve the number of items in container
+     * @return The number of items in container (-1 on error)
+     */
+    virtual int itemCount()
+	{ return -1; }
+
+    /**
+     * Apply properties for QAbstractItemView descendents
+     * @param params List of parameters
+     * @param defVal Default value to set if not found or invalid
+     */
+    virtual void applyItemViewProps(const NamedList& params);
+
+    /**
+     * Begin item edit. The default behaviour start edit for QAbstractItemView descendants
+     * @param item Item to edit
+     * @param what Optional sub-item
+     * @return True on success
+     */
+    virtual bool beginEdit(const String& item, const String* what = 0);
+
+    /**
+     * Retrieve model index for a given item
+     * @param item Item to edit
+     * @param what Optional sub-item
+     * @return Model index for the item, can be invalid
+     */
+     virtual QModelIndex modelIndex(const String& item, const String* what = 0)
+	{ return QModelIndex(); }
+
+    /**
+     * Build a child's widget menu. Connect actions to container slots
+     * @param w The widget
+     * @param params Menu params
+     * @param child Optional widget child target
+     * @param set True to set the menu, false to build it and just return it
+     * @return QMenu pointer or 0
+     */
+    QMenu* buildWidgetItemMenu(QWidget* w, const NamedList* params,
+	const String& child = String::empty(), bool set = true);
+
+    /**
+     * Build a container child name
+     * @param buf Destination buffer
+     * @param name Container widget name
+     * @param item Child name
+     * @return The destination buffer
+     */
+    static inline String& buildChildName(String& buf, const String& name,
+	const String& item) {
+	    buf = name + "_" + item;
+	    return buf;
+	}
+
+    /**
+     * Build a container child name
+     * @param name Container widget name
+     * @param item Child name
+     * @return QString child name
+     */
+    static inline QString buildQChildName(const QString& name, const QString& item)
+	{ return name + "_" + item; }
+
+    /**
+     * Build a container child name from parent property value
+     * @param dest Destination string
+     * @param parent Pointer to parent object
+     * @param prop Property name
+     * @return True on success
+     */
+    static bool buildQChildNameProp(QString& dest, QObject* parent, const char* prop);
+
+    /**
+     * Build a container QString child name
+     * @param name Container widget name
+     * @param item Child name
+     * @return QString child name
+     */
+    static inline QString buildQChildName(const String& name, const String& item) {
+	    String buf;
+	    return QtClient::setUtf8(buildChildName(buf,name,item));
+	}
+
+    /**
+     * Set the list item id property to a list item object
+     * @param obj The object
+     * @param item Item id property value
+     */
+    static inline void setListItemIdProp(QObject* obj, const QString& item)
+	{ obj->setProperty("_yate_widgetlistitemid",QVariant(item)); }
+
+    /**
+     * Retrieve the list item id property from a list item object
+     * @param obj The object
+     * @param item Destination string
+     */
+    static inline void getListItemIdProp(QObject* obj, String& item)
+	{ QtClient::getProperty(obj,"_yate_widgetlistitemid",item); }
+
+    /**
+     * Set the list item property for an item's child object
+     * @param obj The object
+     * @param item Item property value
+     */
+    static inline void setListItemProp(QObject* obj, const QString& item)
+	{ obj->setProperty("_yate_widgetlistitem",QVariant(item)); }
+
+    /**
+     * Retrieve the list item property from an item's child object
+     * @param obj The object
+     * @param item Destination string
+     */
+    static inline void getListItemProp(QObject* obj, String& item)
+	{ QtClient::getProperty(obj,"_yate_widgetlistitem",item); }
+
+    /**
+     * Retrieve the top level QtUIWidget container parent of an object
+     * @param obj The object
+     * @return QtUIWidget pointer or 0 if not found
+     */
+    static QtUIWidget* container(QObject* obj);
+
+protected:
+    /**
+     * Default constructor
+     */
+    QtUIWidget()
+	{}
+
+    /**
+     * Initialize navigation controls
+     * @param params Parameter list
+     */
+    void initNavigation(const NamedList& params);
+
+    /**
+     * Update navigation controls
+     */
+    void updateNavigation();
+
+    /**
+     * Trigger a custom action from an item. Build a list of parameters containing
+     *  the 'item' and the 'list' object identity
+     * @param item The item id
+     * @param action The action name to trigger
+     * @param sender Optional sender (set it to 0 to use getQObject())
+     * @param params Optional extra action parameters
+     * @return True if handled
+     */
+    bool triggerAction(const String& item, const String& action, QObject* sender = 0,
+	NamedList* params = 0);
+
+    /**
+     * Trigger a custom action from already built list params
+     * @param action The action name to trigger
+     * @param params Extra action parameters
+     * @param sender Optional sender (set it to 0 to use getQObject())
+     * @return True if handled
+     */
+    bool triggerAction(const String& action, NamedList& params, QObject* sender = 0);
+
+    /**
+     * Handle a child's action. Retrieve the object identity (using getIdentity()) and
+     *  notify the action 'sender_identity:sender_item_name' to the client
+     * Internally handle next/prev actions if set
+     * @param sender The sender
+     */
+    virtual void onAction(QObject* sender);
+
+    /**
+     * Handle a child's action. Retrieve the object identity (using getIdentity()) and
+     *  notify the toggled 'sender_identity:sender_item_name' event to the client
+     * @param sender The sender
+     * @param on Toggle status
+     */
+    virtual void onToggle(QObject* sender, bool on);
+
+    /**
+     * Handle a child's selection change. Retrieve the object identity and
+     *  notify the select 'sender_identity:sender_item_name' event to the client.
+     * @param sender The sender
+     * @param item Optional selected item if any. Set it to 0 to detect it
+     */
+    virtual void onSelect(QObject* sender, const String* item = 0);
+
+    /**
+     * Handle a child's multiple selection change. Retrieve the object identity and
+     *  notify the select 'sender_identity:sender_item_name' event to the client.
+     * @param sender The sender
+     * @param items Optional selected items. Set it to 0 to detect it
+     */
+    virtual void onSelectMultiple(QObject* sender, const NamedList* items = 0);
+
+    /**
+     * Filter wathed events for children.
+     * Handle child image changing on mouse events
+     * @param watched The object
+     * @param event Event to process
+     * @return True if event filter was removed
+     */
+    virtual bool onChildEvent(QObject* watched, QEvent* event);
+
+    /**
+     * Load an item's widget. Rename children.
+     * Set '_yate_widgetlistitemid' widget property to given name.
+     * Set '_yate_widgetlistitem' to item for each child.
+     * Connect signals for children not having a '_yate_autoconnect' property set to false.
+     * Install event filter for children with '_yate_filterevents' property set to true.
+     * @param parent Parent widget
+     * @param name Widget name
+     * @param ui UI file to load
+     * @return QWidget pointer or 0
+     */
+    QWidget* loadWidget(QWidget* parent, const String& name, const String& ui);
+
+    /**
+     * Load an item's widget using a given type
+     * @param parent Parent widget
+     * @param name Widget name
+     * @param type Item type
+     * @return QWidget pointer or 0
+     */
+    inline QWidget* loadWidgetType(QWidget* parent, const String& name, const String& type) {
+	    QtUIWidgetItemProps* p = getItemProps(type);
+	    if (p && p->m_ui)
+		return loadWidget(parent,name,p->m_ui);
+	    return 0;
+	}
+
+    /**
+     * Apply a QWidget style sheet. Replace ${name} with widget name in style
+     * @param name The widget
+     * @param style The style sheet to apply
+     */
+    void applyWidgetStyle(QWidget* w, const String& style);
+
+    /**
+     * Filter key press events. Retrieve an action associated with the key.
+     * Check if the object is allowed to process the key.
+     * Raise the action
+     * @param obj The object
+     * @param event QKeyEvent event to process
+     * @param filter Filter key or let the object process it
+     * @return True if processed, false if no key was filtered
+     */
+    bool filterKeyEvent(QObject* watched, QKeyEvent* event, bool& filter);
+
+    bool m_wndEvHooked;                  // Event filter already installed in parent window
+    ObjList m_itemProps;
+    QStringList m_saveProps;             // List of properties to be automatically
+                                         //  saved/restored when window owning
+                                         //  this object is initialized/destroyed
+    // Navigation
+    String m_prev;                       // Goto previous item action
+    String m_next;                       // Goto next item action
+    String m_info;                       // Info widget: current index, total ...
+    String m_infoFormat;                 // Data to be displayed in info
+    String m_title;                      // Current item title widget name
+};
+
+/**
+ * This class encapsulates a custom QT object
+ * @short A custom QT object
+ */
+class YQT5_API QtCustomObject : public QObject, public QtUIWidget
+{
+    YCLASS(QtCustomObject,QtUIWidget)
+    Q_CLASSINFO("QtCustomObject","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param name Object's name
+     * @param parent Optional parent object
+     */
+    inline QtCustomObject(const char* name, QObject* parent = 0)
+	: QObject(parent), QtUIWidget(name)
+	{ setObjectName(name);	}
+
+    /**
+     * Retrieve a QObject from this one
+     * @return QObject pointer
+     */
+    virtual QObject* getQObject()
+	{ return static_cast<QObject*>(this); }
+
+    /**
+     * Parent changed notification
+     */
+    virtual void parentChanged()
+	{}
+
+private:
+    QtCustomObject() {}                  // No default constructor
+};
+
+/**
+ * This class encapsulates a custom QT widget
+ * @short A custom QT widget
+ */
+class YQT5_API QtCustomWidget : public QWidget, public QtUIWidget
+{
+    YCLASS(QtCustomWidget,QtUIWidget)
+    Q_CLASSINFO("QtCustomWidget","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param name Widget's name
+     * @param parent Optional parent widget
+     */
+    inline QtCustomWidget(const char* name, QWidget* parent = 0)
+	: QWidget(parent), QtUIWidget(name)
+	{ setObjectName(name);	}
+
+    /**
+     * Retrieve a QObject from this one
+     * @return QObject pointer
+     */
+    virtual QObject* getQObject()
+	{ return static_cast<QObject*>(this); }
+
+protected:
+    /**
+     * Filter events. Call parent onEventFilter(). Return QWidget's event filter
+     * Handle child image changing on mouse events
+     * @param watched The object
+     * @param event Event to process
+     */
+    virtual bool eventFilter(QObject* watched, QEvent* event) {
+	    bool ok = onChildEvent(watched,event);
+	    return QWidget::eventFilter(watched,event) || ok;
+	}
+
+private:
+    QtCustomWidget() {}                  // No default constructor
+};
+
+/**
+ * This class encapsulates a custom QT table
+ * @short A custom QT table widget
+ */
+class YQT5_API QtTable : public QTableWidget, public QtUIWidget
+{
+    YCLASS(QtTable,QtUIWidget)
+    Q_CLASSINFO("QtTable","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param name Table's name
+     * @param parent Optional parent widget
+     */
+    inline QtTable(const char* name, QWidget* parent = 0)
+	: QTableWidget(parent), QtUIWidget(name)
+	{ setObjectName(name); }
+
+    /**
+     * Retrieve a QObject from this one
+     * @return QObject pointer
+     */
+    virtual QObject* getQObject()
+	{ return static_cast<QObject*>(this); }
+
+protected:
+    /**
+     * Filter events. Call parent onEventFilter(). Return QWidget's event filter
+     * Handle child image changing on mouse events
+     * @param watched The object
+     * @param event Event to process
+     */
+    virtual bool eventFilter(QObject* watched, QEvent* event) {
+	    bool ok = onChildEvent(watched,event);
+	    return QTableWidget::eventFilter(watched,event) || ok;
+	}
+
+private:
+    QtTable() {}                         // No default constructor
+};
+
+/**
+ * This class encapsulates a custom QT tree
+ * @short A custom QT tree widget
+ */
+class YQT5_API QtTree : public QTreeWidget, public QtUIWidget
+{
+    YCLASS(QtTree,QtUIWidget)
+    Q_CLASSINFO("QtTree","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param name Tree's name
+     * @param parent Optional parent widget
+     */
+    inline QtTree(const char* name, QWidget* parent = 0)
+	: QTreeWidget(parent), QtUIWidget(name)
+	{ setObjectName(name); }
+
+    /**
+     * Retrieve a QObject from this one
+     * @return QObject pointer
+     */
+    virtual QObject* getQObject()
+	{ return static_cast<QObject*>(this); }
+
+protected:
+    /**
+     * Filter events. Call parent onEventFilter(). Return QWidget's event filter
+     * Handle child image changing on mouse events
+     * @param watched The object
+     * @param event Event to process
+     */
+    virtual bool eventFilter(QObject* watched, QEvent* event) {
+	    bool ok = onChildEvent(watched,event);
+	    return QTreeWidget::eventFilter(watched,event) || ok;
+	}
+
+private:
+    QtTree() {}                          // No default constructor
+};
+
+/**
+ * QT specific sound
+ * @short A QT client sound
+ */
+class YQT5_API QtSound : public ClientSound
+{
+    YCLASS(QtSound,ClientSound)
+public:
+    /**
+     * Constructor
+     * @param name The name of this object
+     * @param file The file to play (should contain the whole path and the file name)
+     * @param device Optional device used to play the file. Set to 0 to use the default one
+     */
+    inline QtSound(const char* name, const char* file, const char* device = 0)
+	: ClientSound(name,file,device), m_sound(0)
+	{ m_native = true; }
+
+protected:
+    virtual bool doStart();
+    virtual void doStop();
+
+private:
+    QSound* m_sound;
+};
+
+/**
+ * @short Base class for Drag&Drop operations
+ */
+class YQT5_API QtDragAndDrop : public QObject, public GenObject
+{
+    YCLASS(QtDragAndDrop,GenObject)
+    Q_CLASSINFO("QtDragAndDrop","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Accept drop enumeration
+     */
+    enum AcceptDrop {
+	None = 0,
+	Always,
+	Ask,
+    };
+
+    /**
+     * Constructor
+     * @param parent Object parent
+     */
+    inline QtDragAndDrop(QObject* parent)
+	: QObject(parent),
+	m_started(false)
+	{}
+
+    /**
+     * Check if started
+     * @return True if started
+     */
+    inline bool started() const
+	{ return m_started; }
+
+    /**
+     * Reset data
+     */
+    virtual void reset();
+
+    /**
+     * Check a string value for 'drag', 'drop', 'both'
+     * @param s The string
+     * @param drag Boolean value to set if drag is enabled
+     * @param drop Boolean value to set if drop is enabled
+     */
+    static void checkEnable(const String& s, bool& drag, bool& drop);
+
+protected:
+    bool m_started;                      // Started flag
+};
+
+/**
+ * This class holds data used for Drop operation
+ * @short Drop data holder
+ */
+class YQT5_API QtDrop : public QtDragAndDrop
+{
+    YCLASS(QtDrop,QtDragAndDrop)
+    Q_CLASSINFO("QtDrop","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param parent Object parent
+     * @param params Optional pointer to object parameters
+     */
+    QtDrop(QObject* parent, const NamedList* params = 0);
+
+    /**
+     * Retrieve drop parameters
+     * @return Drop parameters
+     */
+    inline NamedList& params()
+	{ return m_dropParams; }
+
+    /**
+     * Update parameters from drag enter event
+     * @param e The event
+     * @return True if accepted
+     */
+    bool start(QDragEnterEvent& e);
+
+    /**
+     * Reset data
+     */
+    virtual void reset();
+
+    /**
+     * Get accept type
+     * @param type Type to check
+     * @param defVal Default value to return if not found
+     * @return Accept value
+     */
+    static inline int acceptDropType(const char* type, int defVal)
+	{ return lookup(type,s_acceptDropName,defVal); }
+
+    static const String s_askClientAcceptDrop;
+    static const String s_notifyClientDrop;
+    static const QString s_fileScheme;
+
+    static const TokenDict s_acceptDropName[];
+
+protected:
+    NamedList m_dropParams;              // Drop parameters
+    QStringList m_schemes;               // Known URL Schemes. Accept only these if not empty
+    bool m_acceptFiles;                  // Accept files on drop
+    bool m_acceptDirs;                   // Accept directories on drop
+};
+
+/**
+ * This class holds data used for Drop operation on widgets displaying a list of items
+ * @short Drop data holder for widget list items
+ */
+class YQT5_API QtListDrop : public QtDrop
+{
+    YCLASS(QtListDrop,QtDrop)
+    Q_CLASSINFO("QtListDrop","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param parent Object parent
+     * @param params Optional pointer to object parameters
+     */
+    QtListDrop(QObject* parent, const NamedList* params = 0);
+
+    /**
+     * Check if drop should be accepted on empty space
+     * @return True if drop should be accepted on empty space
+     */
+    inline int acceptOnEmpty() const
+	{ return m_acceptOnEmpty; }
+
+    /**
+     * Set accept drop on empty space
+     * @param val New value for accept drop on empty space
+     */
+    inline void setAcceptOnEmpty(int val)
+	{ m_acceptOnEmpty = val; }
+
+    /**
+     * Update accept
+     * @param list Comma separated list of item types
+     * @param type Accept drop type
+     */
+    void updateAcceptType(const String list, int type);
+
+    /**
+     * Update accept from parameters list
+     * @param params Parameters list
+     */
+    void updateAccept(const NamedList& params);
+
+    /**
+     * Check if an item type can be automatically accepted
+     * @param type Item type to check
+     * @param defVal Value to return if not found
+     * @return Accept value as AcceptDrop enumeration
+     */
+    inline int getAcceptType(const String& type, int defVal = None)
+	{ return NamedInt::lookup(m_acceptItemTypes,type,defVal);}
+
+    /**
+     * Reset data
+     */
+    virtual void reset();
+
+protected:
+    int m_acceptOnEmpty;                 // Accept drop on widget surface not occupied by any item
+    ObjList m_acceptItemTypes;           // Item type to handle drop
+};
+
+/**
+ * Busy widget to show over controls
+ * @short Busy widget to show over controls
+ */
+class YQT5_API QtBusyWidget : public QtCustomWidget
+{
+    YCLASS(QtBusyWidget,QtCustomWidget)
+    Q_CLASSINFO("QtBusyWidget","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param parent Optional parent widget
+     */
+    QtBusyWidget(QWidget* parent = 0);
+
+    /**
+     * Initialize
+     * @param ui UI to load
+     * @param params Busy parameters
+     * @param target Target widget
+     */
+    virtual void init(const String& ui, const NamedList& params, QWidget* target);
+
+    /**
+     * Show or hide the widget
+     * @param on True to show, false to hide
+     */
+    inline void showBusy(bool on) {
+	    if (on)
+		showBusy();
+	    else
+		hideBusy();
+	}
+
+    /**
+     * Show the widget
+     */
+    void showBusy();
+
+    /**
+     * Hide the widget
+     */
+    void hideBusy();
+
+    /**
+     * Show or hide busy widget.
+     * The busy widget must be a target's child whose name is composed from
+     *  target->objectName() + s_busySuffix
+     * @param target The widget to show busy
+     * @param on True to show, false to hide
+     */
+    static inline bool showBusyChild(QWidget* target, bool on) {
+	    QtBusyWidget* w = target ? target->findChild<QtBusyWidget*>(
+		target->objectName() + s_busySuffix) : 0;
+	    if (!w)
+		return false;
+	    w->showBusy(on);
+	    return true;
+	}
+
+    /**
+     * Busy child name suffix
+     */
+    static const QString s_busySuffix;
+
+protected:
+    /**
+     * Filter wathed events
+     * @param watched The object
+     * @param event Event to process
+     * @return True if event filter was removed
+     */
+    virtual bool onChildEvent(QObject* watched, QEvent* event);
+
+    /**
+     * Re-implemented from QWidget
+     */
+    virtual void timerEvent(QTimerEvent* ev);
+
+    /**
+     * Show/hide busy content
+     * @param on True to show, false to hide
+     */
+    virtual void setContent(bool on);
+
+    QWidget* m_target;                   // Widget to show over
+    bool m_shown;                        // Shown flag
+    unsigned int m_delayMs;              // Delay show
+    int m_delayTimer;                    // Delay timer
+    QLabel* m_movieLabel;                // Label showing animation
+
+private:
+    inline void stopDelayTimer() {
+	    if (!m_delayTimer)
+		return;
+	    killTimer(m_delayTimer);
+	    m_delayTimer = 0;
+	}
+    void internalShow();
+};
+
+}; // namespace TelEngine
+
+Q_DECLARE_METATYPE(TelEngine::QtRefObjectHolder)
+
+#endif // __QT5CLIENT_H
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/clients/run-qt5 b/clients/run-qt5
new file mode 100644
index 0000000..e69a284
--- /dev/null
+++ b/clients/run-qt5
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+# run-qt5
+# This file is part of the YATE Project http://YATE.null.ro
+#
+# Yet Another Telephony Engine - a fully featured software PBX and IVR
+# Copyright (C) 2005-2020 Null Team
+#
+# This software is distributed under multiple licenses;
+# see the COPYING file in the main directory for licensing
+# information for this specific distribution.
+#
+# This use of this software may be subject to additional restrictions.
+# See the LEGAL file in the main directory for details.
+#
+# 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.
+
+
+# Script to run the Qt5 client from the build directory
+
+if [ -x yate-qt5 -a -x ../run ]; then
+    # Need to put the path to extra Qt/KDE libraries here
+    # export LD_LIBRARY_PATH=
+    cd ..; exec ./run --executable clients/yate-qt5 "$@"
+else
+    echo "Could not find client executable or run script" >&2
+fi
diff --git a/conf.d/yate-qt5.conf.default b/conf.d/yate-qt5.conf.default
new file mode 100644
index 0000000..524eb83
--- /dev/null
+++ b/conf.d/yate-qt5.conf.default
@@ -0,0 +1,15 @@
+; This minimal file is here just to set the default skin.
+; You can replace it with a more complete version from yate.conf.sample
+
+[localsym]
+h323chan.yate=yes
+
+[client]
+;skin=default
+;style=
+;stylesheet_file=
+;device=
+;greeting=Yate ${version} - ${release}
+
+[modules]
+yiaxchan.yate=no
diff --git a/configure b/configure
index 102e26d..00ad05f 100755
--- a/configure
+++ b/configure
@@ -638,14 +638,14 @@ COREDUMPER_LIB
 COREDUMPER_INC
 HAVE_COREDUMPER
 HAVE_MALLINFO
-QT4_STATIC_MODULES
-QT4_VER
-QT4_MOC
-QT4_LIB_NET
-QT4_INC_NET
-QT4_LIB
-QT4_INC
-HAVE_QT4
+QT5_STATIC_MODULES
+QT5_VER
+QT5_MOC
+QT5_LIB_NET
+QT5_INC_NET
+QT5_LIB
+QT5_INC
+HAVE_QT5
 LIBUSB_LIB
 LIBUSB_INC
 HAVE_LIBUSB
@@ -824,7 +824,7 @@ with_openh323
 with_openssl
 with_zlib
 with_libusb
-with_libqt4
+with_libqt5
 with_qtstatic
 enable_mallinfo
 with_coredumper
@@ -1496,7 +1496,7 @@ Optional Packages:
   --with-zlib=DIR         use zlib for data (de)compression from DIR (default
                           /usr)
   --with-libusb=DIR       use libusb DIR (default /usr)
-  --with-libqt4           use Qt for graphical clients (default)
+  --with-libqt5           use Qt for graphical clients (default)
   --with-qtstatic=MODULES link specific modules with static Qt
   --with-coredumper       use Google coredumper if available (default)
   --with-doxygen=EXE      use doxygen to generate API docs (default: PATH)
@@ -7750,20 +7750,20 @@ fi
 
 
 
-HAVE_QT4=no
-QT4_INC=""
-QT4_LIB=""
-QT4_INC_NET=""
-QT4_LIB_NET=""
-QT4_MOC=""
-QT4_VER=""
-QT4_STATIC_MODULES=""
+HAVE_QT5=no
+QT5_INC=""
+QT5_LIB=""
+QT5_INC_NET=""
+QT5_LIB_NET=""
+QT5_MOC=""
+QT5_VER=""
+QT5_STATIC_MODULES=""
 
-# Check whether --with-libqt4 was given.
-if test "${with_libqt4+set}" = set; then :
-  withval=$with_libqt4; ac_cv_use_libqt4=$withval
+# Check whether --with-libqt5 was given.
+if test "${with_libqt5+set}" = set; then :
+  withval=$with_libqt5; ac_cv_use_libqt5=$withval
 else
-  ac_cv_use_libqt4=yes
+  ac_cv_use_libqt5=yes
 fi
 
 
@@ -7778,31 +7778,31 @@ qtstatic=""
 if [ "x$ac_cv_use_qtstatic" != "xno" ]; then
     qtstatic="--static"
 fi
-QT4_STATIC_MODULES=`echo "$ac_cv_use_qtstatic" | sed 's/,/ /g'`
-if [ "x$ac_cv_use_libqt4" = "xyes" ]; then
-    { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Qt4 >= 4.3.0 using pkg-config" >&5
-$as_echo_n "checking for Qt4 >= 4.3.0 using pkg-config... " >&6; }
-    pkgd="/usr/lib/qt4/$ARCHLIB/pkgconfig"
+QT5_STATIC_MODULES=`echo "$ac_cv_use_qtstatic" | sed 's/,/ /g'`
+if [ "x$ac_cv_use_libqt5" = "xyes" ]; then
+    { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Qt5 >= 5.0.0 using pkg-config" >&5
+$as_echo_n "checking for Qt5 >= 5.0.0 using pkg-config... " >&6; }
+    pkgd="/usr/lib/qt5/$ARCHLIB/pkgconfig"
     verqt=`(pkg-config --modversion QtCore) 2>/dev/null`
     if [ -z "$verqt" -a -d "$pkgd" ]; then
 	export PKG_CONFIG_LIBDIR="$pkgd"
 	verqt=`(pkg-config --modversion QtCore) 2>/dev/null`
     fi
-    incqt=`(pkg-config --cflags QtNetwork QtGui QtXml QtCore) 2>/dev/null | sed 's/QtNetwork/QtUiTools/'`
-    libqt=`(pkg-config --libs $qtstatic QtNetwork QtGui QtXml QtCore) 2>/dev/null | sed 's/QtNetwork/QtUiTools/'`
+    incqt=`(pkg-config --cflags QtNetwork QtGui QtWidgets QtXml QtMultimedia QtCore) 2>/dev/null | sed 's/QtNetwork/QtUiTools/'`
+    libqt=`(pkg-config --libs $qtstatic QtNetwork QtGui QtWidgets QtXml QtMultimedia QtCore) 2>/dev/null | sed 's/QtNetwork/QtUiTools/'`
     if [ "x$incqt" != "x" -a "x$libqt" != "x" ]; then
-	HAVE_QT4=yes
-	QT4_INC="$incqt"
-	QT4_LIB="$libqt"
-	QT4_INC_NET=`(pkg-config --cflags QtNetwork) 2>/dev/null | grep -o '[^ ]*QtNetwork[^ ]*'`
-	QT4_LIB_NET=`(pkg-config --libs QtNetwork) 2>/dev/null | grep -o '[^ ]*QtNetwork[^ ]*'`
-	QT4_MOC=`echo "$incqt" | sed -n 's,^.*-I\([^ ]\+\)/include .*$,\1/bin/moc,p'`
-	test -z "$QT4_MOC" && QT4_MOC=`(which moc-qt4) 2>/dev/null`
-	test -z "$QT4_MOC" && QT4_MOC="moc"
-	QT4_VER=`echo "$verqt" | $csed "$vsed" | $csed "$vsed" | sed 's/\(..\)\.\(..\)\.\(..\)/\1\2\3/'`
+	HAVE_QT5=yes
+	QT5_INC="$incqt"
+	QT5_LIB="$libqt"
+	QT5_INC_NET=`(pkg-config --cflags QtNetwork) 2>/dev/null | grep -o '[^ ]*QtNetwork[^ ]*'`
+	QT5_LIB_NET=`(pkg-config --libs QtNetwork) 2>/dev/null | grep -o '[^ ]*QtNetwork[^ ]*'`
+	QT5_MOC=`echo "$incqt" | sed -n 's,^.*-I\([^ ]\+\)/include .*$,\1/bin/moc,p'`
+	test -z "$QT5_MOC" && QT5_MOC=`(which moc-qt5) 2>/dev/null`
+	test -z "$QT5_MOC" && QT5_MOC="moc"
+	QT5_VER=`echo "$verqt" | $csed "$vsed" | $csed "$vsed" | sed 's/\(..\)\.\(..\)\.\(..\)/\1\2\3/'`
 	ac_cv_use_libqt="no"
-	if [ 1$QT4_VER -lt 1040300 ]; then
-	    HAVE_QT4=no
+	if [ 1$QT5_VER -lt 1050000 ]; then
+	    HAVE_QT5=no
 	    verqt="too old ($verqt)"
 	fi
     else
@@ -7812,9 +7812,9 @@ $as_echo_n "checking for Qt4 >= 4.3.0 using pkg-config... " >&6; }
     { $as_echo "$as_me:${as_lineno-$LINENO}: result: $verqt" >&5
 $as_echo "$verqt" >&6; }
 
-    if [ "x$HAVE_QT4" = "xno" ]; then
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for Qt4 >= 4.3.0 using qmake" >&5
-$as_echo_n "checking for Qt4 >= 4.3.0 using qmake... " >&6; }
+    if [ "x$HAVE_QT0" = "xno" ]; then
+	{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for Qt0 >= 5.0.0 using qmake" >&5
+$as_echo_n "checking for Qt5 >= 5.0.0 using qmake... " >&6; }
 	incqt=`(qmake -query QT_INSTALL_HEADERS) 2>/dev/null`
 	libqt=`(qmake -query QT_INSTALL_LIBS) 2>/dev/null`
 	if [ "x$incqt" = "x**Unknown**" -o "x$libqt" = "x**Unknown**" ]; then
@@ -7822,33 +7822,33 @@ $as_echo_n "checking for Qt4 >= 4.3.0 using qmake... " >&6; }
 	    libqt=""
 	fi
 	if [ "x$incqt" != "x" -a "x$libqt" != "x" ]; then
-	    HAVE_QT4=yes
-	    QT4_INC="-I$incqt -I$incqt/QtUiTools -I$incqt/QtGui -I$incqt/QtXml -I$incqt/QtCore"
+	    HAVE_QT5=yes
+	    QT5_INC="-I$incqt -I$incqt/QtUiTools -I$incqt/QtGui -I$incqt/QtWidgets -I$incqt/QtXml -I$incqt/QtMultimedia -I$incqt/QtCore"
 	    case "$uname_os" in
 		*Darwin)
-		    QT4_INC="-D__USE_WS_X11__ $QT4_INC"
+		    QT5_INC="-D__USE_WS_X11__ $QT5_INC"
 		    ;;
 	    esac
-	    QT4_LIB="-L$libqt -lQtUiTools -lQtGui -lQtXml -lQtCore"
-	    QT4_INC_NET="-I$incqt/QtNetwork"
-	    QT4_LIB_NET="-L$libqt -lQtNetwork"
+	    QT5_LIB="-L$libqt -lQt5UiTools -lQt5Gui -lQt5Widgets -lQt5Xml -lQt5Multimedia -lQt5Core"
+	    QT5_INC_NET="-I$incqt/QtNetwork"
+	    QT5_LIB_NET="-L$libqt -lQtNetwork"
 	    case "$uname_os" in
 		*Darwin)
 		    framework=`(ls "$libqt" | grep QtGui.framework) 2>/dev/null`
 		    if [ "x$framework" != "x" ]; then
-			QT4_INC="-I$incqt  -I$incqt/QtUiTools -I$libqt/QtGui.framework/Headers -I$libqt/QtXml.framework/Headers -I$libqt/QtCore.framework/Headers"
-			QT4_LIB="-L$libqt -F$libqt -lQtUiTools -framework QtGui -framework QtXml -framework QtCore"
-			QT4_INC_NET="-I$libqt/QtNetwork.framework/Headers"
-			QT4_LIB_NET="-framework QtNetwork"
+			QT5_INC="-I$incqt  -I$incqt/QtUiTools -I$libqt/QtGui.framework/Headers -I$libqt/QtXml.framework/Headers -I$libqt/QtCore.framework/Headers"
+			QT5_LIB="-L$libqt -F$libqt -lQtUiTools -framework QtGui -framework QtXml -framework QtCore"
+			QT5_INC_NET="-I$libqt/QtNetwork.framework/Headers"
+			QT5_LIB_NET="-framework QtNetwork"
 		    fi
 		    ;;
 	    esac
-	    QT4_MOC=`(qmake -query QT_INSTALL_BINS) 2>/dev/null`
-	    QT4_MOC="$QT4_MOC/moc"
+	    QT5_MOC=`(qmake -query QT_INSTALL_BINS) 2>/dev/null`
+	    QT5_MOC="$QT5_MOC/moc"
 	    verqt=`(qmake -query QT_VERSION) 2>/dev/null`
-	    QT4_VER=`echo "$verqt" | $csed "$vsed" | $csed "$vsed" | sed 's/\(..\)\.\(..\)\.\(..\)/\1\2\3/'`
-	    if [ 1$QT4_VER -lt 1040300 ]; then
-	    	HAVE_QT4=no
+	    QT5_VER=`echo "$verqt" | $csed "$vsed" | $csed "$vsed" | sed 's/\(..\)\.\(..\)\.\(..\)/\1\2\3/'`
+	    if [ 1$QT5_VER -lt 1050000 ]; then
+	    	HAVE_QT5=no
 	    	verqt="too old ($verqt)"
 	   fi
 	else
@@ -8097,7 +8097,7 @@ fi
 
 
 
-ac_config_files="$ac_config_files packing/rpm/yate.spec packing/portage/yate.ebuild yate.pc yateversn.h yateiss.inc Makefile engine/Makefile modules/Makefile modules/test/Makefile clients/Makefile clients/qt4/Makefile libs/ilbc/Makefile libs/ysip/Makefile libs/yrtp/Makefile libs/ysdp/Makefile libs/yiax/Makefile libs/yjabber/Makefile libs/yscript/Makefile libs/ymgcp/Makefile libs/ysig/Makefile libs/ypbx/Makefile libs/ymodem/Makefile libs/yasn/Makefile libs/ysnmp/Makefile libs/miniwebrtc/Makefile libs/yradio/Makefile share/Makefile share/scripts/Makefile share/skins/Makefile share/sounds/Makefile share/help/Makefile share/data/Makefile conf.d/Makefile"
+ac_config_files="$ac_config_files packing/rpm/yate.spec packing/portage/yate.ebuild yate.pc yateversn.h yateiss.inc Makefile engine/Makefile modules/Makefile modules/test/Makefile clients/Makefile clients/qt5/Makefile libs/ilbc/Makefile libs/ysip/Makefile libs/yrtp/Makefile libs/ysdp/Makefile libs/yiax/Makefile libs/yjabber/Makefile libs/yscript/Makefile libs/ymgcp/Makefile libs/ysig/Makefile libs/ypbx/Makefile libs/ymodem/Makefile libs/yasn/Makefile libs/ysnmp/Makefile libs/miniwebrtc/Makefile libs/yradio/Makefile share/Makefile share/scripts/Makefile share/skins/Makefile share/sounds/Makefile share/help/Makefile share/data/Makefile conf.d/Makefile"
 
 ac_config_files="$ac_config_files yate-config"
 
@@ -8822,7 +8822,7 @@ do
     "modules/Makefile") CONFIG_FILES="$CONFIG_FILES modules/Makefile" ;;
     "modules/test/Makefile") CONFIG_FILES="$CONFIG_FILES modules/test/Makefile" ;;
     "clients/Makefile") CONFIG_FILES="$CONFIG_FILES clients/Makefile" ;;
-    "clients/qt4/Makefile") CONFIG_FILES="$CONFIG_FILES clients/qt4/Makefile" ;;
+    "clients/qt5/Makefile") CONFIG_FILES="$CONFIG_FILES clients/qt5/Makefile" ;;
     "libs/ilbc/Makefile") CONFIG_FILES="$CONFIG_FILES libs/ilbc/Makefile" ;;
     "libs/ysip/Makefile") CONFIG_FILES="$CONFIG_FILES libs/ysip/Makefile" ;;
     "libs/yrtp/Makefile") CONFIG_FILES="$CONFIG_FILES libs/yrtp/Makefile" ;;
diff --git a/configure.ac b/configure.ac
index 3c5b90e..77e4287 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1598,44 +1598,44 @@ AC_SUBST(LIBUSB_INC)
 AC_SUBST(LIBUSB_LIB)
 
 
-HAVE_QT4=no
-QT4_INC=""
-QT4_LIB=""
-QT4_INC_NET=""
-QT4_LIB_NET=""
-QT4_MOC=""
-QT4_VER=""
-QT4_STATIC_MODULES=""
-AC_ARG_WITH(libqt4,AC_HELP_STRING([--with-libqt4],[use Qt for graphical clients (default)]),[ac_cv_use_libqt4=$withval],[ac_cv_use_libqt4=yes])
+HAVE_QT5=no
+QT5_INC=""
+QT5_LIB=""
+QT5_INC_NET=""
+QT5_LIB_NET=""
+QT5_MOC=""
+QT5_VER=""
+QT5_STATIC_MODULES=""
+AC_ARG_WITH(libqt5,AC_HELP_STRING([--with-libqt5],[use Qt for graphical clients (default)]),[ac_cv_use_libqt5=$withval],[ac_cv_use_libqt5=yes])
 AC_ARG_WITH(qtstatic,AC_HELP_STRING([--with-qtstatic=MODULES],[link specific modules with static Qt]),[ac_cv_use_qtstatic=$withval],[ac_cv_use_qtstatic=no])
 qtstatic=""
 if [[ "x$ac_cv_use_qtstatic" != "xno" ]]; then
     qtstatic="--static"
 fi
-QT4_STATIC_MODULES=`echo "$ac_cv_use_qtstatic" | sed 's/,/ /g'`
-if [[ "x$ac_cv_use_libqt4" = "xyes" ]]; then
-    AC_MSG_CHECKING([for Qt4 >= 4.3.0 using pkg-config])
-    pkgd="/usr/lib/qt4/$ARCHLIB/pkgconfig"
+QT5_STATIC_MODULES=`echo "$ac_cv_use_qtstatic" | sed 's/,/ /g'`
+if [[ "x$ac_cv_use_libqt5" = "xyes" ]]; then
+    AC_MSG_CHECKING([for Qt5 >= 5.0.0 using pkg-config])
+    pkgd="/usr/lib/qt5/$ARCHLIB/pkgconfig"
     verqt=`(pkg-config --modversion QtCore) 2>/dev/null`
     if [[ -z "$verqt" -a -d "$pkgd" ]]; then
 	export PKG_CONFIG_LIBDIR="$pkgd"
 	verqt=`(pkg-config --modversion QtCore) 2>/dev/null`
     fi
-    incqt=`(pkg-config --cflags QtNetwork QtGui QtXml QtCore) 2>/dev/null | sed 's/QtNetwork/QtUiTools/'`
-    libqt=`(pkg-config --libs $qtstatic QtNetwork QtGui QtXml QtCore) 2>/dev/null | sed 's/QtNetwork/QtUiTools/'`
+    incqt=`(pkg-config --cflags QtNetwork QtGui QtWidgets QtXml QtMultimedia QtCore) 2>/dev/null | sed 's/QtNetwork/QtUiTools/'`
+    libqt=`(pkg-config --libs $qtstatic QtNetwork QtGui QtWidgets QtXml QtMultimedia QtCore) 2>/dev/null | sed 's/QtNetwork/QtUiTools/'`
     if [[ "x$incqt" != "x" -a "x$libqt" != "x" ]]; then
-	HAVE_QT4=yes
-	QT4_INC="$incqt"
-	QT4_LIB="$libqt"
-	QT4_INC_NET=`(pkg-config --cflags QtNetwork) 2>/dev/null | grep -o '[[^ ]]*QtNetwork[[^ ]]*'`
-	QT4_LIB_NET=`(pkg-config --libs QtNetwork) 2>/dev/null | grep -o '[[^ ]]*QtNetwork[[^ ]]*'`
-	QT4_MOC=`echo "$incqt" | sed -n 's,^.*-I\([[^ ]]\+\)/include .*$,\1/bin/moc,p'`
-	test -z "$QT4_MOC" && QT4_MOC=`(which moc-qt4) 2>/dev/null`
-	test -z "$QT4_MOC" && QT4_MOC="moc"
-	QT4_VER=`echo "$verqt" | $csed "$vsed" | $csed "$vsed" | sed 's/\(..\)\.\(..\)\.\(..\)/\1\2\3/'`
+	HAVE_QT5=yes
+	QT5_INC="$incqt"
+	QT5_LIB="$libqt"
+	QT5_INC_NET=`(pkg-config --cflags QtNetwork) 2>/dev/null | grep -o '[[^ ]]*QtNetwork[[^ ]]*'`
+	QT5_LIB_NET=`(pkg-config --libs QtNetwork) 2>/dev/null | grep -o '[[^ ]]*QtNetwork[[^ ]]*'`
+	QT5_MOC=`echo "$incqt" | sed -n 's,^.*-I\([[^ ]]\+\)/include .*$,\1/bin/moc,p'`
+	test -z "$QT5_MOC" && QT5_MOC=`(which moc-qt5) 2>/dev/null`
+	test -z "$QT5_MOC" && QT5_MOC="moc"
+	QT5_VER=`echo "$verqt" | $csed "$vsed" | $csed "$vsed" | sed 's/\(..\)\.\(..\)\.\(..\)/\1\2\3/'`
 	ac_cv_use_libqt="no"
-	if [[ 1$QT4_VER -lt 1040300 ]]; then
-	    HAVE_QT4=no
+	if [[ 1$QT5_VER -lt 1040300 ]]; then
+	    HAVE_QT5=no
 	    verqt="too old ($verqt)"
 	fi
     else
@@ -1644,8 +1644,8 @@ if [[ "x$ac_cv_use_libqt4" = "xyes" ]]; then
     unset PKG_CONFIG_LIBDIR
     AC_MSG_RESULT([$verqt])
 
-    if [[ "x$HAVE_QT4" = "xno" ]]; then
-	AC_MSG_CHECKING([for Qt4 >= 4.3.0 using qmake])
+    if [[ "x$HAVE_QT5" = "xno" ]]; then
+	AC_MSG_CHECKING([for Qt5 >= 5.0.0 using qmake])
 	incqt=`(qmake -query QT_INSTALL_HEADERS) 2>/dev/null`
 	libqt=`(qmake -query QT_INSTALL_LIBS) 2>/dev/null`
 	if [[ "x$incqt" = "x**Unknown**" -o "x$libqt" = "x**Unknown**" ]]; then
@@ -1653,33 +1653,33 @@ if [[ "x$ac_cv_use_libqt4" = "xyes" ]]; then
 	    libqt=""
 	fi
 	if [[ "x$incqt" != "x" -a "x$libqt" != "x" ]]; then
-	    HAVE_QT4=yes
-	    QT4_INC="-I$incqt -I$incqt/QtUiTools -I$incqt/QtGui -I$incqt/QtXml -I$incqt/QtCore"
+	    HAVE_QT5=yes
+	    QT5_INC="-I$incqt -I$incqt/QtUiTools -I$incqt/QtGui -I$incqt/QtWidgets -I$incqt/QtXml -I$incqt/QtMultimedia -I$incqt/QtCore"
 	    case "$uname_os" in
 		*Darwin)
-		    QT4_INC="-D__USE_WS_X11__ $QT4_INC"
+		    QT5_INC="-D__USE_WS_X11__ $QT5_INC"
 		    ;;
 	    esac
-	    QT4_LIB="-L$libqt -lQtUiTools -lQtGui -lQtXml -lQtCore"
-	    QT4_INC_NET="-I$incqt/QtNetwork"
-	    QT4_LIB_NET="-L$libqt -lQtNetwork"
+	    QT5_LIB="-L$libqt -lQt5UiTools -lQt5Gui -lQt5Widgets -lQt5Xml -lQt5Multimedia -lQt5Core"
+	    QT5_INC_NET="-I$incqt/QtNetwork"
+	    QT5_LIB_NET="-L$libqt -lQt5Network"
 	    case "$uname_os" in
 		*Darwin)
 		    framework=`(ls "$libqt" | grep QtGui.framework) 2>/dev/null`
 		    if [[ "x$framework" != "x" ]]; then
-			QT4_INC="-I$incqt  -I$incqt/QtUiTools -I$libqt/QtGui.framework/Headers -I$libqt/QtXml.framework/Headers -I$libqt/QtCore.framework/Headers"
-			QT4_LIB="-L$libqt -F$libqt -lQtUiTools -framework QtGui -framework QtXml -framework QtCore"
-			QT4_INC_NET="-I$libqt/QtNetwork.framework/Headers"
-			QT4_LIB_NET="-framework QtNetwork"
+			QT5_INC="-I$incqt  -I$incqt/QtUiTools -I$libqt/QtGui.framework/Headers -I$libqt/QtXml.framework/Headers -I$libqt/QtCore.framework/Headers"
+			QT5_LIB="-L$libqt -F$libqt -lQtUiTools -framework QtGui -framework QtXml -framework QtCore"
+			QT5_INC_NET="-I$libqt/QtNetwork.framework/Headers"
+			QT5_LIB_NET="-framework QtNetwork"
 		    fi
 		    ;;
 	    esac
-	    QT4_MOC=`(qmake -query QT_INSTALL_BINS) 2>/dev/null`
-	    QT4_MOC="$QT4_MOC/moc"
+	    QT5_MOC=`(qmake -query QT_INSTALL_BINS) 2>/dev/null`
+	    QT5_MOC="$QT5_MOC/moc"
 	    verqt=`(qmake -query QT_VERSION) 2>/dev/null`
-	    QT4_VER=`echo "$verqt" | $csed "$vsed" | $csed "$vsed" | sed 's/\(..\)\.\(..\)\.\(..\)/\1\2\3/'`
-	    if [[ 1$QT4_VER -lt 1040300 ]]; then
-	    	HAVE_QT4=no
+	    QT5_VER=`echo "$verqt" | $csed "$vsed" | $csed "$vsed" | sed 's/\(..\)\.\(..\)\.\(..\)/\1\2\3/'`
+	    if [[ 1$QT5_VER -lt 1050000 ]]; then
+	        HAVE_QT5=no
 	    	verqt="too old ($verqt)"
 	   fi
 	else
@@ -1689,14 +1689,14 @@ if [[ "x$ac_cv_use_libqt4" = "xyes" ]]; then
     fi
 fi
 
-AC_SUBST(HAVE_QT4)
-AC_SUBST(QT4_INC)
-AC_SUBST(QT4_LIB)
-AC_SUBST(QT4_INC_NET)
-AC_SUBST(QT4_LIB_NET)
-AC_SUBST(QT4_MOC)
-AC_SUBST(QT4_VER)
-AC_SUBST(QT4_STATIC_MODULES)
+AC_SUBST(HAVE_QT5)
+AC_SUBST(QT5_INC)
+AC_SUBST(QT5_LIB)
+AC_SUBST(QT5_INC_NET)
+AC_SUBST(QT5_LIB_NET)
+AC_SUBST(QT5_MOC)
+AC_SUBST(QT5_VER)
+AC_SUBST(QT5_STATIC_MODULES)
 
 
 HAVE_MALLINFO=no
@@ -1872,7 +1872,7 @@ AC_CONFIG_FILES([packing/rpm/yate.spec
                  modules/Makefile
                  modules/test/Makefile
                  clients/Makefile
-                 clients/qt4/Makefile
+                 clients/qt5/Makefile
                  libs/ilbc/Makefile
                  libs/ysip/Makefile
                  libs/yrtp/Makefile
diff --git a/modules/Makefile.in b/modules/Makefile.in
index e1d6439..f6d7cf3 100644
--- a/modules/Makefile.in
+++ b/modules/Makefile.in
@@ -13,12 +13,12 @@ YATE_REVISION:= @PACKAGE_REVISION@
 
 CC  := @CC@ -Wall
 CXX := @CXX@ -Wall
-MOC := @QT4_MOC@
-QT4_INC := @QT4_INC@
-QT4_LIB := @QT4_LIB@
-QT4_INC_NET := @QT4_INC_NET@
-QT4_LIB_NET := @QT4_LIB_NET@
-QT4_STATIC_MODULES :=
+MOC := @QT5_MOC@
+QT5_INC := @QT5_INC@
+QT5_LIB := @QT5_LIB@
+QT5_INC_NET := @QT5_INC_NET@
+QT5_LIB_NET := @QT5_LIB_NET@
+QT5_STATIC_MODULES :=
 HAVE_PGSQL := @HAVE_PGSQL@
 PGSQL_INC := @PGSQL_INC@
 PGSQL_LIB := @PGSQL_LIB@
@@ -94,7 +94,7 @@ PROGS := cdrbuild.yate cdrcombine.yate cdrfile.yate regexroute.yate \
 	radio/dummyradio.yate radio/radiotest.yate
 
 LIBS :=
-DIRS := client server jabber qt4 sip sig radio
+DIRS := client server jabber qt5 sip sig radio
 
 ifneq ($(HAVE_PGSQL),no)
 PROGS := $(PROGS) server/pgsqldb.yate
@@ -124,10 +124,10 @@ ifneq (@HAVE_COREAUDIO@,no)
 PROGS := $(PROGS) client/coreaudio.yate
 endif
 
-ifneq (@HAVE_QT4@,no)
-ifeq (@QT4_STATIC_MODULES@,no)
-PROGS := $(PROGS) qt4/updater.yate qt4/customtable.yate qt4/customtext.yate \
-	qt4/customtree.yate qt4/widgetlist.yate qt4/clientarchive.yate
+ifneq (@HAVE_QT5@,no)
+ifeq (@QT5_STATIC_MODULES@,no)
+PROGS := $(PROGS) qt5/updater.yate qt5/customtable.yate qt5/customtext.yate \
+	qt5/customtree.yate qt5/widgetlist.yate qt5/clientarchive.yate
 endif
 endif
 
@@ -244,7 +244,7 @@ strip: all do-strip
 
 .PHONY: clean
 clean: do-clean
-	@-$(RM) $(PROGS) $(LIBS) *.o qt4/*.o qt4/*.moc core 2>/dev/null
+	@-$(RM) $(PROGS) $(LIBS) *.o qt5/*.o qt5/*.moc core 2>/dev/null
 	@-for i in $(PROGS) ; do \
 	    $(RM) -rf $$i.dSYM 2>/dev/null; \
 	done;
@@ -273,9 +273,9 @@ uninstall: do-uninstall
 subdirs:
 	@mkdir -p $(DIRS)
 
-qt4/%.o: @srcdir@/qt4/%.cpp $(MKDEPS) $(INCFILES)
+qt5/%.o: @srcdir@/qt5/%.cpp $(MKDEPS) $(INCFILES)
 	$(MAKE) $(patsubst %.o,%.moc,$@)
-	$(COMPILE) -c -o $@ $(QT4_INC) -I@top_srcdir@/clients/qt4 -I@srcdir@/qt4 $<
+	$(COMPILE) -c -o $@ $(QT5_INC) -I@top_srcdir@/clients/qt5 -I@srcdir@/qt5 $<
 
 %.o: @srcdir@/%.cpp $(MKDEPS) $(INCFILES)
 	$(COMPILE) -c $<
@@ -302,12 +302,12 @@ server/%.yate: @srcdir@/server/%.cpp $(MKDEPS) $(INCFILES)
 client/%.yate: @srcdir@/client/%.cpp $(MKDEPS) $(INCFILES)
 	mkdir -p client && $(MODCOMP) -o $@ $(LOCALFLAGS) $(EXTERNFLAGS) $< $(LOCALLIBS) $(YATELIBS) $(EXTERNLIBS)
 
-qt4/%.yate: @srcdir@/qt4/%.cpp ../libyateqt4.so $(MKDEPS) $(INCFILES)
+qt5/%.yate: @srcdir@/qt5/%.cpp ../libyateqt5.so $(MKDEPS) $(INCFILES)
 	$(MAKE) $(patsubst %.yate,%.moc,$@)
-	$(MODCOMP) -o $@ $(LOCALFLAGS) $(QT4_INC) $(EXTERNFLAGS) -I@top_srcdir@/clients/qt4 -Iqt4 $< $(LOCALLIBS) ../libyateqt4.so $(YATELIBS) $(QT4_LIB) $(EXTERNLIBS)
+	$(MODCOMP) -o $@ $(LOCALFLAGS) $(QT5_INC) $(EXTERNFLAGS) -I@top_srcdir@/clients/qt5 -Iqt5 $< $(LOCALLIBS) ../libyateqt5.so $(YATELIBS) $(QT5_LIB) $(EXTERNLIBS)
 
-qt4/%.moc: @srcdir@/qt4/%.h $(MKDEPS) $(INCFILES)
-	mkdir -p qt4 && $(MOC) $(DEFS) $(INCLUDES) $(QT4_INC) -I@top_srcdir@/clients/qt4 -I@srcdir@/qt4 -o $@ $<
+qt5/%.moc: @srcdir@/qt5/%.h $(MKDEPS) $(INCFILES)
+	mkdir -p qt5 && $(MOC) $(DEFS) $(INCLUDES) $(QT5_INC) -I@top_srcdir@/clients/qt5 -I@srcdir@/qt5 -o $@ $<
 
 sig/%.yate: @srcdir@/sig/%.cpp $(MKDEPS) $(INCFILES)
 	mkdir -p sig && $(MODCOMP) -o $@ $(LOCALFLAGS) $(EXTERNFLAGS) $< $(LOCALLIBS) $(YATELIBS) $(EXTERNLIBS)
@@ -418,8 +418,8 @@ openssl.yate: EXTERNLIBS = $(OPENSSL_LIB)
 rmanager.yate: EXTERNFLAGS = $(COREDUMP_INC) $(MALLINFO_DEF)
 rmanager.yate: EXTERNLIBS = $(COREDUMP_LIB)
 
-qt4/updater.yate: EXTERNFLAGS = $(QT4_INC_NET)
-qt4/updater.yate: EXTERNLIBS = $(QT4_LIB_NET)
+qt5/updater.yate: EXTERNFLAGS = $(QT5_INC_NET)
+qt5/updater.yate: EXTERNLIBS = $(QT5_LIB_NET)
 
 javascript.yate: ../libyatescript.so ../libs/ypbx/libyatepbx.a
 javascript.yate: LOCALFLAGS = -I@top_srcdir@/libs/yscript -I@top_srcdir@/libs/ypbx
@@ -474,8 +474,8 @@ radio/ybladerf.yate: EXTERNLIBS = $(LIBUSB_LIB)
 ../libs/ypbx/libyatepbx.a: @top_srcdir@/libs/ypbx/yatepbx.h
 	$(MAKE) -C ../libs/ypbx
 
-../libyateqt4.so: @top_srcdir@/clients/qt4/qt4client.h
-	$(MAKE) -C ../clients/qt4
+../libyateqt5.so: @top_srcdir@/clients/qt5/qt5client.h
+	$(MAKE) -C ../clients/qt5
 
 ../libyateasn.so ../libs/yasn/libyasn.a: @top_srcdir@/libs/yasn/yateasn.h
 	$(MAKE) -C ../libs/yasn
diff --git a/modules/qt5/clientarchive.cpp b/modules/qt5/clientarchive.cpp
new file mode 100644
index 0000000..06902c3
--- /dev/null
+++ b/modules/qt5/clientarchive.cpp
@@ -0,0 +1,2117 @@
+/**
+ * clientarchive.cpp
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * Client archive management and UI logic
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+/**
+ * Chat log file format
+ *
+ * Header:
+ *  versionNULLaccountNULLcontactNULLcontact_nameNULL{MARKUP_CHAT|MARKUP_ROOMCHAT|MARKUP_ROOMCHATPRIVATE}NULLNULL
+ * Session:
+ *  MARKUP_SESSIONSTARTsession_timeMARKUP_SESSIONDESCdescNULLNULL
+ * Session items:
+ *   item_time{MARKUP_SENT|MARKUP_RECEIVED|MARKUP_DELAYED}sender_nameNULLchat_textNULLNULL
+*/
+
+#include "clientarchive.h"
+
+namespace { //anonymous
+using namespace TelEngine;
+
+class CASearchThread;                    // Archive search worker thread
+class CARefreshThread;                   // Archive refresh worker thread
+class ChatSession;                       // A chat session entry
+class ChatItem;                          // A chat session item
+class ChatFile;                          // A contact's chat file
+class ChatArchive;                       // Chat archive management
+class CALogic;
+
+#define READ_BUFFER 8192                 // File read buffer
+
+// Markups used in archive files
+#define MARKUP_SESSION_START '%'         // Session start
+#define MARKUP_SESSION_DESC '!'          // Session description start
+#define MARKUP_SENT '>'                  // Sent item
+#define MARKUP_RECV '<'                  // Received item
+#define MARKUP_DELAYED '|'               // Delayed item
+#define MARKUP_CHAT 'c'                  // Regular chat
+#define MARKUP_ROOMCHAT 'r'              // MUC room chat
+#define MARKUP_ROOMCHATPRIVATE 'p'       // MUC private chat
+
+enum CASearchRange {
+    CASearchRangeInvalid = 0,
+    CASearchRangeSession,
+    CASearchRangeContact,
+    CASearchRangeAll
+};
+
+// Archive search worker thread
+class CASearchThread : public Thread
+{
+public:
+    CASearchThread();
+    ~CASearchThread();
+    void startSearching(const String& text, bool next);
+    virtual void run();
+private:
+    void resetSearch();
+    void searchAll(const String& what);
+    void searchCurrentContact(const String& what);
+    bool searchContact(ChatFile* f, const String& what, bool changed);
+
+    bool m_startSearch;                  // Start search flag
+    bool m_searching;                    // Currently searching
+    bool m_next;
+    String m_what;
+    CASearchRange m_range;
+    String m_currentContact;
+    String m_currentSession;
+    bool m_currentSessionFull;
+    bool m_currentContactFull;
+};
+
+// Archive refresh worker thread
+class CARefreshThread : public Thread
+{
+public:
+    CARefreshThread();
+    ~CARefreshThread();
+    virtual void run();
+};
+
+// A chat session entry
+class ChatSession : public String
+{
+public:
+    inline ChatSession(const String& id, const String& name, int64_t offset)
+	: String(id), m_name(name), m_offset(offset), m_length(0)
+	{}
+    String m_name;
+    String m_desc;                       // Description
+    int64_t m_offset;                    // File offset
+    int64_t m_length;                    // Session length (including header)
+};
+
+// A chat session entry
+class ChatItem : public GenObject
+{
+public:
+    inline ChatItem(unsigned int time, int t)
+	: m_time(time), m_type(t)
+	{}
+    unsigned int m_time;                 // Entry time
+    int m_type;                          // Type
+    String m_senderName;                 // Sender name
+    String m_text;                       // Content
+    QString m_search;                    // QString to be used when searching
+};
+
+// A contact's chat (including the file)
+class ChatFile : public Mutex, public RefObject
+{
+    friend class ChatArchive;
+public:
+    // File version. Old versions must be inserted before Current
+    enum Version {
+	Invalid = 0,
+	Current,
+    };
+    // Init object
+    ChatFile(const String& dir, const String& fileName);
+    // Retrieve the file type
+    inline char type() const
+	{ return m_type; }
+    // Retrieve the file account. Lock it before use
+    inline const String& account() const
+	{ return m_account; }
+    // Retrieve the file contact. Lock it before use
+    inline const String& contact() const
+	{ return m_contact; }
+    // Retrieve the file contact name. Lock it before use
+    inline const String& contactName() const
+	{ return m_contactName; }
+    // Retrieve the file contact display name. Lock it before use
+    inline const String& contactDisplayName() const
+	{ return m_contactName ? m_contactName : m_contact; }
+    // Retrieve the id of the room owning a private chat. Lock it before use
+    inline const String& roomId() const
+	{ return m_roomId; }
+    // Retrieve the file sessions. Lock it before use
+    inline const ObjList& sessions() const
+	{ return m_sessions; }
+    // Load the file. Created it if not found and params are given
+    // This method is thread safe
+    virtual bool loadFile(const NamedList* params, String* error);
+    // Write chat to file
+    // This method is thread safe
+    virtual bool writeChat(const NamedList& params);
+    // Load sessions from file
+    // This method is thread safe
+    virtual bool loadSessions(bool forceLoad = false, String* error = 0);
+    // Load a session from file
+    // This method is thread safe
+    virtual bool loadSession(const String& id, ObjList& list, String* error = 0,
+	QString* search = 0);
+    // Retrieve the last session. Lock the object before use
+    virtual ChatSession* lastSession();
+    // Close current write session. Load it if sessions were loaded
+    // This method is thread safe
+    virtual bool closeSession();
+    // Decode a ChatItem from a given buffer. Return it on success
+    ChatItem* decodeChat(bool search, int64_t offset, void* buffer, unsigned int len);
+    // Retrieve the id
+    virtual const String& toString() const
+	{ return m_fileName; }
+protected:
+    virtual void destroyed() {
+	    closeSession();
+	    RefObject::destroyed();
+	}
+    // Set file last error. Close it if requested. Return false
+    bool setFileError(String* error, const char* oper, bool close = false,
+	bool del = false);
+    // Show a chat entry format error
+    inline void showEntryError(int level, const char* oper, int64_t offset) {
+	    Debug(ClientDriver::self(),level,
+		"File '%s' chat entry (offset " FMT64 ") error: %s",
+		m_full.c_str(),offset,oper);
+	}
+    // Set file pos
+    inline bool seekFile(int64_t offset, String* error) {
+	    bool ok = m_file.seek(Socket::SeekBegin,offset) >= 0;
+	    if (!ok)
+		setFileError(0,"seek");
+	    return ok;
+	}
+    // Write a buffer to the file
+    int writeData(const void* buf, unsigned int len, String* error);
+    // Write file header. Close the file if fails
+    virtual bool readFileHeader(String* error);
+    // Update data. Write file header. Close the file and delete it if fails
+    virtual bool writeFileHeader(const NamedList& params, String* error);
+
+    int m_version;
+    char m_type;
+    String m_account;
+    String m_contact;
+    String m_contactName;
+    String m_roomId;                     // Parent room id if this is a private room chat
+    String m_fileName;
+    String m_full;
+    File m_file;
+    unsigned int m_hdrLen;
+    int64_t m_newSessionOffset;          // Recording session file offset
+    DataBlock m_writeBuffer;
+    bool m_sessionsLoaded;
+    ObjList m_sessions;
+};
+
+// The chat archive container
+class ChatArchive : public Mutex
+{
+public:
+    ChatArchive();
+    inline bool loaded() const
+	{ return m_loaded; }
+    // Retrieve the files list. Lock it before use
+    inline const ObjList& items() const
+	{ return m_items; }
+    // Init data when engine starts. Return the index file
+    void init();
+    // Refresh the list. Re-load all archive
+    void refresh();
+    // Clear all
+    void clear(bool memoryOnly);
+    // Clear all logs belonging to a given account
+    void clearAccount(const String& account, ObjList& removedItems);
+    // Remove an item and it's file
+    void delFile(const String& id);
+    // Retrieve a chat file. Return a referenced object
+    ChatFile* loadChatFile(const String& file, bool forceLoad = false);
+    // Retrieve a chat file. Return a referenced object
+    ChatFile* getChatFile(const String& id);
+    // Retrieve a chat file. Return a refferenced object
+    inline ChatFile* getChatFile(const NamedList& params) {
+	    String id;
+	    if (buildChatFileName(id,params))
+		return getChatFile(id);
+	    return 0;
+	}
+    // Retrieve a chat file from session id. Return a refferenced object
+    inline ChatFile* getChatFileBySession(const String& id) {
+	    int pos = id.find('/');
+	    return (pos > 0) ? getChatFile(id.substr(0,pos)) : 0;
+	}
+    // Retrieve a chat file. Return a referenced object
+    ChatFile* getChatFile(const NamedList& params, const NamedList* createParams);
+    // Add a chat message to log
+    bool logChat(NamedList& params);
+    // Close a chat session. Return a referenced pointer if the item's last
+    //  session was loaded into memory
+    ChatFile* closeChat(const NamedList& params);
+    // Build a file name from a list of parameters
+    static inline void buildChatFileName(String& buf, char type, const String& account,
+	const String& contact, const String& nick = String::empty());
+    // Build a file name from a list of parameters
+    static inline bool buildChatFileName(String& buf, const NamedList& params);
+protected:
+    bool m_loaded;                       // Archive loaded
+    String m_dir;                        // Directory containing the archive
+    Configuration m_index;               // Index file
+    ObjList m_items;
+};
+
+// The logic
+class CALogic : public ClientLogic
+{
+public:
+    CALogic(int prio = 0);
+    ~CALogic();
+    // Load notifications
+    virtual bool initializedClient();
+    virtual void exitingClient();
+    // Engine start notification
+    void engineStart(Message& msg);
+    // Actions from UI
+    virtual bool action(Window* wnd, const String& name, NamedList* params = 0);
+    virtual bool select(Window* wnd, const String& name, const String& item,
+	const String& text = String::empty());
+    virtual bool toggle(Window* wnd, const String& name, bool active);
+    // Stop the search thread and wait for terminate
+    void searchStop();
+    // Search thread terminated
+    void searchTerminated()
+	{ m_searchThread = 0; }
+    // Start archive refresh
+    void refreshStart(const String* selected = 0);
+    // Archive refresh terminated. Refresh UI
+    void refreshTerminated();
+    // Stop the refresh thread and wait for terminate
+    void refreshStop();
+    // Set control highlight
+    bool setSearchHistory(const String& what, bool next);
+    // Reset control highlight
+    bool resetSearchHistory(bool reset = true);
+    // Select and set search history. Return true on success
+    bool setSearch(bool reset, const String& file, const String& session,
+	const String& what, bool next);
+protected:
+    // Load a chat item into UI
+    bool loadChat(const NamedList& params);
+    // Close a chat session
+    bool closeChat(const NamedList& params);
+    // Update sessions related to a given item
+    bool updateSessions(const String& id, Window* wnd);
+    // Update session content in UI
+    bool updateSession(const String& id, Window* wnd);
+    // Save current session
+    bool saveSession(Window* wnd, NamedList* params = 0);
+    // Delete selected contact
+    bool delContact(Window* wnd);
+    // Clear all archive
+    bool clearLog(Window* wnd);
+
+    bool m_resetSearchOnSel;             // Reset search when session selection changes
+    CASearchThread* m_searchThread;
+    CARefreshThread* m_refreshThread;
+    String m_selectAfterRefresh;
+    String m_searchText;
+};
+
+
+/*
+ * Module data
+ */
+// UI controls
+static const String s_wndArch = "archive";
+// Prefixes
+static const String s_archPrefix = "archive:";
+// Widgets
+static const String s_logList = "archive_logs_list";
+static const String s_sessList = "archive_session_list";
+static const String s_sessHistory = "archive_session_history";
+static const String s_searchShow = "archive_search_show";
+static const String s_searchHide = "archive_search_hide";
+static const String s_searchEdit = "archive_search_edit";
+static const String s_searchStart = "archive_search_start";
+static const String s_searchPrev = "archive_search_prev";
+static const String s_searchNext = "archive_search_next";
+static const String s_searchRange = "archive_search_range";
+static const String s_searchMatchCase = "archive_search_opt_matchcase";
+static const String s_searchHighlightAll = "archive_search_opt_highlightall";
+// Actions
+static const String& s_actionLogChat = "logchat";
+static const String& s_actionSelectChat = "showchat";
+static const String& s_actionCloseChat = "closechatsession";
+static const String& s_actionRefresh = "archive_refresh";
+static const String& s_actionClear = "clear";
+static const String& s_actionClearNow = "clearnow";
+static const String& s_actionClearAccNow = "clearaccountnow";
+static const String& s_actionDelContact = "delcontact";
+static const String& s_actionDelContactNow = "delcontactnow";
+// Data
+static const DataBlock s_zeroDb(0,1);
+static const String s_crlf = "\r\n";
+static Mutex s_mutex(true,"CALogic");
+static CALogic s_logic(-50);             // The logic
+static ChatArchive s_chatArchive;        // Archive holder
+static CASearchRange s_range = CASearchRangeContact;
+static bool s_matchCase = false;
+static bool s_highlightAll = false;
+// Search range values
+static const TokenDict s_searchListRange[] = {
+    {"Current contact", CASearchRangeContact},
+    {"Current session", CASearchRangeSession},
+    {"All archive",     CASearchRangeAll},
+    {0,0},
+};
+
+// Check if exiting: client is exiting or thread cancel requested
+static bool exiting()
+{
+    return Client::exiting() || Thread::check(false);
+}
+
+// Retrieve the window
+static inline Window* getWindow()
+{
+    return Client::self() ? Client::self()->getWindow(s_wndArch) : 0;
+}
+
+// Retrieve the chat type from a list of parameters
+static inline char chatType(const NamedList& params)
+{
+    if (!params.getBoolValue("muc"))
+	return MARKUP_CHAT;
+    if (params.getBoolValue("roomchat",true))
+	return MARKUP_ROOMCHAT;
+    return MARKUP_ROOMCHATPRIVATE;
+}
+
+// Show a confirm dialog box in a given window
+static bool showConfirm(Window* wnd, const char* text, const char* context)
+{
+    static const String name = "archive_confirm";
+    if (!Client::valid())
+	return false;
+    NamedList p("");
+    p.addParam("text",text);
+    p.addParam("property:" + name + ":_yate_context",context);
+    return Client::self()->createDialog("confirm",wnd,String::empty(),name,&p);
+}
+
+// Show an error dialog box in a given window
+static void showError(Window* wnd, const char* text)
+{
+    static const String name = "archive_error";
+    if (!Client::valid())
+	return;
+    NamedList p("");
+    p.addParam("text",text);
+    Client::self()->createDialog("message",wnd,String::empty(),name,&p);
+}
+
+// Show a dialog used to notify a status and freeze the window
+static void showFreezeDlg(Window* w, const String& name, const char* text)
+{
+    NamedList p("");
+    p.addParam("text",text);
+    p.addParam("show:button_hide",String::boolText(false));
+    p.addParam("_yate_windowflags","title");
+    p.addParam("closable","false");
+    Client::self()->createDialog("message",w,"Archive",name,&p);
+}
+
+// Retrieve the previuos item from a list
+static ObjList* getListPrevItem(const ObjList& list, const String& value)
+{
+    ObjList* last = 0;
+    ObjList* o = list.skipNull();
+    for (; o; o = o->skipNext()) {
+	if (o->get()->toString() == value)
+	    break;
+	last = o;
+    }
+    return o ? last : 0;
+}
+
+// Retrieve the last item from a list
+static ObjList* getListLastItem(const ObjList& list)
+{
+    ObjList* last = 0;
+    for (ObjList* o = list.skipNull(); o; o = o->skipNext())
+	last = o;
+    return last;
+}
+
+// Retrieve the chat type string
+inline const String& chatType(int type)
+{
+    static const String s_out = "chat_out";
+    static const String s_in = "chat_in";
+    static const String s_delayed = "chat_delayed";
+    if (type == MARKUP_SENT)
+	return s_out;
+    if (type == MARKUP_RECV)
+	return s_in;
+    if (type == MARKUP_DELAYED)
+	return s_delayed;
+    return String::empty();
+}
+
+// Retrieve the UI item type from chat file type
+static inline const char* uiItemType(char type)
+{
+    if (type == MARKUP_CHAT)
+	return "chat";
+    if (type == MARKUP_ROOMCHAT)
+	return "roomchat";
+    return "roomprivchat";
+}
+
+// Find 2 NULL values in a buffer. Return buffer len if not found
+unsigned int find2Null(unsigned char* buf, unsigned int len)
+{
+    for (unsigned int n = 0; n < len; n++) {
+	if (buf[n] == 0 && (n < len - 1) && (buf[n + 1] == 0))
+	    return n;
+    }
+    return len;
+}
+
+// Find a line in text buffer (until CR/LF, single CR or LF).
+// Return the line length, excluding the line terminator
+unsigned int findLine(const char* buf, unsigned int len, unsigned int& eolnLen)
+{
+    eolnLen = 0;
+    if (!buf)
+	return 0;
+    unsigned int i = 0;
+    for (; i < len; i++) {
+	if (buf[i] == '\r') {
+	    if (i < len - 1 && buf[i + 1] == '\n')
+		eolnLen = 2;
+	    else
+		eolnLen = 1;
+	    return i;
+	}
+	if (buf[i] == '\n') {
+	    eolnLen = 1;
+	    return i;
+	}
+    }
+    return i;
+}
+
+// Append a string to data block including the terminator
+static void appendString(DataBlock& buf, const String& src)
+{
+    if (src) {
+	DataBlock tmp;
+	tmp.assign((void*)src.c_str(),src.length() + 1,false);
+	buf += tmp;
+	tmp.clear(false);
+    }
+    else
+	buf += s_zeroDb;
+}
+
+// Append an integer value to a data block including a null terminator
+static inline void appendInt(DataBlock& buf, int value)
+{
+    String tmp(value);
+    appendString(buf,tmp);
+}
+
+// Build chat file UI params
+static NamedList* chatFileUiParams(ChatFile* f)
+{
+    if (!f)
+	return 0;
+    Lock lock(f);
+    NamedList* upd = new NamedList(f->toString());
+    upd->addParam("item_type",uiItemType(f->type()));
+    upd->addParam("account",f->account());
+    upd->addParam("contact",f->contact());
+    if (f->type() == MARKUP_CHAT)
+	upd->addParam("name",f->contactDisplayName());
+    else if (f->type() == MARKUP_ROOMCHAT)
+	upd->addParam("name",f->contact());
+    else {
+        upd->addParam("parent",f->roomId());
+	upd->addParam("name",f->contactDisplayName());
+    }
+    return upd;
+}
+
+// Build a chat session UI params
+static NamedList* chatSessionUiParams(ChatSession* s)
+{
+    if (!s)
+	return 0;
+    NamedList* upd = new NamedList(s->toString());
+    String time;
+    Client::self()->formatDateTime(time,(unsigned int)s->m_name.toInteger(),
+	"yyyy.MM.dd hh:mm:ss",false);
+    // Show the first 2 lines from description
+    unsigned int len = s->m_desc.length();
+    unsigned int tmp = 0;
+    unsigned int ln = findLine(s->m_desc.c_str(),len,tmp);
+    if (ln != len) {
+	len = ln + tmp;
+	unsigned int tmp2 = 0;
+	ln = findLine(s->m_desc.c_str() + len,s->m_desc.length() - len,tmp2);
+	if (!ln)
+	    len -= tmp;
+	else
+	    len += ln;
+    }
+    String desc;
+    if (len == s->m_desc.length())
+	desc = s->m_desc;
+    else
+	desc = s->m_desc.substr(0,len);
+    desc.trimBlanks();
+    upd->addParam("datetime",time);
+    upd->addParam("description",desc);
+    upd->addParam("property:toolTip",time + "\r\n" + s->m_desc);
+    return upd;
+}
+
+// Enable/disable search
+static void enableSearch(bool ok)
+{
+    Window* w = getWindow();
+    if (!w)
+	return;
+    const char* text = String::boolText(ok);
+    NamedList p("");
+    p.addParam("active:" + s_searchShow,text);
+    p.addParam("active:" + s_searchHide,text);
+    p.addParam("active:" + s_searchEdit,text);
+    p.addParam("active:" + s_searchStart,text);
+    p.addParam("active:" + s_searchPrev,text);
+    p.addParam("active:" + s_searchNext,text);
+    p.addParam("active:" + s_searchRange,text);
+    p.addParam("active:" + s_searchMatchCase,text);
+    p.addParam("active:" + s_searchHighlightAll,text);
+    p.addParam("active:" + s_actionRefresh,text);
+    Client::self()->setParams(&p,w);
+}
+
+
+/*
+ * ChatFile
+ */
+// Init object
+ChatFile::ChatFile(const String& dir, const String& fileName)
+    : Mutex(true,"Archive::ChatFile"),
+    m_version(Current),
+    m_type(MARKUP_CHAT),
+    m_fileName(fileName),
+    m_full(dir + "/" + fileName),
+    m_hdrLen(0),
+    m_newSessionOffset(0),
+    m_sessionsLoaded(false)
+{
+}
+
+// Load the file. Created it if not found and params are given
+bool ChatFile::loadFile(const NamedList* params, String* error)
+{
+    Lock lock(this);
+    closeSession();
+    m_file.terminate();
+    m_sessionsLoaded = false;
+    m_sessions.clear();
+    bool ok = m_file.openPath(m_full,true,true,params != 0,true,true);
+    if (!ok)
+	return setFileError(error,"open",true);
+    int64_t sz = m_file.length();
+    if (sz < 0)
+	return setFileError(error,"get length",true);
+    // Read/write file header
+    if (sz) {
+	if (!readFileHeader(error))
+	    return false;
+    }
+    else if (!(params && writeFileHeader(*params,error)))
+	return false;
+    m_roomId.clear();
+    // Build the room id if this is a private chat
+    if (m_type == MARKUP_ROOMCHATPRIVATE)
+	ChatArchive::buildChatFileName(m_roomId,MARKUP_ROOMCHAT,m_account,m_contact);
+    return true;
+}
+
+// Write chat to file
+bool ChatFile::writeChat(const NamedList& params)
+{
+    Lock lock(this);
+    const String& text = params["text"];
+    if (!text)
+	return false;
+    String time = params["time"];
+    if (!time)
+	time = (int)Time::now();
+    if (!m_newSessionOffset) {
+	m_newSessionOffset = m_file.seek(Socket::SeekEnd);
+	if (m_newSessionOffset < m_hdrLen)
+	    return false;
+	String tmp;
+	tmp << MARKUP_SESSION_START << time;
+	tmp << MARKUP_SESSION_DESC << text;
+	m_writeBuffer.append(tmp);
+	m_writeBuffer += s_zeroDb;
+	m_writeBuffer += s_zeroDb;
+    }
+    m_writeBuffer.append(time);
+    String type;
+    if (params.getBoolValue("send"))
+	type = MARKUP_SENT;
+    else if (!params.getBoolValue("delayed"))
+	type = MARKUP_RECV;
+    else
+	type = MARKUP_DELAYED;
+    m_writeBuffer.append(type);
+    appendString(m_writeBuffer,params["sender"]);
+    appendString(m_writeBuffer,text);
+    m_writeBuffer += s_zeroDb;
+    int wr = writeData(m_writeBuffer.data(),m_writeBuffer.length(),0);
+    if (wr < 0)
+	return false;
+    if (wr) {
+	if (wr != (int)m_writeBuffer.length())
+	    m_writeBuffer.cut(-wr);
+	else
+	    m_writeBuffer.clear();
+    }
+    return true;
+}
+
+// Load sessions from file
+bool ChatFile::loadSessions(bool forceLoad, String* error)
+{
+    Lock lock(this);
+    if (m_sessionsLoaded && !forceLoad)
+	return true;
+    m_sessionsLoaded = true;
+    m_sessions.clear();
+    int64_t offset = m_hdrLen;
+    if (!seekFile(offset,error))
+	return false;
+    String prefix(toString() + "/");
+    unsigned int index = 0;
+    char rdBuf[READ_BUFFER];
+    DataBlock buf;
+    ChatSession* s = 0;
+    bool ok = true;
+    while (true) {
+	int rd = m_file.readData(rdBuf,sizeof(rdBuf));
+	if (rd < 0) {
+	    ok = setFileError(error,"read");
+	    break;
+	}
+	if (!rd)
+	    break;
+	if (exiting())
+	    break;
+	buf.append(rdBuf,rd);
+	unsigned int n = find2Null((unsigned char*)buf.data(),buf.length());
+	while (n < buf.length()) {
+	    if (exiting())
+		break;
+	    String str((const char*)buf.data(),n);
+	    if ((str.length() > 1) && (str[0] == MARKUP_SESSION_START)) {
+		if (s)
+		    s->m_length = offset - s->m_offset;
+		int pos = str.find(MARKUP_SESSION_DESC);
+		s = new ChatSession(prefix + String(++index),
+		    str.substr(1,pos > 0 ? pos - 1 : 0),offset);
+		if (pos > 0)
+		    s->m_desc = str.substr(pos + 1);
+		m_sessions.append(s);
+	    }
+	    n += 2;
+	    offset += n;
+	    buf.cut(-(int)n);
+	    n = find2Null((unsigned char*)buf.data(),buf.length());
+	}
+    }
+    if (!exiting()) {
+	// Finalize the last session
+	if (s)
+	    s->m_length = offset + buf.length() - s->m_offset;
+    }
+    else {
+	m_sessionsLoaded = false;
+	m_sessions.clear();
+    }
+    return ok;
+}
+
+// Load a session from file
+// This method is thread safe
+bool ChatFile::loadSession(const String& id, ObjList& list, String* error,
+    QString* search)
+{
+    if (!id)
+	return false;
+    Lock lock(this);
+    ObjList* o = m_sessions.find(id);
+    if (!o)
+	return false;
+    ChatSession* s = static_cast<ChatSession*>(o->get());
+    if (!seekFile(s->m_offset,error))
+	return false;
+    bool find = search != 0;
+    Qt::CaseSensitivity cs = s_matchCase ? Qt::CaseSensitive : Qt::CaseInsensitive;
+    char rdBuf[READ_BUFFER];
+    DataBlock buf;
+    bool hdrFound = false;
+    bool ok = !find;
+    int64_t processed = 0;
+    while (processed < s->m_length && !exiting()) {
+	int rd = m_file.readData(rdBuf,sizeof(rdBuf));
+	if (rd < 0) {
+	    ok = setFileError(error,"read");
+	    break;
+	}
+	if (!rd)
+	    break;
+	buf.append(rdBuf,rd);
+	unsigned int n = find2Null((unsigned char*)buf.data(),buf.length());
+	while (n < buf.length()) {
+	    if (exiting())
+		break;
+	    if (hdrFound) {
+		ChatItem* entry = decodeChat(find,s->m_offset + processed,buf.data(),n);
+		if (entry) {
+		    if (!find)
+			list.append(entry);
+		    else {
+			int pos = entry->m_search.indexOf(*search,0,cs);
+			TelEngine::destruct(entry);
+			if (pos >= 0) {
+			    ok = true;
+			    break;
+			}
+		    }
+		}
+	    }
+	    else
+		hdrFound = true;
+	    n += 2;
+	    processed += n;
+	    buf.cut(-(int)n);
+	    if (processed >= s->m_length)
+		break;
+	    n = find2Null((unsigned char*)buf.data(),buf.length());
+	}
+	if (find && ok)
+	    break;
+    }
+    if (!exiting()) {
+	if (processed < s->m_length && !(find && ok))
+	    Debug(ClientDriver::self(),DebugNote,
+		"File '%s' unexpected end of session at offset " FMT64U,
+		m_full.c_str(),s->m_offset + processed);
+    }
+    else
+	list.clear();
+    return ok;
+}
+
+// Retrieve the last session. Lock the object before use
+ChatSession* ChatFile::lastSession()
+{
+    if (!m_sessionsLoaded)
+	loadSessions();
+    ObjList* o = getListLastItem(m_sessions);
+    return o ? static_cast<ChatSession*>(o->get()) : 0;
+}
+
+// Close current write session. Load it if sessions were loaded
+bool ChatFile::closeSession()
+{
+    Lock lock(this);
+    if (m_newSessionOffset && m_writeBuffer.length())
+	writeData(m_writeBuffer.data(),m_writeBuffer.length(),0);
+    m_writeBuffer.clear();
+    bool ok = m_sessionsLoaded && m_newSessionOffset;
+    if (ok) {
+	m_sessionsLoaded = false;
+	m_sessions.clear();
+	loadSessions();
+    }
+    m_newSessionOffset = 0;
+    return ok;
+}
+
+// Decode a ChatItem from a given buffer. Return it on success
+ChatItem* ChatFile::decodeChat(bool search, int64_t offset, void* buffer,
+    unsigned int len)
+{
+    unsigned char* buf = (unsigned char*)buffer;
+    if (!(buf && len))
+	return 0;
+    unsigned int i = 0;
+    // Get time
+    for (; i < len; i++) {
+	switch (buf[i]) {
+	    case '0':
+	    case '1':
+	    case '2':
+	    case '3':
+	    case '4':
+	    case '5':
+	    case '6':
+	    case '7':
+	    case '8':
+	    case '9':
+		continue;
+	}
+	break;
+    }
+    int time = 0;
+    if (i) {
+	String tmp((const char*)buf,i);
+	time = tmp.toInteger();
+    }
+    else
+	showEntryError(DebugNote,"Invalid time",offset);
+    if (i == len) {
+	showEntryError(DebugNote,"Missing type",offset);
+	return 0;
+    }
+    int type = buf[i++];
+    switch (type) {
+	case MARKUP_SENT:
+	case MARKUP_RECV:
+	case MARKUP_DELAYED:
+	    break;
+	case 0:
+	    showEntryError(DebugNote,"Missing type",offset);
+	    return 0;
+	default:
+	    showEntryError(DebugStub,"Unknown type",offset);
+    }
+    if (i == len) {
+	showEntryError(DebugNote,"Unexpected end of entry after type",offset);
+	return 0;
+    }
+    ChatItem* entry = new ChatItem(time,type);
+    entry->m_senderName.assign((const char*)buf + i,len - i);
+    i += entry->m_senderName.length();
+    if (i >= len) {
+	showEntryError(DebugNote,"Unexpected end of chat item after sender name",offset);
+	return entry;
+    }
+    if (buf[i++] != 0) {
+	showEntryError(DebugMild,"Expecting NULL after sender name",offset);
+	return entry;
+    }
+    if (i == len)
+	return entry;
+    if (!search) {
+	entry->m_text.assign((const char*)buf + i,len - i);
+	i += entry->m_text.length();
+    }
+    else {
+	unsigned int start = i;
+	while (i < len && buf[i])
+	    i++;
+	QByteArray a((const char*)buf + start,i - start);
+	entry->m_search = a;
+    }
+    if (i < len)
+	showEntryError(DebugStub,"Got garbage after text",offset);
+    return entry;
+}
+
+// Set file last error. Close it if requested. Return false
+bool ChatFile::setFileError(String* error, const char* oper, bool close, bool del)
+{
+    String tmp;
+    if (!error)
+	error = &tmp;
+    int code = Thread::lastError();
+    Thread::errorString(*error,code);
+    Debug(ClientDriver::self(),DebugNote,"File '%s' %s error: %d %s",m_full.c_str(),
+	oper,code,error->c_str());
+    if (close) {
+	Debug(ClientDriver::self(),DebugInfo,"Closing file '%s'",m_full.c_str());
+	m_file.terminate();
+    }
+    if (del) {
+	Debug(ClientDriver::self(),DebugInfo,"Removing file '%s'",m_full.c_str());
+	File::remove(m_full);
+    }
+    return false;
+}
+
+// Write a string to the file
+int ChatFile::writeData(const void* buf, unsigned int len, String* error)
+{
+    if (m_file.seek(Stream::SeekEnd) <= 0) {
+	setFileError(error,"seek");
+	return -1;
+    }
+    int wr = m_file.writeData(buf,len);
+    if (wr != (int)len && !m_file.canRetry())
+	setFileError(error,"write");
+    return wr;
+}
+
+// Write file header. Close the file if fails
+bool ChatFile::readFileHeader(String* error)
+{
+    m_hdrLen = 0;
+    m_version = Invalid;
+    if (!seekFile(0,error)) {
+	m_file.terminate();
+	return false;
+    }
+    DataBlock buf;
+    unsigned char b[1024];
+    while (true) {
+	int rd = m_file.readData(b,sizeof(b));
+	if (rd < 0)
+	    return setFileError(error,"read",true,false);
+	if (!rd)
+	    return setFileError(error,"short header",true,false);
+	unsigned int n = find2Null(b,rd);
+	buf.append(b,n);
+	if (n < (unsigned int)rd)
+	    break;
+    }
+    if (!buf.length())
+	return setFileError(error,"short header",true,false);
+    unsigned int len = buf.length();
+    const char* s = (const char*)buf.data();
+    String str;
+    bool acc = false;
+    bool cont = false;
+    bool contName = false;
+    while (s) {
+	String str(s,len);
+	if (str.length() != len) {
+	    len = len - str.length() - 1;
+	    if (len)
+		s += str.length() + 1;
+	    else
+		s = 0;
+	}
+	else {
+	    len = 0;
+	    s = 0;
+	}
+	if (m_version == Invalid) {
+	    if (!str)
+		return setFileError(error,"invalid header",true,false);
+	    m_version = str.toInteger();
+	    if (m_version == Invalid || m_version > Current)
+		return setFileError(error,"unsupported version",true,false);
+	}
+	else if (!acc) {
+	    m_account = str;
+	    acc = true;
+	}
+	else if (!cont) {
+	    m_contact = str;
+	    cont = true;
+	}
+	else if (!contName) {
+	    m_contactName = str;
+	    contName = true;
+	}
+	else {
+	    m_type = 0;
+	    if (str.length() == 1)
+		m_type = str[0];
+	    if (m_type != MARKUP_CHAT && m_type != MARKUP_ROOMCHAT &&
+		m_type != MARKUP_ROOMCHATPRIVATE)
+		return setFileError(error,"unsupported chat type",true,false);
+	    break;
+	}
+    }
+    m_hdrLen = buf.length() + 2;
+    return true;
+}
+
+// Write file header. Close the file and delete it if fails
+bool ChatFile::writeFileHeader(const NamedList& params, String* error)
+{
+    m_account = params["account"];
+    m_contact = params["contact"];
+    m_contactName = params["contactname"];
+    m_type = chatType(params);
+    DataBlock buf;
+    appendInt(buf,m_version);
+    appendString(buf,m_account);
+    appendString(buf,m_contact);
+    appendString(buf,m_contactName);
+    buf.append(&m_type,1);
+    buf += s_zeroDb;
+    buf += s_zeroDb;
+    if (m_file.writeData(buf.data(),buf.length()) != (int)buf.length())
+	return setFileError(error,"write",true,true);
+    m_hdrLen = buf.length();
+    return true;
+}
+
+
+/*
+ * ChatArchive
+ */
+ChatArchive::ChatArchive()
+    : Mutex(true,"ChatArchive"),
+    m_loaded(false)
+{
+}
+
+// Init data when client starts
+void ChatArchive::init()
+{
+    m_dir = Engine::runParams().getValue("usercfgpath");
+    m_dir << "/archive";
+    if (!File::exists(m_dir))
+	File::mkDir(m_dir);
+    m_index = m_dir + "/index.conf";
+    m_index.load();
+}
+
+// Refresh the list. Re-load all archive
+void ChatArchive::refresh()
+{
+    Lock lock(this);
+    m_loaded = true;
+    unsigned int n = m_index.sections();
+    for (unsigned int i = 0; i < n; i++) {
+	if (exiting())
+	    break;
+	NamedList* sect = m_index.getSection(i);
+	if (!sect)
+	    continue;
+	const String& type = (*sect)["type"];
+	if (type.length() != 1)
+	    continue;
+	if (type[0] != MARKUP_CHAT && type[0] != MARKUP_ROOMCHAT &&
+	    type[0] != MARKUP_ROOMCHATPRIVATE)
+	    continue;
+	ChatFile* f = loadChatFile(*sect,true);
+	TelEngine::destruct(f);
+    }
+}
+
+// Clear all
+void ChatArchive::clear(bool memoryOnly)
+{
+    Lock lock(this);
+    m_items.clear();
+    if (memoryOnly)
+	return;
+    unsigned int n = m_index.sections();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedList* f = m_index.getSection(i);
+	if (f)
+	    File::remove(m_dir + "/" + *f);
+    }
+    m_index.clearSection();
+    m_index.save();
+}
+
+// Clear all logs belonging to a given account
+void ChatArchive::clearAccount(const String& account, ObjList& removedItems)
+{
+    if (!account)
+	return;
+    Lock lock(this);
+    String prefix("chat_" + String(account.hash()) + "_");
+    unsigned int n = m_index.sections();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedList* f = m_index.getSection(i);
+	if (f && f->startsWith(prefix,false)) {
+	    m_items.remove(*f);
+	    removedItems.append(new String(*f));
+	    File::remove(m_dir + "/" + *f);
+	}
+    }
+    for (ObjList* o = removedItems.skipNull(); o; o = o->skipNext())
+	m_index.clearSection(o->get()->toString());
+    m_index.save();
+}
+
+// Remove an item and it's file
+void ChatArchive::delFile(const String& id)
+{
+    if (!id)
+	return;
+    Lock lock(this);
+    m_items.remove(id);
+    File::remove(m_dir + "/" + id);
+    m_index.clearSection(id);
+    m_index.save();
+}
+
+// Retrieve a chat file. Return a referenced object
+ChatFile* ChatArchive::loadChatFile(const String& file, bool forceLoad)
+{
+    Lock lock(this);
+    ChatFile* f = getChatFile(file);
+    if (!f) {
+	f = new ChatFile(m_dir,file);
+	if (!f->loadFile(0,0)) {
+	    TelEngine::destruct(f);
+	    return 0;
+	}
+	f->ref();
+	m_items.append(f);
+    }
+    lock.drop();
+    f->loadSessions(forceLoad);
+    return f;
+}
+
+// Retrieve a chat file. Return a refferenced object
+ChatFile* ChatArchive::getChatFile(const String& id)
+{
+   Lock lock(this);
+   ObjList* o = m_items.find(id);
+   if (!o)
+       return 0;
+   ChatFile* f = static_cast<ChatFile*>(o->get());
+   f->ref();
+   return f;
+ }
+
+// Retrieve a chat file. Return a refferenced object
+ChatFile* ChatArchive::getChatFile(const NamedList& params,
+    const NamedList* createParams)
+{
+    String fn;
+    buildChatFileName(fn,params);
+    Lock lock(this);
+    ChatFile* f = getChatFile(fn);
+    if (f)
+	return f;
+    f = new ChatFile(m_dir,fn);
+    if (!f->loadFile(createParams,0)) {
+	TelEngine::destruct(f);
+	return 0;
+    }
+    f->lock();
+    m_index.setValue(fn,"type",String(f->type()));
+    m_index.setValue(fn,"account",f->account());
+    m_index.setValue(fn,"contact",f->contact());
+    if (f->contactName() && f->contactName() != m_index.getValue(fn,"contactname"))
+	m_index.setValue(fn,"contactname",f->m_contactName);
+    if (f->type() != MARKUP_ROOMCHATPRIVATE)
+	m_index.clearKey(fn,"room");
+    else
+	m_index.setValue(fn,"room",f->roomId());
+    f->unlock();
+    m_index.save();
+    m_items.append(f);
+    f->ref();
+    return f;
+}
+
+// Add a chat message to log
+bool ChatArchive::logChat(NamedList& params)
+{
+    ChatFile* f = getChatFile(params,&params);
+    bool ok = f && f->writeChat(params);
+    TelEngine::destruct(f);
+    return ok;
+}
+
+// Close a chat session. Add it to the ui if the contact is shown
+ChatFile* ChatArchive::closeChat(const NamedList& params)
+{
+    ChatFile* f = getChatFile(params);
+    if (f && f->closeSession())
+	return f;
+    TelEngine::destruct(f);
+    return 0;
+}
+
+// Build a file name from a list of parameters
+void ChatArchive::buildChatFileName(String& buf, char type, const String& account,
+    const String& contact, const String& nick)
+{
+    buf = "chat_";
+    buf << account.hash() << "_" << String(contact).toLower().hash();
+    if (type == MARKUP_ROOMCHATPRIVATE)
+	buf << "_" << nick.hash();
+    buf << "_" << type;
+}
+
+// Build a file name from a list of parameters
+bool ChatArchive::buildChatFileName(String& buf, const NamedList& params)
+{
+    const String& account = params["account"];
+    const String& contact = params["contact"];
+    if (!(account && contact))
+	return false;
+    char type = chatType(params);
+    const String& nick = (type != MARKUP_ROOMCHATPRIVATE) ?
+	String::empty() : params["contactname"];
+    if (type == MARKUP_ROOMCHATPRIVATE && !nick)
+	return false;
+    buildChatFileName(buf,type,account,contact,nick);
+    return true;
+}
+
+
+/*
+ * CALogic
+ */
+CALogic::CALogic(int prio)
+    : ClientLogic("clientarchive",prio),
+    m_resetSearchOnSel(true),
+    m_searchThread(0),
+    m_refreshThread(0)
+{
+}
+
+CALogic::~CALogic()
+{
+}
+
+bool CALogic::initializedClient()
+{
+    Window* w = getWindow();
+    // Update archive search range
+    for (const TokenDict* d = s_searchListRange; d->value; d++)
+	Client::self()->addOption(s_searchRange,d->token,false,String::empty(),w);
+    Client::self()->setSelect(s_searchRange,lookup(s_range,s_searchListRange),w);
+    // Load options
+    NamedList dummy("");
+    NamedList* arch = Client::s_settings.getSection("clientarchive");
+    if (!arch)
+	arch = &dummy;
+    // Setup window
+    if (w) {
+	const char* no = String::boolText(false);
+	NamedList p("");
+	p.addParam("show:archive_frame_search",no);
+	Client::self()->setParams(&p,w);
+    }
+    return false;
+}
+
+void CALogic::exitingClient()
+{
+    Client::self()->setVisible(s_wndArch,false);
+    // Clear data now: close sessions
+    s_chatArchive.clear(true);
+    // Stop workers
+    searchStop();
+    refreshStop();
+}
+
+void CALogic::engineStart(Message& msg)
+{
+    s_chatArchive.init();
+}
+
+bool CALogic::action(Window* wnd, const String& name, NamedList* params)
+{
+    String act = name;
+    if (act.startSkip(s_archPrefix,false)) {
+	// Chat log actions nedding parameters
+	if (params) {
+	    if (act == s_actionLogChat)
+		return s_chatArchive.logChat(*params);
+	    if (act == s_actionCloseChat)
+		return closeChat(*params);
+	    if (act == s_actionSelectChat) {
+		Window* w = getWindow();
+		if (w) {
+		    String id;
+		    ChatArchive::buildChatFileName(id,*params);
+		    if (s_chatArchive.loaded())
+			Client::self()->setSelect(s_logList,id,w);
+		    else
+			refreshStart(&id);
+		    Client::self()->setVisible(s_wndArch,true,true);
+		}
+		return w != 0;
+	    }
+	    if (act == s_actionClearAccNow) {
+		ObjList removed;
+		s_chatArchive.clearAccount((*params)["account"],removed);
+		Window* w = getWindow();
+		if (w)
+		    for (ObjList* o = removed.skipNull(); o; o = o->skipNext())
+			Client::self()->delTableRow(s_logList,o->get()->toString(),w);
+		return true;
+	    }
+	    if (act == "savesession")
+		return saveSession(wnd,params);
+	    return false;
+	}
+	bool confirm = (act == s_actionClear);
+	if (confirm || act == s_actionClearNow)
+	    return clearLog(confirm ? wnd : 0);
+	confirm = (act == s_actionDelContact);
+	if (confirm || act == s_actionDelContactNow)
+	    return delContact(confirm ? wnd : 0);
+    }
+    // Refresh all
+    if (name == s_actionRefresh) {
+	refreshStart();
+	return true;
+    }
+    // Search
+    bool next = (name == s_searchNext || name == s_searchStart);
+    if (next || name == s_searchPrev) {
+	String tmp;
+	Client::self()->getText(s_searchEdit,tmp,false,wnd);
+	Lock lock(s_mutex);
+	if (m_searchThread) {
+	    if (m_searchText != tmp) {
+		resetSearchHistory();
+		m_searchText = tmp;
+	    }
+	    m_searchThread->startSearching(m_searchText,next);
+	}
+	return true;
+    }
+    bool showSearch = (name == s_searchShow);
+    if (showSearch || name == s_searchHide) {
+	searchStop();
+	Window* w = getWindow();
+	if (showSearch) {
+	    if (!w)
+		return false;
+	    Client::self()->setFocus(s_searchEdit,false,w);
+	    Lock lock(s_mutex);
+	    m_searchThread = new CASearchThread;
+	    m_searchThread->startup();
+ 	}
+	else
+	    resetSearchHistory();
+	Client::self()->setShow("archive_frame_search",showSearch,w);
+	return true;
+    }
+    if (name == "archive_save_session")
+	return saveSession(wnd);
+    return false;
+}
+
+bool CALogic::select(Window* wnd, const String& name, const String& item,
+    const String& text)
+{
+    // Selection changed in log list
+    if (name == s_logList) {
+	updateSessions(item,wnd);
+	return true;
+    }
+    // Selection changed in sessions list
+    if (name == s_sessList) {
+	if (m_resetSearchOnSel)
+	    resetSearchHistory(false);
+	return updateSession(item,wnd);
+    }
+    // Search range
+    if (name == s_searchRange) {
+	int r = lookup(item,s_searchListRange);
+	if (r)
+	    s_range = (CASearchRange)r;
+	return true;
+    }
+    return false;
+}
+
+bool CALogic::toggle(Window* wnd, const String& name, bool active)
+{
+    // Search options
+    if (name == s_searchMatchCase) {
+	s_matchCase = active;
+	return true;
+    }
+    if (name == s_searchHighlightAll) {
+	s_highlightAll = active;
+	return true;
+    }
+    // Window visibility changed
+    if (name == "window_visible_changed") {
+	if (wnd && wnd->id() == s_wndArch) {
+	    if (active && !s_chatArchive.loaded())
+		refreshStart();
+	}
+	return false;
+    }
+    return false;
+}
+
+// Stop the search thread and wait for terminate
+void CALogic::searchStop()
+{
+    s_mutex.lock();
+    if (m_searchThread)
+	m_searchThread->cancel(false);
+    s_mutex.unlock();
+    while (m_searchThread)
+	Thread::idle();
+}
+
+// Start archive refresh
+void CALogic::refreshStart(const String* selected)
+{
+    Window* w = getWindow();
+    if (!w)
+	return;
+    Lock lock(s_mutex);
+    if (selected)
+	m_selectAfterRefresh = *selected;
+    if (m_refreshThread)
+	return;
+    m_refreshThread = new CARefreshThread;
+    lock.drop();
+    showFreezeDlg(w,"archive_refresh","Refreshing ....");
+    m_refreshThread->startup();
+}
+
+// Archive refresh terminated. Refresh UI
+void CALogic::refreshTerminated()
+{
+    s_mutex.lock();
+    String sel = m_selectAfterRefresh;
+    m_refreshThread = 0;
+    m_selectAfterRefresh.clear();
+    Window* w = !exiting() ? getWindow() : 0;
+    s_mutex.unlock();
+    if (!w)
+	return;
+    // Update UI
+    int count = 10;
+    s_chatArchive.lock();
+    NamedList p("");
+    for (ObjList* o = s_chatArchive.items().skipNull(); o; o = o->skipNext()) {
+	if (exiting())
+	    break;
+	ChatFile* f = static_cast<ChatFile*>(o->get());
+	Lock lock(f);
+	f->loadSessions();
+	NamedList* upd = chatFileUiParams(f);
+	// Check if the room is already displayed. Create it if not found
+	if (f->type() == MARKUP_ROOMCHATPRIVATE && f->roomId() &&
+	    !(p.getParam(f->roomId()) ||
+	    Client::self()->getTableRow(s_logList,f->roomId(),0,w))) {
+	    NamedList* upd2 = 0;
+	    ChatFile* parent = s_chatArchive.getChatFile(f->roomId());
+	    if (parent)
+		upd2 = chatFileUiParams(parent);
+	    else {
+		upd2 = new NamedList("");
+		upd2->addParam("item_type",uiItemType(MARKUP_ROOMCHAT));
+		upd2->addParam("account",f->account());
+		upd2->addParam("contact",f->contact());
+		upd2->addParam("name",f->contact());
+	    }
+	    p.addParam(new NamedPointer(f->roomId(),upd2,String::boolText(true)));
+	    TelEngine::destruct(parent);
+	}
+	p.addParam(new NamedPointer(f->toString(),upd,String::boolText(true)));
+	count--;
+	if (!count) {
+	    count = 10;
+	    Client::self()->updateTableRows(s_logList,&p,false,w);
+	    p.clear();
+	}
+    }
+    s_chatArchive.unlock();
+    if (!exiting()) {
+	Client::self()->updateTableRows(s_logList,&p,false,w);
+	if (sel)
+	    Client::self()->setSelect(s_logList,sel,w);
+    }
+    Client::self()->closeDialog("archive_refresh",w);
+}
+
+// Stop the refresh thread and wait for terminate
+void CALogic::refreshStop()
+{
+    s_mutex.lock();
+    if (m_refreshThread)
+	m_refreshThread->cancel(false);
+    s_mutex.unlock();
+    while (m_refreshThread)
+	Thread::idle();
+}
+
+// Close a chat session
+bool CALogic::closeChat(const NamedList& params)
+{
+    ChatFile* f = s_chatArchive.closeChat(params);
+    Window* w = f ? getWindow() : 0;
+    if (w) {
+	String tmp;
+	Client::self()->getSelect(s_logList,tmp,w);
+	if (tmp == f->toString()) {
+	    NamedList p("");
+	    f->lock();
+	    ChatSession* s = f->lastSession();
+	    if (s) {
+		NamedList* upd = chatSessionUiParams(s);
+		p.addParam(new NamedPointer(s->toString(),upd,String::boolText(true)));
+	    }
+	    f->unlock();
+	    Client::self()->updateTableRows(s_sessList,&p,false,w);
+	}
+    }
+    TelEngine::destruct(f);
+    return true;
+}
+
+// Update sessions related to a given item
+bool CALogic::updateSessions(const String& id, Window* wnd)
+{
+    if (!Client::self())
+	return false;
+    Client::self()->clearTable(s_sessList,wnd);
+    ChatFile* f = id ? s_chatArchive.getChatFile(id) : 0;
+    if (!f)
+	return true;
+    f->lock();
+    NamedList p("");
+    for (ObjList* o = f->sessions().skipNull(); o; o = o->skipNext()) {
+	ChatSession* s = static_cast<ChatSession*>(o->get());
+	NamedList* upd = chatSessionUiParams(s);
+	p.addParam(new NamedPointer(s->toString(),upd,String::boolText(true)));
+    }
+    f->unlock();
+    TelEngine::destruct(f);
+    Client::self()->updateTableRows(s_sessList,&p,false,wnd);
+    return true;
+}
+
+// Update session content in UI
+bool CALogic::updateSession(const String& id, Window* wnd)
+{
+    if (!Client::self())
+	return false;
+    Client::self()->clearTable(s_sessHistory,wnd);
+    ChatFile* f = s_chatArchive.getChatFileBySession(id);
+    if (!f)
+	return true;
+    f->lock();
+    ObjList list;
+    f->loadSession(id,list);
+    NamedList p("");
+    for (ObjList* o = list.skipNull(); o; o = o->skipNext()) {
+	ChatItem* e = static_cast<ChatItem*>(o->get());
+	NamedList* upd = new NamedList("");
+	String time;
+	if (e->m_type != MARKUP_DELAYED)
+	    Client::self()->formatDateTime(time,(unsigned int)e->m_time,"hh:mm:ss",false);
+	else
+	    Client::self()->formatDateTime(time,(unsigned int)e->m_time,"dd.MM.yyyy hh:mm:ss",false);
+	upd->addParam("time",time);
+	upd->addParam("text",e->m_text);
+	NamedString* sender = new NamedString("sender",e->m_senderName);
+	if (sender->null()) {
+	    if (e->m_type == MARKUP_SENT)
+		*sender = "me";
+	    else
+		*sender = f->contactDisplayName();
+	}
+	upd->addParam(sender);
+	p.addParam(new NamedPointer(chatType(e->m_type),upd,String::boolText(true)));
+    }
+    f->unlock();
+    TelEngine::destruct(f);
+    Client::self()->addLines(s_sessHistory,&p,0,false,wnd);
+    return true;
+}
+
+// Set control highlight
+bool CALogic::setSearchHistory(const String& what, bool next)
+{
+    Window* w = getWindow();
+    if (!w)
+	return false;
+    NamedList p(s_sessHistory);
+    NamedList* upd = new NamedList("");
+    p.addParam(new NamedPointer("search",upd,String::boolText(true)));
+    upd->addParam("find",what);
+    upd->addParam("matchcase",String::boolText(s_matchCase));
+    upd->addParam("all",String::boolText(s_highlightAll));
+    upd->addParam("next",String::boolText(next));
+    return Client::self()->setParams(&p,w);
+}
+
+// Reset control highlight
+bool CALogic::resetSearchHistory(bool reset)
+{
+    Window* w = getWindow();
+    if (!w)
+	return false;
+    NamedList p(s_sessHistory);
+    NamedList* upd = new NamedList("");
+    p.addParam(new NamedPointer("search",upd,String::boolText(false)));
+    upd->addParam("reset",String::boolText(reset));
+    return Client::self()->setParams(&p,w);
+}
+
+// Select and set search history. Return true on success
+bool CALogic::setSearch(bool reset, const String& file, const String& session,
+    const String& what, bool next)
+{
+    Window* w = getWindow();
+    if (!w)
+	return false;
+    m_resetSearchOnSel = reset;
+    Client::self()->setSelect(s_logList,file,w);
+    bool ok = Client::self()->setSelect(s_sessList,session,w) && setSearchHistory(what,next);
+    m_resetSearchOnSel = true;
+    return ok;
+}
+
+// Save current session
+bool CALogic::saveSession(Window* wnd, NamedList* params)
+{
+    if (!Client::valid())
+	return false;
+    String id;
+    Window* w = getWindow();
+    if (!w)
+	return false;
+    Client::self()->getSelect(s_sessList,id,w);
+    if (!id)
+	return false;
+    if (!params && wnd) {
+	NamedList p("");
+	p.addParam("action",s_archPrefix + "savesession");
+	p.addParam("save",String::boolText(true));
+	p.addParam("filters","Text files (*.txt)|All files (*)");
+	p.addParam("chooseanyfile",String::boolText(true));
+	return Client::self()->chooseFile(wnd,p);
+    }
+    if (!params)
+	return false;
+    const String& file = (*params)["file"];
+    if (!file)
+	return true;
+    const char* oper = 0;
+    while (true) {
+	File::remove(file);
+	File f;
+	if (!f.openPath(file,true,false,true)) {
+	    oper = "open";
+	    break;
+	}
+	String data;
+	Client::self()->getText(s_sessHistory,data,false,w);
+	int retry = 10;
+	unsigned int len = data.length();
+	const char* s = data.c_str();
+	String lineBuf;
+	while (retry && (len || lineBuf)) {
+	    if (!lineBuf) {
+		unsigned int eolnLen = 0;
+		unsigned int ln = findLine(s,len,eolnLen);
+		if (eolnLen == 2)
+		    lineBuf.assign(s,ln + 2);
+		else {
+		    lineBuf.assign(s,ln);
+		    lineBuf << "\r\n";
+		}
+		ln += eolnLen;
+		s += ln;
+		len -= ln;
+	    }
+	    int wr = f.writeData(lineBuf.c_str(),lineBuf.length());
+	    if (wr > 0) {
+		if ((unsigned int)wr == lineBuf.length())
+		    lineBuf.clear();
+		else
+		    lineBuf = lineBuf.substr(wr);
+	    }
+	    else if (!wr)
+		Thread::msleep(2);
+	    else if (f.canRetry())
+		retry--;
+	    else {
+		oper = "write";
+		break;
+	    }
+	}
+	break;
+    }
+    if (!oper)
+	return true;
+    String error;
+    Thread::errorString(error);
+    String text;
+    text << "Failed to " << oper << " '" << file << "'";
+    text.append(error,"\r\n");
+    showError(wnd,text);
+    return false;
+}
+
+// Clear all archive
+bool CALogic::delContact(Window* wnd)
+{
+    String id;
+    Window* w = getWindow();
+    if (!w)
+	return false;
+    Client::self()->getSelect(s_logList,id,w);
+    if (!id)
+	return false;
+    if (wnd &&
+	showConfirm(wnd,"Confirm selected contact log delete?",s_archPrefix + s_actionDelContactNow))
+	return true;
+    s_chatArchive.delFile(id);
+    Client::self()->delTableRow(s_logList,id,w);
+    return true;
+}
+
+// Clear all archive
+bool CALogic::clearLog(Window* wnd)
+{
+    if (wnd &&
+	showConfirm(wnd,"Confirm archive clear?",s_archPrefix + s_actionClearNow))
+	return true;
+    refreshStop();
+    Window* w = getWindow();
+    if (w) {
+	// This will stop the search thread
+	Client::self()->setShow("archive_frame_search",false,w);
+	Client::self()->clearTable(s_logList,w);
+	Client::self()->clearTable(s_sessList,w);
+	Client::self()->clearTable(s_sessHistory,w);
+    }
+    s_chatArchive.clear(false);
+    return true;
+}
+
+
+/*
+ * CASearchThread
+ */
+CASearchThread::CASearchThread()
+    : Thread("CASearchThread"),
+    m_startSearch(false),
+    m_searching(false),
+    m_next(true),
+    m_range(CASearchRangeInvalid),
+    m_currentSessionFull(false),
+    m_currentContactFull(false)
+{
+}
+
+CASearchThread::~CASearchThread()
+{
+    s_logic.searchTerminated();
+}
+
+void CASearchThread::startSearching(const String& text, bool next)
+{
+    CASearchRange old = m_range;
+    resetSearch();
+    Lock lock(s_mutex);
+    m_next = next;
+    m_range = s_range;
+    // Reset data if range changed
+    if (old != s_range || m_what != text) {
+	m_currentContact.clear();
+	m_currentSession.clear();
+	m_currentSessionFull = false;
+	m_currentContactFull = false;
+    }
+    m_what = text;
+    m_startSearch = true;
+}
+
+void CASearchThread::run()
+{
+    Debug(ClientDriver::self(),DebugAll,"%s start running",currentName());
+    while (true) {
+	if (exiting())
+	    break;
+	Lock lock(s_mutex);
+	if (!(m_what && m_startSearch)) {
+	    lock.drop();
+	    Thread::yield();
+	    continue;
+	}
+	String what = m_what;
+	m_startSearch = false;
+	lock.drop();
+	enableSearch(false);
+	m_searching = true;
+	switch (m_range) {
+	    case CASearchRangeSession:
+		s_logic.setSearchHistory(what,m_next);
+		break;
+	    case CASearchRangeContact:
+		searchCurrentContact(what);
+		break;
+	    case CASearchRangeAll:
+		searchAll(what);
+		break;
+	    default:
+		Debug(DebugStub,"%s range %d not implemented",currentName(),m_range);
+	}
+	m_searching = false;
+	enableSearch(true);
+    }
+    Debug(ClientDriver::self(),DebugAll,"%s stop running",currentName());
+};
+
+void CASearchThread::resetSearch()
+{
+    m_range = CASearchRangeInvalid;
+    while (m_searching)
+	Thread::yield();
+}
+
+// Search all archive
+void CASearchThread::searchAll(const String& what)
+{
+    bool changed = false;
+    ObjList items;
+    Window* w = getWindow();
+    if (w) {
+	NamedList p("");
+	Client::self()->getOptions(s_logList,&p,w);
+	unsigned int n = p.length();
+	for (unsigned int i = 0; i < n; i++) {
+	    NamedString* ns = p.getParam(i);
+	    if (ns)
+ 		items.append(new String(ns->name()));
+	}
+    }
+    if (m_currentContact && !items.find(m_currentContact)) {
+	changed = true;
+	m_currentContact.clear();
+	m_currentSession.clear();
+	m_currentSessionFull = false;
+	m_currentContactFull = false;
+    }
+    if (!m_currentContact) {
+	changed = true;
+	m_currentSession.clear();
+	m_currentSessionFull = false;
+	m_currentContactFull = false;
+	ObjList* o = m_next ? items.skipNull() : getListLastItem(items);
+	if (o)
+	    m_currentContact = o->get()->toString();
+	else
+	    return;
+    }
+    bool found = false;
+    String start = m_currentContact;
+    while (!found) {
+	ChatFile* f = 0;
+	while (!f) {
+	    if (m_currentContactFull) {
+		m_currentContactFull = false;
+		if (exiting() || m_range == CASearchRangeInvalid)
+		    break;
+		ObjList* o = 0;
+		if (m_next) {
+		    o = items.find(m_currentContact);
+		    if (o)
+			o = o->skipNext();
+		}
+		else
+		    o = getListPrevItem(items,m_currentContact);
+		if (!o) {
+		    if (m_next)
+			o = items.skipNull();
+		    else
+			o = getListLastItem(items);
+		}
+		if (!o || o->get()->toString() == start)
+		    break;
+		m_currentContact = o->get()->toString();
+		m_currentSession.clear();
+		changed = true;
+	    }
+	    f = s_chatArchive.getChatFile(m_currentContact);
+	}
+	if (!f)
+	    break;
+	// Retrieve the starting session if don't have one
+	if (!m_currentSession) {
+	    changed = true;
+	    m_currentSessionFull = false;
+	    ObjList* o = m_next ? f->sessions().skipNull() : getListLastItem(f->sessions());
+	    if (o)
+		m_currentSession = o->get()->toString();
+	}
+	if (m_currentSession)
+	    found = searchContact(f,what,changed);
+	TelEngine::destruct(f);
+	if (found)
+	    break;
+	m_currentSession.clear();
+	m_currentContactFull = true;
+    }
+    if (!found) {
+	m_currentContact.clear();
+	m_currentSession.clear();
+	m_currentSessionFull = true;
+	m_currentContactFull = true;
+    }
+}
+
+// Search in the current contact
+void CASearchThread::searchCurrentContact(const String& what)
+{
+    ChatFile* f = 0;
+    bool changed = false;
+    if (m_currentSession) {
+	f = s_chatArchive.getChatFileBySession(m_currentSession);
+	if (f) {
+	    String tmp = m_currentSession;
+	    Window* w = getWindow();
+	    if (w)
+		Client::self()->getSelect(s_sessList,tmp,w);
+	    changed = (tmp != m_currentSession);
+	}
+	else
+	    m_currentSession.clear();
+    }
+    if (!m_currentSession) {
+	changed = true;
+	m_currentSessionFull = false;
+	Window* w = getWindow();
+	if (w) {
+	    Client::self()->getSelect(s_sessList,m_currentSession,w);
+	    // Select the first or last session if any
+	    if (!m_currentSession) {
+		NamedList p("");
+		Client::self()->getOptions(s_sessList,&p,w);
+		unsigned int n = p.length();
+		NamedString* ns = 0;
+		for (unsigned int i = 0; i < n; i++) {
+		    ns = p.getParam(i);
+		    if (ns && m_next)
+			break;
+		}
+		if (ns)
+		    m_currentSession = ns->name();
+	    }
+	}
+	f = s_chatArchive.getChatFileBySession(m_currentSession);
+    }
+    if (!f)
+	return;
+    searchContact(f,what,changed);
+    TelEngine::destruct(f);
+}
+
+// Search in given contact contact
+bool CASearchThread::searchContact(ChatFile* f, const String& what, bool changed)
+{
+    if (!f)
+	return false;
+    QString* search = new QString;
+    *search = QtClient::setUtf8(what);
+    f->lock();
+    bool found = false;
+    String start = m_currentSession;
+    while (true) {
+	if (m_currentSessionFull) {
+	    if (exiting() || m_range == CASearchRangeInvalid)
+		break;
+	    ObjList* o = 0;
+	    if (m_next) {
+		o = f->sessions().find(m_currentSession);
+		if (o)
+		    o = o->skipNext();
+	    }
+	    else
+		o = getListPrevItem(f->sessions(),m_currentSession);
+	    if (!o && m_range == CASearchRangeContact) {
+		if (m_next)
+		    o = f->sessions().skipNull();
+		else
+		    o = getListLastItem(f->sessions());
+	    }
+	    if (!o || o->get()->toString() == start) {
+		m_currentContactFull = true;
+		break;
+	    }
+	    m_currentSession = o->get()->toString();
+	    m_currentSessionFull = false;
+	    changed = true;
+	}
+	if (exiting() || m_range == CASearchRangeInvalid)
+	    break;
+	ObjList list;
+	found = f->loadSession(m_currentSession,list,0,search);
+	if (exiting() || m_range == CASearchRangeInvalid) {
+	    found = false;
+	    break;
+	}
+	if (found) {
+	    f->unlock();
+	    found = s_logic.setSearch(changed,f->toString(),m_currentSession,what,m_next);
+	    f->lock();
+	    if (found) {
+		m_currentSessionFull = s_highlightAll;
+		break;
+	    }
+	}
+	m_currentSessionFull = true;
+    }
+    f->unlock();
+    if (!found) {
+	m_currentSession.clear();
+	m_currentSessionFull = false;
+    }
+    if (search)
+	delete search;
+    return found;
+}
+
+
+/*
+ * CARefreshThread
+ */
+CARefreshThread::CARefreshThread()
+    : Thread("CARefreshThread")
+{
+}
+
+CARefreshThread::~CARefreshThread()
+{
+    s_logic.refreshTerminated();
+}
+
+void CARefreshThread::run()
+{
+    Debug(ClientDriver::self(),DebugAll,"%s start running",currentName());
+    s_chatArchive.refresh();
+    Debug(ClientDriver::self(),DebugAll,"%s stop running",currentName());
+}
+
+} // namespace anonymous
+
+#include "clientarchive.moc"
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/clientarchive.h b/modules/qt5/clientarchive.h
new file mode 100644
index 0000000..9c5ac32
--- /dev/null
+++ b/modules/qt5/clientarchive.h
@@ -0,0 +1,33 @@
+/**
+ * clientarchive.h
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * Client archive management and UI logic
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#ifndef __CLIENTARCHIVE_H
+#define __CLIENTARCHIVE_H
+
+#include <qt5client.h>
+
+using namespace TelEngine;
+namespace { // anonymous
+
+}; // anonymous namespace
+
+#endif // __CLIENTARCHIVE_H
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/customtable.cpp b/modules/qt5/customtable.cpp
new file mode 100644
index 0000000..d4cb5bf
--- /dev/null
+++ b/modules/qt5/customtable.cpp
@@ -0,0 +1,710 @@
+/**
+ * customtable.cpp
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Custom table implementation
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2010-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#include "customtable.h"
+
+using namespace TelEngine;
+namespace { // anonymous
+
+// The factory
+class CustomTableFactory : public UIFactory
+{
+public:
+    inline CustomTableFactory(const char* name = "CustomTableFactory")
+	: UIFactory(name)
+	{ m_types.append(new String("CustomTable")); }
+    virtual void* create(const String& type, const char* name, NamedList* params = 0);
+};
+
+// Utility class used to disable/enable a table sorting and widget update flag
+class SafeWidget
+{
+public:
+    SafeWidget(QTableWidget* table)
+	: m_widget(table), m_table(0) {
+	    if (m_widget)
+		m_widget->setUpdatesEnabled(false);
+	    if (table && table->isSortingEnabled()) {
+		m_table = table;
+		m_table->setSortingEnabled(false);
+	    }
+	}
+    ~SafeWidget()
+	{ drop(); }
+    inline void drop() {
+	    if (m_table)
+		m_table->setSortingEnabled(true);
+	    if (m_widget)
+		m_widget->setUpdatesEnabled(true);
+	    m_widget = 0;
+	    m_table = 0;
+	}
+private:
+    QWidget* m_widget;
+    QTableWidget* m_table;
+};
+
+static CustomTableFactory s_factory;
+
+static inline const String& objListItem(ObjList* list, int index)
+{
+    GenObject* gen = list ? (*list)[index] : 0;
+    return gen ? gen->toString() : String::empty();
+}
+
+
+/*
+ * CustomTable
+ */
+// Constructor for a custom table
+CustomTable::CustomTable(const char *name, const NamedList& params, QWidget* parent)
+    : QtTable(name,parent),
+    m_rowHeight(0), m_horzHeader(true),
+    m_notifyItemChanged(false), m_notifySelChgOnRClick(true),
+    m_contextMenu(0), m_changing(false)
+{
+    // Build properties
+    QtClient::buildProps(this,params["buildprops"]);
+    // Set horizontal header
+    QHeaderView* h = horizontalHeader();
+    if (h)
+	h->setHighlightSections(false);
+    ObjList* cols = params["hheader_columns"].split(',',false);
+    ObjList* title = params["hheader_columns_title"].split(',',true);
+    ObjList* check = params["hheader_columns_check"].split(',',false);
+    ObjList* size = params["hheader_columns_size"].split(',',true);
+    ObjList* resize = params["hheader_columns_resize"].split(',',true);
+    ObjList* emptyTitle = params["hheader_columns_allowemptytitle"].split(',',true);
+    int n = cols->count();
+    setColumnCount(n);
+    for (int i = 0; i < n; i++) {
+	String id = objListItem(cols,i);
+	String text = objListItem(title,i);
+	if (!text) {
+	    String tmp = id;
+	    if (!emptyTitle->find(tmp.toLower()))
+		text = id;
+	}
+	QTableWidgetItem* it = new QTableWidgetItem(QtClient::setUtf8(text));
+	id.toLower();
+	it->setData(ColumnId,QVariant(QtClient::setUtf8(id)));
+	if (check->find(id))
+	    it->setData(ColumnItemCheckable,QVariant(true));
+	setHorizontalHeaderItem(i,it);
+	if (!h)
+	    continue;
+	// Set column width
+	int w = objListItem(size,i).toInteger();
+	if (w > 0)
+	    h->resizeSection(i,w);
+	// Set column resize mode
+	const String& resizeMode = objListItem(resize,i);
+	if (resizeMode == "fixed")
+	    h->setSectionResizeMode(i,QHeaderView::Fixed);
+	else if (resizeMode == "stretch")
+	    h->setSectionResizeMode(i,QHeaderView::Stretch);
+	else if (resizeMode == "contents")
+	    h->setSectionResizeMode(i,QHeaderView::ResizeToContents);
+	else
+	    h->setSectionResizeMode(i,QHeaderView::Interactive);
+    }
+    TelEngine::destruct(cols);
+    TelEngine::destruct(title);
+    TelEngine::destruct(check);
+    TelEngine::destruct(size);
+    TelEngine::destruct(resize);
+    TelEngine::destruct(emptyTitle);
+    // Init properties
+    m_saveProps << "_yate_col_widths";
+    m_saveProps << "_yate_sorting";
+    setSelectionMode(QAbstractItemView::SingleSelection);
+    setSelectionBehavior(QAbstractItemView::SelectRows);
+    setEditTriggers(QAbstractItemView::NoEditTriggers);
+    setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding);
+    // Connect signals
+    QtClient::connectObjects(this,SIGNAL(cellChanged(int,int)),this,SLOT(itemCellChanged(int,int)));
+    // Apply parameters
+    setParams(params);
+}
+
+CustomTable::~CustomTable()
+{
+}
+
+bool CustomTable::setParams(const NamedList& params)
+{
+    SafeWidget tbl(this);
+    QtUIWidget::setParams(params);
+    unsigned int n = params.length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* param = params.getParam(i);
+	if (!param)
+	    continue;
+	if (param->name() == "filtervalue")
+	    setFilter(*param);
+	else if (param->name() == "dynamiccellclicked")
+	    setProperty("dynamicCellClicked",QVariant(QString(*param)));
+	else if (param->name() == "dynamicnoitemselchanged")
+	    setProperty("dynamicNoItemSelChanged",QVariant(QString(*param)));
+	else if (param->name().startsWith("property:")) {
+	    String prop = param->name().substr(9);
+	    QWidget* target = this;
+	    if (prop.startSkip("hheader:",false))
+		target = horizontalHeader();
+	    if (target)
+		QtClient::setProperty(target,prop,*param);
+	}
+	else if (param->name() == "menu") {
+	    // Re-build the context menu
+	    if (m_contextMenu) {
+		QtClient::deleteLater(m_contextMenu);
+		m_contextMenu = 0;
+	    }
+	    NamedList* menu = static_cast<NamedList*>(param->getObject(YATOM("NamedList")));
+	    if (menu) {
+		// Get parent window receiving menu events
+		QtWindow* wnd = static_cast<QtWindow*>(window());
+		if (wnd)
+		    m_contextMenu = QtClient::buildMenu(*menu,*menu,wnd,SLOT(action()),
+			SLOT(toggled(bool)),this);
+	    }
+	}
+	else if (param->name() == "notifyselchgonrightclick")
+	    m_notifySelChgOnRClick = param->toBoolean(m_notifySelChgOnRClick);
+	else if (param->name() == "filterby") {
+	    setFilter();
+	    m_filterBy.clear();
+	    ObjList* list = param->split(',',false);
+	    for (ObjList* o = list->skipNull(); o; o = o->skipNext()) {
+		String* s = static_cast<String*>(o->get());
+		m_filterBy.append(QtClient::setUtf8(s->toLower()));
+	    }
+	    TelEngine::destruct(list);
+	}
+    }
+    tbl.drop();
+    return true;
+}
+
+bool CustomTable::getOptions(NamedList& items)
+{
+    int n = rowCount();
+    for (int i = 0; i < n; i++) {
+	String id;
+	if (getId(id,i) && id)
+	    items.addParam(id,"");
+    }
+    return true;
+}
+
+bool CustomTable::addTableRow(const String& item, const NamedList* data, bool atStart)
+{
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::addTableRow(%s,%p,%u)",
+	name().c_str(),item.c_str(),data,atStart);
+    SafeWidget tbl(this);
+    int row = atStart ? 0 : rowCount();
+    insertRow(row);
+    if (setRow(row,data,item))
+	return true;
+    removeRow(row);
+    return false;
+}
+
+// Add or set one or more table row(s). Screen update is locked while changing the table.
+// Each data list element is a NamedPointer carrying a NamedList with item parameters.
+// The name of an element is the item to update.
+// Set element's value to boolean value 'true' to add a new item if not found
+//  or 'false' to set an existing one. Set it to empty string to delete the item
+bool CustomTable::updateTableRows(const NamedList* data, bool atStart)
+{
+    if (!data)
+	return true;
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::updateTableRows(%p,%u)",
+	name().c_str(),data,atStart);
+    // Remember selected item
+    bool ok = true;
+    SafeWidget tbl(this);
+    unsigned int n = data->length();
+    ObjList add;
+    // Delete and update rows
+    for (unsigned int i = 0; i < n; i++) {
+	if (Client::exiting())
+	    break;
+	// Get item and the list of parameters
+	NamedString* ns = data->getParam(i);
+	if (!ns)
+	    continue;
+	// Delete ?
+	if (ns->null()) {
+	    int row = getRow(ns->name());
+	    if (row >= 0)
+		removeRow(row);
+	    else
+		ok = false;
+	    continue;
+	}
+	// Set item or postpone add
+	int row = getRow(ns->name());
+	if (row >= 0)
+	    setRow(row,YOBJECT(NamedList,ns));
+	else if (ns->toBoolean())
+	    add.append(ns)->setDelete(false);
+	else
+	    ok = false;
+    }
+    n = add.count();
+    if (n) {
+	int row = rowCount();
+	if (row < 0)
+	    row = 0;
+	// Append if not requested to insert at start or table is empty
+	if (!(atStart && row))
+	    setRowCount(row + n);
+	else {
+	    for (unsigned int i = 0; i < n; i++)
+		insertRow(0);
+	}
+	for (ObjList* o = add.skipNull(); o; row++, o = o->skipNext()) {
+	    NamedString* ns = static_cast<NamedString*>(o->get());
+	    if (!setRow(row,YOBJECT(NamedList,ns),ns->name()))
+		ok = false;
+	}
+    }
+    return ok;
+}
+
+bool CustomTable::delTableRow(const String& item)
+{
+    SafeWidget tbl(this);
+    int row = getRow(item);
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::delTableRow(%s) found=%d",
+	name().c_str(),item.c_str(),row);
+    if (row < 0)
+	return false;
+    removeRow(row);
+    return true;
+}
+
+bool CustomTable::setTableRow(const String& item, const NamedList* data)
+{
+    SafeWidget tbl(this);
+    int row = getRow(item);
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::setTableRow(%s,%p) found=%d",
+	name().c_str(),item.c_str(),data,row);
+    if (row < 0)
+	return false;
+    return setRow(row,data);
+}
+
+bool CustomTable::getTableRow(const String& item, NamedList* data)
+{
+    int row = getRow(item);
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::getTableRow(%s,%p) found=%d",
+	name().c_str(),item.c_str(),data,row);
+    if (row < 0)
+	return false;
+    if (!data)
+	return true;
+    int n = columnCount();
+    for (int i = 1; i < n; i++) {
+	String name;
+	bool checkable = false;
+	QTableWidgetItem* h = getColumnId(name,checkable,i);
+	if (!(h && name))
+	    continue;
+	QTableWidgetItem* it = QTableWidget::item(row,i);
+	if (!it)
+	    continue;
+	NamedString* ns = new NamedString(name);
+	QtClient::getUtf8(*ns,it->text());
+	data->setParam(ns);
+	if (checkable)
+	    data->setParam("check:" + name,String::boolText(it->checkState() == Qt::Checked));
+    }
+    return true;
+}
+
+bool CustomTable::clearTable()
+{
+    setRowCount(0);
+    return true;
+}
+
+// Set the selected entry
+bool CustomTable::setSelect(const String& item)
+{
+    if (!item)
+	return true;
+    int row = getRow(item);
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::setSelect(%s) found=%d",
+	name().c_str(),item.c_str(),row);
+    if (row < 0)
+	return false;
+    setCurrentCell(row,1);
+    return true;
+}
+
+bool CustomTable::getSelect(String& item)
+{
+    int row = currentRow();
+    QTableWidgetItem* it = 0;
+    if (row >= 0) {
+	it = QTableWidget::item(row,0);
+	if (it)
+	    QtClient::getUtf8(item,it->text());
+    }
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::getSelect() found=(%d,%s)",
+	name().c_str(),row,item.c_str());
+    return it != 0;
+}
+
+// Find a table row by its item id
+int CustomTable::getRow(const String& item)
+{
+    const QString tmp = QtClient::setUtf8(item);
+    for (int i = 0; i < rowCount(); i++) {
+	QTableWidgetItem* it = this->item(i,0);
+	if (it && it->text() == tmp)
+	    return i;
+    }
+    return -1;
+}
+
+// Find a table row id by its row index
+bool CustomTable::getId(String& item, int row)
+{
+    QTableWidgetItem* it = this->item(row,0);
+    if (it)
+	QtClient::getUtf8(item,it->text());
+    return it != 0;
+}
+
+// Find a column by its label. Return -1 if not found
+QTableWidgetItem* CustomTable::getColumnId(String& id, bool& checkable, int col)
+{
+    QTableWidgetItem* it = horizontalHeaderItem(col);
+    if (!it)
+	return 0;
+    QVariant var = it->data(ColumnId);
+    if (var.type() == QVariant::String)
+	QtClient::getUtf8(id,var.toString());
+    else {
+	QtClient::getUtf8(id,it->text());
+	id.toLower();
+    }
+    var = it->data(ColumnItemCheckable);
+    checkable = var.toBool();
+    return it;
+}
+
+// Find a column by its label. Return -1 if not found
+int CustomTable::getColumn(const QString& name, bool hidden, bool caseInsensitive)
+{
+    static QString ht("hidden:");
+    QString what = name;
+    if (hidden)
+	what.insert(0,ht);
+    Qt::CaseSensitivity cs = caseInsensitive ? Qt::CaseInsensitive : Qt::CaseSensitive;
+    int n = columnCount();
+    for (int i = 0; i < n; i++) {
+	QTableWidgetItem* it = horizontalHeaderItem(i);
+	if (!it)
+	    continue;
+	QVariant var = it->data(ColumnId);
+	if (var.type() == QVariant::String) {
+	    if (0 == var.toString().compare(what,cs))
+		return i;
+	}
+	else if (0 == it->text().compare(what,cs))
+	    return i;
+    }
+    return -1;
+}
+
+// (de)activate enter key press action
+void CustomTable::setEnterPressNotify(bool value)
+{
+    QAction* act = findChild<QAction*>(m_enterKeyActionName);
+    if (act) {
+	if (!value) {
+	    QWidget::removeAction(act);
+	    QtClient::deleteLater(act);
+	}
+	return;
+    }
+    if (!value)
+	return;
+    act = new QAction("",this);
+    act->setObjectName(m_enterKeyActionName);
+    act->setShortcut(QKeySequence(Qt::Key_Return));
+    act->setShortcutContext(Qt::WidgetShortcut);
+    act->setProperty("_yate_autoconnect",QVariant(false));
+    QWidget::addAction(act);
+    QtClient::connectObjects(act,SIGNAL(triggered()),this,SLOT(actionTriggered()));
+}
+
+// Retrieve table columns widths
+QString CustomTable::getColWidths()
+{
+    String widths;
+    int n = columnCount();
+    for (int i = 0; i < n; i++)
+	widths.append(String(columnWidth(i)),",",true);
+    return QtClient::setUtf8(widths);
+}
+
+// Set the table columns widths string
+void CustomTable::setColWidths(QString value)
+{
+    QHeaderView* hdr = horizontalHeader();
+    bool skipLast = hdr && hdr->stretchLastSection();
+    QStringList list = value.split(',');
+    for (int i = 0; i < list.size(); i++) {
+	if (skipLast && i == columnCount() - 1)
+	    break;
+	bool ok = true;
+	int w = list[i].toInt(&ok);
+	if (ok && w >= 0)
+	    setColumnWidth(i,w);
+    }
+}
+
+// Retrieve table sorting
+QString CustomTable::getSorting()
+{
+    String sorting;
+    if (isSortingEnabled()) {
+	QHeaderView* h = horizontalHeader();
+	int col = h ? h->sortIndicatorSection() : -1;
+	if (col >= 0)
+	    sorting << col << "," <<
+		String::boolText(Qt::AscendingOrder == h->sortIndicatorOrder());
+    }
+    return QtClient::setUtf8(sorting);
+}
+
+// Set the table sorting
+void CustomTable::setSorting(QString value)
+{
+    QStringList list = value.split(',');
+    if (list.size() < 2)
+	return;
+    bool ok = true;
+    int col = list[0].toInt(&ok);
+    if (ok && col >= 0 && col < columnCount()) {
+	String tmp;
+	QtClient::getUtf8(tmp,list[1]);
+	sortItems(col,tmp.toBoolean(true) ? Qt::AscendingOrder : Qt::DescendingOrder);
+    }
+}
+
+// Setup a row
+bool CustomTable::setRow(int row, const NamedList* data, const String& item)
+{
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::setRow(%d,%p,%s)",
+	name().c_str(),row,data,item.c_str());
+    m_changing = true;
+    int n = columnCount();
+    // First init
+    if (item) {
+	// Set row id
+	setItem(row,0,new QTableWidgetItem(QtClient::setUtf8(item)));
+	// Set row height
+	if (m_rowHeight > 0)
+	    QTableWidget::setRowHeight(row,m_rowHeight);
+	// Set checkable columns
+	for (int i = 1; i < n; i++) {
+	    String name;
+	    bool checkable = false;
+	    getColumnId(name,checkable,i);
+	    if (!checkable)
+		continue;
+	    QTableWidgetItem* it = QTableWidget::item(row,i);
+	    if (!it) {
+		it = new QTableWidgetItem;
+		setItem(row,i,it);
+	    }
+	    it->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsSelectable | Qt::ItemIsEnabled);
+	    it->setCheckState(Qt::Unchecked);
+	}
+    }
+    if (!data) {
+	m_changing = false;
+	return true;
+    }
+    for (int i = 1; i < n; i++) {
+	String name;
+	bool checkable = false;
+	getColumnId(name,checkable,i);
+	if (!name)
+	    continue;
+	String* text = data->getParam(name);
+	String* img = data->getParam(name + "_image");
+	String* check = checkable ? data->getParam("check:" + name) : 0;
+	if (!(text || img || check))
+	    continue;
+	QTableWidgetItem* it = QTableWidget::item(row,i);
+	if (!it) {
+	    it = new QTableWidgetItem;
+	    setItem(row,i,it);
+	    if (!checkable)
+		it->setFlags(it->flags() & ~Qt::ItemFlags(Qt::ItemIsUserCheckable));
+	    else {
+		it->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsSelectable);
+		it->setCheckState(Qt::Unchecked);
+	    }
+	}
+	if (text)
+	    it->setText(QtClient::setUtf8(*text));
+	if (check)
+	    it->setCheckState(check->toBoolean() ? Qt::Checked : Qt::Unchecked);
+	if (img)
+	    it->setIcon(QIcon(QtClient::setUtf8(*img)));
+    }
+    m_changing = false;
+    return true;
+}
+
+// Handle item cell content changes
+void CustomTable::onCellChanged(int row, int col)
+{
+    if (m_changing || row < 0 || !m_notifyItemChanged)
+	return;
+    String item;
+    getId(item,row);
+    if (item)
+	triggerAction(item,"listitemchanged",this);
+}
+
+void CustomTable::contextMenuEvent(QContextMenuEvent* e)
+{
+    int yMax = rowCount() * rowHeight(0);
+    if (yMax < e->y())
+	return;
+    if (m_contextMenu)
+	m_contextMenu->exec(e->globalPos());
+}
+
+// Catch a mouse press event
+// Disable selection change signal on right button events
+void CustomTable::mousePressEvent(QMouseEvent* event)
+{
+    if (event->button() == Qt::RightButton && !m_notifySelChgOnRClick) {
+	int row = rowAt(event->y());
+	if (row >= 0 && row != currentRow()) {
+	    // Disconnect and re-connect only if connected
+	    QtWindow* wnd = 0;
+	    QVariant var = property("dynamicNoItemSelChanged");
+	    if (!var.toBool())
+		wnd = QtClient::parentWindow(this);
+	    if (wnd)
+		disconnect(this,SIGNAL(itemSelectionChanged()),
+		    wnd,SLOT(selectionChanged()));
+	    setCurrentCell(row,1);
+	    if (wnd)
+		QtClient::connectObjects(this,SIGNAL(itemSelectionChanged()),
+		    wnd,SLOT(selectionChanged()));
+	    event->accept();
+	}
+	return;
+    }
+    QTableWidget::mousePressEvent(event);
+}
+
+// Slot for triggered signals received from actions added to the table
+void CustomTable::actionTriggered()
+{
+    if (!sender() || currentRow() < 0)
+	return;
+    if (sender()->objectName() == m_enterKeyActionName)
+	onAction(this);
+}
+
+// Set filter (hide not matching items)
+void CustomTable::setFilter(const String& value)
+{
+    DDebug(ClientDriver::self(),DebugAll,"CustomTable(%s)::setFilter(%s)",
+	name().c_str(),value.c_str());
+    SafeWidget tbl(this);
+    QString tmp = QtClient::setUtf8(value);
+    if (tmp == m_filterValue)
+	return;
+    m_filterValue = tmp;
+    // Match rows and show or hide them
+    int rows = rowCount();
+    int cols = columnCount();
+    for (int row = 0; row < rows; row++)
+	for (int col = 0; col < cols; col++)
+	    if (updateFilter(row,col))
+		break;
+}
+
+// Check if the current filter matches a row. Show it if matched, hide it otherwise.
+bool CustomTable::updateFilter(int row, int col)
+{
+    bool hide = !rowFilterMatch(row,col);
+    if (hide == isRowHidden(row))
+	return false;
+    setRowHidden(row,hide);
+    return true;
+}
+
+// Check if the current filter matches a row
+bool CustomTable::rowFilterMatch(int row, int col)
+{
+    for (int i = m_filterBy.size() - 1; i >= 0; i--) {
+	QTableWidgetItem* hdr = horizontalHeaderItem(col);
+	if (!hdr || hdr->text() != m_filterBy[i])
+	    continue;
+	QTableWidgetItem* it = item(row,col);
+	if (it && it->text().contains(m_filterValue,Qt::CaseInsensitive))
+	    return true;
+    }
+    return false;
+}
+
+
+/*
+ * CustomTableFactory
+ */
+// Build CustomTable
+void* CustomTableFactory::create(const String& type, const char* name, NamedList* params)
+{
+    if (!params)
+	return 0;
+    QWidget* parentWidget = 0;
+    String* wndname = params->getParam("parentwindow");
+    if (!TelEngine::null(wndname)) {
+	String* wName = params->getParam("parentwidget");
+	QtWindow* wnd = static_cast<QtWindow*>(Client::self()->getWindow(*wndname));
+	if (wnd && !TelEngine::null(wName))
+	    parentWidget = wnd->findChild<QWidget*>(QtClient::setUtf8(*wName));
+    }
+    if (type == "CustomTable")
+	return new CustomTable(name,*params,parentWidget);
+    return 0;
+}
+
+}; // anonymous namespace
+
+#include "customtable.moc"
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/customtable.h b/modules/qt5/customtable.h
new file mode 100644
index 0000000..7d800f1
--- /dev/null
+++ b/modules/qt5/customtable.h
@@ -0,0 +1,396 @@
+/**
+ * customtable.h
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * A custom table
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#ifndef __CUSTOMTABLE_H
+#define __CUSTOMTABLE_H
+
+#include <qt5client.h>
+
+using namespace TelEngine;
+namespace { // anonymous
+
+class CustomTable : public QtTable
+{
+    YCLASS(CustomTable,QtTable)
+    Q_CLASSINFO("CustomTable","Yate")
+    Q_OBJECT
+    Q_PROPERTY(QStringList _yate_save_props READ saveProps WRITE setSaveProps(QStringList))
+    Q_PROPERTY(bool _yate_notifyitemchanged READ getNotifyItemChanged WRITE setNotifyItemChanged(bool))
+    Q_PROPERTY(bool _yate_horizontalheader READ getHHeader WRITE setHHeader(bool))
+    Q_PROPERTY(bool _yate_notifyonenterpressed READ enterPressNotify WRITE setEnterPressNotify(bool))
+    Q_PROPERTY(int _yate_rowheight READ getRowHeight WRITE setRowHeight(int))
+    Q_PROPERTY(QString _yate_col_widths READ getColWidths WRITE setColWidths(QString))
+    Q_PROPERTY(QString _yate_sorting READ getSorting WRITE setSorting(QString))
+public:
+    /**
+     * Table item data roles
+     */
+    enum CustomRoles {
+	ColumnId = Qt::UserRole + 1,             // Column id
+	ColumnItemCheckable = Qt::UserRole + 2,  // Column items are checkable
+    };
+
+    /**
+     * Constructor
+     * @param name The name of the table
+     * @param params Parameters for building the table
+     * @param parent Optional parent
+     */
+    CustomTable(const char* name, const NamedList& params, QWidget* parent = 0);
+
+    /**
+     * Destructor
+     */
+    ~CustomTable();
+
+    /**
+     * Check if the table has a filter set
+     * @return True if a filter is set
+     */
+    inline bool hasFilter() const
+	{ return 0 != m_filterBy.count() && m_filterValue.length(); }
+
+    /**
+     * Function for setting the properties of the table
+     * @param params List that contains the properties to be set and their values
+     * @return True if it has succeeded, false if it hasn't
+     */
+    virtual bool setParams(const NamedList& params);
+
+    /**
+     * Obtain all the entries that the table contains
+     * @param items List to be filled with all the entries the table contains
+     * @return True if there are elements, false if the table is empty
+     */
+    virtual bool getOptions(NamedList& items);
+
+    /**
+     * Add a new entry to the table
+     * @param item The new entry's object name
+     * @param data The parameters for building the new entry
+     * @param asStart True if the entry is to be inserted at the start of
+     *   the table, false if it is to be appended
+     * @return True if the entry has been added, false otherwise
+     */
+    virtual bool addTableRow(const String& item, const NamedList* data = 0,
+	bool atStart = false);
+
+    /**
+     * Add or set one or more table row(s). Screen update is locked while changing the table.
+     * Each data list element is a NamedPointer carrying a NamedList with item parameters.
+     * The name of an element is the item to update.
+     * Set element's value to boolean value 'true' to add a new item if not found
+     *  or 'false' to set an existing one. Set it to empty string to delete the item
+     * @param data The list of items to add/set/delete
+     * @param atStart True to add new items at start, false to add them to the end
+     * @return True if the operation was successfull
+     */
+    virtual bool updateTableRows(const NamedList* data, bool atStart = false);
+
+    /**
+     * Delete an entry from the table
+     * @param item Name of the object to be deleted
+     * @return True if the entry has been deleted, false otherwise
+     */
+    virtual bool delTableRow(const String& item);
+
+    /**
+     * Set/change the properties of a table entry
+     * @param item Name of the entry for which the properties will be set
+     * @param data List of properties to be set and their values
+     * @return True if the entry has been found and set, false if the entry hasn't been found
+     */
+    virtual bool setTableRow(const String& item, const NamedList* data);
+
+    /** Get the values of requested properties for an entry
+     * @param item Name of the searched entry
+     * @param data List of the properties for which the value is requested.
+     *  It will be filled wiht the properties' values
+     * @return True if the entry is found and the list filled,
+     *  false if the entry is not found
+     */
+    virtual bool getTableRow(const String& item, NamedList* data = 0);
+
+    /**
+     * Delete all table content
+     * @return True if it succeeds
+     */
+    virtual bool clearTable();
+
+    /**
+     * Set the selected entry
+     * @param item String containing the new selection
+     * @return True if the operation was successfull
+     */
+    virtual bool setSelect(const String& item);
+
+    /**
+     * Obtain the selected entry
+     * @param item String in which the selected entry name is to be returned
+     * @return True if something is selected, false otherwise
+     */
+    virtual bool getSelect(String& item);
+
+    /**
+     * Retrieve the 0 based index of the current item
+     * @return The index of the current item (-1 on error or container empty)
+     */
+    virtual int currentItemIndex()
+	{ return QTableWidget::currentRow(); }
+
+    /**
+     * Retrieve the number of items in container
+     * @return The number of items in container (-1 on error)
+     */
+    virtual int itemCount()
+	{ return QTableWidget::rowCount(); }
+
+    /**
+     * Find a table row by its item id
+     * @param item Item name to find
+     * @return The row or -1 if not found
+     */
+    int getRow(const String& item);
+
+    /**
+     * Find a table row id by its row index
+     * @param item Item id to fill
+     * @param row Table row
+     * @return True if the row item was found
+     */
+    bool getId(String& item, int row);
+
+    /**
+     * Find a table column id by its column index
+     * @param id Column id to fill
+     * @param checkable Column checkable flag
+     * @param row Table row
+     * @return QTableWidgetItem pointer or 0 if not found
+     */
+    QTableWidgetItem* getColumnId(String& id, bool& checkable, int col);
+
+    /**
+     * Find a column by its label. Return -1 if not found
+     * @param text Column label text to find
+     * @param hidden True to find a hidden column (search by 'hidden:' prefix)
+     * @param caseInsensitive True to make a case insensitive comparison
+     * @return The column index or -1 if not found
+     */
+    int getColumn(const QString& text, bool hidden = false, bool caseInsensitive = true);
+
+    /**
+     * Find a column by its label. Return -1 if not found
+     * @param text Column label text to find
+     * @param hidden True to find a hidden column (search by 'hidden:' prefix)
+     * @param caseInsensitive True to make a case insensitive comparison
+     * @return The column index or -1 if not found
+     */
+    inline int getColumn(const char* text, bool hidden = false, bool caseInsensitive = true)
+	{ return getColumn(QtClient::setUtf8(text),hidden,caseInsensitive); }
+
+    /**
+     * Check if this table is notifying item changed
+     * @return True if this table is notifying item changed
+     */
+    bool getNotifyItemChanged()
+	{ return m_notifyItemChanged; }
+
+    /**
+     * Set/reset item changed notification flag
+     * @param on True to notify item changes, false to disable the notification
+     */
+    void setNotifyItemChanged(bool on)
+	{ m_notifyItemChanged = on; }
+
+    /**
+     * Check if the horizontal header should be visible
+     * @return True if the horizontal header should be visible
+     */
+    bool getHHeader()
+	{ return m_horzHeader; }
+
+    /**
+     * Show/hide the horizontal header
+     * @param on True to show the horizontal header, false to hide it
+     */
+    void setHHeader(bool on) {
+	    m_horzHeader = on;
+	    QHeaderView* h = horizontalHeader();
+	    if (h)
+		h->setVisible(on);
+	}
+
+    /**
+     * Check if enter key press action is active. Does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     * @return False
+     */
+    bool enterPressNotify()
+	{ return false; }
+
+    /**
+     * (de)activate enter key press action
+     * @param value True to activate the enter key press action, false to disable it
+     */
+    void setEnterPressNotify(bool value);
+
+    /**
+     * Retrieve the table's default row height
+     * @return Table's default row height
+     */
+    int getRowHeight()
+	{ return m_rowHeight; }
+
+    /**
+     * Set the table's default row height
+     * @param value Table's new default row height
+     */
+    void setRowHeight(int value)
+	{ m_rowHeight = value; }
+
+    /**
+     * Retrieve table columns widths
+     * @return Comma separated list of columns widths
+     */
+    QString getColWidths();
+
+    /**
+     * Set the table columns widths string
+     * @param value Comma separated list of columns widths
+     */
+    void setColWidths(QString value);
+
+    /**
+     * Retrieve table sorting
+     * @return Table sorting string
+     */
+    QString getSorting();
+
+    /**
+     * Set the table sorting
+     * @param value Table sorting value
+     */
+    void setSorting(QString value);
+
+protected:
+    /**
+     * Setup a row
+     * @param row An existing row index
+     * @param data Row parameters
+     * @param item Set the row's id if not empty
+     * @return True on success
+     */
+    virtual bool setRow(int row, const NamedList* data,
+	const String& item = String::empty());
+
+    /**
+     * Handle item cell content changes
+     * @param row Item row
+     * @param col Item column
+     */
+    virtual void onCellChanged(int row, int col);
+
+    /**
+     * Catch a context menu event and show the context menu
+     * @param e Context menu event
+     */
+    virtual void contextMenuEvent(QContextMenuEvent* e);
+
+    /**
+     * Catch a mouse press event
+     * Disable selection change signal on right button events
+     * @param event Mouse press event
+     */
+    virtual void mousePressEvent(QMouseEvent* event);
+
+protected slots:
+    /**
+     * Handle item children actions
+     */
+    void itemChildAction()
+	{ onAction(sender()); }
+
+    /**
+     * Handle item children toggles
+     */
+    void itemChildToggle(bool on)
+	{ onToggle(sender(),on); }
+
+    /**
+     * Handle item children select
+     */
+    void itemChildSelect()
+	{ onSelect(sender()); }
+
+    /**
+     * Handle item cell changed
+     */
+    void itemCellChanged(int row, int col)
+	{ onCellChanged(row,col); }
+
+    /**
+     * Slot for triggered signals received from actions added to the table
+     */
+    void actionTriggered();
+
+private:
+    /**
+     * Set filter (hide not matching items)
+     * @param value Filter value
+     */
+    void setFilter(const String& value = String::empty());
+
+    /**
+     * Check if the current filter matches a row. Show it if matched, hide it otherwise.
+     * @param row The row to check
+     * @param col The column containing the widget to check
+     * @return True if the row visibility changed
+     */
+    bool updateFilter(int row, int col);
+
+    /**
+     * Check if the current filter matches a row
+     * @param row The row to check
+     * @param col The column containing the widget to check
+     * @return True if match
+     */
+    bool rowFilterMatch(int row, int col);
+
+    int m_rowHeight;
+    bool m_horzHeader;                   // Show/hide the horizontal header
+    bool m_notifyItemChanged;            // Notify 'listitemchanged' action
+    bool m_notifySelChgOnRClick;         // Notify selection changed on mouse right button click
+    QMenu* m_contextMenu;
+    QString m_enterKeyActionName;        // The name of the Enter key pressed action
+    // Filter
+    QStringList m_filterBy;              // List of cell widget children name whose text is used to filter
+                                         //  the table rows
+    QString m_filterValue;               // The filter value
+    // Notifications
+    bool m_changing;                     // Content is changing from client (not from user):
+                                         //  avoid notifications
+};
+
+}; // anonymous namespace
+
+#endif // __CUSTOMTABLE_H
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/customtext.cpp b/modules/qt5/customtext.cpp
new file mode 100644
index 0000000..d106ba8
--- /dev/null
+++ b/modules/qt5/customtext.cpp
@@ -0,0 +1,611 @@
+/**
+ * customtext.cpp
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Custom text edit objects
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2010-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#include "customtext.h"
+
+using namespace TelEngine;
+namespace { // anonymous
+
+// The factory
+class CustomTextFactory : public UIFactory
+{
+public:
+    inline CustomTextFactory(const char* name = "CustomFactory")
+	: UIFactory(name)
+	{ m_types.append(new String("CustomTextEdit")); }
+    virtual void* create(const String& type, const char* name, NamedList* params = 0);
+};
+
+// Scroll an area to the end if has a vertical scroll bar
+class ScrollToEnd
+{
+public:
+    inline ScrollToEnd(QAbstractScrollArea* area)
+	: m_area(area)
+	{}
+    inline ~ScrollToEnd() {
+	    QScrollBar* bar = m_area ? m_area->verticalScrollBar() : 0;
+	    if (bar)
+		bar->setSliderPosition(bar->maximum());
+	}
+private:
+    QAbstractScrollArea* m_area;
+};
+
+static CustomTextFactory s_factory;
+// Global list of URL handlers
+static NamedList s_urlHandlers("");
+
+
+// Check if a char is a word break one (including NULL)
+static inline bool isWordBreak(char c)
+{
+    return (c == ' ' || c == '\t' || c == '\r' || c == '\n' || !c);
+}
+
+// Check if a char should be ignored from URL end (including NULL)
+static inline bool isIgnoreUrlEnd(char c)
+{
+    return (c == '.' || c == ';' || c == ':' || c == '?' || c == '!');
+}
+
+// Move a cursor at document start/end.
+// Adjust position by 'blocks' count
+// Select if required and blocks is not 0
+static void moveCursor(QTextCursor& c, bool atStart, int blocks = 0,
+    bool select = false)
+{
+    c.movePosition(!atStart ? QTextCursor::End : QTextCursor::Start);
+    if (!blocks)
+	return;
+    c.movePosition(!atStart ? QTextCursor::PreviousBlock : QTextCursor::NextBlock,
+	select ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor,
+	blocks > 0 ? blocks : -blocks);
+}
+
+/*
+ * CustomTextFormat
+ */
+// Constructor. Build a Block type
+CustomTextFormat::CustomTextFormat(const String& id, const char* color, const char* bgcolor)
+    : NamedString(id),
+    m_type(Block), m_blockFormat(0), m_charFormat(0)
+{
+    m_blockFormat = new QTextBlockFormat;
+    if (bgcolor)
+	m_blockFormat->setBackground(QColor(bgcolor));
+    m_charFormat = new QTextCharFormat;
+    if (color)
+	m_charFormat->setForeground(QColor(color));
+}
+
+// Constructor. Build a Html/Plain type
+CustomTextFormat::CustomTextFormat(const String& id, const char* value, bool html)
+    : NamedString(id,value),
+    m_type(html ? Html : Plain), m_blockFormat(0), m_charFormat(0)
+{
+}
+
+CustomTextFormat::~CustomTextFormat()
+{
+    if (m_blockFormat)
+	delete m_blockFormat;
+    if (m_charFormat)
+	delete m_charFormat;
+}
+
+// Add/insert text into an edit widget
+int CustomTextFormat::insertText(QTextEdit* edit, const String& text, bool atStart, int blocks)
+{
+    QTextDocument* doc = edit ? edit->document() : 0;
+    if (!doc)
+	return 0;
+    QTextCursor c(doc);
+    moveCursor(c,atStart,blocks,false);
+    int oldBlocks = doc->blockCount();
+    c.insertBlock();
+    c.movePosition(QTextCursor::PreviousBlock,QTextCursor::MoveAnchor);
+    // Insert text
+    if (type() == Html)
+	c.insertHtml(QtClient::setUtf8(text));
+    else {
+	if (m_blockFormat)
+	    c.setBlockFormat(*m_blockFormat);
+	if (m_charFormat)
+	    c.setCharFormat(*m_charFormat);
+	c.insertText(QtClient::setUtf8(text));
+    }
+    return doc->blockCount() - oldBlocks;
+}
+
+// Set text from value. Replace text parameters if not empty
+void CustomTextFormat::buildText(String& text, const NamedList* params,
+    CustomTextEdit* owner, bool lineBrBefore)
+{
+    if (null())
+	return;
+    if (lineBrBefore)
+	text = ((type() == Html) ? "<br>" : "\r\n");
+    text << *this;
+    NamedList dummy("");
+    const NamedList* repl = &dummy;
+    if (params) {
+	// Escape or replace HTML markups.
+	// Make a copy of the input list if we are going to change it
+	if (type() == Html) {
+	    dummy = *params;
+	    unsigned int n = dummy.length();
+	    for (unsigned int i = 0; i < n; i++) {
+		String* s = dummy.getParam(i);
+		if (!TelEngine::null(s)) {
+		    Client::plain2html(*s);
+		    if (owner)
+			owner->replace(*s);
+		}
+	    }
+	}
+	else
+	    repl = params;
+    }
+    repl->replaceParams(text);
+}
+
+
+/*
+ * TextFragmentList
+ */
+// Restore this list in the document
+void TextFragmentList::restore(QTextDocument* doc)
+{
+    if (doc) {
+	for (int i = 0; i < m_list.size(); i++) {
+	    QTextCursor c(doc);
+	    c.movePosition(QTextCursor::NextCharacter,QTextCursor::MoveAnchor,
+		m_list[i].m_docPos);
+	    c.movePosition(QTextCursor::NextCharacter,QTextCursor::KeepAnchor,
+		m_list[i].toPlainText().length());
+	    c.removeSelectedText();
+	    c.insertHtml(m_list[i].toHtml());
+	}
+    }
+    m_list.clear();
+};
+
+
+/*
+ * CustomTextEdit
+ */
+// Constructor
+CustomTextEdit::CustomTextEdit(const char* name, const NamedList& params, QWidget* parent)
+    : QtCustomWidget(name,parent),
+    m_edit(0),
+    m_debug(false),
+    m_items(""),
+    m_defItem(String::empty(),"",false),
+    m_followUrl(true),
+    m_urlHandlers(""),
+    m_tempItemCount(0),
+    m_tempItemReplace(true),
+    m_lastFoundPos(-1)
+{
+    // Build properties
+    QtClient::buildProps(this,params["buildprops"]);
+    m_edit = new QTextBrowser(this);
+    m_edit->setObjectName(params.getValue("textedit_name",this->name() + "_textedit"));
+    m_edit->setOpenLinks(false);
+    m_edit->setOpenExternalLinks(false);
+    m_edit->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding);
+    QtClient::setWidget(this,m_edit);
+    m_searchFoundFormat.setBackground(QBrush(QColor("darkgreen")));
+    m_searchFoundFormat.setForeground(QBrush(QColor("white")));
+    m_debug = params.getBoolValue("_yate_debug_widget");
+    if (m_debug) {
+	m_items.addParam(new CustomTextFormat(String(-1),"white"));       // Output() or client set status
+	m_items.addParam(new CustomTextFormat(String(0),"yellow","red")); // DebugFail - blinking yellow on red
+	m_items.addParam(new CustomTextFormat(String(1),"yellow","red")); // Unnamed   - yellow on red
+	m_items.addParam(new CustomTextFormat(String(2),"white","red"));  // DebugCrit - white on red
+	m_items.addParam(new CustomTextFormat(String(3),"lightgrey","red")); // DebugConf - gray on red
+	m_items.addParam(new CustomTextFormat(String(4),"red"));          // DebugStub - red on black
+	m_items.addParam(new CustomTextFormat(String(5),"orangered"));    // DebugWarn - light red on black
+	m_items.addParam(new CustomTextFormat(String(6),"yellow"));       // DebugMild - yellow on black
+	m_items.addParam(new CustomTextFormat(String(7),"lightgreen"));   // DebugNote - light green on black
+	m_items.addParam(new CustomTextFormat(String(8),"white"));        // DebugCall - white on black
+	m_items.addParam(new CustomTextFormat(String(9),"cyan"));         // DebugInfo - light cyan on black
+	m_items.addParam(new CustomTextFormat(String(10),"teal"));        // DebugAll  - cyan on black
+    }
+    setParams(params);
+    // Connect signals
+    QtClient::connectObjects(m_edit,SIGNAL(anchorClicked(const QUrl&)),this,SLOT(urlTrigerred(const QUrl&)));
+}
+
+// Set parameters
+bool CustomTextEdit::setParams(const NamedList& params)
+{
+    static const String s_setRichItem = "set_richtext_item";
+    static const String s_setPlainItem = "set_plaintext_item";
+    static const String s_search = "search";
+    unsigned int n = params.length();
+    bool ok = true;
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* ns = params.getParam(i);
+	if (!(ns && ns->name()))
+	    continue;
+	if (ns->name() == s_setRichItem)
+	    setItem(*ns,true);
+	else if (ns->name() == s_setPlainItem)
+	    setItem(*ns,false);
+	else if (ns->name() == s_search)
+	    ok = setSearchHighlight(ns->toBoolean(),YOBJECT(NamedList,ns)) && ok;
+	else {
+	    // Prefixed parameters
+	    String tmp(ns->name());
+	    if (tmp.startSkip("set_url_handler:",false)) {
+		// Set handler from prefix[{scheme}]=formatting_template
+		if (!tmp)
+		    continue;
+		if (!m_urlHandlers.c_str())
+		    m_urlHandlers.assign(s_urlHandlers.c_str());
+		// Check for optional scheme
+		int pos = tmp.find('{');
+		if (pos <= 0 || tmp[tmp.length() - 1] != '}')
+		    m_urlHandlers.setParam(new CustomTextEditUrl(tmp,*ns));
+		else
+		    m_urlHandlers.setParam(new CustomTextEditUrl(tmp.substr(0,pos),*ns,
+			tmp.substr(pos + 1,tmp.length() - pos - 2)));
+	    }
+	    else if (tmp.startSkip("property:",false)) {
+		QObject* target = m_edit;
+		if (tmp.startSkip(name() + ":",false))
+		    target = this;
+		if (!QtClient::setProperty(target,tmp,*ns))
+		    ok = false;
+	    }
+	}
+    }
+    return ok;
+}
+
+// Append or insert text lines to this widget
+bool CustomTextEdit::addLines(const NamedList& lines, unsigned int max, bool atStart)
+{
+    unsigned int n = lines.length();
+    if (!n)
+	return true;
+    ScrollToEnd scroll(m_edit);
+    // Remove the temporary item(s)
+    if (m_tempItemCount && m_tempItemReplace) {
+	removeBlocks(m_tempItemCount);
+	m_tempItemCount = 0;
+    }
+    if (!m_debug) {
+	String text;
+	CustomTextFormat* last = 0;
+	// Line format: item=
+	// Each parameter may contain an optional list of parameters to be replaced in item
+	for (unsigned int i = 0; i < n; i++) {
+	    NamedString* ns = lines.getParam(i);
+	    if (!ns)
+		continue;
+	    CustomTextFormat* crt = find(ns->name());
+	    if (!crt)
+		crt = &m_defItem;
+	    if (last && last->type() != crt->type() && text) {
+		// Format changed: insert text now and reset it
+		insert(*last,text,atStart);
+		text.clear();
+	    }
+	    last = crt;
+	    if (last != &m_defItem) {
+		String tmp;
+		last->buildText(tmp,YOBJECT(NamedList,ns),this,!text.null());
+		text << tmp;
+	    }
+	    else
+		text << ns->name();
+	}
+	if (last && text)
+	    insert(*last,text,atStart);
+    }
+    else {
+	// Handle 'max'
+	QTextDocument* doc = m_edit->document();
+	if (doc)
+	    doc->setMaximumBlockCount((int)max);
+	// Line format: text=debuglevel
+	for (unsigned int i = 0; i < n; i++) {
+	    NamedString* ns = lines.getParam(i);
+	    if (!ns)
+		continue;
+	    CustomTextFormat* f = find(*ns);
+	    // Use default output if not found
+	    if (!f)
+		f = find(String("-1"));
+	    if (f) {
+		// Ignore CR, LF or CR/LF at text end: we are adding a block
+		unsigned int n = 0;
+		if (ns->name().endsWith("\r\n"))
+		    n = 2;
+		else if (ns->name().length()) {
+		    int pos = ns->name().length() - 1;
+		    if (ns->name()[pos] == '\r' || ns->name()[pos] == '\n')
+			n = 1;
+		}
+		if (n)
+		    insert(*f,ns->name().substr(0,ns->name().length() - n),atStart);
+		else
+		    insert(*f,ns->name(),atStart);
+	    }
+	}
+    }
+    return true;
+}
+
+// Set the displayed text of this widget
+bool CustomTextEdit::setText(const String& text, bool richText)
+{
+    ScrollToEnd scroll(m_edit);
+    m_edit->clear();
+    if (richText)
+	m_edit->insertHtml(QtClient::setUtf8(text));
+    else
+	m_edit->insertPlainText(QtClient::setUtf8(text));
+    return true;
+}
+
+// Retrieve the displayed text of this widget
+bool CustomTextEdit::getText(String& text, bool richText)
+{
+    if (richText)
+	QtClient::getUtf8(text,m_edit->toHtml());
+    else
+	QtClient::getUtf8(text,m_edit->toPlainText());
+    return true;
+}
+
+// Add/change/clear a pre-formatted item (item must be name[:[value])
+void CustomTextEdit::setItem(const String& value, bool html)
+{
+    if (!value)
+	return;
+    int pos = value.find(':');
+    if (pos > 0 && pos != (int)value.length() - 1) {
+	String id = value.substr(0,pos);
+	String val = value.substr(pos + 1);
+	CustomTextFormat* f = find(id);
+	// Remove existing if format changes
+	if (f && ((html && f->type() != CustomTextFormat::Html) ||
+	    (!html && f->type() == CustomTextFormat::Plain))) {
+	    m_items.clearParam(f);
+	    f = 0;
+	}
+	if (!f)
+	    m_items.addParam(new CustomTextFormat(id,val,html));
+	else
+	    f->assign(val);
+    }
+    else if (pos < 0)
+	m_items.clearParam(value);
+    else if (pos > 0)
+	m_items.clearParam(value.substr(0,pos));
+}
+
+// Set/reset text highlight
+bool CustomTextEdit::setSearchHighlight(bool on, NamedList* params)
+{
+    if (!on) {
+	m_lastFoundPos = -1;
+	if (params && params->getBoolValue("reset",true))
+	    m_searchFound.restore(m_edit->document());
+	else
+	    m_searchFound.m_list.clear();
+	return true;
+    }
+    if (!params)
+	return false;
+    QTextDocument* doc = m_edit->document();
+    if (!doc)
+	return false;
+    QString find = QtClient::setUtf8(params->getValue("find"));
+    if (!find.length())
+	return false;
+    Qt::CaseSensitivity cs = params->getBoolValue("matchcase") ?
+	Qt::CaseSensitive : Qt::CaseInsensitive;
+    bool found = false;
+    QString text = doc->toPlainText();
+    if (params->getBoolValue("all")) {
+	m_lastFoundPos = -1;
+	m_searchFound.restore(doc);
+	int pos = -1;
+	do {
+	    pos = text.indexOf(find,pos + 1,cs);
+	    if (pos >= 0)
+		handleFound(pos,find.length());
+	}
+	while (pos >= 0);
+	if (m_searchFound.m_list.size()) {
+	    found = true;
+	    ensureCharVisible(m_searchFound.m_list[0].m_docPos);
+	}
+    }
+    else {
+	if (params->getBoolValue("next"))
+	    m_lastFoundPos = text.indexOf(find,m_lastFoundPos >= 0 ? m_lastFoundPos + 1 : 0,cs);
+	else if (m_lastFoundPos < 0)
+	    m_lastFoundPos = text.lastIndexOf(find,-1,cs);
+	else if (m_lastFoundPos)
+	    m_lastFoundPos = text.lastIndexOf(find,m_lastFoundPos - 1,cs);
+	if (m_lastFoundPos >= 0) {
+	    found = true;
+	    m_searchFound.restore(doc);
+	    handleFound(m_lastFoundPos,find.length());
+	    ensureCharVisible(m_lastFoundPos);
+	}
+    }
+    return found;
+}
+
+// Ensure the character at a given position is visible
+void CustomTextEdit::ensureCharVisible(int pos)
+{
+    QTextCursor show(m_edit->document());
+    show.movePosition(QTextCursor::NextCharacter,QTextCursor::MoveAnchor,pos);
+    m_edit->setTextCursor(show);
+    m_edit->ensureCursorVisible();
+}
+
+// Replace string sequences with formatted text
+void CustomTextEdit::replace(String& text)
+{
+    if (!text)
+	return;
+    // Replace URLs ?
+    if (m_followUrl) {
+	const NamedList& urls = m_urlHandlers.c_str() ? m_urlHandlers : s_urlHandlers;
+	unsigned int n = urls.length();
+	for (int start = 0; start < (int)text.length();) {
+	    int len = 1;
+	    for (unsigned int i = 0; i < n; i++) {
+		const CustomTextEditUrl* ns = static_cast<CustomTextEditUrl*>(urls.getParam(i));
+		// Parameter name is the URL prefix
+		if (!(ns && ns->name()))
+		    continue;
+		if (ns->name().length() >= text.length() - start)
+		    continue;
+		// Get html template from parameter value or list name
+		const char* templ = *ns ? ns->c_str() : urls.c_str();
+		if (TelEngine::null(templ))
+		    continue;
+		// Check for prefix match
+		if (::strncmp(text.c_str() + start,ns->name().c_str(),ns->name().length()))
+		    continue;
+		// Detect url end
+		int end = start + (int)ns->name().length();
+		while (!isWordBreak(text[end]))
+		    end++;
+		// Go back 1 char if the last one should be ignored
+		if ((end > start + (int)ns->name().length()) && isIgnoreUrlEnd(text[end - 1]))
+		    end--;
+		len = end - start;
+		// Replace the URL if have something after prefix
+		if (len <= (int)ns->name().length()) {
+		    len++;
+		    break;
+		}
+		// Check if we have a scheme to prepend for this one
+		String url = text.substr(start,len);
+		NamedList p("");
+		p.addParam("url-display",url);
+		p.addParam("url",ns->m_scheme ? (ns->m_scheme + url) : url);
+		String u = templ;
+		p.replaceParams(u);
+		text = text.substr(0,start) + u + text.substr(end);
+		len = (int)u.length();
+		break;
+	    }
+	    start += len;
+	}
+    }
+}
+
+// Insert text using a given format. Update temporary item length if appropriate
+void CustomTextEdit::insert(CustomTextFormat& fmt, const String& text, bool atStart)
+{
+    int n = fmt.insertText(m_edit,text,atStart,m_tempItemReplace ? 0 : m_tempItemCount);
+    if (m_tempItemName != fmt.toString()) {
+	// Reset counter if temporary item was replaced
+	if (m_tempItemReplace)
+	    m_tempItemCount = 0;
+    }
+    else
+	m_tempItemCount = !atStart ? n : -n;
+}
+
+// Remove blocks from edit widget
+void CustomTextEdit::removeBlocks(int blocks)
+{
+    if (!blocks)
+	return;
+    QTextDocument* doc = m_edit->document();
+    if (!doc)
+	return;
+    QTextCursor c(doc);
+    moveCursor(c,blocks < 0,blocks,true);
+    c.removeSelectedText();
+}
+
+// URL clicked notification
+void CustomTextEdit::urlTrigerred(const QUrl& url)
+{
+    if (!(m_followUrl && Client::valid()))
+	return;
+    String tmp;
+    QtClient::getUtf8(tmp,url.toString());
+    XDebug(ClientDriver::self(),DebugAll,"CustomTextEdit(%s)::urlTrigerred(%s)",
+	name().c_str(),tmp.c_str());
+    Client::self()->openUrl(tmp);
+}
+
+// Handle found item. Add data to found items. Set formatting
+void CustomTextEdit::handleFound(int pos, int len)
+{
+    QTextCursor c(m_edit->document());
+    c.movePosition(QTextCursor::NextCharacter,QTextCursor::MoveAnchor,pos);
+    c.movePosition(QTextCursor::NextCharacter,QTextCursor::KeepAnchor,len);
+    m_searchFound.add(c);
+    QString sel = c.selectedText();
+    c.removeSelectedText();
+    c.insertText(sel,m_searchFoundFormat);
+}
+
+
+/*
+ * CustomTextFactory
+ */
+// Build objects
+void* CustomTextFactory::create(const String& type, const char* name, NamedList* params)
+{
+    // Init URL handlers
+    if (!s_urlHandlers.c_str()) {
+	s_urlHandlers.assign("<a href=\"${url}\"><span style=\"text-decoration: underline; color:#0000ff;\">${url-display}</span></a>");
+	s_urlHandlers.addParam(new CustomTextEditUrl("http://"));
+	s_urlHandlers.addParam(new CustomTextEditUrl("https://"));
+	s_urlHandlers.addParam(new CustomTextEditUrl("www.","","http://"));
+    }
+    if (!params)
+	return 0;
+    QWidget* parentWidget = 0;
+    String* wndname = params->getParam("parentwindow");
+    if (!TelEngine::null(wndname)) {
+	String* wName = params->getParam("parentwidget");
+	QtWindow* wnd = static_cast<QtWindow*>(Client::self()->getWindow(*wndname));
+	if (wnd && !TelEngine::null(wName))
+	    parentWidget = wnd->findChild<QWidget*>(QtClient::setUtf8(*wName));
+    }
+    if (type == "CustomTextEdit")
+	return new CustomTextEdit(name,*params,parentWidget);
+    return 0;
+}
+
+}; // anonymous namespace
+
+#include "customtext.moc"
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/customtext.h b/modules/qt5/customtext.h
new file mode 100644
index 0000000..5580bf7
--- /dev/null
+++ b/modules/qt5/customtext.h
@@ -0,0 +1,386 @@
+/**
+ * customtext.h
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Custom text edit objects
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#ifndef __CUSTOMTEXT_H
+#define __CUSTOMTEXT_H
+
+#include "qt5client.h"
+
+using namespace TelEngine;
+namespace { // anonymous
+
+class CustomTextFormat;                  // Custom QTextEdit format entry
+class CustomTextEditUrl;                 // Custom text edit url
+class TextFragment;                      // A formatted text document fragment
+class TextFragmentList;                  // A text fragment container
+class CustomTextEdit;                    // Custom QTextEdit
+
+/**
+ * Implements interfaces used to add/insert text into a CustomTextEdit widget
+ * The value of the NamedString may contain a template used to replace parameters
+ * @short A custom QTextEdit format entry
+ */
+class CustomTextFormat : public NamedString
+{
+    YCLASS(CustomTextFormat,NamedString)
+public:
+    /**
+     * Text format type enumeration
+     */
+    enum Type {
+	Html,                            // HTML formatted text
+	Plain,                           // Plain text
+	Block,                           // Use QT format class(es)
+    };
+
+    /**
+     * Constructor. Build a Block type
+     */
+    CustomTextFormat(const String& id, const char* color, const char* bgcolor = 0);
+
+    /**
+     * Constructor. Build a Html/Plain type
+     */
+    CustomTextFormat(const String& id, const char* value, bool html);
+
+    /**
+     * Destructor
+     */
+    virtual ~CustomTextFormat();
+
+    /**
+     * Retrieve this object's type
+     */
+    inline Type type() const
+	{ return m_type; }
+
+    /**
+     * Add/insert text into an edit widget
+     * @param edit Edit widget
+     * @param text Text buffer
+     * @param atStart True to insert at start, false to append
+     * @param blocks The number of blocks to skip if inserted at start or insert before if added
+     * @return The number of blocks added
+     */
+    int insertText(QTextEdit* edit, const String& text, bool atStart, int blocks);
+
+    /**
+     * Set text from value. Replace text parameters if not empty
+     * @param text Text buffer
+     * @param params Parameters to replace
+     * @param owner Text edit owner
+     * @param lineBrBefore True to append a libe break before it
+     */
+    void buildText(String& text, const NamedList* params, CustomTextEdit* owner,
+	bool lineBrBefore);
+
+private:
+    Type m_type;
+    QTextBlockFormat* m_blockFormat;
+    QTextCharFormat* m_charFormat;
+};
+
+/**
+ * This class holds an url definition with an optional scheme
+ * NamedString's value may contain optional formatting template
+ * @short Custom text edit url
+ */
+class CustomTextEditUrl : public NamedString
+{
+public:
+    inline CustomTextEditUrl(const char* name, const char* value = 0, const char* scheme = 0)
+	: NamedString(name,value),
+	m_scheme(scheme)
+	{}
+    String m_scheme;
+};
+
+/**
+ * This class keeps a formatted text document fragment along
+ *  with document position
+ * @short A formatted text document fragment
+ */
+class TextFragment : public QTextDocumentFragment
+{
+public:
+    /**
+     * Constructor. Build a text fragment from a cursor's selection
+     * @param c The cursor
+     */
+    inline TextFragment(const QTextCursor& c)
+	: QTextDocumentFragment(c),
+	m_docPos(c.selectionStart())
+	{}
+
+    /**
+     * Copy constructor
+     * @param other Source text fragment
+     */
+    inline TextFragment(const TextFragment& other)
+	: QTextDocumentFragment(other), m_docPos(other.m_docPos)
+	{}
+
+    /**
+     * The position of this fragment in the document
+     */
+    int m_docPos;
+
+private:
+    TextFragment() {};
+};
+
+/**
+ * This class implements a TextFragment container
+ * @short A text fragment container
+ */
+class TextFragmentList
+{
+public:
+    /**
+     * Restore all fragments in the document. Clear the list
+     * @param doc The document
+     */
+    void restore(QTextDocument* doc);
+
+    /**
+     * Build and append a text fragment from a cursor's selection
+     * @param c The cursor
+     */
+    inline void add(QTextCursor& c)
+	{ m_list.append(TextFragment(c)); }
+
+    /**
+     * The fragments owned by this container
+     */
+    QList<TextFragment> m_list;
+};
+
+/**
+ * This class holds custom text edit widget with abilities to add pre-formated
+ *  parameterized text
+ * @short A custom text edit widget
+ */
+class CustomTextEdit : public QtCustomWidget
+{
+    YCLASS(CustomTextEdit,QtCustomWidget)
+    Q_CLASSINFO("CustomTextEdit","Yate")
+    Q_OBJECT
+    Q_PROPERTY(bool _yate_followurl READ followUrl WRITE setFollowUrl(bool))
+    Q_PROPERTY(QString _yate_tempitemname READ tempItemName WRITE setTempItemName(QString))
+    Q_PROPERTY(int _yate_tempitemcount READ tempItemCount WRITE setTempItemCount(int))
+    Q_PROPERTY(bool _yate_tempitemreplace READ tempItemReplace WRITE setTempItemReplace(bool))
+public:
+    /**
+     * Constructor
+     * @param name Object name
+     * @param params Object parameters
+     * @param parent Optional parent
+     */
+    CustomTextEdit(const char* name, const NamedList& params, QWidget* parent);
+
+    /**
+     * Set parameters. Add text
+     * @param params Parameter list
+     * @return True on success
+     */
+    virtual bool setParams(const NamedList& params);
+
+    /**
+     * Clear the edit widget
+     * @return True
+     */
+    virtual bool clearTable() {
+	    m_edit->clear();
+	    return true;
+	}
+
+    /**
+     * Append or insert text lines to this widget
+     * @param name The name of the widget
+     * @param lines List containing the lines
+     * @param max The maximum number of lines allowed to be displayed. Set to 0 to ignore
+     * @param atStart True to insert, false to append
+     * @return True on success
+     */
+    virtual bool addLines(const NamedList& lines, unsigned int max, bool atStart = false);
+
+    /**
+     * Set the displayed text of this widget
+     * @param text Text value to set
+     * @param richText True if the text contains format data
+     * @return True on success
+     */
+    virtual bool setText(const String& text, bool richText = false);
+
+    /**
+     * Retrieve the displayed text of this widget
+     * @param text Text value
+     * @param richText True to retrieve formatted data
+     * @return True on success
+     */
+    virtual bool getText(String& text, bool richText = false);
+
+    /**
+     * Add/change/clear a pre-formatted item (item must be name[:[value])
+     * @param value Formatted item to set or clear
+     * @param html True to add rich text, false to add plain text
+     */
+    void setItem(const String& value, bool html);
+
+    /**
+     * Set/reset search text highlight
+     * @param on True to set, false to reset
+     * @param params Parameters. Ignored it reset
+     * @return True if reset or a match was found. False otherwise
+     */
+    bool setSearchHighlight(bool on, NamedList* params);
+
+    /**
+     * Ensure the character at a given position is visible
+     * @param pos The position in the document
+     */
+    void ensureCharVisible(int pos);
+
+    /**
+     * Replace string sequences with formatted text
+     * @param text Text buffer
+     */
+    void replace(String& text);
+
+    /**
+     * Insert text using a given format. Update temporary item length if appropriate
+     * @param fmt Format to use
+     * @param text Text to insert
+     * @param atStart Insert at start or append
+     */
+    void insert(CustomTextFormat& fmt, const String& text, bool atStart);
+
+    /**
+     * Remove blocks from edit widget
+     * @param blocks The number of blocks to remove, negative to remove from start
+     */
+    void removeBlocks(int blocks);
+
+    /**
+     * Retrieve the value of _yate_followurl property
+     * @return The value of _yate_followurl property
+     */
+    bool followUrl()
+	{ return m_followUrl; }
+
+    /**
+     * Set the value of _yate_followurl property
+     * @param value The new value of _yate_followurl property
+     */
+    void setFollowUrl(bool value)
+	{ m_followUrl = value; }
+
+    /**
+     * Retrieve the value of _yate_tempitemname property
+     * @return The value of _yate_tempitemname property
+     */
+    QString tempItemName()
+	{ return QtClient::setUtf8(m_tempItemName); }
+
+    /**
+     * Set the value of _yate_tempitemname property
+     * @param value The new value of _yate_tempitemname property
+     */
+    void setTempItemName(QString value)
+	{ QtClient::getUtf8(m_tempItemName,value); }
+
+    /**
+     * Retrieve the value of _yate_tempitemcount property
+     * @return The value of _yate_tempitemcount property
+     */
+    int tempItemCount()
+	{ return m_tempItemCount; }
+
+    /**
+     * Set the value of _yate_tempitemcount property
+     * @param value The new value of _yate_tempitemcount property
+     */
+    void setTempItemCount(int value) {
+	    if (!value && m_tempItemCount)
+		removeBlocks(m_tempItemCount);
+	    m_tempItemCount = value;
+	}
+
+    /**
+     * Retrieve the value of _yate_tempitemreplace property
+     * @return The value of _yate_tempitemreplace property
+     */
+    bool tempItemReplace()
+	{ return m_tempItemReplace; }
+
+    /**
+     * Set the value of _yate_tempitemreplace property
+     * @param value The new value of _yate_tempitemreplace property
+     */
+    void setTempItemReplace(bool value)
+	{ m_tempItemReplace = value; }
+
+public slots:
+    /**
+     * URL clicked notification
+     * Use this slot instead of QT open external links:
+     *  displayed text will be cleared if the link is not handled
+     */
+    void urlTrigerred(const QUrl& url);
+
+protected:
+    /**
+     * Handle found item. Add data to found items. Set formatting
+     * @param pos The position in document
+     * @param len Found text length
+     */
+    void handleFound(int pos, int len);
+
+    /**
+     * Retrieve a custom text format object
+     * @param name Item name
+     * @return CustomTextFormat pointer or 0 if not found
+     */
+    inline CustomTextFormat* find(const String& name)
+	{ return YOBJECT(CustomTextFormat,m_items.getParam(name)); }
+
+    QTextBrowser* m_edit;                // The edit widget
+    bool m_debug;                        // This is a debug widget
+    NamedList m_items;                   // Formatted items
+    CustomTextFormat m_defItem;          // Default text format used to add plain text
+                                         //  when an item is not found
+    bool m_followUrl;                    // Follow URLs
+    NamedList m_urlHandlers;             // List specific URL handlers
+    String m_tempItemName;               // Temporary last item name
+    int m_tempItemCount;                 // Temporary last item count
+                                         //  negative: start, positive: end, 0: none
+    bool m_tempItemReplace;              // Replace (delete) temporary item(s)
+    // Search
+    TextFragmentList m_searchFound;      // Last found data: restore it on request
+    QTextCharFormat m_searchFoundFormat; // Found item(s) formatting
+    int m_lastFoundPos;                  // Last found position in document
+};
+
+}; // anonymous namespace
+
+#endif // __CUSTOMTEXT_H
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/customtree.cpp b/modules/qt5/customtree.cpp
new file mode 100644
index 0000000..966f345
--- /dev/null
+++ b/modules/qt5/customtree.cpp
@@ -0,0 +1,4059 @@
+/**
+ * customtree.cpp
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Custom QtTree based objects
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2010-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#include "customtree.h"
+
+#ifndef _WINDOWS
+#include <dirent.h>
+#include <sys/stat.h>
+#endif
+
+using namespace TelEngine;
+namespace { // anonymous
+
+// The factory
+class CustomTreeFactory : public UIFactory
+{
+public:
+    inline CustomTreeFactory(const char* name = "CustomTreeFactory")
+	: UIFactory(name) {
+	    m_types.append(new String("ContactList"));
+	    m_types.append(new String("QtCustomTree"));
+	    m_types.append(new String("FileListTree"));
+	}
+    virtual void* create(const String& type, const char* name, NamedList* params = 0);
+};
+
+// Utility class used to disable/enable a tree sorting flag
+// Disable tree sorting upon creation and enable it on destruction
+// Objects of this class should be created in methods changing
+//  tree content
+class SafeTree
+{
+public:
+    inline SafeTree(QTreeWidget* tree)
+	: m_tree(tree), m_sorting(false) {
+	    if (!tree)
+		return;
+	    m_tree->setUpdatesEnabled(false);
+	    if (tree->isSortingEnabled()) {
+		m_sorting = tree->isSortingEnabled();
+		m_tree->setSortingEnabled(false);
+	    }
+	}
+    inline ~SafeTree() {
+	    if (!m_tree)
+		return;
+	    if (m_sorting)
+		m_tree->setSortingEnabled(true);
+	    m_tree->setUpdatesEnabled(true);
+	}
+private:
+    QTreeWidget* m_tree;
+    bool m_sorting;
+};
+
+// Inc/dec an integer value
+class SafeInt
+{
+public:
+    inline SafeInt(int* value)
+	: m_value(value) {
+	    if (m_value)
+		(*m_value)++;
+	}
+    inline ~SafeInt() {
+	    if (m_value)
+		(*m_value)--;
+	}
+protected:
+    int* m_value;
+};
+
+// Utility class used to restore selection
+class TreeRestoreSel
+{
+public:
+    inline TreeRestoreSel(QtCustomTree* tree, const String& check = String::empty())
+	: m_tree(tree) {
+	    if (!tree)
+		return;
+	    tree->getSelect(m_sel);
+	    if (m_sel && check && m_sel != check)
+		m_sel.clear();
+	}
+    inline ~TreeRestoreSel() {
+	    if (m_tree && m_sel)
+		m_tree->setSelect(m_sel);
+	}
+private:
+    QtCustomTree* m_tree;
+    String m_sel;
+};
+
+
+static CustomTreeFactory s_factory;
+static const String s_noGroupId(MD5("Yate").hexDigest() + "_NOGROUP");
+static const String s_offline("offline");
+static NamedList s_delegateCommon("");
+
+// Set size from string
+static inline void setSize(QSize& size, const String& s)
+{
+    if (!s)
+	return;
+    int pos = s.find(',');
+    if (pos < 0) {
+	int val = s.toInteger(0,0,0);
+	size = QSize(val,val);
+    }
+    else
+	size = QSize(s.substr(0,pos).toInteger(0,0,0),s.substr(pos + 1).toInteger(0,0,0));
+}
+
+// Utility: compare strings
+// return -1 if s1 < s2, 0 if s1 == s2, 1 if s1 > s2
+static inline int compareStr(const QString& s1, const QString& s2,
+    Qt::CaseSensitivity cs)
+{
+    if (cs == Qt::CaseSensitive) {
+	if (s1 == s2)
+	    return 0;
+	return (s1 < s2) ? -1 : 1;
+    }
+    return s1.compare(s2,cs);
+}
+
+// Utility: compare a single key item
+static bool caseInsensitiveLessThan(const QtTreeItemKey& left,
+    const QtTreeItemKey& right)
+{
+    return compareStr(left.second,right.second,Qt::CaseInsensitive) < 0;
+}
+
+// Utility: compare a single key item
+static bool caseInsensitiveGreaterThan(const QtTreeItemKey& left,
+    const QtTreeItemKey& right)
+{
+    return compareStr(left.second,right.second,Qt::CaseInsensitive) > 0;
+}
+
+// Utility: compare a single key item
+static bool caseSensitiveLessThan(const QtTreeItemKey& left,
+    const QtTreeItemKey& right)
+{
+    return compareStr(left.second,right.second,Qt::CaseSensitive) < 0;
+}
+
+// Utility: compare a single key item
+static bool caseSensitiveGreaterThan(const QtTreeItemKey& left,
+    const QtTreeItemKey& right)
+{
+    return compareStr(left.second,right.second,Qt::CaseSensitive) > 0;
+}
+
+// Utility: sort
+static inline void stableSort(QVector<QtTreeItemKey>& v,
+    Qt::SortOrder order, Qt::CaseSensitivity cs)
+{
+    if (order == Qt::AscendingOrder) {
+	if (cs == Qt::CaseInsensitive)
+	    std::stable_sort(v.begin(),v.end(),caseInsensitiveLessThan);
+	else
+	    std::stable_sort(v.begin(),v.end(),caseSensitiveLessThan);
+    }
+    else if (cs == Qt::CaseInsensitive)
+	std::stable_sort(v.begin(),v.end(),caseInsensitiveGreaterThan);
+    else
+	std::stable_sort(v.begin(),v.end(),caseSensitiveGreaterThan);
+}
+
+// Utility: sort
+
+// Retrieve a string from a list
+static inline const String& objListItem(ObjList* list, int index)
+{
+    GenObject* gen = list ? (*list)[index] : 0;
+    return gen ? gen->toString() : String::empty();
+}
+
+int replaceHtmlParams(String& str, const NamedList& list, bool spaceEol = false)
+{
+    int p1 = 0;
+    int cnt = 0;
+    while ((p1 = str.find("${",p1)) >= 0) {
+	int p2 = str.find('}',p1 + 2);
+	if (p2 <= 0)
+	    return -1;
+	String param = str.substr(p1 + 2,p2 - p1 - 2);
+	param.trimBlanks();
+	int defValPos = param.find('$');
+	if (defValPos < 0)
+	    param = list.getValue(param);
+	else {
+	    // param is in ${<name>$<default>} format
+	    String def = param.substr(defValPos + 1);
+	    param = list.getValue(param.substr(0,defValPos).trimBlanks());
+	    if (!param && def)
+		param = list.getValue(def.trimBlanks());
+	}
+	if (param)
+	    Client::plain2html(param,spaceEol);
+	str = str.substr(0,p1) + param + str.substr(p2 + 1);
+	// advance search offset past the string we just replaced
+	p1 += param.length();
+	cnt++;
+    }
+    return cnt;
+}
+
+
+/*
+ * QtCellGridDraw
+ */
+// Set draw pen
+void QtCellGridDraw::setPen(Position pos, QPen pen)
+{
+#define QtCellGridSetPen(val,p) \
+    if (0 != (pos & val)) { \
+	p = pen; \
+	m_flags |= val; \
+    }
+    QtCellGridSetPen(Left,m_left);
+    QtCellGridSetPen(Top,m_top);
+    QtCellGridSetPen(Right,m_right);
+    QtCellGridSetPen(Bottom,m_bottom);
+}
+
+// Set draw pens from a list of parameters
+void QtCellGridDraw::setPen(const NamedList& params)
+{
+    setPen(Left,params);
+    setPen(Top,params);
+    setPen(Right,params);
+    setPen(Bottom,params);
+}
+
+// Set pen from parameters list
+void QtCellGridDraw::setPen(Position pos, const NamedList& params)
+{
+    String prefix("griddraw_");
+    if (pos == Left)
+	prefix << "left";
+    else if (pos == Top)
+	prefix << "top";
+    else if (pos == Right)
+	prefix << "right";
+    else if (pos == Bottom)
+	prefix << "bottom";
+    else
+	return;
+    QPen pen;
+    bool ok = false;
+    const String& color = params[prefix + "_color"];
+    if (color) {
+	ok = true;
+	if (color[0] == '#')
+	    pen.setColor(QColor(color.substr(1).toInteger(0,16)));
+	else
+	    pen.setColor(QColor(color));
+    }
+    if (ok)
+	setPen(pos,pen);
+}
+
+// Draw the borders
+void QtCellGridDraw::draw(QPainter* p, QRect& rect, bool isFirstRow, bool isFirstColumn,
+    bool isLastRow, bool isLastColumn) const
+{
+    if (!(p && flag(Pos)))
+	return;
+    if (0 != (m_flags & Left) && (!isFirstColumn || flag(DrawStart))) {
+	p->setPen(m_left);
+	p->drawLine(rect.topLeft(),rect.bottomLeft());
+    }
+    if (0 != (m_flags & Top) && (!isFirstRow || flag(DrawStart))) {
+	p->setPen(m_top);
+	p->drawLine(rect.topLeft(),rect.topRight());
+    }
+    if (0 != (m_flags & Right) && (!isLastColumn || flag(DrawEnd))) {
+	p->setPen(m_right);
+	p->drawLine(rect.topRight(),rect.bottomRight());
+    }
+    if (0 != (m_flags & Bottom) && (!isLastRow || flag(DrawEnd))) {
+	p->setPen(m_bottom);
+	p->drawLine(rect.bottomLeft(),rect.bottomRight());
+    }
+}
+
+
+//
+// QtTreeDrag
+//
+QtTreeDrag::QtTreeDrag(QObject* parent, const NamedList* params)
+    : QObject(parent),
+    m_urlBuilder(0)
+{
+    if (!params)
+	return;
+    const String& fmt = (*params)[YSTRING("_yate_drag_url_template")];
+    if (fmt)
+	setUrlBuilder(fmt,(*params)[YSTRING("_yate_drag_url_queryparams")]);
+}
+
+// Set the URL builder, set to NULL if fmt is empty
+void QtTreeDrag::setUrlBuilder(const String& fmt, const String& queryParams)
+{
+    if (m_urlBuilder)
+	QtClient::deleteLater(m_urlBuilder);
+    if (fmt)
+	m_urlBuilder = new QtUrlBuilder(this,fmt,queryParams);
+    else
+	m_urlBuilder = 0;
+}
+
+// Build MIME data for a list of items
+QMimeData* QtTreeDrag::mimeData(const QList<QTreeWidgetItem*> items) const
+{
+    if (!m_urlBuilder)
+	return 0;
+    int n = items.size();
+    if (n < 1)
+	return 0;
+    QList<QUrl> urls;
+    for (int i = 0; i < n; i++) {
+	QtTreeItem* it = static_cast<QtTreeItem*>(items[i]);
+	QUrl url = m_urlBuilder->build(*it);
+	if (!url.isEmpty())
+	    urls.append(url);
+    }
+    QMimeData* data = new QMimeData;
+    if (urls.size() > 0)
+	data->setUrls(urls);
+    return data;
+}
+
+
+//
+// QtTreeItemProps
+//
+// Set a button's action, create if it not found
+bool QtTreeItemProps::setPaintButtonAction(const String& name, const String& action)
+{
+    QtPaintButtonDesc* b = QtPaintButtonDesc::find(m_paintItemsDesc,name);
+    if (b)
+	b->m_params.assign(action);
+    return b != 0;
+}
+
+// Set a button's parameter, create it if not found
+bool QtTreeItemProps::setPaintButtonParam(const String& name, const String& param,
+    const String& value)
+{
+    if (!(name && param))
+	return false;
+    QtPaintButtonDesc* b = QtPaintButtonDesc::find(m_paintItemsDesc,name);
+    if (!b)
+	return false;
+    if (param == YSTRING("_yate_iconsize"))
+	setSize(b->m_iconSize,value);
+    else if (param == YSTRING("_yate_size"))
+	setSize(b->m_size,value);
+    else
+	b->m_params.setParam(param,value);
+    return true;
+}
+
+
+//
+// QtTreeItem
+//
+QtTreeItem::QtTreeItem(const char* id, int type, const char* text, bool storeExp)
+    : QTreeWidgetItem(type),
+    NamedList(id),
+    m_storeExp(storeExp),
+    m_heightDelta(0),
+    m_filtered(true),
+    m_extraPaintRight(0)
+{
+    if (!TelEngine::null(text))
+	QTreeWidgetItem::setText(0,QtClient::setUtf8(text));
+    XDebug(ClientDriver::self(),DebugAll,"QtTreeItem(%s) type=%d [%p]",id,type,this);
+}
+
+QtTreeItem::~QtTreeItem()
+{
+    TelEngine::destruct(m_extraPaintRight);
+    XDebug(ClientDriver::self(),DebugAll,"~QtTreeItem(%s) type=%d [%p]",c_str(),type(),this);
+}
+
+// Set a column's icon from a list of parameter cname_image
+void QtTreeItem::setImage(int col, const String& cname, const NamedList& list, int role)
+{
+    String* s = cname ? list.getParam(cname + "_image") : 0;
+    if (!s)
+	return;
+    if (role <= Qt::UserRole)
+	QTreeWidgetItem::setIcon(col,QIcon(QtClient::setUtf8(*s)));
+    else
+	setData(col,role,QtClient::setUtf8(*s));
+}
+
+// Update item filtered flag
+bool QtTreeItem::setFilter(const NamedList* filter)
+{
+    if (!filter) {
+	m_filtered = true;
+	return true;
+    }
+    int params = 0;
+    m_filtered = false;
+    NamedIterator iter(*this);
+    for (const NamedString* ns = 0; 0 != (ns = iter.get()); params++) {
+	if (!*ns)
+	    continue;
+	const String& match = (*filter)[ns->name()];
+	if (ns->find(match) >= 0) {
+	    m_filtered = true;
+	    break;
+	}
+    }
+    if (!params)
+	m_filtered = true;
+    return m_filtered;
+}
+
+// Set extra data to paint on right side of the item
+void QtTreeItem::setExtraPaintRight(QtPaintItems* obj)
+{
+    TelEngine::destruct(m_extraPaintRight);
+    m_extraPaintRight = obj;
+    QVariant var;
+    if (m_extraPaintRight)
+	var = QtRefObjectHolder::setVariant(m_extraPaintRight);
+    setData(0,QtCustomTree::RoleQtDrawItems,var);
+}
+
+// Set extra paint buttons on right side of the item
+void QtTreeItem::setExtraPaintRightButtons(const String& list, QtTreeItemProps* props)
+{
+    if (!list) {
+	setExtraPaintRight(0);
+	return;
+    }
+    QtPaintItems* items = new QtPaintItems(list);
+    if (props) {
+	ObjList* pList = list.split(',');
+	for (ObjList* o = pList->skipNull(); o; o = o->skipNext()) {
+	    String* s = static_cast<String*>(o->get());
+	    QtPaintButtonDesc* b = QtPaintButtonDesc::find(props->m_paintItemsDesc,*s,false);
+	    if (b)
+		items->append(*b);
+	}
+	TelEngine::destruct(pList);
+    }
+    items->itemsAdded();
+    setExtraPaintRight(items);
+}
+
+
+/*
+ * QtCustomTree
+ */
+QtCustomTree::QtCustomTree(const char* name, const NamedList& params, QWidget* parent,
+    bool applyParams)
+    : QtTree(name,parent),
+    m_notifyItemChanged(false),
+    m_hasCheckableCols(false),
+    m_menu(0),
+    m_autoExpand(false),
+    m_rowHeight(-1),
+    m_changing(0),
+    m_filter(0),
+    m_haveWidgets(false),
+    m_haveDrawQtItems(false),
+    m_setCurrentColumn(-1),
+    m_drop(0),
+    m_acceptDropOnEmpty(QtDrop::Ask),
+    m_drag(0),
+    m_drawBranches(false),
+    m_timerTriggerSelect(0),
+    m_lastItemDrawHover(0)
+{
+    setIndentation(0);
+    setUniformRowHeights(false);
+    setFrameShape(QFrame::NoFrame);
+    setRootIsDecorated(false);
+    // Add item props translation
+    addItemType(QTreeWidgetItem::Type,"default");
+    NamedIterator iter(params);
+    int typeN = 0;
+    for (const NamedString* ns = 0; 0 != (ns = iter.get());) {
+	if (ns->name() == YSTRING("buildprops"))
+	    // Build properties
+	    QtClient::buildProps(this,*ns);
+	else if (ns->name() == YSTRING("_yate_tree_additemtype")) {
+	    // Add item types
+	    if (*ns)
+		addItemType(TypeCount + typeN++,*ns);
+	}
+	else if (ns->name() == YSTRING("vertical_scroll_policy")) {
+	    // Vertical scroll policy
+	    if (*ns == YSTRING("item"))
+		QTreeWidget::setVerticalScrollMode(QAbstractItemView::ScrollPerItem);
+	    else if (*ns == YSTRING("pixel"))
+		QTreeWidget::setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+	}
+	else if (ns->name() == YSTRING("_yate_set_draganddrop")) {
+	    // Drag & Drop
+	    bool drag = false;
+	    bool drop = false;
+	    QtDragAndDrop::checkEnable(*ns,drag,drop);
+	    if (drag) {
+		m_drag = new QtTreeDrag(this,&params);
+		setDragEnabled(true);
+	    }
+	    if (drop) {
+		m_drop = new QtListDrop(this,&params);
+		setAcceptDrops(true);
+	    }
+	}
+	else if (ns->name() == YSTRING("_yate_widgetattributes"))
+	    QtClient::setWidgetAttributes(this,*ns);
+	else if (ns->name() == YSTRING("_yate_set_currentcolumn"))
+	    // Current column to set when index changes
+	    m_setCurrentColumn = getColumnNo(*ns);
+	else if (ns->name() == YSTRING("_yate_busywidget"))
+	    QtClient::buildBusy(this,this,*ns,params);
+	else if (ns->name() == YSTRING("property:rootIsDecorated"))
+	    m_drawBranches = true;
+    }
+    QTreeWidgetItem* hdr = headerItem();
+    if (hdr) {
+	String* columns = params.getParam("columns");
+	if (TelEngine::null(columns))
+	    hdr->setHidden(true);
+	else {
+	    QHeaderView* header = QTreeView::header();
+	    ObjList* id = columns->split(',',false);
+	    ObjList* title = params["columns.title"].split(',',true);
+	    ObjList* width = params["columns.width"].split(',',true);
+	    ObjList* sizeMode = params["columns.resize"].split(',',true);
+	    ObjList* check = params["columns.check"].split(',',false);
+	    ObjList* emptyTitle = params["columns.allowemptytitle"].split(',',false);
+	    setColumnCount(id->count());
+	    int n = 0;
+	    for (ObjList* o = id->skipNull(); o; o = o->skipNext(), n++) {
+		String* name = static_cast<String*>(o->get());
+		String caption = objListItem(title,n);
+		if (!caption) {
+		    String tmp = *name;
+		    if (!emptyTitle->find(tmp.toLower()))
+			caption = *name;
+		}
+		hdr->setText(n,QtClient::setUtf8(caption));
+		hdr->setData(n,RoleId,QtClient::setUtf8(name->toLower()));
+		int ww = objListItem(width,n).toInteger(-1);
+		if (ww > 0)
+		    setColumnWidth(n,ww);
+		if (check->find(*name)) {
+		    hdr->setData(n,RoleCheckable,QVariant(true));
+		    m_hasCheckableCols = true;
+		}
+		// Header
+		if (!header)
+		    continue;
+		const String& szMode = header ? objListItem(sizeMode,n) : String::empty();
+		if (szMode == "fixed")
+		    header->setSectionResizeMode(n,QHeaderView::Fixed);
+		else if (szMode == "stretch")
+		    header->setSectionResizeMode(n,QHeaderView::Stretch);
+		else if (szMode == "contents")
+		    header->setSectionResizeMode(n,QHeaderView::ResizeToContents);
+		else
+		    header->setSectionResizeMode(n,QHeaderView::Interactive);
+	    }
+	    TelEngine::destruct(id);
+	    TelEngine::destruct(title);
+	    TelEngine::destruct(width);
+	    TelEngine::destruct(sizeMode);
+	    TelEngine::destruct(check);
+	    TelEngine::destruct(emptyTitle);
+	}
+    }
+    // Create item delegates
+    if (!s_delegateCommon) {
+	s_delegateCommon.assign(" ");
+	s_delegateCommon.addParam("role_display",String(RoleHtmlDelegate));
+	s_delegateCommon.addParam("role_image",String(RoleImage));
+	s_delegateCommon.addParam("role_background",String(RoleBackground));
+	s_delegateCommon.addParam("role_margins",String(RoleMargins));
+	s_delegateCommon.addParam("role_qtdrawitems",String(RoleQtDrawItems));
+    }
+    QList<QAbstractItemDelegate*> dlgs = QtItemDelegate::buildDelegates(this,params,&s_delegateCommon);
+    QStringList cNames;
+    for (int i = 0; i < dlgs.size(); i++) {
+	QtItemDelegate* dlg = qobject_cast<QtItemDelegate*>(dlgs[i]);
+	if (!dlg) {
+	    delete dlgs[i];
+	    continue;
+	}
+	if (cNames.size() < 1)
+	    cNames = columnIDs();
+	dlg->updateColumns(cNames);
+	QList<int>& cols = dlg->columns();
+	for (int i = 0; i < cols.size(); i++)
+	    setItemDelegateForColumn(cols[i],dlg);
+	if (cols.size() < 1)
+	    setItemDelegate(dlg);
+    }
+    if (hdr && !dlgs.size()) {
+	String* htmlDlg = params.getParam("htmldelegate");
+	if (!TelEngine::null(htmlDlg)) {
+	    ObjList* l = htmlDlg->split(',',false);
+	    for (ObjList* o = l->skipNull(); o; o = o->skipNext()) {
+		String* s = static_cast<String*>(o->get());
+		int col = s->toInteger(-1);
+		if (col < 0)
+		    col = getColumn(*s);
+		if (col < 0 || col >= columnCount())
+		    continue;
+		hdr->setData(col,RoleHtmlDelegate,true);
+		String prefix;
+		prefix << name << ".htmldelegate." << col;
+		NamedList pp(prefix);
+		pp.copySubParams(params,String("delegateparam.") + *s + ".");
+		pp.setParam(prefix + ".role_display",String(RoleHtmlDelegate));
+		pp.setParam(prefix + ".role_image",String(RoleImage));
+		pp.setParam(prefix + ".role_background",String(RoleBackground));
+		pp.setParam(prefix + ".role_margins",String(RoleMargins));
+		pp.setParam(prefix + ".role_qtdrawitems",String(RoleQtDrawItems));
+		QtHtmlItemDelegate* dlg = new QtHtmlItemDelegate(this,pp);
+		XDebug(ClientDriver::self(),DebugNote,
+		    "QtCustomTree(%s) setting html item delegate (%p,%s) for column %d [%p]",
+		    name,dlg,dlg->toString().c_str(),col,this);
+		setItemDelegateForColumn(col,dlg);
+	    }
+	    TelEngine::destruct(l);
+	}
+    }
+    // Grid
+    m_gridDraw.setPen(params);
+    // Connect signals
+    QtClient::connectObjects(this,SIGNAL(itemSelectionChanged()),
+	this,SLOT(itemSelChangedSlot()));
+    QtClient::connectObjects(this,SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)),
+	this,SLOT(itemDoubleClickedSlot(QTreeWidgetItem*,int)));
+    QtClient::connectObjects(this,SIGNAL(itemActivated(QTreeWidgetItem*,int)),
+	this,SLOT(itemDoubleClickedSlot(QTreeWidgetItem*,int)));
+    QtClient::connectObjects(this,SIGNAL(itemExpanded(QTreeWidgetItem*)),
+	this,SLOT(itemExpandedSlot(QTreeWidgetItem*)));
+    QtClient::connectObjects(this,SIGNAL(itemCollapsed(QTreeWidgetItem*)),
+	this,SLOT(itemCollapsedSlot(QTreeWidgetItem*)));
+    QtClient::connectObjects(this,SIGNAL(itemChanged(QTreeWidgetItem*,int)),
+	this,SLOT(itemChangedSlot(QTreeWidgetItem*,int)));
+    // Set params
+    applyItemViewProps(params);
+    if (applyParams)
+	setParams(params);
+}
+
+// Destructor
+QtCustomTree::~QtCustomTree()
+{
+    TelEngine::destruct(m_filter);
+}
+
+// Method re-implemented from QTreeWidget.
+// Draw item grid if set
+void QtCustomTree::drawRow(QPainter* p, const QStyleOptionViewItem& opt,
+    const QModelIndex& idx) const
+{
+    QTreeWidget::drawRow(p,opt,idx);
+    if (m_gridDraw.flag(QtCellGridDraw::Pos)) {
+	p->save();
+	int row = idx.row();
+	int lastCol = columnCount() - 1;
+	for (int i = 0; i <= lastCol; i++) {
+	    QModelIndex s = idx.sibling(row,i);
+	    if (s.isValid()) {
+		QRect r = visualRect(s);
+		m_gridDraw.draw(p,r,!row,!i,false,i == lastCol);
+	    }
+	}
+	p->restore();
+    }
+}
+
+// Retrieve item type definition from [type:]value. Create if not found
+QtUIWidgetItemProps* QtCustomTree::getItemProps(QString& in, String& value)
+{
+    String type;
+    int pos = in.indexOf(':');
+    if (pos >= 0) {
+	QtClient::getUtf8(type,in.left(pos));
+	QtClient::getUtf8(value,in.right(in.length() - pos - 1));
+    }
+    else
+	QtClient::getUtf8(value,in);
+    if (!type)
+	type = itemPropsName(QTreeWidgetItem::Type);
+    QtUIWidgetItemProps* p = QtUIWidget::getItemProps(type);
+    if (!p) {
+	p = new QtTreeItemProps(type);
+	m_itemProps.append(p);
+    }
+    return p;
+}
+
+// Set params
+bool QtCustomTree::setParams(const NamedList& params)
+{
+    SafeInt safeChg(&m_changing);
+    bool ok = QtUIWidget::setParams(params);
+    ok = QtUIWidget::setParams(this,params) && ok;
+    buildMenu(m_menu,params.getParam(YSTRING("menu")));
+    NamedString* filter = params.getParam(YSTRING("filter"));
+    if (filter) {
+	TelEngine::destruct(m_filter);
+	NamedList* p = YOBJECT(NamedList,filter);
+	if (p && p->count())
+	    m_filter = new NamedList(*p);
+	checkItemFilter();
+    }
+    return ok;
+}
+
+// Retrieve an item
+bool QtCustomTree::getTableRow(const String& item, NamedList* data)
+{
+    SafeInt safeChg(&m_changing);
+    QtTreeItem* it = find(item);
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::getTableRow(%s) found=%p [%p]",
+	name().c_str(),item.c_str(),it,this);
+    if (!it)
+	return false;
+    if (data) {
+	data->copyParams(*it);
+	// Get checked items
+	if (m_hasCheckableCols) {
+	    QTreeWidgetItem* hdr = headerItem();
+	    int n = hdr ? columnCount() : 0;
+	    for (int i = 0; i < n; i++) {
+		if (!hdr->data(i,RoleCheckable).toBool())
+		    continue;
+		String id;
+		getItemData(id,*hdr,i);
+		if (!id)
+		    continue;
+		bool checked = it->checkState(i) != Qt::Unchecked;
+		data->setParam("check:" + id,String::boolText(checked));
+	    }
+	}
+	QWidget* w = itemWidget(it,0);
+	if (w)
+	    getParams(w,*data);
+    }
+    return true;
+}
+
+bool QtCustomTree::setTableRow(const String& item, const NamedList* data)
+{
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::setTableRow(%s,%p) [%p]",
+	name().c_str(),item.c_str(),data,this);
+    QtTreeItem* it = find(item);
+    if (!it)
+	return false;
+    if (!data)
+	return true;
+    SafeTree tree(this);
+    SafeInt safeChg(&m_changing);
+    return updateItem(*it,*data);
+}
+
+// Add a new account or contact
+bool QtCustomTree::addTableRow(const String& item, const NamedList* data, bool atStart)
+{
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::addTableRow(%s,%p,%u) [%p]",
+	name().c_str(),item.c_str(),data,atStart,this);
+    if (!data)
+	return false;
+    if (find(item))
+	return false;
+    SafeTree tree(this);
+    SafeInt safeChg(&m_changing);
+    QtTreeItem* parent = 0;
+    int type = QTreeWidgetItem::Type;
+    if (data) {
+	type = itemType((*data)["item_type"]);
+	const String& pName = (*data)["parent"];
+	if (pName) {
+	    parent = find(pName);
+	    if (!parent) {
+		Debug(ClientDriver::self(),DebugAll,
+		    "QtCustomTree(%s)::addTableRow(%s,%p,%u) parent '%s' not found [%p]",
+		    name().c_str(),item.c_str(),data,atStart,pName.c_str(),this);
+		return false;
+	    }
+	}
+    }
+    QtTreeItem* it = new QtTreeItem(item,type);
+    if (data)
+	it->copyParams(*data);
+    if (addChild(it,atStart,parent))
+	return !data || updateItem(*it,*data);
+    TelEngine::destruct(it);
+    return false;
+}
+
+// Remove an item from tree
+bool QtCustomTree::delTableRow(const String& item)
+{
+    if (!item)
+	return false;
+    SafeInt safeChg(&m_changing);
+    QtTreeItem* it = find(item);
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::delTableRow(%s) found=%p [%p]",
+	name().c_str(),item.c_str(),it,this);
+    if (!it)
+	return false;
+    removeItem(it);
+    return true;
+}
+
+// Add, set or remove one or more items.
+// Each data list element is a NamedPointer carrying a NamedList with item parameters.
+// The name of an element is the item to update.
+// Set element's value to boolean value 'true' to add a new item if not found
+//  or 'false' to set an existing one. Set it to empty string to delete the item
+bool QtCustomTree::updateTableRows(const NamedList* data, bool atStart)
+{
+    if (!data)
+	return true;
+    SafeInt safeChg(&m_changing);
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::updateTableRows() [%p]",
+	name().c_str(),this);
+    SafeTree tree(this);
+    scheduleDelayedItemsLayout();
+    QList<QTreeWidgetItem*> removed;
+    bool ok = false;
+    NamedIterator iter(*data);
+    for (const NamedString* ns = 0; 0 != (ns = iter.get());) {
+	if (!ns->name())
+	    continue;
+	QtTreeItem* item = find(ns->name());
+	if (!ns->null()) {
+	    NamedList* params = YOBJECT(NamedList,ns);
+	    if (!params) {
+		ok = (0 != item) || ok;
+		continue;
+	    }
+	    if (item)
+		ok = updateItem(*item,*params) || ok;
+	    else if (ns->toBoolean())
+		ok = addTableRow(ns->name(),params,atStart) || ok;
+	}
+	else if (item) {
+	    removed.append(item);
+	    ok = true;
+	}
+    }
+    removeItems(removed);
+    executeDelayedItemsLayout();
+    return ok;
+}
+
+// Retrieve the current selection
+bool QtCustomTree::setSelect(const String& item)
+{
+    QtTreeItem* it = item ? find(item) : 0;
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::setSelect(%s) found=%p [%p]",
+	name().c_str(),item.c_str(),it,this);
+    if (it)
+	setCurrentItem(it);
+    else if (item)
+	setCurrentItem(0);
+    return it || !item;
+}
+
+// Retrieve the current selection
+bool QtCustomTree::getSelect(String& item)
+{
+    QList<QTreeWidgetItem*> list = selectedItems();
+    bool ok = list.size() > 0 && list[0];
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::getSelect(%s) found=%u [%p]",
+	name().c_str(),item.c_str(),ok,this);
+    if (ok)
+	item = (static_cast<QtTreeItem*>(list[0]))->id();
+    return ok;
+}
+
+// Retrieve multiple selection
+bool QtCustomTree::getSelect(NamedList& items)
+{
+    QList<QTreeWidgetItem*> sel = selectedItems();
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::getSelect(%p) found=%u [%p]",
+	name().c_str(),&items,sel.size(),this);
+    addItems(items,sel);
+    return 0 != sel.size();
+}
+
+// Remove all items from tree
+bool QtCustomTree::clearTable()
+{
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::clearTable() [%p]",
+	name().c_str(),this);
+    SafeInt safeChg(&m_changing);
+    QTreeWidget::clear();
+    return true;
+}
+
+// Catch item selection changed signal
+void QtCustomTree::itemSelChangedSlot()
+{
+    stopSelectTriggerTimer();
+    QList<QTreeWidgetItem*> sel = selectedItems();
+    int nSel = sel.size();
+    DDebug(ClientDriver::self(),DebugAll,
+	"QtCustomTree(%s)::itemSelChangedSlot() sel=%d [%p]",
+	name().c_str(),nSel,this);
+    if (m_haveWidgets) {
+	for (int i = 0; i < nSel; i++)
+	    applyStyleSheet(static_cast<QtTreeItem*>(sel[i]),true);
+    }
+    if (nSel <= 0)
+	onSelect(this,&(String::empty()));
+    else if (nSel == 1)
+	onSelect(this,&(static_cast<QtTreeItem*>(sel[0])->toString()));
+    else {
+	NamedList list("");
+	addItems(list,sel);
+	onSelectMultiple(this,&list);
+    }
+}
+
+// Re-implemented from QTreeWidget
+void QtCustomTree::timerEvent(QTimerEvent* ev)
+{
+    if (m_timerTriggerSelect && ev->timerId() == m_timerTriggerSelect) {
+	stopSelectTriggerTimer();
+	itemSelChangedSlot();
+	return;
+    }
+    QtTree::timerEvent(ev);
+}
+
+// Re-implemented from QTreeWidget
+void QtCustomTree::drawBranches(QPainter* painter, const QRect& rect,
+    const QModelIndex& index) const
+{
+    if (m_drawBranches)
+	QtTree::drawBranches(painter,rect,index);
+}
+
+// Re-implemented from QTreeWidget
+QMimeData* QtCustomTree::mimeData(const QList<QTreeWidgetItem*> items) const
+{
+    QMimeData* data = m_drag ? m_drag->mimeData(items) : 0;
+    return data ? data : QtTree::mimeData(items);
+}
+
+// Re-implemented from QAbstractItemView
+void QtCustomTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
+{
+    QTreeWidget::selectionChanged(selected,deselected);
+    QList<QtTreeItem*> unsel;
+    QModelIndexList unselIndexes = deselected.indexes();
+    if (unselIndexes.size() > 0)
+	unsel = findItems(unselIndexes);
+    DDebug(ClientDriver::self(),DebugAll,
+	"QtCustomTree(%s)::onSelChanged() desel=%d [%p]",
+	name().c_str(),unsel.size(),this);
+    if (m_haveWidgets)
+	for (int i = 0; i < unsel.size(); i++)
+	    applyStyleSheet(unsel[i],false);
+}
+
+// Re-implemented from QAbstractItemView
+void QtCustomTree::currentChanged(const QModelIndex& current, const QModelIndex& previous)
+{
+    QtTree::currentChanged(current,previous);
+    if (m_setCurrentColumn >= 0 && m_setCurrentColumn != current.column() &&
+	m_setCurrentColumn < columnCount()) {
+	QTreeWidgetItem* it = itemFromIndex(current);
+	if (it) {
+	    QModelIndex idx = indexFromItem(it,m_setCurrentColumn);
+	    if (idx.isValid())
+		setCurrentIndex(idx);
+	}
+    }
+}
+
+// Re-implemented from QWidget
+void QtCustomTree::dragEnterEvent(QDragEnterEvent* e)
+{
+    if (m_drop)
+	handleDropEvent(e);
+#ifdef XDEBUG
+    String tmp = " ";
+    QtClient::dumpMime(tmp,e->mimeData());
+    Debug(ClientDriver::self(),DebugAll,"QtCustomTree(%s) DRAG ENTER MIME: [%p]%s",
+	name().c_str(),this,tmp.safe());
+#endif
+}
+
+// Re-implemented from QWidget
+void QtCustomTree::dropEvent(QDropEvent* e)
+{
+    if (m_drop && m_drop->started())
+	handleDropEvent(e);
+}
+
+// Re-implemented from QWidget
+void QtCustomTree::dragMoveEvent(QDragMoveEvent* e)
+{
+    if (m_drop && m_drop->started())
+	handleDropEvent(e);
+}
+
+// Re-implemented from QWidget
+void QtCustomTree::dragLeaveEvent(QDragLeaveEvent* e)
+{
+    if (m_drop && m_drop->started())
+	m_drop->reset();
+}
+
+// Re-implemented from QWidget
+void QtCustomTree::mouseMoveEvent(QMouseEvent* e)
+{
+    QtTree::mouseMoveEvent(e);
+    if (m_haveDrawQtItems) {
+	QtTreeItem* it = static_cast<QtTreeItem*>(itemAt(e->pos()));
+	if (m_lastItemDrawHover && m_lastItemDrawHover != it &&
+	    m_lastItemDrawHover->extraPaintRight() &&
+	    m_lastItemDrawHover->extraPaintRight()->setHover(false)) {
+	    m_lastItemDrawHover->extraPaintRight()->setPressed(false);
+	    QtTree::repaint(m_lastItemDrawHover->extraPaintRight()->displayRect());
+	}
+	if (it && it->extraPaintRight()) {
+	    if (it->extraPaintRight()->displayRect().contains(e->pos())) {
+		if (it->extraPaintRight()->setHover(e->pos()))
+		    QtTree::repaint(it->extraPaintRight()->displayRect());
+	    }
+	    else if (it->extraPaintRight()->setHover(false))
+		QtTree::repaint(it->extraPaintRight()->displayRect());
+	}
+	m_lastItemDrawHover = it;
+    }
+}
+
+// Re-implemented from QWidget
+void QtCustomTree::mousePressEvent(QMouseEvent* e)
+{
+    QtTree::mousePressEvent(e);
+    if (e->button() == Qt::LeftButton &&
+	m_lastItemDrawHover && m_lastItemDrawHover->extraPaintRight() &&
+	m_lastItemDrawHover->extraPaintRight()->displayRect().contains(e->pos()) &&
+	m_lastItemDrawHover->extraPaintRight()->mousePressed(true,e->pos()))
+	QtTree::repaint(m_lastItemDrawHover->extraPaintRight()->displayRect());
+}
+
+// Re-implemented from QWidget
+void QtCustomTree::mouseReleaseEvent(QMouseEvent* e)
+{
+    QtTree::mouseReleaseEvent(e);
+    if (e->button() == Qt::LeftButton &&
+	m_lastItemDrawHover && m_lastItemDrawHover->extraPaintRight()) {
+	String action;
+	if (m_lastItemDrawHover->extraPaintRight()->mousePressed(false,e->pos(),&action)) {
+	    if (action)
+		triggerAction(m_lastItemDrawHover->id(),action,this);
+	    QtTree::repaint(m_lastItemDrawHover->extraPaintRight()->displayRect());
+	}
+    }
+}
+
+// Re-implemented from QTreeView
+void QtCustomTree::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end)
+{
+    if (m_lastItemDrawHover) {
+	QModelIndex idx = indexFromItem(m_lastItemDrawHover);
+	if (idx.isValid() && idx.row() >= start && idx.row() <= end)
+	    m_lastItemDrawHover = 0;
+    }
+    QtTree::rowsAboutToBeRemoved(parent,start,end);
+}
+
+// Retrieve tree sorting
+QString QtCustomTree::getSorting()
+{
+    String t;
+    QHeaderView* h = isSortingEnabled() ? QTreeView::header() : 0;
+    if (h) {
+	int col = h->sortIndicatorSection();
+	int sort = h->sortIndicatorOrder();
+	if (col >= 0 && col < columnCount()) {
+	    String id;
+	    QTreeWidgetItem* hdr = headerItem();
+	    if (hdr)
+		getItemData(id,*hdr,col);
+	    t << (id ? id : String(col)) << "," << String::boolText(sort == Qt::AscendingOrder);
+	}
+    }
+    return QtClient::setUtf8(t);
+}
+
+// Set tree sorting
+void QtCustomTree::updateSorting(const String& key, Qt::SortOrder sort)
+{
+    SafeInt safeChg(&m_changing);
+    QHeaderView* h = QTreeView::header();
+    if (!h)
+	return;
+    int col = key.toInteger(-1);
+    if (col < 0)
+	col = getColumn(key);
+    if (col >= 0 && col < columnCount())
+	h->setSortIndicator(col,sort);
+}
+
+// Build a tree context menu
+bool QtCustomTree::buildMenu(QMenu*& menu, NamedString* ns)
+{
+    if (!ns)
+	return false;
+    NamedList* p = YOBJECT(NamedList,ns);
+    if (!p)
+	return false;
+    if (menu)
+	QtClient::deleteLater(menu);
+    // Check if we are part of a widget list container item
+    QtUIWidget* container = QtUIWidget::container(this);
+    if (!container)
+	menu = QtClient::buildMenu(*p,0,0,0,0,this);
+    else
+	menu = container->buildWidgetItemMenu(this,p,String::empty(),false);
+    return true;
+}
+
+// Retrieve all items' id
+bool QtCustomTree::getOptions(NamedList& items)
+{
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::getOptions() [%p]",
+	name().c_str(),this);
+    findItems(items);
+    return true;
+}
+
+// Retrieve a QObject list containing container items
+QList<QObject*> QtCustomTree::getContainerItems()
+{
+    QList<QObject*> list;
+    QList<QtTreeItem*> items = findItems();
+    for (int i = 0; i < items.size(); i++) {
+	QWidget* w = itemWidget(items[i],0);
+	if (w)
+	    list.append(static_cast<QObject*>(w));
+    }
+    return list;
+}
+
+// Retrieve model index for a given item
+QModelIndex QtCustomTree::modelIndex(const String& item, const String* what)
+{
+    int col = TelEngine::null(what) ? 0 : getColumn(*what);
+    if (col < 0)
+	return QModelIndex();
+    QtTreeItem* it = find(item);
+    if (it)
+	return indexFromItem(it,col);
+    return QModelIndex();
+}
+
+// Find a tree item
+QtTreeItem* QtCustomTree::find(const String& id, QtTreeItem* start, bool includeStart,
+    bool recursive)
+{
+    if (start && includeStart && id == start->id())
+	return start;
+    QTreeWidgetItem* root = start ? static_cast<QTreeWidgetItem*>(start) : invisibleRootItem();
+    if (!root)
+	return 0;
+    int n = root->childCount();
+    for (int i = 0; i < n; i++) {
+	QtTreeItem* item = static_cast<QtTreeItem*>(root->child(i));
+	if (!item)
+	    continue;
+	if (id == item->id() ||
+	    (recursive && 0 != (item = find(id,item,false,true))))
+	    return item;
+    }
+    return 0;
+}
+
+// Find all tree items
+QList<QtTreeItem*> QtCustomTree::findItems(bool recursive, QtTreeItem* start)
+{
+    QList<QtTreeItem*> list;
+    QTreeWidgetItem* root = start ? static_cast<QTreeWidgetItem*>(start) : invisibleRootItem();
+    if (!root)
+	return list;
+    int n = root->childCount();
+    for (int i = 0; i < n; i++) {
+	QtTreeItem* item = static_cast<QtTreeItem*>(root->child(i));
+	if (!item)
+	    continue;
+	list.append(item);
+	if (recursive) {
+	    QList<QtTreeItem*> tmp = findItems(true,item);
+	    list += tmp;
+	}
+    }
+    return list;
+}
+
+// Find all tree items having a given id
+QList<QtTreeItem*> QtCustomTree::findItems(const String& id, QtTreeItem* start,
+    bool includeStart, bool recursive)
+{
+    QList<QtTreeItem*> list;
+    if (start && includeStart && id == start->id())
+	list.append(start);
+    QTreeWidgetItem* root = start ? static_cast<QTreeWidgetItem*>(start) : invisibleRootItem();
+    if (!root)
+	return list;
+    int n = root->childCount();
+    for (int i = 0; i < n; i++) {
+	QtTreeItem* item = static_cast<QtTreeItem*>(root->child(i));
+	if (!item)
+	    continue;
+	if (id == item->id())
+	    list.append(item);
+	if (recursive) {
+	    QList<QtTreeItem*> tmp = findItems(id,item,false,true);
+	    list += tmp;
+	}
+    }
+    return list;
+}
+
+// Find all tree items having a given type
+QList<QtTreeItem*> QtCustomTree::findItems(int type, QtTreeItem* start,
+    bool includeStart, bool recursive)
+{
+    QList<QtTreeItem*> list;
+    if (start && includeStart && type == start->type())
+	list.append(start);
+    QTreeWidgetItem* root = start ? static_cast<QTreeWidgetItem*>(start) : invisibleRootItem();
+    if (!root)
+	return list;
+    int n = root->childCount();
+    for (int i = 0; i < n; i++) {
+	QtTreeItem* item = static_cast<QtTreeItem*>(root->child(i));
+	if (!item)
+	    continue;
+	if (type == item->type())
+	    list.append(item);
+	if (recursive) {
+	    QList<QtTreeItem*> tmp = findItems(type,item,false,true);
+	    list += tmp;
+	}
+    }
+    return list;
+}
+
+// Find all tree items from model
+QList<QtTreeItem*> QtCustomTree::findItems(QModelIndexList list)
+{
+    QList<QtTreeItem*> l;
+    QTreeWidgetItem* root = invisibleRootItem();
+    if (!root)
+	return l;
+    for (int i = 0; i < list.size(); i++) {
+	QModelIndex& idx = list[i];
+	if (!idx.isValid())
+	    continue;
+	QtTreeItem* it = static_cast<QtTreeItem*>(itemFromIndex(idx));
+	if (it && !l.contains(it))
+	    l.append(it);
+    }
+    return l;
+}
+
+// Find al tree items
+void QtCustomTree::findItems(NamedList& list, QtTreeItem* start, bool includeStart,
+    bool recursive)
+{
+    if (start && includeStart)
+	list.setParam(start->id(),"");
+    QTreeWidgetItem* root = start ? static_cast<QTreeWidgetItem*>(start) : invisibleRootItem();
+    if (!root)
+	return;
+    int n = root->childCount();
+    for (int i = 0; i < n; i++) {
+	QtTreeItem* item = static_cast<QtTreeItem*>(root->child(i));
+	if (!item)
+	    continue;
+	list.setParam(item->id(),"");
+	if (recursive)
+	    findItems(list,item,false,true);
+    }
+}
+
+// Add a child to a given item
+QtTreeItem* QtCustomTree::addChild(QtTreeItem* child, int pos, QtTreeItem* parent)
+{
+    if (!child)
+	return 0;
+    SafeInt safeChg(&m_changing);
+    QTreeWidgetItem* root = parent ? static_cast<QTreeWidgetItem*>(parent) : invisibleRootItem();
+    if (!root)
+	return 0;
+    DDebug(ClientDriver::self(),DebugAll,
+	"QtCustomTree(%s) adding child '%s' type=%d parent=%p pos=%d",
+	name().c_str(),child->id().c_str(),child->type(),parent,pos);
+    setItemRowHeight(child);
+    if (pos < 0 || pos >= root->childCount())
+	root->addChild(child);
+    else
+	root->insertChild(pos,child);
+    setupItem(child);
+    itemAdded(*child,parent);
+    return child;
+}
+
+// Add a list of children to a given item
+void QtCustomTree::addChildren(QList<QTreeWidgetItem*> list, int pos, QtTreeItem* parent)
+{
+    SafeInt safeChg(&m_changing);
+    QTreeWidgetItem* root = parent ? static_cast<QTreeWidgetItem*>(parent) : invisibleRootItem();
+    if (!root)
+	return;
+    for (int i = 0; i < list.size(); i++)
+	setItemRowHeight(list[i]);
+    if (pos < 0 || pos >= root->childCount())
+	root->addChildren(list);
+    else
+	root->insertChildren(pos,list);
+    for (int i = 0; i < list.size(); i++) {
+	QtTreeItem* item = static_cast<QtTreeItem*>(list[i]);
+	if (!item)
+	    continue;
+	setupItem(item);
+	itemAdded(*item,parent);
+    }
+}
+
+// Setup an item. Load its widget if not found
+void QtCustomTree::setupItem(QtTreeItem* item)
+{
+    if (!item)
+	return;
+    SafeInt safeChg(&m_changing);
+    // Set widget
+    QWidget* w = itemWidget(item,0);
+    if (!w) {
+	w = loadWidgetType(this,item->id(),itemPropsName(item->type()));
+	if (w) {
+	    m_haveWidgets = true;
+	    w->setAutoFillBackground(true);
+	    XDebug(ClientDriver::self(),DebugAll,
+		"QtCustomTree(%s) set widget (%p,%s) for child '%s' [%p]",
+		name().c_str(),w,YQT_OBJECT_NAME(w),item->id().c_str(),this);
+	    // Adjust widget to row height if configured,
+	    // or row height to widget otherwise
+	    QSize sz = item->sizeHint(0);
+	    int h = getItemRowHeight(item->type());
+	    if (h > 0)
+		w->setFixedHeight(std::max<int>(h,sz.height()) + item->m_heightDelta);
+	    else {
+		sz.setHeight(w->height());
+		// We have no particular width constraint but must ensure that
+		// the size we give as a hint is valid because otherwise it
+		// will be ignored, at least with Qt 5.15.2
+		sz.setWidth(std::max<int>(0,sz.width()));
+		item->setSizeHint(0,sz);
+	    }
+	    setItemWidget(item,0,w);
+	    applyStyleSheet(item,item->isSelected());
+	}
+    }
+    // Set checkable columns
+    uncheckItem(*item);
+}
+
+// Set and item's row height hint
+void QtCustomTree::setItemRowHeight(QTreeWidgetItem* item)
+{
+    if (!item)
+	return;
+    int h = getItemRowHeight(item->type());
+    if (h <= 0)
+	return;
+    QSize sz = item->sizeHint(0);
+    sz.setHeight(h + (static_cast<QtTreeItem*>(item))->m_heightDelta);
+    // We have no particular width constraint but must ensure that the size we
+    // give as a hint is valid because otherwise it will be ignored, at least
+    // with Qt 5.15.2
+    sz.setWidth(std::max<int>(0,sz.width()));
+    item->setSizeHint(0,sz);
+    QWidget* w = itemWidget(item,0);
+    if (w)
+	w->setFixedHeight(sz.height());
+}
+
+// Retrieve a list with column IDs
+QStringList QtCustomTree::columnIDs()
+{
+    QStringList tmp;
+    QTreeWidgetItem* hdr = headerItem();
+    int n = hdr ? columnCount() : 0;
+    for (int i = 0; i < n; i++)
+	tmp.append(hdr->data(i,RoleId).toString());
+    return tmp;
+}
+
+// Retrieve a column name
+bool QtCustomTree::getColumnName(String& buf, int col)
+{
+    QTreeWidgetItem* hdr = 0;
+    if (col >= 0 && col < columnCount())
+	hdr = headerItem();
+    if (!hdr)
+	return false;
+    getItemData(buf,*hdr,col);
+    return true;
+}
+
+// Retrieve a column by it's id
+int QtCustomTree::getColumn(const String& id)
+{
+    QTreeWidgetItem* hdr = headerItem();
+    int n = hdr ? columnCount() : 0;
+    for (int i = 0; i < n; i++) {
+	String tmp;
+	getItemData(tmp,*hdr,i);
+	if (tmp == id)
+	    return i;
+    }
+    return -1;
+}
+
+// Show or hide empty children.
+void QtCustomTree::showEmptyChildren(bool show, QtTreeItem* parent)
+{
+    QTreeWidgetItem* root = parent ? static_cast<QTreeWidgetItem*>(parent) : invisibleRootItem();
+    if (!root)
+	return;
+    SafeTree tree(this);
+    SafeInt safeChg(&m_changing);
+    int n = root->childCount();
+    for (int i = 0; i < n; i++) {
+	QtTreeItem* item = static_cast<QtTreeItem*>(root->child(i));
+	if (!item)
+	    continue;
+	if (show) {
+	    showItem(*item,true);
+	    continue;
+	}
+	// Find a displayed child. Hide the item if not found
+	QTreeWidgetItem* child = 0;
+	int nc = item->childCount();
+	for (int j = 0; j < nc; j++, child = 0) {
+	    child = item->child(j);
+	    if (child && !child->isHidden())
+		break;
+	}
+	showItem(*item,child != 0);
+    }
+}
+
+// Set the expanded/collapsed image of an item
+void QtCustomTree::setStateImage(QtTreeItem& item, QtTreeItemProps* p)
+{
+    if (!p)
+	p = treeItemProps(item.type());
+    if (!(p && p->m_stateWidget))
+	return;
+    SafeInt safeChg(&m_changing);
+    NamedList tmp("");
+    const String& img = item.isExpanded() ? p->m_stateExpandedImg : p->m_stateCollapsedImg;
+    tmp.addParam("image:" + p->m_stateWidget,img);
+    tmp.addParam(p->m_stateWidget + "_image",img);
+    updateItem(item,tmp);
+}
+
+// Set an item props ui
+void QtCustomTree::setItemUi(QString value)
+{
+    String tmp;
+    QtUIWidgetItemProps* p = getItemProps(value,tmp);
+    p->m_ui = tmp;
+}
+
+// Set an item props style sheet
+void QtCustomTree::setItemStyle(QString value)
+{
+    String tmp;
+    QtUIWidgetItemProps* p = getItemProps(value,tmp);
+    p->m_styleSheet = tmp;
+}
+
+// Set an item props selected style sheet
+void QtCustomTree::setItemSelectedStyle(QString value)
+{
+    String tmp;
+    QtUIWidgetItemProps* p = getItemProps(value,tmp);
+    p->m_selStyleSheet = tmp;
+}
+
+// Set an item props accept drop
+void QtCustomTree::setItemAcceptDrop(QString value)
+{
+    String tmp;
+    QtUIWidgetItemProps* p = getItemProps(value,tmp);
+    p->m_acceptDrop = QtDrop::acceptDropType(tmp,QtDrop::None);
+}
+
+// Set an item props state widget name
+void QtCustomTree::setItemStateWidget(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (p)
+	p->m_stateWidget = tmp;
+}
+
+// Set an item's expanded image
+void QtCustomTree::setExpandedImage(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (p)
+	p->m_stateExpandedImg = Client::s_skinPath + tmp;
+}
+
+// Set an item's collapsed image
+void QtCustomTree::setItemCollapsedImage(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (p)
+	p->m_stateCollapsedImg = Client::s_skinPath + tmp;
+}
+
+// Set an item's tooltip template
+void QtCustomTree::setItemTooltip(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (p)
+	p->m_toolTip = tmp;
+}
+
+// Set an item's statistics widget name
+void QtCustomTree::setItemStatsWidget(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (p)
+	p->m_statsWidget = tmp;
+}
+
+// Set an item's statistics template
+void QtCustomTree::setItemStatsTemplate(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (p)
+	p->m_statsTemplate = tmp;
+}
+
+// Set an item props height
+void QtCustomTree::setItemHeight(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (p)
+	p->m_height = tmp.toInteger(-1);
+}
+
+// Set an item props background
+void QtCustomTree::setItemBg(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (!p)
+	return;
+    if (tmp) {
+	if (tmp[0] == '#')
+	    p->m_bg = QBrush(QColor(tmp.substr(1).toInteger(0,16)));
+	else if (tmp.startSkip("color:",false))
+	    p->m_bg = QBrush(QColor(tmp.c_str()));
+	else
+	    p->m_bg = QBrush();
+    }
+    else
+	p->m_bg = QBrush();
+}
+
+// Set an item props margins
+// Order: left,top,right,bottom
+void QtCustomTree::setItemMargins(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (!p)
+	return;
+    p->m_margins = QRect();
+    if (!tmp)
+	return;
+    ObjList* list = tmp.split(',');
+    int i = 0;
+    for (ObjList* o = list; o; o = o->next(), i++) {
+	int val = o->get() ? o->get()->toString().toInteger() : 0;
+	if (i == 0)
+	    p->m_margins.setLeft(val);
+	else if (i == 1)
+	    p->m_margins.setTop(val);
+	else if (i == 2)
+	    p->m_margins.setRight(val);
+	else if (i == 3)
+	    p->m_margins.setBottom(val);
+    }
+    TelEngine::destruct(list);
+}
+
+// Set an item props editable
+void QtCustomTree::setItemEditable(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (p)
+	p->m_editable = tmp.toBoolean();
+}
+
+// Set an item's paint button and action
+// Format [type:][button_name:]action_name
+void QtCustomTree::setItemPaintButton(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (!p)
+	return;
+    if (!tmp)
+	return;
+    int pos = tmp.find(':');
+    if (pos < 0) {
+	p->setPaintButtonAction(tmp,tmp);
+	return;
+    }
+    String name = tmp.substr(0,pos);
+    if (name)
+	p->setPaintButtonAction(name,tmp.substr(pos + 1));
+}
+
+// Set an item's paint button parameter
+// Format [type:]button_name:param_name[:param_value]
+void QtCustomTree::setItemPaintButtonParam(QString value)
+{
+    String tmp;
+    QtTreeItemProps* p = YOBJECT(QtTreeItemProps,getItemProps(value,tmp));
+    if (!p)
+	return;
+    int pos = tmp.find(':');
+    if (pos < 1)
+	return;
+    String name = tmp.substr(0,pos);
+    tmp = tmp.substr(pos + 1);
+    if (!tmp)
+	return;
+    pos = tmp.find(':');
+    if (!pos)
+	return;
+    if (pos > 0)
+	p->setPaintButtonParam(name,tmp.substr(0,pos),tmp.substr(pos + 1));
+    else
+	p->setPaintButtonParam(name,tmp);
+}
+
+// Retrieve a comma separated list with column widths
+QString QtCustomTree::colWidths()
+{
+    if (!columnCount())
+	return QString();
+    String t;
+    int cols = columnCount();
+    for (int i = 0; i < cols; i++)
+	t.append(String(columnWidth(i)),",");
+    return QtClient::setUtf8(t);
+}
+
+// Set column widths
+void QtCustomTree::setColWidths(QString widths)
+{
+    if (!columnCount())
+	return;
+    QStringList list = widths.split(",");
+    for (int i = 0; i < list.size(); i++) {
+	if (!list[i].length())
+	    continue;
+	int width = list[i].toInt();
+	if (width > 0)
+	    setColumnWidth(i,width);
+    }
+}
+
+// Set sorting (column and order)
+void QtCustomTree::setSorting(QString s)
+{
+    SafeInt safeChg(&m_changing);
+    if (!s.length()) {
+	updateSorting(String::empty(),Qt::AscendingOrder);
+	return;
+    }
+    String key;
+    String order;
+    int pos = s.indexOf(QChar(','));
+    if (pos >= 0) {
+	QtClient::getUtf8(key,s.left(pos));
+	QtClient::getUtf8(order,s.right(s.length() - pos - 1));
+    }
+    else
+	QtClient::getUtf8(key,s);
+    updateSorting(key,order.toBoolean(true) ? Qt::AscendingOrder : Qt::DescendingOrder);
+}
+
+// Retrieve items expanded status value
+QString QtCustomTree::itemsExpStatus()
+{
+    String tmp;
+    for (int i = 0; i < m_expStatus.size(); i++) {
+	String val;
+	val << m_expStatus[i].first.uriEscape(',') << "=" <<
+	    String::boolText(m_expStatus[i].second > 0);
+	tmp.append(val,",");
+    }
+    return QtClient::setUtf8(tmp);
+}
+
+// Set items expanded status value
+void QtCustomTree::setItemsExpStatus(QString s)
+{
+    m_expStatus.clear();
+    QStringList list = s.split(",",Qt::SkipEmptyParts);
+    for (int i = 0; i < list.size(); i++) {
+	String id;
+	String value;
+	int pos = list[i].lastIndexOf('=');
+	if (pos > 0) {
+	    QtClient::getUtf8(id,list[i].left(pos));
+	    int n = list[i].size() - pos - 1;
+	    if (n)
+		QtClient::getUtf8(value,list[i].right(n));
+	}
+	else
+	    QtClient::getUtf8(id,list[i]);
+	if (id) {
+	    id = id.uriUnescape();
+	    m_expStatus.append(QtTokenDict(id,value.toBoolean(m_autoExpand) ? 1 : 0));
+	}
+    }
+}
+
+// Add items as list parameter
+void QtCustomTree::addItems(NamedList& dest, QList<QTreeWidgetItem*> items)
+{
+    for (int i = 0; i < items.size(); i++)
+	dest.addParam(static_cast<QtTreeItem*>(items[i])->toString(),"");
+}
+
+// Apply item widget style sheet
+void QtCustomTree::applyStyleSheet(QtTreeItem* item, bool selected)
+{
+    if (!item)
+	return;
+    QWidget* w = itemWidget(item,0);
+    if (!w)
+	return;
+    QtUIWidgetItemProps* p = QtUIWidget::getItemProps(itemPropsName(item->type()));
+    if (p)
+	applyWidgetStyle(w,selected ? p->m_selStyleSheet : p->m_styleSheet);
+}
+
+// Process item double click
+void QtCustomTree::onItemDoubleClicked(QtTreeItem* item, int column)
+{
+    if (item && Client::self())
+	onAction(this);
+}
+
+// Item expanded/collapsed notification
+void QtCustomTree::onItemExpandedChanged(QtTreeItem* item)
+{
+    if (!item)
+	return;
+    if (item->m_storeExp)
+	setStoreExpStatus(item->id(),item->isExpanded());
+    QtTreeItemProps* props = treeItemProps(item);
+    if (props) {
+	setStateImage(*item,props);
+	applyItemStatistics(*item,props);
+    }
+}
+
+// Process item changed signal
+void QtCustomTree::onItemChanged(QtTreeItem* item, int column)
+{
+    if (m_changing || !m_notifyItemChanged || !item)
+	return;
+    NamedList p("");
+    QString s = item->text(column);
+    if (s.size() > 0) {
+	String col;
+	getColumnName(col,column);
+	if (col)
+	    QtClient::getUtf8(p,"text." + col,s);
+    }
+    closePersistentEditor(static_cast<QTreeWidgetItem*>(item),column);
+    triggerAction(item->id(),"listitemchanged",this,&p);
+}
+
+// Catch a context menu event and show the context menu
+void QtCustomTree::contextMenuEvent(QContextMenuEvent* e)
+{
+    QtTreeItem* it = static_cast<QtTreeItem*>(itemAt(e->pos()));
+    QMenu* menu = contextMenu(it);
+    if (!menu)
+	menu = m_menu;
+    if (!menu)
+	return;
+    menu->exec(e->globalPos());
+}
+
+// Update a tree item
+bool QtCustomTree::updateItem(QtTreeItem& item, const NamedList& params)
+{
+    SafeInt safeChg(&m_changing);
+    SafeTree safeTree(this);
+    DDebug(ClientDriver::self(),DebugAll,"QtCustomTree(%s)::updateItem(%p,%s) [%p]",
+	name().c_str(),&item,item.id().c_str(),this);
+    bool all = (&params == &item);
+    if (!all)
+	item.copyParams(params);
+    const NamedList& p = all ? (const NamedList&)item : params;
+    QTreeWidgetItem* hdr = headerItem();
+    QtTreeItemProps* props = treeItemProps(item.type());
+    int n = columnCount();
+    QModelIndex idx;
+    for (int col = 0; col < n; col++) {
+	if (!col) {
+	    String* hp = params.getParam(YSTRING("_yate_itemheight_delta"));
+	    if (hp) {
+		item.m_heightDelta = hp->toInteger();
+		setItemRowHeight(&item);
+		doItemsLayout();
+	    }
+	    if (props) {
+		String* showActions = params.getParam(YSTRING("_yate_showactions"));
+		QtPaintItems* pItems = item.extraPaintRight();
+		if (showActions &&
+		    ((!pItems && *showActions) || (pItems && *showActions != pItems->name()))) {
+		    item.setExtraPaintRightButtons(*showActions,props);
+		    m_haveDrawQtItems = true;
+		    setMouseTracking(true);
+		}
+	    }
+	}
+	QWidget* w = itemWidget(&item,col);
+	if (w) {
+	    QtUIWidget::setParams(w,p);
+	    continue;
+	}
+	if (!hdr)
+	    continue;
+	String id;
+	getItemData(id,*hdr,col);
+	item.setText(col,id,p);
+	item.setCheckState(col,id,p);
+	int imageRole = Qt::UserRole;
+	if (props) {
+	    // Set brush
+	    if (props->m_bg != QBrush())
+		item.setData(col,RoleBackground,props->m_bg);
+	    if (idx.isValid())
+		idx = idx.sibling(idx.row(),col);
+	    else
+		idx = indexFromItem(&item,col);
+	    QtHtmlItemDelegate* html = 0;
+	    if (idx.isValid())
+		html = qobject_cast<QtHtmlItemDelegate*>(itemDelegate(idx));
+	    if (html) {
+		// HTML delegate
+		imageRole = html->roleImage();
+		if (html->roleDisplayText() == RoleHtmlDelegate) {
+		    QStringList qList;
+		    String s = props->m_styleSheet;
+		    if (s)
+			replaceHtmlParams(s,item,true);
+		    qList.append(QtClient::setUtf8(s));
+		    s = props->m_selStyleSheet;
+		    if (s) {
+			replaceHtmlParams(s,item);
+			qList.append(QtClient::setUtf8(s));
+		    }
+		    item.setData(col,RoleHtmlDelegate,qList);
+		}
+	    }
+	}
+	item.setImage(col,id,p,imageRole);
+    }
+    applyItemTooltip(item);
+    checkItemFilter(&item,false);
+    return true;
+}
+
+// Get the context menu associated with a given item
+QMenu* QtCustomTree::contextMenu(QtTreeItem* item)
+{
+    return 0;
+}
+
+// Item added notification
+void QtCustomTree::itemAdded(QtTreeItem& item, QtTreeItem* parent)
+{
+    SafeInt safeChg(&m_changing);
+    checkItemFilter(&item,false);
+    bool on = m_autoExpand;
+    if (item.m_storeExp) {
+	int n = getStoreExpStatus(item.id());
+	if (n >= 0)
+	    on = (n > 0);
+	else
+	    setStoreExpStatus(item.id(),on);
+    }
+    QtTreeItemProps* props = treeItemProps(item);
+    bool editable = false;
+    item.setExpanded(on);
+    if (props) {
+	setStateImage(item,props);
+	applyItemTooltip(item,props);
+	applyItemStatistics(item,props);
+	applyItemMargins(item,true,props);
+	editable = props->m_editable;
+    }
+    if (editable)
+	item.setFlags(item.flags() | Qt::ItemIsEditable);
+    else
+	item.setFlags(item.flags() & ~Qt::ItemIsEditable);
+    if (parent)
+	applyItemStatistics(*parent);
+}
+
+// Handle item visiblity changes
+void QtCustomTree::itemVisibleChanged(QtTreeItem& item)
+{
+    SafeInt safeChg(&m_changing);
+    // Uncheck columns for invisible item
+    if (item.isHidden())
+	uncheckItem(item);
+}
+
+// Check item filter
+void QtCustomTree::checkItemFilter(QtTreeItem* item, bool recursive)
+{
+    QTreeWidgetItem* root = 0;
+    if (item) {
+	item->setFilter(m_filter);
+	itemFilterChanged(*item);
+	if (recursive)
+	    root = static_cast<QTreeWidgetItem*>(item);
+    }
+    else if (recursive)
+	root = invisibleRootItem();
+    int nc = root ? root->childCount() : 0;
+    for (int i = 0; i < nc; i++) {
+	QtTreeItem* it = static_cast<QtTreeItem*>(root->child(i));
+	checkItemFilter(it,true);
+    }
+}
+
+// Handle item filter changes
+void QtCustomTree::itemFilterChanged(QtTreeItem& item)
+{
+    showItem(item,item.filterMatched());
+}
+
+// Uncheck all checkable columns in a given item
+void QtCustomTree::uncheckItem(QtTreeItem& item)
+{
+    if (!m_hasCheckableCols)
+	return;
+    SafeInt safeChg(&m_changing);
+    QTreeWidgetItem* hdr = headerItem();
+    int n = hdr ? columnCount() : 0;
+    for (int i = 0; i < n; i++)
+	if (hdr->data(i,RoleCheckable).toBool())
+	    item.setCheckState(i,false);
+}
+
+// Remove an item
+void QtCustomTree::removeItem(QtTreeItem* it, bool* setSelTimer)
+{
+    if (!it)
+	return;
+    bool sel = shouldSetSelTimer(*it);
+    QTreeWidgetItem* parent = it->parent();
+    if (parent && parent != invisibleRootItem()) {
+	parent->removeChild(it);
+	applyItemStatistics(*static_cast<QtTreeItem*>(parent));
+    }
+    TelEngine::destruct(it);
+    if (setSelTimer)
+	*setSelTimer = sel;
+    else if (sel)
+	startSelectTriggerTimer();
+}
+
+// Remove a list of items
+void QtCustomTree::removeItems(QList<QTreeWidgetItem*> items)
+{
+    bool setSelTimer = false;
+    for (int i = 0; i < items.size(); i++) {
+	bool sel = false;
+	removeItem(static_cast<QtTreeItem*>(items[i]),&sel);
+	setSelTimer = setSelTimer || sel;
+    }
+    if (setSelTimer)
+	startSelectTriggerTimer();
+}
+
+// Update a tree item's tooltip
+void QtCustomTree::applyItemTooltip(QtTreeItem& item, QtTreeItemProps* p)
+{
+    if (!p)
+	p = treeItemProps(item);
+    if (!(p && p->m_toolTip))
+	return;
+    String tooltip = p->m_toolTip;
+    item.replaceParams(tooltip);
+    for (int n = columnCount() - 1; n >= 0; n--)
+	item.setToolTip(n,QtClient::setUtf8(tooltip));
+}
+
+// Fill a list with item statistics.
+void QtCustomTree::fillItemStatistics(QtTreeItem& item, NamedList& list)
+{
+    list.addParam("count",String(item.childCount()));
+}
+
+// Update a tree item's statistics
+void QtCustomTree::applyItemStatistics(QtTreeItem& item, QtTreeItemProps* p)
+{
+    if (!p)
+	p = treeItemProps(item);
+    if (!(p && p->m_statsTemplate))
+	return;
+    SafeInt safeChg(&m_changing);
+    String text;
+    if (!item.isExpanded()) {
+	text = p->m_statsTemplate;
+	NamedList list("");
+	fillItemStatistics(item,list);
+	list.replaceParams(text);
+    }
+    NamedList params("");
+    if (p->m_statsWidget)
+	params.addParam(p->m_statsWidget,text);
+    else
+	params.addParam("statistics",text);
+    updateItem(item,params);
+}
+
+// Update a tree item's margins
+void QtCustomTree::applyItemMargins(QtTreeItem& item, bool set, QtTreeItemProps* p)
+{
+    if (!p)
+	p = treeItemProps(item);
+    if (!p)
+	return;
+    for (int n = columnCount() - 1; n >= 0; n--)
+	item.setData(n,RoleMargins,set ? p->m_margins : QRect());
+}
+
+// Store (update) to or remove from item expanded status storage an item
+void QtCustomTree::setStoreExpStatus(const String& id, bool on, bool store)
+{
+    if (!id)
+	return;
+    for (int i = 0; i < m_expStatus.size(); i++)
+	if (m_expStatus[i].first == id) {
+	    m_expStatus[i].second = on ? 1 : 0;
+	    return;
+	}
+    m_expStatus.append(QtTokenDict(id,on ? 1 : 0));
+}
+
+// Retrieve the expanded status of an item from storage
+int QtCustomTree::getStoreExpStatus(const String& id)
+{
+    if (!id)
+	return -1;
+    for (int i = 0; i < m_expStatus.size(); i++)
+	if (m_expStatus[i].first == id)
+	    return m_expStatus[i].second;
+    return -1;
+}
+
+// Handle drop events
+bool QtCustomTree::handleDropEvent(QDropEvent* e)
+{
+    if (!m_drop)
+	return false;
+    QDragMoveEvent* move = 0;
+    QDragEnterEvent* enter = 0;
+    if (e->type() == QEvent::DragMove) {
+	if (!m_drop->started())
+	    return false;
+	move = static_cast<QDragMoveEvent*>(e);
+    }
+    else if (e->type() == QEvent::DragEnter) {
+	if (!m_drop->start(*static_cast<QDragEnterEvent*>(e)))
+	    return false;
+	// Init drop accept params
+	String always;
+	String none;
+	String ask;
+	for (ObjList* o = m_itemPropsType.skipNull(); o ; o = o->skipNext()) {
+	    NamedInt* ni = static_cast<NamedInt*>(o->get());
+	    QtUIWidgetItemProps* p = QtUIWidget::getItemProps(*ni);
+	    if (!p)
+		continue;
+	    if (p->m_acceptDrop == QtDrop::Always)
+		always.append(*ni,",");
+	    else if (p->m_acceptDrop == QtDrop::None)
+		none.append(*ni,",");
+	    else if (p->m_acceptDrop == QtDrop::Ask)
+		ask.append(*ni,",");
+	}
+	m_drop->setAcceptOnEmpty(m_acceptDropOnEmpty);
+	m_drop->updateAcceptType(always,QtDrop::Always);
+	m_drop->updateAcceptType(none,QtDrop::None);
+	m_drop->updateAcceptType(ask,QtDrop::Ask);
+	enter = static_cast<QDragEnterEvent*>(e);
+	move = static_cast<QDragMoveEvent*>(e);
+    }
+    else if (e->type() == QEvent::Drop) {
+	if (!m_drop->started())
+	    return false;
+    }
+    else
+	return false;
+    int acceptDrop = QtDrop::None;
+    QtTreeItem* it = static_cast<QtTreeItem*>(itemAt(e->pos()));
+    if (it)
+	acceptDrop = m_drop->getAcceptType(itemPropsName(it->type()));
+    else
+	acceptDrop = m_drop->acceptOnEmpty();
+    // Done if drop event
+    if (e->type() == QEvent::Drop) {
+	bool ok = false;
+	// Notify ?
+	if (acceptDrop != QtDrop::None) {
+	    if (it) {
+		m_drop->params().setParam(YSTRING("item"),it->toString());
+		m_drop->params().setParam(YSTRING("item_type"),itemPropsName(it->type()));
+	    }
+	    ok = triggerAction(QtDrop::s_notifyClientDrop,m_drop->params(),this);
+	}
+	m_drop->reset();
+	if (ok)
+	    e->accept();
+	else
+	    e->ignore();
+	return ok;
+    }
+    if (acceptDrop == QtDrop::Ask) {
+	if (it) {
+	    m_drop->params().setParam(YSTRING("item"),it->toString());
+	    m_drop->params().setParam(YSTRING("item_type"),itemPropsName(it->type()));
+	}
+	if (triggerAction(QtDrop::s_askClientAcceptDrop,m_drop->params(),this)) {
+	    if (enter && !m_drop->params().getBoolValue(YSTRING("_yate_accept_drop"),true)) {
+		m_drop->reset();
+		enter->ignore(rect());
+		return false;
+	    }
+	    // Update allowed item types and empty space
+	    m_drop->updateAccept(m_drop->params());
+	    if (it)
+		acceptDrop = m_drop->getAcceptType(itemPropsName(it->type()));
+	    else
+	    	acceptDrop = m_drop->acceptOnEmpty();
+	}
+    }
+    if (it && move) {
+	if (acceptDrop != QtDrop::None)
+	    move->accept(QTreeWidget::visualItemRect(it));
+	else
+	    move->ignore(QTreeWidget::visualItemRect(it));
+    }
+    else if (acceptDrop != QtDrop::None)
+	e->accept();
+    else
+	e->ignore();
+    if (enter)
+	enter->acceptProposedAction();
+    return true;
+}
+
+// Check if an item has any selected child
+bool QtCustomTree::hasSelectedChild(QtTreeItem& item)
+{
+    for (int i = item.childCount() - 1; i >= 0; i--) {
+	QtTreeItem* ch = static_cast<QtTreeItem*>(item.child(i));
+	if (ch && (ch->isSelected() || hasSelectedChild(*ch)))
+	    return true;
+    }
+    return false;
+}
+
+
+/*
+ * ContactList
+ */
+ContactList::ContactList(const char* name, const NamedList& params, QWidget* parent)
+    : QtCustomTree(name,params,parent,false),
+    m_flatList(true),
+    m_showOffline(true),
+    m_hideEmptyGroups(true),
+    m_expStatusGrp(true),
+    m_menuContact(0),
+    m_menuChatRoom(0),
+    m_sortOrder(Qt::AscendingOrder),
+    m_compareNameCs(Qt::CaseSensitive)
+{
+    XDebug(ClientDriver::self(),DebugAll,"ContactList(%s) [%p]",name,this);
+    // Add item props translation
+    addItemType(TypeContact,"contact");
+    addItemType(TypeChatRoom,"chatroom");
+    addItemType(TypeGroup,"group");
+    m_savedIndent = indentation();
+    m_noGroupText = "None";
+    setParams(params);
+}
+
+// Set params
+bool ContactList::setParams(const NamedList& params)
+{
+    SafeInt safeChg(&m_changing);
+    bool ok = QtCustomTree::setParams(params);
+    buildMenu(m_menuContact,params.getParam("contactmenu"));
+    buildMenu(m_menuChatRoom,params.getParam("chatroommenu"));
+    return ok;
+}
+
+// Update a contact
+bool ContactList::setTableRow(const String& item, const NamedList* data)
+{
+    SafeInt safeChg(&m_changing);
+    DDebug(ClientDriver::self(),DebugAll,"ContactList(%s)::setTableRow(%s,%p)",
+	name().c_str(),item.c_str(),data);
+    ContactItem* c = findContact(item);
+    if (!c)
+	return false;
+    if (!data)
+	return true;
+    SafeTree tree(this);
+    bool changed = c->updateName(*data,m_compareNameCs);
+    if (!changed && !m_flatList)
+	changed = c->groupsWouldChange(*data);
+    if (!changed)
+	updateContact(item,*data);
+    else
+	replaceContact(*c,*data);
+    listChanged();
+    return true;
+}
+
+// Add a new account or contact
+bool ContactList::addTableRow(const String& item, const NamedList* data, bool atStart)
+{
+    SafeInt safeChg(&m_changing);
+    DDebug(ClientDriver::self(),DebugAll,"ContactList(%s)::addTableRow(%s,%p,%u)",
+	name().c_str(),item.c_str(),data,atStart);
+    if (!data)
+	return false;
+    if (find(item))
+	return false;
+    SafeTree tree(this);
+    addContact(item,*data);
+    listChanged();
+    return true;
+}
+
+// Remove an item from tree
+bool ContactList::delTableRow(const String& item)
+{
+    SafeInt safeChg(&m_changing);
+    DDebug(ClientDriver::self(),DebugAll,"ContactList(%s)::delTableRow(%s)",
+	name().c_str(),item.c_str());
+    if (!item)
+	return false;
+    SafeTree tree(this);
+    bool ok = removeContact(item);
+    listChanged();
+    return ok;
+}
+
+// Add, set or remove one or more contacts.
+// Each data list element is a NamedPointer carrying a NamedList with item parameters.
+// The name of an element is the item to update.
+// Set element's value to boolean value 'true' to add a new item if not found
+//  or 'false' to set an existing one. Set it to empty string to delete the item
+bool ContactList::updateTableRows(const NamedList* data, bool atStart)
+{
+    if (!data)
+	return true;
+    DDebug(ClientDriver::self(),DebugAll,"ContactList(%s)::updateTableRows()",
+	name().c_str());
+    SafeTree tree(this);
+    SafeInt safeChg(&m_changing);
+    bool ok = false;
+    QList<QTreeWidgetItem*> list;
+    QTreeWidgetItem* root = invisibleRootItem();
+    bool empty = root && !root->childCount();
+    NamedIterator iter(*data);
+    for (const NamedString* ns = 0; 0 != (ns = iter.get());) {
+	if (!ns->name())
+	    continue;
+	if (!ns->null()) {
+	    NamedList* params = YOBJECT(NamedList,ns);
+	    if (!empty) {
+		if (!params)
+		    ok = (0 != find(ns->name())) || ok;
+		else if (ns->toBoolean() || find(ns->name()))
+		    ok = updateContact(ns->name(),*params) || ok;
+	    }
+	    else if (params)
+		list.append(createContact(ns->name(),*params));
+	}
+	else
+	    ok = removeContact(ns->name()) || ok;
+    }
+    if (!empty)
+	listChanged();
+    else {
+	setContacts(list);
+	ok = true;
+    }
+    return ok;
+}
+
+// Count online/total contacts in a group.
+void ContactList::countContacts(QtTreeItem* grp, int& total, int& online)
+{
+    QList<QtTreeItem*> c = findItems(TypeContact,grp,true,false);
+    QList<QtTreeItem*> r = findItems(TypeChatRoom,grp,true,false);
+    total = c.size() + r.size();
+    online = 0;
+    for (int i = 0; i < c.size(); i++)
+	if (!(static_cast<ContactItem*>(c[i]))->offline())
+	    online++;
+    for (int j = 0; j < r.size(); j++)
+	if (!(static_cast<ContactItem*>(r[j]))->offline())
+	    online++;
+}
+
+// Contact list changed notification
+void ContactList::listChanged()
+{
+    // Hide empty groups
+    if (!m_flatList)
+	showEmptyChildren(!m_hideEmptyGroups);
+    // Update contact count in groups
+    if (!m_flatList) {
+	QList<QtTreeItem*> grps = findItems(TypeGroup,0,true,false);
+	for (int i = 0; i < grps.size(); i++) {
+	    if (!grps[i])
+		continue;
+	    applyItemStatistics(*(grps[i]));
+	}
+    }
+}
+
+// Find a contact
+ContactItem* ContactList::findContact(const String& id, QList<QtTreeItem*>* list)
+{
+    QList<QtTreeItem*> local;
+    if (!list)
+	list = &local;
+    *list = findItems(id);
+    for (int i = 0; i < list->size(); i++) {
+	QtTreeItem* it = static_cast<QtTreeItem*>((*list)[i]);
+	if (isContactType(it->type()) && it->id() == id)
+	    return static_cast<ContactItem*>(it);
+    }
+    return 0;
+}
+
+// Set '_yate_nogroup_caption' property
+void ContactList::setNoGroupCaption(QString value)
+{
+    SafeInt safeChg(&m_changing);
+    QtClient::getUtf8(m_noGroupText,value);
+}
+
+// Set contact grouping
+void ContactList::setFlatList(bool flat)
+{
+    if (flat == m_flatList)
+	return;
+    QTreeWidgetItem* root = invisibleRootItem();
+    if (!root)
+	return;
+    SafeTree tree(this);
+    SafeInt safeChg(&m_changing);
+    TreeRestoreSel sel(this);
+    setCurrentItem(0);
+    // Retrieve (take) contacts
+    QList<QTreeWidgetItem*> c = root->takeChildren();
+    // Shown by group: remove groups and contact duplicates
+    if (!m_flatList) {
+	for (int i = 0; i < c.size(); i++) {
+	    c << c[i]->takeChildren();
+	    if (c[i]->type() == TypeGroup) {
+		delete c[i];
+		c[i] = 0;
+	    }
+	}
+	for (int i = 0; i < c.size(); i++) {
+	    if (!c[i])
+		continue;
+	    for (int j = i + 1; j < c.size(); j++) {
+		QtTreeItem* cc = static_cast<QtTreeItem*>(c[j]);
+		if (cc && cc->id() == (static_cast<QtTreeItem*>(c[i]))->id()) {
+		    delete c[j];
+		    c[j] = 0;
+		}
+	    }
+	}
+	// Make sure the list contains valid pointers
+	for (int i = 0; i < c.size();)
+	    if (c[i])
+		i++;
+	    else
+		c.removeAt(i);
+    }
+    // Set new grouping
+    m_flatList = flat;
+    // Save/restore indendation
+    if (!m_flatList)
+	setIndentation(m_savedIndent);
+    else {
+	m_savedIndent = indentation();
+	setIndentation(0);
+    }
+    setContacts(c);
+}
+
+// Show or hide offline contacts
+void ContactList::setShowOffline(bool value)
+{
+    if (m_showOffline == value)
+	return;
+    m_showOffline = value;
+    QTreeWidgetItem* root = invisibleRootItem();
+    if (!root)
+	return;
+    SafeTree tree(this);
+    SafeInt safeChg(&m_changing);
+    String sel;
+    getSelect(sel);
+    setCurrentItem(0);
+    QList<QtTreeItem*> list = findItems(TypeContact);
+    for (int i = 0; i < list.size(); i++) {
+	ContactItem* c = static_cast<ContactItem*>(list[i]);
+	if (!c)
+	    continue;
+	if (c->offline())
+	    showItem(*c,m_showOffline);
+    }
+    listChanged();
+    // Avoid selecting a hidden item
+    QtTreeItem* it = sel ? find(sel) : 0;
+    if (it && !it->isHidden())
+	setCurrentItem(it);
+}
+
+// Retrieve tree sorting
+QString ContactList::getSorting()
+{
+    if (!m_sortKey)
+	return QtCustomTree::getSorting();
+    String tmp = m_sortKey;
+    tmp << "," << String::boolText(m_sortOrder == Qt::AscendingOrder);
+    return QtClient::setUtf8(tmp);
+}
+
+// Set tree sorting
+void ContactList::updateSorting(const String& key, Qt::SortOrder sort)
+{
+    SafeInt safeChg(&m_changing);
+    if (!isSortingEnabled()) {
+	m_sortKey = key;
+	m_sortOrder = sort;
+    }
+    else
+	QtCustomTree::updateSorting(key,sort);
+}
+
+// Optimized add. Set the whole tree
+void ContactList::setContacts(QList<QTreeWidgetItem*>& list)
+{
+    SafeInt safeChg(&m_changing);
+    // Add contacts to tree
+    if (m_flatList) {
+	sortContacts(list);
+	addChildren(list,-1,0);
+    }
+    else {
+	ContactItemList cil;
+	for (int i = 0; i < list.size(); i++)
+	    createContactTree(static_cast<ContactItem*>(list[i]),cil);
+	if (cil.m_groups.size()) {
+	    addChildren(cil.m_groups);
+	    for (int i = 0; i < cil.m_groups.size(); i++) {
+		sortContacts(cil.m_contacts[i]);
+		QtTreeItem* grp = static_cast<QtTreeItem*>(cil.m_groups[i]);
+		addChildren(cil.m_contacts[i],-1,grp);
+	    }
+	}
+    }
+    listChanged();
+}
+
+// Create a contact
+ContactItem* ContactList::createContact(const String& id, const NamedList& params)
+{
+    ContactItem* c = new ContactItem(id,params);
+    c->copyParams(params);
+    c->updateName(params,m_compareNameCs);
+    return c;
+}
+
+// Add or update a contact
+bool ContactList::updateContact(const String& id, const NamedList& params)
+{
+    if (TelEngine::null(id))
+	return false;
+    DDebug(ClientDriver::self(),DebugAll,"ContactList(%s)::updateContact(%s)",
+	name().c_str(),id.c_str());
+    SafeInt safeChg(&m_changing);
+    QList<QtTreeItem*> list;
+    ContactItem* c = findContact(id,&list);
+    if (!c) {
+	addContact(id,params);
+	return true;
+    }
+    bool changed = c->updateName(params,m_compareNameCs);
+    if (!changed && !m_flatList)
+	changed = c->groupsWouldChange(params);
+    if (!changed) {
+	for (int i = 0; i < list.size(); i++)
+	    if (isContactType(list[i]->type()) && list[i]->id() == id)
+		updateContact(*(static_cast<ContactItem*>(list[i])),params);
+    }
+    else
+	replaceContact(*c,params);
+    return true;
+}
+
+// Remove a contact from tree
+bool ContactList::removeContact(const String& id)
+{
+    DDebug(ClientDriver::self(),DebugAll,"ContactList(%s)::removeContact(%s)",
+	name().c_str(),id.c_str());
+    SafeInt safeChg(&m_changing);
+    if (m_flatList) {
+	QtTreeItem* it = find(id,0,false,false);
+	if (it)
+	    delete it;
+	return it != 0;
+    }
+    // Remove from each group
+    QTreeWidgetItem* root = QTreeWidget::invisibleRootItem();
+    if (!root)
+	return false;
+    bool ok = false;
+    while (true) {
+	int start = 0;
+        int n = root->childCount();
+	for (; start < n; start++) {
+	    QtTreeItem* it = static_cast<QtTreeItem*>(root->child(start));
+	    if (!it)
+		continue;
+	    QtTreeItem* c = find(id,it,false,false);
+	    if (!c)
+		continue;
+	    ok = true;
+	    delete c;
+	    // Remove empty group and restart
+	    if (!it->childCount()) {
+		delete it;
+		if (start < n - 1)
+		    break;
+	    }
+	}
+	if (start == n)
+	    break;
+    }
+    return ok;
+}
+
+// Update a contact
+bool ContactList::updateContact(ContactItem& c, const NamedList& params, bool all)
+{
+#ifdef DEBUG
+    String tmp;
+    params.dump(tmp," ");
+    Debug(ClientDriver::self(),DebugAll,"ContactList(%s)::updateContact(%p,%s) all=%u %s",
+	name().c_str(),&c,c.id().c_str(),all,tmp.safe());
+#endif
+    QtCustomTree::updateItem(c,params);
+    // Show/hide
+    if (c.type() == TypeContact && !m_showOffline)
+	showItem(c,!c.offline());
+    return true;
+}
+
+// Update a contact
+bool ContactList::updateItem(QtTreeItem& item, const NamedList& params)
+{
+    if (isContactType(item.type()))
+	return updateContact(*static_cast<ContactItem*>(&item),params);
+    return QtCustomTree::updateItem(item,params);
+}
+
+// Get the context menu associated with a given item
+QMenu* ContactList::contextMenu(QtTreeItem* item)
+{
+    if (!item)
+	return QtCustomTree::contextMenu(0);
+    if (item->type() == TypeContact) {
+	if (m_menuContact)
+	    return m_menuContact;
+    }
+    if (item->type() == TypeChatRoom) {
+	if (m_menuChatRoom)
+	    return m_menuChatRoom;
+    }
+    else if (item->type() == TypeGroup)
+	return m_menu;
+    return QtCustomTree::contextMenu(item);
+}
+
+// Item added notification
+void ContactList::itemAdded(QtTreeItem& item, QtTreeItem* parent)
+{
+    SafeInt safeChg(&m_changing);
+    QtCustomTree::itemAdded(item,parent);
+    DDebug(ClientDriver::self(),DebugAll,"ContactList(%s)::itemAdded(%p,%p) type=%d id=%s",
+	name().c_str(),&item,parent,item.type(),item.id().c_str());
+    if (isContactType(item.type())) {
+	ContactItem* c = static_cast<ContactItem*>(&item);
+	updateContact(*c,*c);
+	return;
+    }
+    if (item.type() != TypeGroup)
+	return;
+    // Set group name
+    QWidget* w = itemWidget(&item,0);
+    if (!w) {
+	QtCustomTree::updateItem(item,item);
+	return;
+    }
+    QtWindow* wnd = QtClient::parentWindow(this);
+    if (!wnd)
+	return;
+    String text;
+    QtClient::getUtf8(text,item.text(0));
+    String n;
+    QtClient::getUtf8(n,w->objectName());
+    String buf;
+    wnd->setText(buildChildName(buf,n,"group"),text,false);
+}
+
+// Fill a list with item statistics
+void ContactList::fillItemStatistics(QtTreeItem& item, NamedList& list)
+{
+    if (item.type() != TypeGroup)
+	return;
+    int total = 0;
+    int online = 0;
+    countContacts(&item,total,online);
+    list.addParam("total",String(total));
+    list.addParam("online",String(online));
+}
+
+// Update a tree item's margins
+void ContactList::applyItemMargins(QtTreeItem& item, bool set, QtTreeItemProps* p)
+{
+    set = !m_flatList && item.type() != TypeGroup;
+    QtCustomTree::applyItemMargins(item,set,p);
+}
+
+// Retrieve a group item from root or create a new one
+QtTreeItem* ContactList::getGroup(const String& name, bool create)
+{
+    const String& grp = name ? name : s_noGroupId;
+    if (!grp)
+	return 0;
+    // Check if the group already exists
+    QList<QtTreeItem*> list = findItems(grp,0,false,false);
+    for (int i = 0; i < list.size(); i++) {
+	if (list[i]->id() == grp && list[i]->type() == TypeGroup)
+	    return list[i];
+    }
+    if (!create)
+	return 0;
+    QTreeWidgetItem* root = invisibleRootItem();
+    if (!root)
+	return 0;
+    const String& gText = name ? name : m_noGroupText;
+    XDebug(ClientDriver::self(),DebugAll,"ContactList(%s) creating group id=%s text='%s'",
+	this->name().c_str(),grp.c_str(),gText.c_str());
+    // Always keep 'no group' the last one
+    // Insert any other group before it
+    int pos = -1;
+    if (grp != s_noGroupId) {
+	QtTreeItem* noGrp = getGroup(s_noGroupId,false);
+	if (noGrp)
+	    pos = root->indexOfChild(noGrp);
+    }
+    QtTreeItem* g = createGroup(grp,gText,m_expStatusGrp);
+    if (!addChild(g,pos))
+	TelEngine::destruct(g);
+    return g;
+}
+
+// Add a contact
+void ContactList::addContact(const String& id, const NamedList& params)
+{
+    SafeInt safeChg(&m_changing);
+    ContactItem* c = createContact(id,params);
+    if (m_flatList) {
+	addContact(c);
+	return;
+    }
+    ContactItemList cil;
+    createContactTree(c,cil);
+    for (int i = 0; i < cil.m_groups.size(); i++) {
+	QtTreeItem* cg = static_cast<QtTreeItem*>(cil.m_groups[i]);
+	if (cil.m_contacts[i].size()) {
+	    ContactItem* item = static_cast<ContactItem*>((cil.m_contacts[i])[0]);
+	    QtTreeItem* grp = getGroup(cg->id() != s_noGroupId ? cg->id() : String::empty());
+	    if (grp)
+		addContact(item,grp);
+	    else
+		TelEngine::destruct(item);
+	}
+	TelEngine::destruct(cg);
+    }
+}
+
+// Add a contact to a specified parent
+void ContactList::addContact(ContactItem* c, QtTreeItem* parent)
+{
+    if (!c)
+	return;
+    SafeInt safeChg(&m_changing);
+    int pos = -1;
+    if (m_sortKey == "name") {
+	bool asc = (m_sortOrder == Qt::AscendingOrder);
+	QTreeWidgetItem* p = parent ? (QTreeWidgetItem*)parent : invisibleRootItem();
+	int n = p ? p->childCount() : 0;
+	for (int i = 0; i < n; i++) {
+	    ContactItem* item = static_cast<ContactItem*>(p->child(i));
+	    int comp = compareStr(c->m_name,item->m_name,m_compareNameCs);
+	    if (comp && (asc == (comp < 0))) {
+		pos = i;
+		break;
+	    }
+	}
+    }
+    QtCustomTree::addChild(c,pos,parent);
+}
+
+// Replace an existing contact. Remove it and add it again
+void ContactList::replaceContact(ContactItem& c, const NamedList& params)
+{
+    if (!c)
+	return;
+    TreeRestoreSel sel(this,c.id());
+    SafeInt safeChg(&m_changing);
+    String id = c.id();
+    NamedList p(c);
+    p.copyParams(params);
+    removeContact(id);
+    addContact(id,p);
+}
+
+// Create contact structure (groups and lists)
+void ContactList::createContactTree(ContactItem* c, ContactItemList& cil)
+{
+    if (!c)
+	return;
+    SafeInt safeChg(&m_changing);
+    bool noGrp = true;
+    ObjList* grps = c->groups();
+    for (ObjList* o = grps->skipNull(); o; o = o->skipNext()) {
+	String* grp = static_cast<String*>(o->get());
+	if (grp->null())
+	    continue;
+	noGrp = false;
+	int index = cil.getGroupIndex(*grp,*grp,m_expStatusGrp);
+	if (o->skipNext())
+	    cil.m_contacts[index].append(createContact(c->id(),*c));
+	else
+	    cil.m_contacts[index].append(c);
+    }
+    TelEngine::destruct(grps);
+    if (noGrp) {
+	int index = cil.getGroupIndex(s_noGroupId,m_noGroupText,m_expStatusGrp);
+	cil.m_contacts[index].append(c);
+    }
+}
+
+// Sort contacts
+void ContactList::sortContacts(QList<QTreeWidgetItem*>& list)
+{
+    if (!list.size())
+	return;
+    SafeInt safeChg(&m_changing);
+    if (m_sortKey == "name") {
+	QVector<QtTreeItemKey> v(list.size());
+	for (int i = 0; i < list.size(); i++) {
+	    v[i].first = list[i];
+	    v[i].second = (static_cast<ContactItem*>(list[i]))->m_name;
+	}
+	stableSort(v,m_sortOrder,m_compareNameCs);
+	for (int i = 0; i < list.size(); i++)
+	    list[i] = v[i].first;
+    }
+}
+
+
+/*
+ * ContactItem
+ */
+// Update name. Return true if changed
+bool ContactItem::updateName(const NamedList& params, Qt::CaseSensitivity cs)
+{
+    const String* name = params.getParam("name");
+    if (!name)
+	return false;
+    QString s = QtClient::setUtf8(*name);
+    if (!compareStr(m_name,s,cs))
+	return false;
+    m_name = s;
+    return true;
+}
+
+// Check if groups would change
+bool ContactItem::groupsWouldChange(const NamedList& params)
+{
+    String* grps = params.getParam("groups");
+    if (!grps)
+	return false;
+    bool changed = false;
+    ObjList* cgroups = groups();
+    ObjList* newList = Client::splitUnescape(*grps);
+    ObjList* o = 0;
+    for (o = newList->skipNull(); o && !changed; o = o->skipNext())
+	changed = !cgroups->find(o->get()->toString());
+    for (o = cgroups->skipNull(); o && !changed; o = o->skipNext())
+	changed = !newList->find(o->get()->toString());
+    TelEngine::destruct(newList);
+    TelEngine::destruct(cgroups);
+    return changed;
+}
+
+// Check if the contact status is 'offline'
+bool ContactItem::offline()
+{
+    String* status = getParam("status");
+    return status && *status == s_offline;
+}
+
+
+/*
+ * ContactItemList
+ */
+int ContactItemList::getGroupIndex(const String& id, const String& text, bool expStat)
+{
+    for (int i = 0; i < m_groups.size(); i++) {
+	QtTreeItem* item = static_cast<QtTreeItem*>(m_groups[i]);
+	if (item->id() == id)
+	    return i;
+    }
+    int pos = m_groups.size();
+    if (pos && id != s_noGroupId &&
+	(static_cast<QtTreeItem*>(m_groups[pos - 1]))->id() == s_noGroupId)
+	pos--;
+    m_groups.insert(pos,ContactList::createGroup(id,text,expStat));
+    m_contacts.insert(pos,QtTreeItemList());
+    return pos;
+}
+
+//
+// FileItem
+//
+FileItem::FileItem(int type, const char* name, const String& path,
+    QFileIconProvider* prov)
+    : String(name),
+    m_type(type), m_icon(0)
+{
+    FileListTree::buildFileFullName(m_fullName,path,name);
+    if (prov)
+	m_icon = new QIcon(FileListTree::fileIcon(type,m_fullName,prov));
+}
+
+FileItem::FileItem(const String& path, QFileIconProvider* prov)
+    : String(FileListTree::s_upDir),
+    m_type(FileListTree::TypeDir), m_icon(0)
+{
+    Client::removeLastNameInPath(m_fullName,path);
+    if (prov)
+	m_icon = new QIcon(FileListTree::fileIcon(m_type,m_fullName,prov));
+}
+
+FileItem::~FileItem()
+{
+    if (m_icon)
+	delete m_icon;
+}
+
+
+//
+// DirListThread
+//
+// Skip special directories (. or ..)
+static inline bool skipSpecial(const char* s)
+{
+    return *s && *s == '.' && (!s[1] || (s[1] == '.' && !s[2]));
+}
+
+void DirListThread::run()
+{
+    ObjList* dirs = m_listDirs ? &m_dirs : 0;
+    ObjList* files = m_listFiles ? &m_files : 0;
+    XDebug(QtDriver::self(),DebugAll,"DirListThread(%s) starting [%p]",
+	m_dir.c_str(),this);
+#ifdef _WINDOWS
+    String name(m_dir);
+    if (!name.endsWith("\\"))
+	name << "\\";
+    name << "*";
+    // Init find
+    WIN32_FIND_DATAA d;
+    HANDLE hFind = ::FindFirstFileA(name,&d);
+    if (hFind == INVALID_HANDLE_VALUE) {
+	m_error = ::GetLastError();
+	if (m_error == ERROR_NO_MORE_FILES)
+	    m_error = 0;
+	runTerminated();
+	return;
+    }
+    // Enumerate content
+    ::SetLastError(0);
+    do {
+	if (isFinished())
+	    break;
+        if (d.dwFileAttributes & FILE_ATTRIBUTE_DEVICE ||
+	    skipSpecial(d.cFileName))
+	    continue;
+        if (d.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+	    if (dirs)
+		dirs = addItem(FileListTree::TypeDir,d.cFileName,m_dirs,dirs);
+        }
+	else if (files)
+	    files = addItem(FileListTree::TypeFile,d.cFileName,m_files,files);
+    }
+    while (::FindNextFileA(hFind,&d));
+    if (isRunning()) {
+	m_error = ::GetLastError();
+	if (m_error == ERROR_NO_MORE_FILES)
+	    m_error = 0;
+    }
+    else
+	m_error = ERROR_CANCELLED;
+    ::FindClose(hFind);
+#else
+    errno = 0;
+    DIR* dir = ::opendir(m_dir);
+    if (!dir) {
+	m_error = errno;
+	runTerminated();
+	return;
+    }
+    struct dirent* entry;
+    while ((entry = ::readdir(dir)) != 0) {
+	if (isFinished())
+	    break;
+	if (skipSpecial(entry->d_name))
+	    continue;
+#ifdef _DIRENT_HAVE_D_TYPE
+	if (entry->d_type == DT_DIR) {
+	    if (dirs)
+		dirs = addItem(FileListTree::TypeDir,entry->d_name,m_dirs,dirs);
+	}
+	else if (entry->d_type == DT_REG && files)
+	    files = addItem(FileListTree::TypeFile,entry->d_name,m_files,files);
+#else
+	struct stat stat_buf;
+	String p;
+	p << m_dir << "/" << entry->d_name;
+	if (::stat(p,&stat_buf))
+	    break;
+	if (S_ISDIR(stat_buf.st_mode)) {
+	    if (dirs)
+		dirs = addItem(FileListTree::TypeDir,entry->d_name,m_dirs,dirs);
+	}
+	else if (S_ISREG(stat_buf.st_mode) && files)
+	    files = addItem(FileListTree::TypeFile,entry->d_name,m_files,files);
+#endif // _DIRENT_HAVE_D_TYPE
+    }
+    if (isRunning())
+	m_error = errno;
+    else
+	m_error = ECANCELED;
+    ::closedir(dir);
+#endif // _WINDOWS
+    runTerminated();
+}
+
+ObjList* DirListThread::addItemSort(ObjList& list, FileItem* it)
+{
+    if (!it)
+	return 0;
+    ObjList* o = list.skipNull();
+    bool asc = (m_sort == QtClient::SortAsc);
+    for (; o; o = o->skipNext()) {
+	FileItem* crt = static_cast<FileItem*>(o->get());
+	int cmp = m_caseSensitive ? ::strcmp(*it,*crt) : ::strcasecmp(*it,*crt);
+	if (!cmp)
+	    continue;
+	// Ascending ?
+	if (asc) {
+	    if (cmp > 0)
+		continue;
+	}
+	else if (cmp < 0)
+	    continue;
+	return o->insert(it);
+    }
+    if (o)
+	return o->append(it);
+    return list.append(it);
+}
+
+// Called when terminated from run()
+void DirListThread::runTerminated()
+{
+    XDebug(QtDriver::self(),DebugAll,"DirListThread(%s) finished error=%d [%p]",
+	m_dir.c_str(),m_error,this);
+    if (m_error)
+	return;
+    // Add up dir
+    if (m_listDirs && m_listUpDir)
+	m_dirs.insert(new FileItem(m_dir,m_iconProvider));
+}
+
+
+//
+// FileListTree
+//
+const String FileListTree::s_upDir = "..";
+
+static inline void setRootPath(String& path)
+{
+#ifdef _WINDOWS
+    path = "";
+#else
+    path = "/";
+#endif
+}
+
+static inline int getPathType(const String& s, int defVal)
+{
+    if (s == YSTRING("upthenhome"))
+	return FileListTree::PathUpThenHome;
+    if (s == YSTRING("home"))
+	return FileListTree::PathHome;
+    if (s == YSTRING("root"))
+	return FileListTree::PathRoot;
+    if (s == YSTRING("none"))
+	return FileListTree::PathNone;
+    return defVal;
+}
+
+static inline void removePathSepEnd(String& s)
+{
+    Client::removeEndsWithPathSep(s,s);
+}
+
+// Constructor
+FileListTree::FileListTree(const char* name, const NamedList& params, QWidget* parent)
+    : QtCustomTree(name,params,parent,false),
+    m_fileSystemList(false),
+    m_autoChangeDir(true),
+    m_listFiles(false),
+    m_sort(QtClient::SortNone),
+    m_listOnFailure(PathUpThenHome),
+    m_iconProvider(0),
+    m_dirListThread(0)
+{
+    XDebug(ClientDriver::self(),DebugAll,"FileListTree(%s) [%p]",name,this);
+    // Add item props translation
+    addItemType(TypeDir,"dir");
+    addItemType(TypeFile,"file");
+    addItemType(TypeDrive,"drive");
+    // Set some defaults
+    if (params.getBoolValue(YSTRING("filelist_default_itemstyle"))) {
+	setItemStyle("dir:<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\"> li { white-space: pre-wrap; }</style></head><body style=\" font-size:12px; font-weight:bold; font-style:normal;\"><p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">${name}</p></body></html>");
+	setItemStyle("file:<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\"> li { white-space: pre-wrap; }</style></head><body style=\" font-size:12px; font-weight:400; font-style:normal;\"><p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">${name}</p></body></html>");
+	setItemStyle("drive:<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\"> li { white-space: pre-wrap; }</style></head><body style=\" font-size:12px; font-weight:bold; font-style:normal;\"><p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">${name}</p></body></html>");
+    }
+    String* ihs = params.getParam(YSTRING("filelist_default_itemheight"));
+    int ih = 0;
+    if (ihs) {
+	if (ihs->toBoolean())
+	    ih = 16;
+	else
+	    ih = ihs->toInteger();
+    }
+    if (ih > 0) {
+	String tmp(ih);
+	setItemHeight(QtClient::setUtf8("dir:" + tmp));
+	setItemHeight(QtClient::setUtf8("file:" + tmp));
+	setItemHeight(QtClient::setUtf8("drive:" + tmp));
+	setUniformRowHeights(true);
+    }
+    // Display contents from file system
+    m_fileSystemList = params.getBoolValue(YSTRING("filelist_filesystemlist"));
+    if (m_fileSystemList) {
+	m_sort = QtClient::SortAsc;
+	m_nameParam = params.getValue(YSTRING("filelist_filesystemlist_name_column"),"name");
+	m_autoChangeDir = params.getBoolValue(YSTRING("filelist_filesystemlist_autochangedir"),true);
+	m_listFiles = params.getBoolValue(YSTRING("filelist_filesystemlist_listfiles"),true);
+	if (params.getBoolValue(YSTRING("filelist_filesystemlist_showicons")))
+	    m_iconProvider = new QFileIconProvider;
+	m_listOnFailure = getPathType(params[YSTRING("filelist_filesystemlist_listonfailure")],
+	    PathUpThenHome);
+    }
+    setParams(params);
+    if (m_fileSystemList) {
+	String* s = params.getParam(YSTRING("filelist_filesystemlist_startpath"));
+	if (s) {
+	    int t = getPathType(*s,PathRoot);
+	    if (t == PathHome)
+		setFsPath(QDir::homePath());
+	    else if (t != PathNone)
+		setFsPath();
+	}
+    }
+}
+
+// Destructor
+FileListTree::~FileListTree()
+{
+    setDirListThread(false);
+    if (m_iconProvider)
+	delete m_iconProvider;
+}
+
+// Set _yate_filesystem_path property
+void FileListTree::setFsPath(QString path)
+{
+    if (!m_fileSystemList)
+	return;
+    String tmp;
+    QtClient::getUtf8(tmp,QDir::toNativeSeparators(path));
+    setFsPath(tmp);
+}
+
+// Set _yate_refresh property
+void FileListTree::setRefresh(QString val)
+{
+    if (m_fileSystemList)
+	setFsPath(m_fsPath);
+}
+
+// Change file system path, refresh data
+void FileListTree::setFsPath(const String& path, bool force)
+{
+    if (!m_fileSystemList)
+	return;
+    if (!force && m_fsPath == path)
+	return;
+    String old = m_fsPath;
+    m_fsPath = path;
+    removePathSepEnd(m_fsPath);
+    if (!m_fsPath) {
+	setRootPath(m_fsPath);
+#ifdef _WINDOWS
+	ObjList tmp;
+	QFileInfoList l = QDir::drives();
+	for (int i = 0; i < l.size(); i++) {
+	    QFileInfo fi = l[i];
+	    String n;
+	    QtClient::getUtf8(n,QDir::toNativeSeparators(fi.absoluteFilePath()));
+	    removePathSepEnd(n);
+	    if (n)
+		tmp.append(new FileItem(TypeDrive,n,String::empty(),m_iconProvider));
+	}
+	refresh(0,0,&tmp);
+	m_acceptDropOnEmpty = QtDrop::None;
+	return;
+#endif
+    }
+    if (setDirListThread(true)) {
+	m_acceptDropOnEmpty = QtDrop::Always;
+	clearTable();
+    }
+    else {
+	m_acceptDropOnEmpty = QtDrop::None;
+	m_fsPath = old;
+    }
+}
+
+static void addFileItems(QList<QTreeWidgetItem*>& items, ObjList* list,
+    const String& nameParam, int iconCol)
+{
+    if (!list)
+	return;
+    for (ObjList* o = list->skipNull(); o; o = o->skipNext()) {
+	FileItem* f = static_cast<FileItem*>(o->get());
+	QtTreeItem* it = new QtTreeItem(f->m_fullName,f->m_type);
+	it->addParam(nameParam,*f);
+	if (iconCol >= 0 && f->m_icon)
+	    it->setIcon(iconCol,*(f->m_icon));
+	items.append(it);
+    }
+}
+
+// Directory listing thread finished notification
+void FileListTree::refresh(ObjList* dirs, ObjList* files, ObjList* drives)
+{
+    clearTable();
+    SafeInt safeChg(&m_changing);
+    QList<QTreeWidgetItem*> list;
+    int col = getColumnNo(m_nameParam);
+    addFileItems(list,drives,m_nameParam,col);
+    addFileItems(list,dirs,m_nameParam,col);
+    addFileItems(list,files,m_nameParam,col);
+    addChildren(list);
+    for (int i = 0; i < list.size(); i++) {
+	QtTreeItem* it = static_cast<QtTreeItem*>(list[i]);
+	updateItem(*it,*it);
+    }
+}
+
+// Sort a list of items
+void FileListTree::sortItems(QList<QTreeWidgetItem*>& list, int type)
+{
+    if (type != TypeDir && type != TypeFile)
+	return;
+    QVector<QtTreeItemKey> v(list.size());
+    for (int i = 0; i < list.size(); i++) {
+	v[i].first = list[i];
+	v[i].second = QtClient::setUtf8(static_cast<QtTreeItem*>(list[i])->getValue(m_nameParam));
+    }
+    stableSort(v,Qt::AscendingOrder,Qt::CaseInsensitive);
+    for (int i = 0; i < list.size(); i++)
+	list[i] = v[i].first;
+}
+
+// Retrieve the icon for a given item
+QIcon FileListTree::icon(QtTreeItem& item)
+{
+    return fileIcon(item.type(),item.toString(),m_iconProvider);
+}
+
+// Retrieve the icon for a given item type
+QIcon FileListTree::fileIcon(int type, const String& name, QFileIconProvider* provider)
+{
+    if (!provider)
+	return QIcon();
+    if (type == TypeDir || type == TypeFile) {
+	QFileInfo fi(QtClient::setUtf8(name));
+	return provider->icon(fi);
+    }
+    if (type == TypeDrive)
+	return provider->icon(QFileIconProvider::Drive);
+    return QIcon();
+}
+
+// Catch dir list thread terminate signal
+void FileListTree::onDirThreadTerminate()
+{
+    if (!m_dirListThread)
+	return;
+    QThread* th = qobject_cast<QThread*>(sender());
+    if (th != m_dirListThread)
+	return;
+    DirListThread* t = static_cast<DirListThread*>(th);
+    bool ok = !t->m_error;
+    if (ok)
+	refresh(t->m_listDirs ? &t->m_dirs : 0,t->m_listFiles ? &t->m_files : 0);
+    else if (QtDriver::self() && QtDriver::self()->debugAt(DebugNote)) {
+	String s;
+	Thread::errorString(s,t->m_error);
+	Debug(QtDriver::self(),DebugNote,"FileListTree(%s) failed to list '%s': %d '%s' [%p]",
+	    name().c_str(),m_fsPath.c_str(),t->m_error,s.c_str(),this);
+    }
+    QtBusyWidget::showBusyChild(this,false);
+    resetThread();
+    if (ok)
+	return;
+    if (m_listOnFailure == PathUpThenHome) {
+	// Up dir, then home
+	if (!isHomePath()) {
+	    if (isRootPath(m_fsPath)) {
+		setFsPath(QDir::homePath());
+		return;
+	    }
+	    int pos = m_fsPath.rfind(*Engine::pathSeparator());
+	    if (pos >= 0)
+		setFsPath(m_fsPath.substr(0,pos));
+	    else
+		setFsPath();
+	    return;
+	}
+    }
+    else if (m_listOnFailure == PathRoot) {
+	// Try root path
+	if (!isRootPath(m_fsPath)) {
+	    setFsPath();
+	    return;
+	}
+    }
+    // Always try home path if something set
+    if (m_listOnFailure != PathNone && !isHomePath()) {
+	setFsPath(QDir::homePath());
+	return;
+    }
+    m_fsPath = "";
+    m_acceptDropOnEmpty = QtDrop::None;
+    refresh(0,0);
+}
+
+// Start/stop dir list thread
+bool FileListTree::setDirListThread(bool on)
+{
+    QtBusyWidget::showBusyChild(this,false);
+    resetThread();
+    if (!on)
+	return true;
+    DirListThread* t = new DirListThread(0,m_fsPath,true,m_listFiles);
+    if (!QtClient::connectObjects(t,SIGNAL(finished()),
+	this,SLOT(onDirThreadTerminate()))) {
+	t->deleteLater();
+	return false;
+    }
+    t->m_iconProvider = m_iconProvider;
+    t->m_listUpDir = !isRootPath(m_fsPath);
+    t->m_sort = m_sort;
+    QtBusyWidget::showBusyChild(this,true);
+    m_dirListThread = t;
+    m_dirListThread->start();
+    return true;
+}
+
+// Process item double click
+void FileListTree::onItemDoubleClicked(QtTreeItem* item, int column)
+{
+    if (item && item->type() != TypeFile && m_fileSystemList && m_autoChangeDir)
+	setFsPath(item->toString());
+    else
+	QtCustomTree::onItemDoubleClicked(item,column);
+}
+
+void FileListTree::resetThread()
+{
+    if (!m_dirListThread)
+	return;
+    QThread* t = m_dirListThread;
+    m_dirListThread = 0;
+    t->disconnect();
+    t->exit();
+    t->deleteLater();
+}
+
+
+//
+// QtPaintItemDesc
+//
+QtPaintButtonDesc* QtPaintItemDesc::button()
+{
+    return 0;
+}
+
+
+//
+// QtPaintButtonDesc
+//
+QtPaintButtonDesc* QtPaintButtonDesc::button()
+{
+    return this;
+}
+
+// Find a button in a list
+QtPaintButtonDesc* QtPaintButtonDesc::find(ObjList& list, const String& name,
+    bool create)
+{
+    if (!name)
+	return 0;
+    ObjList* o = list.find(name);
+    if (!o && create)
+	o = list.append(new QtPaintButtonDesc(name));
+    return o ? static_cast<QtPaintItemDesc*>(o->get())->button() : 0;
+}
+
+
+//
+// QtPaintItem
+//
+// Set hover state
+bool QtPaintItem::setHover(bool on)
+{
+    if (m_hover == on)
+	return false;
+    m_hover = on;
+    return true;
+}
+
+// Set pressed state
+bool QtPaintItem::setPressed(bool on)
+{
+    if (m_pressed == on)
+	return false;
+    m_pressed = on;
+    return true;
+}
+
+// Retrieve the item name
+const String& QtPaintItem::toString() const
+{
+    return name();
+}
+
+
+//
+// QtPaintButton
+//
+QtPaintButton::QtPaintButton(QtPaintButtonDesc& desc)
+    : QtPaintItem(desc,desc.m_size),
+    m_image(0),
+    m_iconSize(desc.m_iconSize),
+    m_iconOffset(0,0)
+{
+    if (m_iconSize.width() > m_size.width())
+	m_iconSize.setWidth(m_size.width());
+    if (m_iconSize.height() > m_size.height())
+	m_iconSize.setHeight(m_size.height());
+    m_iconOffset.setWidth((m_size.width() - m_iconSize.width()) / 2);
+    m_iconOffset.setHeight((m_size.height() - m_iconSize.height()) / 2);
+    m_action = desc.m_params;
+    m_image = &m_normalImage;
+    loadImages(desc.m_params);
+    updateOptState();
+}
+
+// Load button images
+void QtPaintButton::loadImages(const NamedList& params)
+{
+    loadImage(m_normalImage,params,YSTRING("_yate_normal_icon"));
+    if (!loadImage(m_hoverImage,params,YSTRING("_yate_hover_icon")))
+	m_hoverImage = m_normalImage;
+    if (!loadImage(m_pressedImage,params,YSTRING("_yate_pressed_icon")))
+	m_pressedImage = m_normalImage;
+}
+
+// Set hover state
+bool QtPaintButton::setHover(bool on)
+{
+    if (!QtPaintItem::setHover(on))
+	return false;
+    if (!on)
+	m_pressed = false;
+    updateOptState();
+    return true;
+}
+
+// Set pressed state
+bool QtPaintButton::setPressed(bool on)
+{
+    if (!QtPaintItem::setPressed(on))
+	return false;
+    updateOptState();
+    return true;
+}
+
+// Draw the button
+void QtPaintButton::draw(QPainter* painter, const QRect& rect)
+{
+    m_displayRect = rect;
+    if (!(painter && m_image))
+	return;
+    QPoint p(m_iconOffset.width() + rect.x(),m_iconOffset.height() + rect.y());
+    painter->drawPixmap(p,*m_image);
+}
+
+// Load an image, adjust its size
+bool QtPaintButton::loadImage(QPixmap& pixmap, const NamedList& params, const String& param)
+{
+    if (!QtClient::getSkinPathPixmapFromCache(pixmap,params[param]))
+	return false;
+    // Adjust size
+    if (pixmap.size() != m_iconSize)
+	pixmap = pixmap.scaled(m_iconSize,Qt::KeepAspectRatio);
+    return true;
+}
+
+// Update option state
+void QtPaintButton::updateOptState()
+{
+    if (m_enabled) {
+	if (m_hover) {
+	    if (!m_pressed)
+	    	m_image = &m_hoverImage;
+	    else
+		m_image = &m_pressedImage;
+	}
+	else if (m_pressed)
+	    m_image = &m_pressedImage;
+	else
+	    m_image = &m_normalImage;
+    }
+    else
+	m_image = &m_normalImage;
+}
+
+
+//
+// QtPaintItems
+//
+// Add an item from description
+void QtPaintItems::append(QtPaintItemDesc& desc)
+{
+    QtPaintButtonDesc* bDesc = desc.button();
+    if (!bDesc)
+	return;
+    QtPaintButton* b = new QtPaintButton(*bDesc);
+    m_items.remove(b->toString());
+    m_items.append(b);
+}
+
+// Calculate area needed to paint
+void QtPaintItems::itemsAdded()
+{
+    m_size.setHeight(0);
+    m_size.setWidth(0);
+    for (ObjList* o = m_items.skipNull(); o; o = o->skipNext()) {
+	QtPaintItem* item = static_cast<QtPaintItem*>(o->get());
+	if (m_size.width())
+	    m_size.setWidth(m_size.width() + m_itemSpace);
+	m_size.setWidth(m_size.width() + item->size().width());
+	if (m_size.height() < item->size().height())
+	    m_size.setHeight(item->size().height());
+    }
+    if (m_size.width())
+	m_size.setWidth(m_size.width() + m_margins.x() + m_margins.width());
+    if (m_size.height())
+	m_size.setHeight(m_size.height() + m_margins.y() + m_margins.height());
+}
+
+// Set hover. Update item at position
+bool QtPaintItems::setHover(const QPoint& pos)
+{
+    bool chg = setHover(true);
+    if (m_lastItemHover) {
+	if (m_lastItemHover->displayRect().contains(pos))
+	    return chg;
+	m_lastItemHover->setHover(false);
+	m_lastItemHover = 0;
+	chg = true;
+    }
+    for (ObjList* o = m_items.skipNull(); !m_lastItemHover && o; o = o->skipNext()) {
+	QtPaintItem* it = static_cast<QtPaintItem*>(o->get());
+	if (it->displayRect().contains(pos))
+	    m_lastItemHover = it;
+    }
+    if (m_lastItemHover)
+	chg = m_lastItemHover->setHover(true) || chg;
+    return chg;
+}
+
+// Set hover state
+bool QtPaintItems::setHover(bool on)
+{
+    if (!QtPaintItem::setHover(on))
+	return false;
+    if (on)
+	return true;
+    if (m_lastItemHover) {
+	m_lastItemHover->setHover(false);
+	m_lastItemHover = 0;
+    }
+    return true;
+}
+
+// Mouse pressed/released. Update item at position
+bool QtPaintItems::mousePressed(bool on, const QPoint& pos, String* action)
+{
+    bool chg = false;
+    if (m_lastItemHover) {
+	if (m_lastItemHover->displayRect().contains(pos)) {
+	    chg = m_lastItemHover->setPressed(on);
+	    if (chg && !on && action)
+		*action = m_lastItemHover->action();
+	}
+	else
+	    chg = m_lastItemHover->setPressed(false);
+    }
+    return setPressed(on);
+}
+
+// Set pressed state
+bool QtPaintItems::setPressed(bool on)
+{
+    bool chg = !on && m_lastItemHover && m_lastItemHover->setPressed(false);
+    return QtPaintItem::setPressed(on) || chg;
+}
+
+// Draw items.
+void QtPaintItems::draw(QPainter* painter, const QRect& rect)
+{
+    m_displayRect = rect;
+    if (!painter)
+	return;
+    painter->save();
+    int maxX = rect.x() + rect.width();
+    int x = rect.x() + m_margins.x();
+    int y = rect.y() + m_margins.y();
+    for (ObjList* o = m_items.skipNull(); o && (x < maxX); o = o->skipNext()) {
+	QtPaintItem* item = static_cast<QtPaintItem*>(o->get());
+	QRect r(x,y,item->size().width(),item->size().height());
+	painter->setClipRect(r);
+	item->draw(painter,r);
+	x += item->size().width() + m_itemSpace;
+    }
+    painter->restore();
+}
+
+
+//
+// QtItemDelegate
+//
+QtItemDelegate::QtItemDelegate(QObject* parent, const NamedList& params)
+    : QItemDelegate(parent),
+    String(params),
+    m_drawFocus(true),
+    m_roleDisplayText(Qt::DisplayRole),
+    m_roleImage(Qt::UserRole),
+    m_roleBackground(Qt::UserRole),
+    m_roleMargins(Qt::UserRole),
+    m_roleQtDrawItems(Qt::UserRole)
+{
+    static const String s_drawfocus = "drawfocus";
+    static const String s_columns = "columns";
+    static const String s_editableCols = "editable_cols";
+    static const String s_role_display = "role_display";
+    static const String s_role_image = "role_image";
+    static const String s_role_background = "role_background";
+    static const String s_role_margins = "role_margins";
+    static const String s_role_qtdrawitems = "role_qtdrawitems";
+    static const String s_noRoles = "noroles";
+    static const String s_noImageRole = "noimagerole";
+
+    String pref = params;
+    if (pref)
+	pref << ".";
+    NamedIterator iter(params);
+    bool noRoleImage = false;
+    bool noRoles = false;
+    for (const NamedString* ns = 0; 0 != (ns = iter.get());) {
+	if (ns->name() == s_drawfocus)
+	    m_drawFocus = ns->toBoolean();
+	else if (ns->name() == s_columns)
+	    m_columnsStr = QtClient::setUtf8(*ns).split(',',Qt::SkipEmptyParts);
+	else if (ns->name() == s_editableCols)
+	    m_editableColsStr = QtClient::setUtf8(*ns).split(',',Qt::SkipEmptyParts);
+	else if (ns->name() == s_noImageRole)
+	    noRoleImage = ns->toBoolean();
+	else if (ns->name() == s_noRoles)
+	    noRoles = ns->toBoolean();
+	else if (pref && ns->name().startsWith(pref,false)) {
+	    // Handle parameters set from code (not configurable)
+	    String tmp = ns->name().substr(pref.length());
+	    if (tmp == s_role_display)
+		m_roleDisplayText = ns->toInteger(Qt::DisplayRole);
+	    else if (tmp == s_role_image)
+		m_roleImage = ns->toInteger(Qt::UserRole);
+	    else if (tmp == s_role_background)
+		m_roleBackground = ns->toInteger(Qt::UserRole);
+	    else if (tmp == s_role_margins)
+		m_roleMargins = ns->toInteger(Qt::UserRole);
+	    else if (tmp == s_role_qtdrawitems)
+		m_roleQtDrawItems = ns->toInteger(Qt::UserRole);
+	}
+    }
+    // Disable role(s)
+    if (noRoles) {
+	m_roleDisplayText = Qt::DisplayRole;
+	m_roleImage = Qt::UserRole;
+	m_roleBackground = Qt::UserRole;
+	m_roleMargins = Qt::UserRole;
+    }
+    else {
+	if (noRoleImage)
+	    m_roleImage = Qt::UserRole;
+    }
+#ifdef XDEBUG
+    String dump;
+    params.dump(dump," ");
+    Debug(DebugAll,"QtItemDelegate(%s) created: %s [%p]",c_str(),dump.c_str(),this);
+#endif
+}
+
+// Utility: translate name to int value
+static void setIntListName(QList<int>& dest, QStringList& values, QStringList& cNames,
+    bool unique = true)
+{
+    dest.clear();
+    for (int i = 0; i < values.size(); i++) {
+	bool ok = false;
+	int val = values[i].toInt(&ok);
+	if (!ok)
+	    val = cNames.indexOf(values[i]);
+	if (val >= 0 && !(unique && dest.contains(val)))
+	    dest.append(val);
+    }
+}
+
+// Update column position from column names.
+// 'cNames' must be the column names in their order, starting from 0
+void QtItemDelegate::updateColumns(QStringList& cNames)
+{
+    setIntListName(m_columns,m_columnsStr,cNames);
+    setIntListName(m_editableCols,m_editableColsStr,cNames);
+}
+
+void QtItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
+    const QModelIndex& index) const
+{
+    QStyleOptionViewItem opt = setOptions(index,option);
+    opt.features = option.features;
+    // Prepare painter
+    painter->save();
+    // Retrieve check
+    QRect checkRect;
+    Qt::CheckState checkState = Qt::Unchecked;
+    QVariant checkVar = index.data(Qt::CheckStateRole);
+    if (checkVar.isValid()) {
+	checkState = static_cast<Qt::CheckState>(checkVar.toInt());
+	checkRect = doCheck(opt,opt.rect,checkVar);
+    }
+    // Retrieve image (decoration)
+    QPixmap pixmap;
+    QRect decorationRect;
+    bool isStd = (m_roleImage <= Qt::UserRole);
+    QVariant pVar = index.data(isStd ? Qt::DecorationRole : m_roleImage);
+    if (pVar.isValid()) {
+	if (isStd)
+	    pixmap = decoration(opt,pVar);
+	else {
+	    QString file = pVar.toString();
+	    QtClient::getPixmapFromCache(pixmap,file);
+	    // Resize the pixmap
+	    if (!pixmap.isNull())
+		pixmap = pixmap.scaled(opt.decorationSize.width(),
+		    opt.decorationSize.height(),Qt::KeepAspectRatio);
+	}
+	decorationRect = QRect(QPoint(0,0),pixmap.size());
+    }
+    // Retrieve text to display
+    QString text = getDisplayText(opt,index);
+    QRect displayRect = opt.rect;
+    displayRect.setWidth(INT_MAX/256);
+    displayRect = textRectangle(painter,displayRect,opt.font,text);
+    // Retrieve margins and apply them
+    QRect margins;
+    if (m_roleMargins != Qt::UserRole) {
+	pVar = index.data(m_roleMargins);
+	if (pVar.type() == QVariant::Rect) {
+	    margins = pVar.toRect();
+	    applyMargins(opt.rect,margins,true);
+	}
+    }
+    QtPaintItems* extraPaint = 0;
+    if (m_roleQtDrawItems != Qt::UserRole) {
+	pVar = index.data(m_roleQtDrawItems);
+	if (pVar.type() == QVariant::UserType) {
+	    QtRefObjectHolder holder = pVar.value<QtRefObjectHolder>();
+	    extraPaint = static_cast<QtPaintItems*>((RefObject*)holder.m_refObj);
+	}
+    }
+    // Calculate layout
+    doLayout(opt,&checkRect,&decorationRect,&displayRect,false);
+    // Draw the item
+    if (m_roleMargins != Qt::UserRole)
+	applyMargins(opt.rect,margins,false);
+    drawBackground(painter,opt,index);
+    if (m_roleMargins != Qt::UserRole)
+	applyMargins(opt.rect,margins,true);
+    drawCheck(painter,opt,checkRect,checkState);
+    drawDecoration(painter,opt,decorationRect,pixmap);
+    if (extraPaint && extraPaint->size().width()) {
+	// Steal extra paint area from text display
+	int w = extraPaint->size().width();
+	if (w < displayRect.width())
+	    displayRect.setWidth(displayRect.width() - w);
+	else {
+	    w = displayRect.width();
+	    displayRect.setWidth(0);
+	}
+	int y = displayRect.y();
+	int h = extraPaint->size().height();
+	int delta = (displayRect.height() - h) / 2;
+	if (delta) {
+	    if (delta > 0)
+		y += delta;
+	    else
+		h += delta;
+	}
+	QRect rect(displayRect.x() + displayRect.width(),y,w,h);
+	extraPaint->draw(painter,rect);
+    }
+    drawDisplay(painter,opt,displayRect,text);
+    if (m_roleMargins != Qt::UserRole)
+	applyMargins(opt.rect,margins,false);
+    drawFocus(painter,opt,displayRect);
+    // Restore painter
+    painter->restore();
+}
+
+// Build a list of delegates. Return a list of QtItemDelegate
+QList<QAbstractItemDelegate*> QtItemDelegate::buildDelegates(QObject* parent, const NamedList& params,
+    const NamedList* common, const String& prefix)
+{
+    QList<QAbstractItemDelegate*> list;
+    String pref = prefix;
+    for (int n = 0; true; n++) {
+	if (n)
+	    pref << "." << n;
+	NamedString* ns = params.getParam(pref);
+	if (!ns) {
+	    if (n)
+		break;
+	    continue;
+	}
+	NamedList p(pref);
+	pref << ".";
+	p.copySubParams(params,pref);
+	if (common) {
+	    NamedIterator iter(*common);
+	    for (const NamedString* ns = 0; 0 != (ns = iter.get());)
+		p.addParam(pref + ns->name(),*ns);
+	}
+	QAbstractItemDelegate* dlg = build(parent,*ns,p);
+	if (dlg)
+	    list.append(dlg);
+    }
+    return list;
+}
+
+// Build a delegate
+QAbstractItemDelegate* QtItemDelegate::build(QObject* parent, const String& cls,
+    NamedList& params)
+{
+    if (!cls || cls == YSTRING("QtItemDelegate"))
+	return new QtItemDelegate(parent,params);
+    if (cls == YSTRING("QtHtmlItemDelegate"))
+	return new QtHtmlItemDelegate(parent,params);
+    QObject* obj = (QObject*)UIFactory::build(cls,String::empty(),&params);
+    if (!obj)
+	return 0;
+    QAbstractItemDelegate* d = qobject_cast<QAbstractItemDelegate*>(obj);
+    if (d)
+	return d;
+    delete obj;
+    return 0;
+}
+
+// Retrieve display text for a given index
+QString QtItemDelegate::getDisplayText(const QStyleOptionViewItem& opt,
+    const QModelIndex& index) const
+{
+    QVariant var = index.data(m_roleDisplayText);
+    if (var.type() == QVariant::StringList) {
+	QStringList list = var.toStringList();
+	if (!list.size())
+	    return QString();
+	// 1 item or not selected: return the first string
+	if (list.size() == 1 || 0 == (opt.state & QStyle::State_Selected))
+	    return list[0];
+	return list[1];
+    }
+    if (var.canConvert(QVariant::String))
+	return var.toString();
+    return QString();
+}
+
+void QtItemDelegate::drawBackground(QPainter* painter, const QStyleOptionViewItem& opt,
+    const QModelIndex& index) const
+{
+    QVariant var;
+    if (m_roleBackground != Qt::UserRole)
+	var = index.data(m_roleBackground);
+    if (!var.isValid()) {
+	QItemDelegate::drawBackground(painter,opt,index);
+	return;
+    }
+    if (var.canConvert(QMetaType::QBrush)) {
+	QPointF oldBO = painter->brushOrigin();
+	painter->setBrushOrigin(opt.rect.topLeft());
+	painter->fillRect(opt.rect,qvariant_cast<QBrush>(var));
+	painter->setBrushOrigin(oldBO);
+    }
+    else
+	Debug(DebugNote,"QtItemDelegate(%s) unhandled background variant type=%s",
+	    c_str(),var.typeName());
+}
+
+void QtItemDelegate::drawDecoration(QPainter* painter, const QStyleOptionViewItem& opt,
+    const QRect& rect, const QPixmap& pixmap) const
+{
+    if (pixmap.isNull() || !rect.isValid())
+	return;
+    QPoint p = QStyle::alignedRect(opt.direction,opt.decorationAlignment,
+	pixmap.size(),rect).topLeft();
+    painter->drawPixmap(p,pixmap);
+}
+
+void QtItemDelegate::drawFocus(QPainter* painter, const QStyleOptionViewItem& opt,
+    const QRect& rect) const
+{
+    if (!m_drawFocus)
+	return;
+    QItemDelegate::drawFocus(painter,opt,rect);
+}
+
+QWidget* QtItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option,
+    const QModelIndex& index) const
+{
+    if (m_editableCols.size() && !m_editableCols.contains(index.column()))
+	return 0;
+    return QItemDelegate::createEditor(parent,option,index);
+}
+
+// Apply item margins
+void QtItemDelegate::applyMargins(QRect& dest, const QRect& src, bool inc) const
+{
+    if (inc) {
+	dest.setLeft(dest.left() + src.left());
+	dest.setTop(dest.top() + src.top());
+	dest.setRight(dest.right() - src.right());
+	dest.setBottom(dest.bottom() - src.bottom());
+    }
+    else {
+	dest.setLeft(dest.left() - src.left());
+	dest.setTop(dest.top() - src.top());
+	dest.setRight(dest.right() + src.right());
+	dest.setBottom(dest.bottom() + src.bottom());
+    }
+}
+
+
+//
+// QtHtmlItemDelegate
+//
+void QtHtmlItemDelegate::drawDisplay(QPainter* painter, const QStyleOptionViewItem& opt,
+    const QRect& rect, const QString& text) const
+{
+    if (text.isEmpty())
+	return;
+    QTextDocument doc;
+    doc.setHtml(text);
+    QAbstractTextDocumentLayout* layout = doc.documentLayout();
+    if (!layout)
+	return;
+    QAbstractTextDocumentLayout::PaintContext context;
+    painter->save();
+    painter->setClipRect(rect);
+    QSize sz(layout->documentSize().toSize());
+    int y = rect.y();
+    if (sz.height()) {
+	// Align vcenter and bottom (top is the default for document)
+	if (0 != (opt.displayAlignment & Qt::AlignVCenter))
+	    y += (rect.height() - sz.height()) / 2;
+	else if (0 != (opt.displayAlignment & Qt::AlignBottom))
+	    y += rect.height() - sz.height();
+    }
+    painter->translate(rect.x(),y);
+    layout->draw(painter,context);
+    painter->restore();
+}
+
+
+//
+// CustomTreeFactory
+//
+// Build objects
+void* CustomTreeFactory::create(const String& type, const char* name, NamedList* params)
+{
+    if (!params)
+	return 0;
+
+    QWidget* parentWidget = 0;
+    String* wndname = params->getParam("parentwindow");
+    if (!TelEngine::null(wndname)) {
+	String* wName = params->getParam("parentwidget");
+	QtWindow* wnd = static_cast<QtWindow*>(Client::self()->getWindow(*wndname));
+	if (wnd && !TelEngine::null(wName))
+	    parentWidget = wnd->findChild<QWidget*>(QtClient::setUtf8(*wName));
+    }
+    if (type == "ContactList")
+        return new ContactList(name,*params,parentWidget);
+    if (type == "FileListTree")
+        return new FileListTree(name,*params,parentWidget);
+    if (type == "QtCustomTree")
+        return new QtCustomTree(name,*params,parentWidget);
+    return 0;
+}
+
+}; // anonymous namespace
+
+#include "customtree.moc"
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/customtree.h b/modules/qt5/customtree.h
new file mode 100644
index 0000000..c4f6f46
--- /dev/null
+++ b/modules/qt5/customtree.h
@@ -0,0 +1,2466 @@
+/**
+ * customtree.h
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Custom QtTree based objects
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#ifndef __CUSTOMTREE_H
+#define __CUSTOMTREE_H
+
+#include "qt5client.h"
+
+using namespace TelEngine;
+namespace { // anonymous
+
+class QtCellGridDraw;                    // Draw cell grid
+class QtTreeItemProps;                   // Tree widget container item properties
+class QtTreeDrag;                        // Drag data builder
+class QtTreeItem;                        // A tree widget item
+class QtCustomTree;                      // A custom tree widget
+class ContactList;                       // A contact list tree
+class ContactItem;                       // A contact list contact
+class ContactItemList;                   // Groups and contact items belonging to them
+class FileListTree;                      // Specialized tree showing directories and files
+class QtPaintItemDesc;                   // Generic item description (base class)
+class QtPaintButtonDesc;                 // Button description
+class QtPaintItem;                       // Custom painted item
+class QtPaintButton;                     // Custom painted button
+class QtPaintItems;                      // Holds items to paint
+class QtPaintImages;                     // Holds images to paint
+class QtItemDelegate;                    // Custom item delegate
+class QtHtmlItemDelegate;                // Custom HTML item delegate
+
+typedef QList<QTreeWidgetItem*> QtTreeItemList;
+typedef QPair<QTreeWidgetItem*,QString> QtTreeItemKey;
+typedef QPair<String,int> QtTokenDict;
+
+
+/**
+ * Utility used to draw a cell grid (borders)
+ * @short Draw cell grid (borders)
+ */
+class QtCellGridDraw
+{
+public:
+    /**
+     * Position and flags enumeration
+     */
+    enum Position {
+	None = 0,
+	Left = 1,
+	Top = 2,
+	Right = 4,
+	Bottom = 8,
+	DrawStart = 16,
+	DrawEnd = 32,
+	// Masks
+	Pos = Left | Top | Right | Bottom,
+    };
+
+    /**
+     * Constructor
+     * @param flags Optional flags
+     */
+    explicit inline QtCellGridDraw(int flags = 0)
+	: m_flags(flags)
+	{}
+
+    /**
+     * Retrieve specific flags if set
+     * @param val Flags to retrieve
+     * @return Draw flags masked with given value
+     */
+    inline int flag(int val) const
+	{ return (m_flags & val); }
+
+    /**
+     * Set draw pen
+     * @param pos Position to set pen (Left, Right, Top or Bottom)
+     * @param pen Pen to set
+     */
+    void setPen(Position pos, QPen pen);
+
+    /**
+     * Set draw pens from a list of parameters
+     * @param params Parameter list
+     */
+    void setPen(const NamedList& params);
+
+    /**
+     * Set pen from parameters list
+     * @param pos Position to set
+     * @param params Parameter list
+     */
+    void setPen(Position pos, const NamedList& params);
+
+    /**
+     * Draw the borders
+     * @param p The painter to use
+     * @param rect Cell rectangle
+     * @param isFirstRow True if drawing the first row
+     * @param isFirstColumn True if drawing the first column
+     * @param isLastRow True if drawing the last row
+     * @param isLastColumn True if drawing the last column
+     */
+    void draw(QPainter* p, QRect& rect, bool isFirstRow, bool isFirstColumn,
+	bool isLastRow, bool isLastColumn) const;
+
+protected:
+    int m_flags;
+    QPen m_left;
+    QPen m_top;
+    QPen m_right;
+    QPen m_bottom;
+};
+
+
+/**
+ * This class holds data about a tree widget container item
+ * @short Tree widget container item properties
+ */
+class QtTreeItemProps : public QtUIWidgetItemProps
+{
+    YCLASS(QtTreeItemProps,QtUIWidgetItemProps)
+public:
+    /**
+     * Constructor
+     * @param type Item type
+     */
+    explicit inline QtTreeItemProps(const String& type)
+	: QtUIWidgetItemProps(type),
+	m_height(-1), m_editable(false)
+	{}
+
+    /**
+     * Set a button's action, create if it not found
+     * @param name Button name
+     * @param action Button action
+     * @return True on success, false if 'name' was found but is not a button
+     */
+    bool setPaintButtonAction(const String& name, const String& action);
+
+    /**
+     * Set a button's parameter, create it if not found
+     * @param name Button name
+     * @param param Parameter to set
+     * @param value Parameter value
+     * @return True on success, false if 'name' was found but is not a button
+     */
+    bool setPaintButtonParam(const String& name, const String& param,
+	const String& value = String::empty());
+
+    int m_height;                        // Item height
+    String m_stateWidget;                // Item widget or column showing the state
+    String m_stateExpandedImg;           // Image to show when expanded
+    String m_stateCollapsedImg;          // Image to show when collapsed
+    String m_toolTip;                    // Tooltip template
+    String m_statsWidget;                // Item widget showing statistics while collapsed
+    String m_statsTemplate;              // Statistics template (may include ${count} for children count)
+    QBrush m_bg;                         // Item background
+    QRect m_margins;                     // Item internal margins
+    bool m_editable;                     // Item is editable
+    ObjList m_paintItemsDesc;            // Paint items description
+};
+
+
+/**
+ * This class holds data used to build tree drag data
+ * @short Drag data builder
+ */
+class QtTreeDrag : public QObject, public GenObject
+{
+    YCLASS(QtTreeDrag,GenObject)
+    Q_CLASSINFO("QtTreeDrag","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param parent Object parent
+     * @param params Optional parameters
+     */
+    QtTreeDrag(QObject* parent, const NamedList* params = 0);
+
+    /**
+     * Set the URL builder, set to NULL if fmt is empty
+     * @param format Format to use when building base URL
+     * @param queryParams Query params to add to URL
+     */
+    void setUrlBuilder(const String& fmt = String::empty(),
+	const String& queryParams = String::empty());
+
+    /**
+     * Build MIME data for a list of items
+     * @param item The list
+     * @return QMimeData pointer, 0 on failure
+     */
+    QMimeData* mimeData(const QList<QTreeWidgetItem*> items) const;
+
+protected:
+    QtUrlBuilder* m_urlBuilder;
+};
+
+
+/**
+ * This class holds a custom tree widget item
+ * @short A tree widget item
+ */
+class QtTreeItem : public QTreeWidgetItem, public NamedList
+{
+    YCLASS(QtTreeItem,NamedList)
+public:
+    /**
+     * Constructor
+     * @param id Item id
+     * @param type Item type
+     * @param text Optional text for item column 0
+     * @param storeExp Set it to true to (re)store item expanded state
+     */
+    QtTreeItem(const char* id, int type = Type,	const char* text = 0, bool storeExp = false);
+
+    /**
+     * Destructor
+     */
+    ~QtTreeItem();
+
+    /**
+     * Set a column's text from a list of parameter cname
+     * @param col Column to set
+     * @param cname Column name
+     * @param list The list containing the parameter
+     */
+    inline void setText(int col, const String& cname, const NamedList& list) {
+	    String* s = cname ? list.getParam(cname) : 0;
+	    if (s)
+		QTreeWidgetItem::setText(col,QtClient::setUtf8(*s));
+	}
+
+    /**
+     * Set a column's icon from a list of parameter cname_image
+     * @param col Column to set
+     * @param cname Column name
+     * @param list The list containing the parameter
+     * @param role Set image file path in this role if greater then Qt::UserRole
+     */
+    void setImage(int col, const String& cname, const NamedList& list,
+	int role = Qt::UserRole);
+
+    /**
+     * Set a column's check state from boolean value
+     * @param col Column to set
+     * @param check Check state
+     */
+    inline void setCheckState(int col, bool check)
+	{ QTreeWidgetItem::setCheckState(col,check ? Qt::Checked : Qt::Unchecked); }
+
+    /**
+     * Set a column's check state from a list of parameter check:cname
+     * @param col Column to set
+     * @param cname Column name
+     * @param list The list containing the parameter
+     */
+    inline void setCheckState(int col, const String& cname, const NamedList& list) {
+	    String* s = cname ? list.getParam("check:" + cname) : 0;
+	    if (s)
+		setCheckState(col,s->toBoolean());
+	}
+
+    /**
+     * Retrieve the item id
+     * @return Item id
+     */
+    inline const String& id() const
+	{ return toString(); }
+
+    /**
+     * Check if the item is filtered (filter matched)
+     * @return True if the item is filtered
+     */
+    inline bool filterMatched() const
+	{ return m_filtered; }
+
+    /**
+     * Update item filtered flag. Set it to true if the parameter list pointer is 0
+     * @param filter Filter parameter list
+     * @return Filtered value
+     */
+    bool setFilter(const NamedList* filter);
+
+    /**
+     * Retrieve extra data to paint on right side of the item
+     * @return QtPaintItems pointer held by this item (may be 0)
+     */
+    inline QtPaintItems* extraPaintRight() const
+	{ return m_extraPaintRight; }
+
+    /**
+     * Set extra data to paint on right side of the item
+     * @param obj Object to set
+     */
+    void setExtraPaintRight(QtPaintItems* obj = 0);
+
+    /**
+     * Set extra paint buttons on right side of the item
+     * @param list Buttons list
+     * @param props Item props containing the description
+     */
+    void setExtraPaintRightButtons(const String& list, QtTreeItemProps* props);
+
+    /**
+     * Save/restore item expanded status
+     */
+    bool m_storeExp;
+
+    /**
+     * Item height delta from global item size
+     */
+    int m_heightDelta;
+
+protected:
+    bool m_filtered;                     // Item filtered flag
+    QtPaintItems* m_extraPaintRight;     // Extra items to paint on right side
+};
+
+
+/**
+ * This class holds a custom tree widget
+ * @short QT based tree widget
+ */
+class QtCustomTree : public QtTree
+{
+    YCLASS(QtCustomTree,QtTree)
+    Q_CLASSINFO("QtCustomTree","Yate")
+    Q_OBJECT
+    Q_PROPERTY(QStringList _yate_save_props READ saveProps WRITE setSaveProps(QStringList))
+    Q_PROPERTY(bool autoExpand READ autoExpand WRITE setAutoExpand(bool))
+    Q_PROPERTY(int rowHeight READ rowHeight WRITE setRowHeight(int))
+    Q_PROPERTY(bool _yate_horizontalheader READ getHHeader WRITE setHHeader(bool))
+    Q_PROPERTY(bool _yate_notifyitemchanged READ getNotifyItemChanged WRITE setNotifyItemChanged(bool))
+    Q_PROPERTY(QString _yate_itemui READ itemUi WRITE setItemUi(QString))
+    Q_PROPERTY(QString _yate_itemstyle READ itemStyle WRITE setItemStyle(QString))
+    Q_PROPERTY(QString _yate_itemselectedstyle READ itemSelectedStyle WRITE setItemSelectedStyle(QString))
+    Q_PROPERTY(QString _yate_itemacceptdrop READ itemAcceptDrop WRITE setItemAcceptDrop(QString))
+    Q_PROPERTY(QString _yate_itemacceptdroponempty READ itemAcceptDropOnEmpty WRITE setItemAcceptDropOnEmpty(QString))
+    Q_PROPERTY(QString _yate_itemstatewidget READ itemStateWidget WRITE setItemStateWidget(QString))
+    Q_PROPERTY(QString _yate_itemexpandedimage READ itemExpandedImage WRITE setExpandedImage(QString))
+    Q_PROPERTY(QString _yate_itemcollapsedimage READ itemCollapsedImage WRITE setItemCollapsedImage(QString))
+    Q_PROPERTY(QString _yate_itemtooltip READ itemTooltip WRITE setItemTooltip(QString))
+    Q_PROPERTY(QString _yate_itemstatswidget READ itemStatsWidget WRITE setItemStatsWidget(QString))
+    Q_PROPERTY(QString _yate_itemstatstemplate READ itemStatsTemplate WRITE setItemStatsTemplate(QString))
+    Q_PROPERTY(QString _yate_itemheight READ itemHeight WRITE setItemHeight(QString))
+    Q_PROPERTY(QString _yate_itembackground READ itemBg WRITE setItemBg(QString))
+    Q_PROPERTY(QString _yate_itemmargins READ itemMargins WRITE setItemMargins(QString))
+    Q_PROPERTY(QString _yate_itemeditable READ itemEditable WRITE setItemEditable(QString))
+    Q_PROPERTY(QString _yate_itempaintbutton READ itemPaintButton WRITE setItemPaintButton(QString))
+    Q_PROPERTY(QString _yate_itempaintbuttonparam READ itemPaintButtonParam WRITE setItemPaintButtonParam(QString))
+    Q_PROPERTY(QString _yate_col_widths READ colWidths WRITE setColWidths(QString))
+    Q_PROPERTY(QString _yate_sorting READ sorting WRITE setSorting(QString))
+    Q_PROPERTY(QString _yate_itemsexpstatus READ itemsExpStatus WRITE setItemsExpStatus(QString))
+public:
+    /**
+     * List item type enumeration
+     */
+    enum ItemType {
+	TypeCount = QTreeWidgetItem::UserType
+    };
+
+    /**
+     * List item data role
+     */
+    enum ItemDataRole {
+	RoleId = Qt::UserRole + 1,       // Item id (used in headers)
+	RoleCheckable,                   // Column checkable (used in headers)
+	RoleHtmlDelegate,                // Headers: true if a column has a custom html item delegate
+	                                 // Rows: QStringList with data
+	RoleImage,                       // Role containing item image file name
+	RoleBackground,                  // Role containing item background color
+	RoleMargins,                     // Role containing item internal margins
+	RoleQtDrawItems,                 // Role containing extra display data (QObject descendent)
+	RoleCount
+    };
+
+    /**
+     * Constructor
+     * @param name Object name
+     * @param params Parameters
+     * @param parent Optional parent
+     * @param applyParams Apply parameters (call setParams())
+     */
+    QtCustomTree(const char* name, const NamedList& params, QWidget* parent = 0,
+	bool applyParams = true);
+
+    /**
+     * Destructor
+     */
+    virtual ~QtCustomTree();
+
+    /**
+     * Method re-implemented from QTreeWidget.
+     * Draw item grid if set
+     */
+    virtual void drawRow(QPainter* p, const QStyleOptionViewItem& opt,
+	const QModelIndex& idx) const;
+
+    /**
+     * Retrieve item type definition from [type:]value. Create it if not found
+     * @param in Input string
+     * @param value Item property value
+     * @return QtUIWidgetItemProps pointer or 0
+     */
+    virtual QtUIWidgetItemProps* getItemProps(QString& in, String& value);
+
+    /**
+     * Set object parameters
+     * @param params Parameters list
+     * @return True on success
+     */
+    virtual bool setParams(const NamedList& params);
+
+    /**
+     * Retrieve an item
+     * @param item Item id
+     * @param data Item parameters to fill
+     * @return True on success
+     */
+    virtual bool getTableRow(const String& item, NamedList* data = 0);
+
+    /**
+     * Update an existing item
+     * @param item Item id
+     * @param data Item parameters
+     * @return True on success
+     */
+    virtual bool setTableRow(const String& item, const NamedList* data);
+
+    /**
+     * Add a new entry (account or contact) to the tree
+     * @param item Item id
+     * @param data Item parameters
+     * @param asStart True if the entry is to be inserted at the start of
+     *   the table, false if it is to be appended
+     * @return True if the entry has been added, false otherwise
+     */
+    virtual bool addTableRow(const String& item, const NamedList* data = 0,
+	bool atStart = false);
+
+    /**
+     * Remove an item from tree
+     * @param item Item id
+     * @return True on success
+     */
+    virtual bool delTableRow(const String& item);
+
+    /**
+     * Add, set or remove one or more items.
+     * Screen update is locked while changing the tree.
+     * Each data list element is a NamedPointer carrying a NamedList with item parameters.
+     * The name of an element is the item to update.
+     * Set element's value to boolean value 'true' to add a new item if not found
+     *  or 'false' to set an existing one. Set it to empty string to delete the item
+     * @param data The list of items to add/set/delete
+     * @param atStart True to add new items at start, false to add them to the end
+     * @return True on success
+     */
+    virtual bool updateTableRows(const NamedList* data, bool atStart = false);
+
+    /**
+     * Set the widget's selection
+     * @param item String containing the new selection
+     * @return True if the operation was successfull
+     */
+    virtual bool setSelect(const String& item);
+
+    /**
+     * Retrieve the current selection
+     * @param item String to fill with selected item id
+     * @return True on success
+     */
+    virtual bool getSelect(String& item);
+
+    /**
+     * Retrieve multiple selection
+     * @param items List to be to filled with selection's contents
+     * @return True if the operation was successfull
+     */
+    virtual bool getSelect(NamedList& items);
+
+    /**
+     * Remove all items from tree
+     * @return True
+     */
+    virtual bool clearTable();
+
+    /**
+     * Retrieve all items' id
+     * @param items List to fill with widget's items
+     * @return True
+     */
+    virtual bool getOptions(NamedList& items);
+
+    /**
+     * Retrieve a QObject list containing tree item widgets
+     * @return The list of container item widgets
+     */
+    virtual QList<QObject*> getContainerItems();
+
+    /**
+     * Retrieve model index for a given item
+     * @param item Item to edit
+     * @param what Optional sub-item
+     * @return Model index for the item, can be invalid
+     */
+     virtual QModelIndex modelIndex(const String& item, const String* what = 0);
+
+    /**
+     * Update a tree item
+     * @param item Item to update
+     * @param params Item parameters
+     * @return True on success
+     */
+    virtual bool updateItem(QtTreeItem& item, const NamedList& params);
+
+    /**
+     * Find a tree item
+     * @param id Item id
+     * @param start Optional start item. Set it to 0 to start with root item
+     * @param includeStart Include start item in id check.
+     *  Set it to false to check start children only
+     * @param recursive True to make a recursive search,
+     *  false to check only start first level children
+     * @return QTreeItem pointer or 0
+     */
+    virtual QtTreeItem* find(const String& id, QtTreeItem* start = 0,
+	bool includeStart = true, bool recursive = true);
+
+    /**
+     * Find all tree items
+     * @param recursive True to make a recursive search, false to add only direct children
+     * @param parent Optional parent item. Set it to 0 to use the root item
+     * @return The list of items
+     */
+     QList<QtTreeItem*> findItems(bool recursive = true, QtTreeItem* parent = 0);
+
+    /**
+     * Find all tree items having a given id
+     * @param id Item id
+     * @param start Optional start item. Set it to 0 to start with root item
+     * @param includeStart Include start item in id check.
+     *  Set it to false to check start children only
+     * @param recursive True to make a recursive search,
+     *  false to check only start first level children
+     * @return The list of items
+     */
+     QList<QtTreeItem*> findItems(const String& id, QtTreeItem* start = 0,
+	bool includeStart = true, bool recursive = true);
+
+    /**
+     * Find all tree items having a given type
+     * @param id Item type
+     * @param start Optional start item. Set it to 0 to start with root item
+     * @param includeStart Include start item in id check.
+     *  Set it to false to check start children only
+     * @param recursive True to make a recursive search,
+     *  false to check only start first level children
+     * @return The list of items
+     */
+     QList<QtTreeItem*> findItems(int type, QtTreeItem* start = 0,
+	bool includeStart = true, bool recursive = true);
+
+    /**
+     * Find all tree items from model
+     * @param list Model index list
+     * @return The list of items
+     */
+     QList<QtTreeItem*> findItems(QModelIndexList list);
+
+    /**
+     * Find all tree items
+     * @param list List to fill
+     * @param start Optional start item. Set it to 0 to start with root item
+     * @param includeStart Include start item in id check.
+     *  Set it to false to check start children only
+     * @param recursive True to make a recursive search,
+     *  false to check only start first level children
+     */
+     void findItems(NamedList& list, QtTreeItem* start = 0,
+	bool includeStart = true, bool recursive = true);
+
+    /**
+     * Add a child to a given item
+     * @param child Child to add
+     * @param pos Position to insert. Negative to add after the last child
+     * @param parent The parent item. Set it to 0 to add to the root
+     * @return QtTreeItem pointer on failure, 0 on success
+     */
+    QtTreeItem* addChild(QtTreeItem* child, int pos = -1, QtTreeItem* parent = 0);
+
+    /**
+     * Add a child to a given item
+     * @param child Child to add
+     * @param atStart True to insert at start, false to add aftr the last child
+     * @param parent The parent item. Set it to 0 to add to the root
+     * @return QtTreeItem pointer on failure, 0 on success
+     */
+    inline QtTreeItem* addChild(QtTreeItem* child, bool atStart, QtTreeItem* parent = 0)
+	{ return addChild(child,atStart ? 0 : -1,parent); }
+
+    /**
+     * Add a list of children to a given item
+     * @param list Children to add
+     * @param pos Position to insert. Negative to add after the last child
+     * @param parent The parent item. Set it to 0 to add to the root
+     */
+    void addChildren(QList<QTreeWidgetItem*> list, int pos = -1, QtTreeItem* parent = 0);
+
+    /**
+     * Setup an item. Load its widget if not found
+     * @param item Item to setup
+     */
+    void setupItem(QtTreeItem* item);
+
+    /**
+     * Retrieve and item's row height by type
+     * @param item Item to set
+     * @return Item row height
+     */
+    inline int getItemRowHeight(int type) const {
+	    QtTreeItemProps* p = treeItemProps(type);
+	    return (p && p->m_height > 0) ? p->m_height : m_rowHeight;
+	}
+
+    /**
+     * Set and item's row height hint
+     * @param item Item to set
+     */
+    void setItemRowHeight(QTreeWidgetItem* item);
+
+    /**
+     * Retrieve item properties associated with a given type
+     * @param type Item type
+     * @return QtTreeItemProps poinetr or 0 if not found
+     */
+    inline QtTreeItemProps* treeItemProps(int type) const {
+	    QtUIWidgetItemProps* pt = QtUIWidget::getItemProps(itemPropsName(type));
+	    return YOBJECT(QtTreeItemProps,pt);
+	}
+
+    /**
+     * Retrieve item properties associated with a given item
+     * @param item Item address
+     * @return QtTreeItemProps poinetr or 0 if not found
+     */
+    inline QtTreeItemProps* treeItemProps(QtTreeItem& item) const
+	{ return treeItemProps(item.type()); }
+
+    /**
+     * Retrieve item properties associated with a given item
+     * @param item Item pointer
+     * @return QtTreeItemProps poinetr or 0 if not found
+     */
+    inline QtTreeItemProps* treeItemProps(QtTreeItem* item) const
+	{ return item ? treeItemProps(item->type()) : 0; }
+
+    /**
+     * Retrieve string data associated with a column
+     * @param buf Destination string
+     * @param item The tree item whose data to retreive
+     * @param column Column to retrieve
+     * @param role Data role to retrieve, defaults to id
+     */
+    inline void getItemData(String& buf, QTreeWidgetItem& item, int column,
+	int role = RoleId)
+	{ QtClient::getUtf8(buf,item.data(column,role).toString()); }
+
+    /**
+     * Retrieve boolean data associated with a column
+     * @param column Column to retrieve
+     * @param role Data role to retrieve
+     * @param item Optional item, use tree header item if 0
+     * @return The boolean value for the given column and role
+     */
+    inline bool getBoolItemData(int column, int role, QTreeWidgetItem* item = 0) {
+	    if (!item)
+		item = headerItem();
+	    return item && item->data(column,role).toBool();
+	}
+
+    /**
+     * Retrieve a list with column IDs
+     * @return QStringList containing column IDs
+     */
+    QStringList columnIDs();
+
+    /**
+     * Retrieve a column id by column number
+     * @param buf Destination buffer
+     * @param col column number
+     * @return True if found
+     */
+    bool getColumnName(String& buf, int col);
+
+    /**
+     * Retrieve a column by it's id
+     * @param id The column id to find
+     * @return Column number, -1 if not found
+     */
+    int getColumn(const String& id);
+
+    /**
+     * Convert a value to int, retrieve a column index
+     * @param str Column number or name
+     * @return Column number, -1 if not found
+     */
+    inline int getColumnNo(const String& str) {
+	    int val = str.toInteger(-1);
+	    return val >= 0 ? val : getColumn(str);
+	}
+
+    /**
+     * Show or hide an item
+     * @param item The item
+     * @param show True to show, false to hide
+     */
+    inline void showItem(QtTreeItem& item, bool show) {
+	    if (item.isHidden() != show)
+		return;
+	    item.setHidden(!show);
+	    itemVisibleChanged(item);
+	}
+
+    /**
+     * Show or hide empty children.
+     * An empty item is an item without children or with all children hidden
+     * @param show True to show, false to hide
+     * @param parent The parent item. Set it to 0 to add to the root
+     */
+    void showEmptyChildren(bool show, QtTreeItem* parent = 0);
+
+    /**
+     * Set the expanded/collapsed image of an item
+     * @param item The item to set
+     * @param props Optional pointer to item props, detect it if 0
+     */
+    void setStateImage(QtTreeItem& item, QtTreeItemProps* props = 0);
+
+    /**
+     * Retrieve the auto expand property
+     * @return The value of the auto expand property
+     */
+    bool autoExpand()
+	{ return m_autoExpand; }
+
+    /**
+     * Set the auto expand property
+     * @param autoExpand The new value of the auto expand property
+     */
+    void setAutoExpand(bool autoExpand)
+	{ m_autoExpand = autoExpand; }
+
+    /**
+     * Retrieve the row height
+     * @return The row height
+     */
+    int rowHeight()
+	{ return m_rowHeight; }
+
+    /**
+     * Set the row height
+     * @param h The new value of the row height
+     */
+    void setRowHeight(int h)
+	{ m_rowHeight = h; }
+
+    /**
+     * Check if the horizontal header is visible
+     * @return True if the horizontal is visible
+     */
+    bool getHHeader() {
+	    QTreeWidgetItem* h = headerItem();
+	    return h && !h->isHidden();
+	}
+
+    /**
+     * Show/hide the horizontal header
+     * @param on True to show the horizontal header, false to hide it
+     */
+    void setHHeader(bool on) {
+	    QTreeWidgetItem* h = headerItem();
+	    if (h)
+		h->setHidden(!on);
+	}
+
+    /**
+     * Check if this table is notifying item changed
+     * @return True if this table is notifying item changed
+     */
+    bool getNotifyItemChanged()
+	{ return m_notifyItemChanged; }
+
+    /**
+     * Set/reset item changed notification flag
+     * @param on True to notify item changes, false to disable the notification
+     */
+    void setNotifyItemChanged(bool on)
+	{ m_notifyItemChanged = on; }
+
+    /**
+     * Read _yate_itemui property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemUi()
+	{ return QString(); }
+
+    /**
+     * Set an item props ui
+     * @param value Item props ui. Format [type:]ui_name
+     */
+    void setItemUi(QString value);
+
+    /**
+     * Read _yate_itemstyle property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemStyle()
+	{ return QString(); }
+
+    /**
+     * Set an item props style sheet
+     * @param value Item props style sheet. Format [type:]stylesheet
+     */
+    void setItemStyle(QString value);
+
+    /**
+     * Read _yate_itemselectedstyle property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemSelectedStyle()
+	{ return QString(); }
+
+    /**
+     * Set an item props selected style sheet
+     * @param value Item props selected style sheet. Format [type:]stylesheet
+     */
+    void setItemSelectedStyle(QString value);
+
+    /**
+     * Read _yate_itemacceptdrop property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemAcceptDrop()
+	{ return QString(); }
+
+    /**
+     * Set an item props accept drop
+     * @param value Item props accept drop. Format [type:]acceptdrop
+     */
+    void setItemAcceptDrop(QString value);
+
+    /**
+     * Read _yate_itemacceptdroponempty property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemAcceptDropOnEmpty()
+	{ return QString(); }
+
+    /**
+     * Set accept drop on empty space
+     * @param value Accept drop on empty space
+     */
+    void setItemAcceptDropOnEmpty(QString value) {
+	    String tmp;
+	    QtClient::getUtf8(tmp,value);
+	    m_acceptDropOnEmpty = QtDrop::acceptDropType(tmp,QtDrop::None);
+	}
+
+    /**
+     * Read _yate_itemstatewidget property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemStateWidget()
+	{ return QString(); }
+
+    /**
+     * Set an item props state widget name
+     * @param value Item props state widget name. Format [type:]widgetname
+     */
+    void setItemStateWidget(QString value);
+
+    /**
+     * Read _yate_itemexpandedimage property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemExpandedImage()
+	{ return QString(); }
+
+    /**
+     * Set an item's expanded image
+     * @param value Item props expanded image. Format [type:]imagefile
+     */
+    void setExpandedImage(QString value);
+
+    /**
+     * Read _yate_itemcollapsedimage property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemCollapsedImage()
+	{ return QString(); }
+
+    /**
+     * Set an item's collapsed image
+     * @param value Item props collapsed image. Format [type:]imagefile
+     */
+    void setItemCollapsedImage(QString value);
+
+    /**
+     * Read _yate_itemtooltip property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemTooltip()
+	{ return QString(); }
+
+    /**
+     * Set an item's tooltip template
+     * @param value Item props tooltip template. Format [type:]imagefile
+     */
+    void setItemTooltip(QString value);
+
+    /**
+     * Read _yate_itemstatswidget property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemStatsWidget()
+	{ return QString(); }
+
+    /**
+     * Set an item's statistics widget name
+     * @param value Item props statistics widget name. Format [type:]widget_name
+     */
+    void setItemStatsWidget(QString value);
+
+    /**
+     * Read _yate_itemstatstemplate property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemStatsTemplate()
+	{ return QString(); }
+
+    /**
+     * Set an item's statistics template
+     * @param value Item props statistics template. Format [type:]template
+     */
+    void setItemStatsTemplate(QString value);
+
+    /**
+     * Read _yate_itemheight property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemHeight()
+	{ return QString(); }
+
+    /**
+     * Set an item props height
+     * @param value Item props height. Format [type:]height
+     */
+    void setItemHeight(QString value);
+
+    /**
+     * Read _yate_itembackground property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemBg()
+	{ return QString(); }
+
+    /**
+     * Set an item props background
+     * @param value Item props background. Format [type:]background
+     */
+    void setItemBg(QString value);
+
+    /**
+     * Read _yate_itemmargins property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemMargins()
+	{ return QString(); }
+
+    /**
+     * Set an item props margins
+     * @param value Item props margins. Format [type:]margins where
+     *  margins is a comma separated list of item internal margins left,top,right,bottom
+     */
+    void setItemMargins(QString value);
+
+    /**
+     * Read _yate_itemeditable property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemEditable()
+	{ return QString(); }
+
+    /**
+     * Set an item props editable
+     * @param value Item props margins. Format [type:]editable
+     */
+    void setItemEditable(QString value);
+
+    /**
+     * Read _yate_itempaintbutton property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemPaintButton()
+	{ return QString(); }
+
+    /**
+     * Set an item's paint button and action
+     * @param value Item paint button action. Format [type:][button_name:]action_name
+     */
+    void setItemPaintButton(QString value);
+
+    /**
+     * Read _yate_itempaintbuttonparam property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemPaintButtonParam()
+	{ return QString(); }
+
+    /**
+     * Set an item's paint button parameter
+     * @param value Item paint button parameter. Format [type:]button_name:param_name[:param_value]
+     */
+    void setItemPaintButtonParam(QString value);
+
+    /**
+     * Retrieve a comma separated list with column widths
+     * @return Comma separated list containing column widths
+     */
+    QString colWidths();
+
+    /**
+     * Set column widths
+     * @param witdhs Comma separated list containing column widths
+     */
+    void setColWidths(QString widths);
+
+    /**
+     * Retrieve tree sorting string (column and order)
+     * @return Sorting string
+     */
+    QString sorting()
+	{ return getSorting(); }
+
+    /**
+     * Set sorting (column and order)
+     * @param s Sorting string
+     */
+    void setSorting(QString s);
+
+    /**
+     * Retrieve items expanded status value
+     * @return Items expanded status value
+     */
+    QString itemsExpStatus();
+
+    /**
+     * Set items expanded status value
+     * param s Items expanded status value
+     */
+    void setItemsExpStatus(QString s);
+
+    /**
+     * Add items as list parameter
+     * @param Parameter list
+     * *param items Items list
+     */
+    static void addItems(NamedList& dest, QList<QTreeWidgetItem*> items);
+
+protected slots:
+    /**
+     * Handle item children actions
+     */
+    void itemChildAction()
+	{ onAction(sender()); }
+
+    /**
+     * Handle item children toggles
+     */
+    void itemChildToggle(bool on)
+	{ onToggle(sender(),on); }
+
+    /**
+     * Handle item children select
+     */
+    void itemChildSelect()
+	{ onSelect(sender()); }
+
+    /**
+     * Catch item double click
+     * @param item The item
+     * @param column Clicked column
+     */
+    void itemDoubleClickedSlot(QTreeWidgetItem* item, int column)
+	{ onItemDoubleClicked(static_cast<QtTreeItem*>(item),column); }
+
+    /**
+     * Catch item expanded signal
+     * @param item The item
+     */
+    void itemExpandedSlot(QTreeWidgetItem* item)
+	{ onItemExpandedChanged(static_cast<QtTreeItem*>(item)); }
+
+    /**
+     * Catch item collapsed signal
+     * @param item The item
+     */
+    void itemCollapsedSlot(QTreeWidgetItem* item)
+	{ onItemExpandedChanged(static_cast<QtTreeItem*>(item)); }
+
+    /**
+     * Catch item changed signal
+     */
+    void itemChangedSlot(QTreeWidgetItem* item, int column)
+	{ onItemChanged(static_cast<QtTreeItem*>(item),column); }
+
+    /**
+     * Catch item selection changed signal
+     */
+    void itemSelChangedSlot();
+
+protected:
+    /**
+     * Re-implemented from QTreeWidget
+     */
+    virtual void timerEvent(QTimerEvent* e);
+
+    /**
+     * Re-implemented from QTreeWidget
+     */
+    virtual void drawBranches(QPainter* painter, const QRect& rect,
+	const QModelIndex& index) const;
+
+    /**
+     * Re-implemented from QTreeWidget
+     */
+    virtual QMimeData* mimeData(const QList<QTreeWidgetItem*> items) const;
+
+    /**
+     * Re-implemented from QAbstractItemView
+     */
+    virtual void selectionChanged(const QItemSelection& selected,
+	const QItemSelection& deselected);
+
+    /**
+     * Re-implemented from QAbstractItemView
+     */
+    virtual void currentChanged(const QModelIndex& current, const QModelIndex& previous);
+
+    /**
+     * Re-implemented from QWidget
+     */
+    virtual void dragEnterEvent(QDragEnterEvent* e);
+
+    /**
+     * Re-implemented from QWidget
+     */
+    virtual void dropEvent(QDropEvent* e);
+
+    /**
+     * Re-implemented from QWidget
+     */
+    virtual void dragMoveEvent(QDragMoveEvent* e);
+
+    /**
+     * Re-implemented from QWidget
+     */
+    virtual void dragLeaveEvent(QDragLeaveEvent* e);
+
+    /**
+     * Re-implemented from QWidget
+     */
+    virtual void mouseMoveEvent(QMouseEvent* e);
+
+    /**
+     * Re-implemented from QWidget
+     */
+    virtual void mousePressEvent(QMouseEvent* e);
+
+    /**
+     * Re-implemented from QWidget
+     */
+    virtual void mouseReleaseEvent(QMouseEvent* e);
+
+    /**
+     * Re-implemented from QTreeView
+     */
+    virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end);
+
+    /**
+     * Retrieve the item props name associated with tree item type
+     * @param type Item type
+     * @return Item props name or empty if not found
+     */
+    inline const String& itemPropsName(int type) const
+	{ return NamedInt::lookupName(m_itemPropsType,type); }
+
+    /**
+     * Retrieve the item type integer value from associated string (name)
+     * @param name Item type name
+     * @return Associated item type integer value. QTreeWidgetItem::Type if not found
+     */
+    inline int itemType(const String& name) const
+	{ return NamedInt::lookup(m_itemPropsType,name,QTreeWidgetItem::Type); }
+
+    /**
+     * Add item prop to name translation
+     * @param type Item type
+     * @param name Type name
+     */
+    inline void addItemType(int type, const char* name)
+	{ NamedInt::addToListUniqueName(m_itemPropsType,new NamedInt(name,type)); }
+
+    /**
+     * Retrieve tree sorting
+     * @return Sorting string
+     */
+    virtual QString getSorting();
+
+    /**
+     * Set tree sorting
+     * @param key Sorting key
+     * @param sort Sort order
+     */
+    virtual void updateSorting(const String& key, Qt::SortOrder sort);
+
+    /**
+     * Build a tree context menu
+     * @param menu Menu to replace on success
+     * @param ns Pointer to received parameter
+     * @return True on success
+     */
+    bool buildMenu(QMenu*& menu, NamedString* ns);
+
+    /**
+     * Apply item widget style sheet
+     * @param item Target item
+     * @param selected True to apply selected item style
+     */
+    void applyStyleSheet(QtTreeItem* item, bool selected);
+
+    /**
+     * Process item double click
+     * @param item The item
+     * @param column Clicked column
+     */
+    virtual void onItemDoubleClicked(QtTreeItem* item, int column);
+
+    /**
+     * Item expanded/collapsed notification
+     * @param item The item
+     */
+    virtual void onItemExpandedChanged(QtTreeItem* item);
+
+    /**
+     * Process item changed signal
+     */
+    virtual void onItemChanged(QtTreeItem* item, int column);
+
+    /**
+     * Catch a context menu event and show the context menu
+     * @param e Context menu event
+     */
+    virtual void contextMenuEvent(QContextMenuEvent* e);
+
+    /**
+     * Get the context menu associated with a given item
+     * @param item The item (can be 0)
+     * @return QMenu pointer or 0
+     */
+    virtual QMenu* contextMenu(QtTreeItem* item);
+
+    /**
+     * Item added notification
+     * @param item Added item
+     * @param parent The parent of the added tree item. 0 if added to the root
+     */
+    virtual void itemAdded(QtTreeItem& item, QtTreeItem* parent);
+
+    /**
+     * Handle item visiblity changes
+     * @param item Changed item
+     */
+    virtual void itemVisibleChanged(QtTreeItem& item);
+
+    /**
+     * Check item filter
+     * @param item Optional item. Check root if 0
+     * @param recursive True to check recursive (check children's children also)
+     */
+    void checkItemFilter(QtTreeItem* item = 0, bool recursive = true);
+
+    /**
+     * Handle item filter changes
+     * @param item The item
+     */
+    virtual void itemFilterChanged(QtTreeItem& item);
+
+    /**
+     * Uncheck all checkable columns in a given item
+     * @param item The item
+     */
+    virtual void uncheckItem(QtTreeItem& item);
+
+    /**
+     * Remove an item
+     * @param item Item to remove
+     * @param setSelTimer Optional boolean to be set if select trigger timer should be started,
+     *  set it to 0 to let this method handle the timer
+     */
+    virtual void removeItem(QtTreeItem* item, bool* setSelTimer = 0);
+
+    /**
+     * Remove a list of items
+     * @param items Items to remove
+     */
+    virtual void removeItems(QList<QTreeWidgetItem*> items);
+
+    /**
+     * Update a tree item's tooltip
+     * @param item Item to update
+     * @param props Optional pointer to item props, detect it if 0
+     */
+    virtual void applyItemTooltip(QtTreeItem& item, QtTreeItemProps* props = 0);
+
+    /**
+     * Fill a list with item statistics.
+     * The default implementation fills a 'count' parameter with the number of item children
+     * @param item The tree item
+     * @param list The list to fill
+     */
+    virtual void fillItemStatistics(QtTreeItem& item, NamedList& list);
+
+    /**
+     * Update a tree item's statistics
+     * @param item Item to update
+     * @param props Optional pointer to item props, detect it if 0
+     */
+    void applyItemStatistics(QtTreeItem& item, QtTreeItemProps* props = 0);
+
+    /**
+     * Update a tree item's margins
+     * @param item Item to update
+     * @param set True to set from item props, false to set an empty rect
+     * @param props Optional pointer to item props, detect it if 0
+     */
+    virtual void applyItemMargins(QtTreeItem& item, bool set = true,
+	QtTreeItemProps* props = 0);
+
+    /**
+     * Store (update) to or remove from item expanded status storage an item
+     * @param id Item id
+     * @param on Expanded status
+     * @param store True to store, false to remove
+     */
+    void setStoreExpStatus(const String& id, bool on, bool store = true);
+
+    /**
+     * Retrieve the expanded status of an item from storage
+     * @param id Item id
+     * @return 1 if expanded, 0 if collapsed, -1 if not found
+     */
+    int getStoreExpStatus(const String& id);
+
+    /**
+     * Handle drop events
+     * @param e The event
+     * @return True if accepted
+     */
+    bool handleDropEvent(QDropEvent* e);
+
+    /**
+     * Check if an item has any selected child
+     * @param item The item to check
+     * @return True if it has at least 1 selected child
+     */
+    bool hasSelectedChild(QtTreeItem& item);
+
+    /**
+     * Check if select trigger timer should be started
+     * @param item The item to check
+     * @return True if select trigger timer should be started
+     */
+    inline bool shouldSetSelTimer(QtTreeItem& item)
+	{ return !item.isSelected() && hasSelectedChild(item); }
+
+    /**
+     * Stop select trigger timer
+     */
+    inline void stopSelectTriggerTimer() {
+	    if (!m_timerTriggerSelect)
+		return;
+	    killTimer(m_timerTriggerSelect);
+	    m_timerTriggerSelect = 0;
+	}
+
+    /**
+     * Start select trigger timer
+     */
+    inline void startSelectTriggerTimer() {
+	    stopSelectTriggerTimer();
+	    m_timerTriggerSelect = startTimer(500);
+	}
+
+    bool m_notifyItemChanged;            // Notify 'listitemchanged' action
+    bool m_hasCheckableCols;             // True if we have checkable columns
+    QMenu* m_menu;                       // Tree context menu
+    bool m_autoExpand;                   // Items are expanded when added
+    int m_rowHeight;                     // Tree row height
+    ObjList m_itemPropsType;             // Tree item type to item props translation
+    QList<QtTokenDict> m_expStatus;      // List of stored item IDs and expanded status
+    QtCellGridDraw m_gridDraw;
+    int m_changing;                      // Content is changing from client (not from user):
+                                         //  avoid notifications
+    NamedList* m_filter;                 // Item filter
+    bool m_haveWidgets;                  // True if we loaded any widget
+    bool m_haveDrawQtItems;              // True if we have any custom drawn data in items
+    int m_setCurrentColumn;              // Column to set when current index changed
+    QtListDrop* m_drop;                  // Drop handler
+    int m_acceptDropOnEmpty;             // Accept drop on widget surface not occupied by any item
+    QtTreeDrag* m_drag;                  // Drag data builder
+    bool m_drawBranches;                 // Allow parent to draw branches
+    int m_timerTriggerSelect;            // Trigger select timer id
+    QtTreeItem* m_lastItemDrawHover;     // Last item we used to update custom drawn hover
+};
+
+
+/**
+ * This class holds a contact list tree
+ * @short A contact list tree
+ */
+class ContactList : public QtCustomTree
+{
+    YCLASS(ContactList,QtCustomTree)
+    Q_CLASSINFO("ContactList","Yate")
+    Q_OBJECT
+    Q_PROPERTY(QString _yate_nogroup_caption READ noGroupCaption WRITE setNoGroupCaption(QString))
+    Q_PROPERTY(bool _yate_flatlist READ flatList WRITE setFlatList(bool))
+    Q_PROPERTY(bool _yate_showofflinecontacts READ showOffline WRITE setShowOffline(bool))
+    Q_PROPERTY(bool _yate_hideemptygroups READ hideEmptyGroups WRITE setHideEmptyGroups(bool))
+    Q_PROPERTY(bool _yate_comparecase READ cmpNameCs WRITE setCmpNameCs(bool))
+public:
+    /**
+     * List item type enumeration
+     */
+    enum ItemType {
+	TypeContact = QtCustomTree::TypeCount,
+	TypeChatRoom = QtCustomTree::TypeCount + 1,
+	TypeGroup,
+    };
+
+    /**
+     * Constructor
+     * @param name The name of the object
+     * @param params List parameters
+     * @param parent List parent
+     */
+    ContactList(const char* name, const NamedList& params, QWidget* parent);
+
+    /**
+     * Set list parameters
+     * @param params Parameter list
+     * @return True on success
+     */
+    virtual bool setParams(const NamedList& params);
+
+    /**
+     * Update an existing item
+     * @param item Item id
+     * @param data Item parameters
+     * @return True on success
+     */
+    virtual bool setTableRow(const String& item, const NamedList* data);
+
+    /**
+     * Add a new entry (account or contact) to the tree
+     * @param item Item id
+     * @param data Item parameters
+     * @param asStart True if the entry is to be inserted at the start of
+     *   the table, false if it is to be appended
+     * @return True if the entry has been added, false otherwise
+     */
+    virtual bool addTableRow(const String& item, const NamedList* data = 0,
+	bool atStart = false);
+
+    /**
+     * Remove an item from tree
+     * @param item Item id
+     * @return True on success
+     */
+    virtual bool delTableRow(const String& item);
+
+    /**
+     * Add, set or remove one or more contacts.
+     * Screen update is locked while changing the tree.
+     * Each data list element is a NamedPointer carrying a NamedList with item parameters.
+     * The name of an element is the item to update.
+     * Set element's value to boolean value 'true' to add a new item if not found
+     *  or 'false' to set an existing one. Set it to empty string to delete the item
+     * @param data The list of items to add/set/delete
+     * @param atStart True to add new items at start, false to add them to the end
+     * @return True on success
+     */
+    virtual bool updateTableRows(const NamedList* data, bool atStart = false);
+
+    /**
+     * Count online/total contacts in a group.
+     * @param grp The group item
+     * @param total The number of contacts in the group
+     * @param online The number of online contacts in the group
+     */
+    virtual void countContacts(QtTreeItem* grp, int& total, int& online);
+
+    /**
+     * Contact list changed notification
+     * This method is called each time a contact is added, removed or changed or
+     *  properties affecting display are changed
+     */
+    virtual void listChanged();
+
+    /**
+     * Update a tree item
+     * @param item Item to update
+     * @param params Item parameters
+     * @return True on success
+     */
+    virtual bool updateItem(QtTreeItem& item, const NamedList& params);
+
+    /**
+     * Find a contact
+     * @param id Contact id
+     * @param list Optional list to be filled with items having the given id
+     * @return ContactItem pointer or 0 if not found
+     */
+    ContactItem* findContact(const String& id, QList<QtTreeItem*>* list = 0);
+
+    /**
+     * Retrieve the value of '_yate_nogroup_caption' property
+     * @return The value of '_yate_nogroup_caption' property
+     */
+    QString noGroupCaption()
+	{ return QtClient::setUtf8(m_noGroupText); }
+
+    /**
+     * Set '_yate_nogroup_caption' property
+     * @param value The new value for '_yate_nogroup_caption' property
+     */
+    void setNoGroupCaption(QString value);
+
+    /**
+     * Check if the list is flat
+     * @return True if contacts are not grouped
+     */
+    bool flatList()
+	{ return m_flatList; }
+
+    /**
+     * Set the flat list property
+     * @param flat The new value of the flat list property
+     */
+    void setFlatList(bool flat);
+
+    /**
+     * Check if offline contacts are shown
+     * @return True if the list displaying offline contacts
+     */
+    bool showOffline()
+	{ return m_showOffline; }
+
+    /**
+     * Show or hide offline contacts
+     * @param value True to show, false to hide offline contacts
+     */
+    void setShowOffline(bool value);
+
+    /**
+     * Check if empty groups are hidden
+     * @return True if empty groups are hidden
+     */
+    bool hideEmptyGroups()
+	{ return m_hideEmptyGroups; }
+
+    /**
+     * Show or hide empty groups
+     * @param value True to hide, false to show empty groups
+     */
+    void setHideEmptyGroups(bool value) {
+	    if (m_hideEmptyGroups == value)
+		return;
+	    m_hideEmptyGroups = value;
+	    if (!m_flatList)
+		showEmptyChildren(!m_hideEmptyGroups);
+	}
+
+    /**
+     * Retrieve contact name comparison
+     * @return True if contact names are compared case sensitive
+     */
+    bool cmpNameCs()
+	{ return m_compareNameCs == Qt::CaseSensitive; }
+
+    /**
+     * Set contact name comparison
+     * @return True to compare contact names case sensitive
+     */
+    void setCmpNameCs(bool value)
+	{ m_compareNameCs = (value ? Qt::CaseSensitive : Qt::CaseInsensitive); }
+
+    /**
+     * Check if a given type is a contact or chat room
+     * @param type Type to check
+     * @return True if the type is contact or chat room
+     */
+    static inline bool isContactType(int type)
+	{ return type == TypeContact || type == TypeChatRoom; }
+
+    /**
+     * Get contact type from a string value
+     * @param val The string
+     * @return Contact type value
+     */
+    static inline int contactType(const String& val) {
+	    if (!val || val != "chatroom")
+		return TypeContact;
+	    return TypeChatRoom;
+	}
+
+    /**
+     * Create a group item
+     * @param id Group id
+     * @param name Group name
+     * @param expStat Expanded state (re)store indicator
+     * @return Valid QtTreeItem pointer
+     */
+    static inline QtTreeItem* createGroup(const String& id, const String& name, bool expStat) {
+	    QtTreeItem* g = new QtTreeItem(id,TypeGroup,name,expStat);
+	    g->addParam("name",name);
+	    return g;
+	}
+
+protected:
+    /**
+     * Retrieve tree sorting
+     * @return Sorting string
+     */
+    virtual QString getSorting();
+
+    /**
+     * Set tree sorting
+     * @param key Sorting key
+     * @param sort Sort order
+     */
+    virtual void updateSorting(const String& key, Qt::SortOrder sort);
+
+    /**
+     * Optimized add. Set the whole tree
+     * @param list The list of contacts to set
+     */
+    void setContacts(QList<QTreeWidgetItem*>& list);
+
+    /**
+     * Create a contact
+     * @param id Contact id
+     * @param params Contact parameters
+     * @return ContactItem pointer
+     */
+    ContactItem* createContact(const String& id, const NamedList& params);
+
+    // Update contact count in a group
+    void updateGroupCountContacts(QtTreeItem& item);
+
+    // Add or update a contact
+    bool updateContact(const String& id, const NamedList& params);
+
+    // Update a contact
+    bool updateContact(ContactItem& c, const NamedList& params, bool all = true);
+
+    // Remove a contact from tree
+    bool removeContact(const String& id);
+
+    /**
+     * Get the context menu associated with a given item
+     * @param item The item (can be 0)
+     * @return QMenu pointer or 0
+     */
+    virtual QMenu* contextMenu(QtTreeItem* item);
+
+    /**
+     * Item added notification
+     * @param item Added item
+     * @param parent The parent of the added tree item. 0 if added to the root
+     */
+    virtual void itemAdded(QtTreeItem& item, QtTreeItem* parent);
+
+    /**
+     * Fill a list with item statistics.
+     * The default implementation fills a 'count' parameter with the number of item children
+     * @param item The tree item
+     * @param list The list to fill
+     */
+    virtual void fillItemStatistics(QtTreeItem& item, NamedList& list);
+
+    /**
+     * Update a tree item's margins
+     * @param item Item to update
+     * @param set True to set from item props, false to set an empty rect
+     * @param props Optional pointer to item props, detect it if 0
+     */
+    virtual void applyItemMargins(QtTreeItem& item, bool set = true,
+	QtTreeItemProps* props = 0);
+
+    /**
+     * Retrieve a group item from root or create a new one
+     * @param name Group name or empry to use the empty group
+     * @param create True to create if not found
+     * @return QtTreeItem pointer or 0
+     */
+    QtTreeItem* getGroup(const String& name = String::empty(), bool create = true);
+
+    /**
+     * Add a contact to the list
+     * @param id Contact id
+     * @param params Contact parameters
+     */
+    void addContact(const String& id, const NamedList& params);
+
+    /**
+     * Add a contact to a specified parent
+     * @param c The contact to add
+     * @param grp Optional parent
+     */
+    void addContact(ContactItem* c, QtTreeItem* parent = 0);
+
+    /**
+     * Replace an existing contact. Remove it and add it again
+     * @param c The contact item
+     * @param params Contact parameters
+     */
+    void replaceContact(ContactItem& c, const NamedList& params);
+
+    /**
+     * Create contact structure (groups and lists)
+     * @param c The contact to add
+     * @param cil Contact structure
+     */
+    void createContactTree(ContactItem* c, ContactItemList& cil);
+
+    /**
+     * Compare two contacts's name
+     * @param c1 First contact
+     * @param c2 Second contact
+     * @return -1 if c1 < c2, 0 if c1 == c2, 1 if c1 > c2
+     */
+    int compareContactName(ContactItem* c1, ContactItem* c2);
+
+    /**
+     * Sort contacts
+     * @param list The list of contacts to sort
+     */
+    void sortContacts(QList<QTreeWidgetItem*>& list);
+
+private:
+    int m_savedIndent;
+    bool m_flatList;                     // Flat list
+    bool m_showOffline;                  // Show or hide offline contacts
+    bool m_hideEmptyGroups;              // Show or hide empty groups
+    bool m_expStatusGrp;                 // Save/restore groups expanded status
+    String m_noGroupText;                // Group text to show for contacts not belonging to any group
+    QMap<QString,QString> m_statusOrder; // Status order (names are mapped to status icons)
+    QMenu* m_menuContact;
+    QMenu* m_menuChatRoom;
+    // Sorting
+    String m_sortKey;                    // Sorting key
+    Qt::SortOrder m_sortOrder;           // Sort order
+    Qt::CaseSensitivity m_compareNameCs; // Contact name case comparison
+};
+
+
+/**
+ * This class holds a contact list contact tree item
+ * @short A contact list contact
+ */
+class ContactItem : public QtTreeItem
+{
+    YCLASS(ContactItem,QtTreeItem)
+public:
+    inline ContactItem(const char* id, const NamedList& p = NamedList::empty(),
+	bool contact = true)
+	: QtTreeItem(id,ContactList::contactType(p["type"]))
+	{}
+    // Build and return a list of groups
+    inline ObjList* groups() const
+	{ return Client::splitUnescape((*this)["groups"]); }
+    // Update name. Return true if changed
+    bool updateName(const NamedList& params, Qt::CaseSensitivity cs);
+    // Check if groups would change
+    bool groupsWouldChange(const NamedList& params);
+    // Check if the contact status is 'offline'
+    bool offline();
+
+    QString m_name;
+};
+
+
+/**
+ * Utility class used to hold contact groups along with contacts
+ * @short Groups and contact items belonging to them
+ */
+class ContactItemList
+{
+public:
+    /**
+     * Retrieve a group. Create it if not found. Create contact list entry when a group is created
+     * @param id Group id
+     * @param text Group text
+     * @param expStat Expanded state (re)store indicator for created item
+     * @return Valid groups index
+     */
+    int getGroupIndex(const String& id, const String& text, bool expStat);
+
+    QList<QTreeWidgetItem*> m_groups;
+    QList<QtTreeItemList> m_contacts;
+};
+
+
+/**
+ * File list item description. The String holds the file name
+ * @short A file list item
+ */
+class FileItem : public String
+{
+public:
+    /**
+     * Constructor
+     * @param type File item type
+     * @param name File name
+     * @param path File path
+     * @param prov Optional file icon provider
+     */
+    FileItem(int type, const char* name, const String& path,
+	QFileIconProvider* prov = 0);
+
+    /**
+     * Constructor. Build a FileListTree up directory
+     * @param path The path
+     * @param prov Optional file icon provider
+     */
+    FileItem(const String& path, QFileIconProvider* prov = 0);
+
+    /**
+     * Destructor
+     */
+    ~FileItem();
+
+    int m_type;
+    String m_fullName;
+    QIcon* m_icon;
+};
+
+
+/**
+ * Load local directory content
+ * @short Thread used to load local directory content
+ */
+class DirListThread : public QThread
+{
+    Q_CLASSINFO("DirListThread","Yate")
+    Q_OBJECT
+public:
+    inline DirListThread(QObject* parent, const String& dir, bool dirs = true,
+	bool files = true)
+	: QThread(parent),
+	m_dir(dir), m_error(0), m_listDirs(dirs), m_listUpDir(false),
+	m_listFiles(files), m_iconProvider(0), m_sort(QtClient::SortNone),
+	m_caseSensitive(false)
+	{}
+    virtual void run();
+
+    String m_dir;
+    int m_error;
+    bool m_listDirs;
+    bool m_listUpDir;
+    bool m_listFiles;
+    ObjList m_dirs;
+    ObjList m_files;
+    QFileIconProvider* m_iconProvider;
+    int m_sort;
+    bool m_caseSensitive;
+
+protected:
+    inline ObjList* addItem(int type, const char* name, ObjList& list, ObjList* last) {
+	    FileItem* it = new FileItem(type,name,m_dir,m_iconProvider);
+	    if (m_sort == QtClient::SortNone)
+		return last->append(it);
+	    return addItemSort(list,it);
+	}
+    ObjList* addItemSort(ObjList& list, FileItem* it);
+    // Called when terminated from run()
+    void runTerminated();
+};
+
+
+/**
+ * This class holds a file list tree
+ * @short Specialized tree showing directories and files
+ */
+class FileListTree : public QtCustomTree
+{
+    YCLASS(FileListTree,QtCustomTree)
+    Q_CLASSINFO("FileListTree","Yate")
+    Q_OBJECT
+    Q_PROPERTY(QString _yate_filesystem_path READ fsPath WRITE setFsPath(QString))
+    Q_PROPERTY(QString _yate_refresh READ refresh WRITE setRefresh(QString))
+public:
+    enum FileListPathType {
+	PathNone = 0,
+	PathRoot,
+	PathHome,
+	PathUpThenHome,
+    };
+
+    /**
+     * List item type enumeration
+     */
+    enum ItemType {
+	TypeDir = QtCustomTree::TypeCount,
+	TypeFile = QtCustomTree::TypeCount + 1,
+	TypeDrive = QtCustomTree::TypeCount + 2,
+    };
+
+    /**
+     * Constructor
+     * @param name The name of the object
+     * @param params List parameters
+     * @param parent List parent
+     */
+    FileListTree(const char* name, const NamedList& params, QWidget* parent);
+
+    /**
+     * Destructor
+     */
+    ~FileListTree();
+
+    /**
+     * Retrieve _yate_filesystem_path property
+     */
+    QString fsPath()
+	{ return QtClient::setUtf8(m_fsPath); }
+
+    /**
+     * Set _yate_filesystem_path property
+     */
+    void setFsPath(QString path);
+
+    /**
+     * Read _yate_refresh property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString refresh()
+	{ return QString(); }
+
+    /**
+     * Set _yate_refresh property
+     */
+    void setRefresh(QString val);
+
+    /**
+     * Change file system path, refresh data
+     * @param path New path
+     * @param force Force updating even if path didn't changed
+     */
+    void setFsPath(const String& path = String::empty(), bool force = true);
+
+    /**
+     * Check if current path is home path
+     * @return True if current path is the home one
+     */
+    inline bool isHomePath()
+	{ return fsPath() == QDir::toNativeSeparators(QDir::homePath()); }
+
+    /**
+     * Refresh data
+     * @param dir List of directory children
+     * @param files List of files children
+     * @param drives List of drives children
+     */
+    void refresh(ObjList* dirs, ObjList* files, ObjList* drives = 0);
+
+    /**
+     * Sort a list of items
+     * @param list Items to sort
+     * @param type Item type
+     */
+    virtual void sortItems(QList<QTreeWidgetItem*>& list, int type);
+
+    /**
+     * Retrieve the icon for a given item
+     * @param item The item
+     * @return The item icon
+     */
+    virtual QIcon icon(QtTreeItem& item);
+
+    /**
+     * Retrieve the icon for a given item type
+     * @param type Item type
+     * @param name Item name
+     * @param provider The icon provider
+     * @return The item icon
+     */
+    static QIcon fileIcon(int type, const String& name, QFileIconProvider* provider);
+
+    /**
+     * Build file full name
+     * @param buf Destination buffer
+     * @param path File path
+     * @param name File name
+     */
+    static inline void buildFileFullName(String& buf, const char* path, const char* name) {
+	    buf = path;
+	    if (!isRootPath(buf))
+		buf << Engine::pathSeparator();
+#ifndef _WINDOWS
+	    else if (!buf)
+		buf << Engine::pathSeparator();
+#endif
+	    buf << name;
+	}
+
+    /**
+     * Check if a path root
+     * @param path Path to check
+     * @return True if the given path is root
+     */
+    static inline bool isRootPath(const String& path) {
+#ifdef _WINDOWS
+	    return path.null();
+#else
+	    return path.null() || (path.at(0) == '/' && !path.at(1));
+#endif
+	}
+
+    static const String s_upDir;
+
+protected slots:
+
+    /**
+     * Catch dir list thread terminate signal
+     */
+    void onDirThreadTerminate();
+
+protected:
+    /**
+     * Start/stop dir list thread
+     */
+    bool setDirListThread(bool on);
+
+    /**
+     * Process item double click
+     * @param item The item
+     * @param column Clicked column
+     */
+    virtual void onItemDoubleClicked(QtTreeItem* item, int column);
+
+    /**
+     * Reset the thread
+     */
+    void resetThread();
+
+    bool m_fileSystemList;               // Show file system dir content
+    bool m_autoChangeDir;                // Auto change directory
+    bool m_listFiles;                    // List files in current directory
+    int m_sort;                          // Sort files
+    int m_listOnFailure;                 // What to list when fails current directory
+    QFileIconProvider* m_iconProvider;   // The icon provider
+    String m_nameParam;                  // Item name column
+    String m_fsPath;                     // Current path
+    QThread* m_dirListThread;            // Dir list thread
+};
+
+
+/**
+ * This class implements a generic item description
+ * @short Generic item description (base class)
+ */
+class QtPaintItemDesc : public String
+{
+    YCLASS(QtPaintItemDesc,String)
+public:
+    /**
+     * Constructor
+     * @param name Object name
+     */
+    inline QtPaintItemDesc(const char* name = 0)
+	: String(name)
+	{}
+
+    /**
+     * Get a QtPaintButtonDesc from this object
+     * @return QtPaintButtonDesc pointer or 0
+     */
+    virtual QtPaintButtonDesc* button();
+
+    QSize m_size;
+};
+
+
+/**
+ * This class implements a generic item description
+ * @short Generic item description (base class)
+ */
+class QtPaintButtonDesc : public QtPaintItemDesc
+{
+    YCLASS(QtPaintButtonDesc,QtPaintItemDesc)
+public:
+    /**
+     * Constructor
+     * @param name Object name
+     */
+    inline QtPaintButtonDesc(const char* name = 0)
+	: QtPaintItemDesc(name), m_params(""), m_iconSize(16,16)
+	{ m_size = QSize(16,16); }
+
+    /**
+     * Get a QtPaintButtonDesc from this object
+     * @return QtPaintButtonDesc pointer
+     */
+    virtual QtPaintButtonDesc* button();
+
+    /**
+     * Find a button in a list
+     * @param list List to search in
+     * @param name Button name
+     * @param create True (default) to create if not found
+     * @return QtPaintButtonDesc pointer or 0 if found and not a button
+     */
+    static QtPaintButtonDesc* find(ObjList& list, const String& name,
+	bool create = true);
+
+    NamedList m_params;
+    QSize m_iconSize;
+};
+
+
+/**
+ * This class implements an item to be painted
+ * @short An item to be painted
+ */
+class QtPaintItem : public RefObject
+{
+    YCLASS(QtPaintItem,GenObject)
+public:
+    /**
+     * Constructor
+     * @param name Object name
+     * @param size Object size
+     */
+    inline QtPaintItem(const char* name, QSize size)
+	: m_enabled(true), m_hover(false), m_pressed(false),
+	m_size(size), m_name(name)
+	{}
+
+    /**
+     * Retrieve the item name
+     * @return Item name
+     */
+    inline const String& name() const
+	{ return m_name; }
+
+    /**
+     * Retrieve item pressed state
+     * @return Item pressed state
+     */
+    inline bool pressed() const
+	{ return m_pressed; }
+
+    /**
+     * Retrieve item size
+     * @return Item size
+     */
+    inline const QSize& size() const
+	{ return m_size; }
+
+    /**
+     * Retrieve the rectangle this item is drawn in
+     * @return Item display rectangle
+     */
+    inline const QRect& displayRect() const
+	{ return m_displayRect; }
+
+    /**
+     * Retrieve the item action
+     * @return Item action
+     */
+    inline const String& action() const
+	{ return m_action; }
+
+    /**
+     * Set hover state
+     * @param on Hover state
+     * @return True if hover state changed
+     */
+    virtual bool setHover(bool on);
+
+    /**
+     * Set pressed state
+     * @param on Pressed state
+     * @return True if pressed state changed
+     */
+    virtual bool setPressed(bool on);
+
+    /**
+     * Draw the item
+     * @param painter Painter used to draw
+     * @param rect Rectangle to paint in
+     */
+    virtual void draw(QPainter* painter, const QRect& rect) = 0;
+
+    /**
+     * Retrieve the item name
+     * @return Item name
+     */
+    virtual const String& toString() const;
+
+protected:
+    bool m_enabled;
+    bool m_hover;
+    bool m_pressed;
+    String m_action;
+    QSize m_size;
+    QRect m_displayRect;
+
+private:
+    String m_name;
+};
+
+
+/**
+ * This class implements an item to be painted
+ * @short An item to be painted
+ */
+class QtPaintButton : public QtPaintItem
+{
+    YCLASS(QtPaintButton,QtPaintItem)
+public:
+    /**
+     * Constructor
+     * @param desc Button description
+     */
+    QtPaintButton(QtPaintButtonDesc& desc);
+
+    /**
+     * Load button images
+     * @param params Parameters list
+     */
+    void loadImages(const NamedList& params);
+
+    /**
+     * Set hover state
+     * @param on Hover state
+     * @True if hover state changed
+     */
+    virtual bool setHover(bool on);
+
+    /**
+     * Set pressed state
+     * @param on Pressed state
+     * @return True if pressed state changed
+     */
+    virtual bool setPressed(bool on);
+
+    /**
+     * Draw the item
+     * @param painter Painter used to draw
+     * @param rect Rectangle to paint in
+     */
+    virtual void draw(QPainter* painter, const QRect& rect);
+
+protected:
+    // Load an image, adjust its size
+    bool loadImage(QPixmap& pixmap, const NamedList& params, const String& param);
+    // Update state options
+    void updateOptState();
+
+    QPixmap m_normalImage;
+    QPixmap m_hoverImage;
+    QPixmap m_pressedImage;
+    QPixmap* m_image;
+    QSize m_iconSize;
+    QSize m_iconOffset;                       // Draw icon offset
+};
+
+
+/**
+ * This class implements a list of items to be painted
+ * @short Custom item delegate
+ */
+class QtPaintItems : public QtPaintItem
+{
+    YCLASS(QtPaintItems,QtPaintItem)
+public:
+    /**
+     * Constructor
+     * @param name Object name
+     */
+    inline QtPaintItems(const char* name = 0)
+	: QtPaintItem(name,QSize(0,0)),
+	m_margins(10,0,6,0), m_itemSpace(4),
+	m_lastItemHover(0)
+	{}
+
+    /**
+     * Add an item from description
+     * @param desc Item description
+     */
+    void append(QtPaintItemDesc& desc);
+
+    /**
+     * Calculate area needed to paint.
+     * This method should be called after all items are set
+     */
+    void itemsAdded();
+
+    /**
+     * Set hover. Update item at position
+     * @param pos Position to check
+     * @return True if state changed (needs repaint)
+     */
+    bool setHover(const QPoint& pos);
+
+    /**
+     * Set hover state
+     * @param on Hover state
+     * @return True if hover state changed
+     */
+    virtual bool setHover(bool on);
+
+    /**
+     * Mouse pressed/released. Update item at position
+     * @param on Pressed state
+     * @param pos Position to check
+     * @param action Pointer to action to be set on mouse release
+     * @return True if state changed (needs repaint)
+     */
+    bool mousePressed(bool on, const QPoint& pos, String* action = 0);
+
+    /**
+     * Set pressed state
+     * @param on Pressed state
+     * @return True if pressed state changed
+     */
+    virtual bool setPressed(bool on);
+
+    /**
+     * Draw items
+     * @param painter Painter used to draw
+     * @param rect Rect to paint in
+     */
+    virtual void draw(QPainter* painter, const QRect& rect);
+
+protected:
+    ObjList m_items;
+    QRect m_margins;
+    int m_itemSpace;
+    QtPaintItem* m_lastItemHover;        // Last item we handle mouse hover for
+};
+
+
+/**
+ * This class implements a custom item delegate
+ * @short Custom item delegate
+ */
+class QtItemDelegate : public QItemDelegate, public String
+{
+    YCLASS(QtItemDelegate,String)
+    Q_CLASSINFO("QtItemDelegate","Yate")
+    Q_OBJECT
+public:
+    QtItemDelegate(QObject* parent, const NamedList& params = NamedList::empty());
+    virtual void paint(QPainter* painter, const QStyleOptionViewItem& option,
+	const QModelIndex& index) const;
+    inline QList<int>& columns()
+	{ return m_columns; }
+    inline int roleDisplayText() const
+	{ return m_roleDisplayText; }
+    inline int roleImage() const
+	{ return m_roleImage; }
+    // Update column position from column names.
+    // 'cNames' must be the column names in their order, starting from 0
+    void updateColumns(QStringList& cNames);
+    // Build a list of delegates
+    static QList<QAbstractItemDelegate*> buildDelegates(QObject* parent, const NamedList& params,
+	const NamedList* common = 0, const String& prefix = "itemdelegate");
+    // Build a delegate
+    static QAbstractItemDelegate* build(QObject* parent, const String& cls, NamedList& params);
+protected:
+    // Retrieve display text for a given index
+    virtual QString getDisplayText(const QStyleOptionViewItem& opt,
+	const QModelIndex& index) const;
+    // Inherited methods
+    virtual void drawBackground(QPainter* painter, const QStyleOptionViewItem& opt,
+	const QModelIndex& index) const;
+    virtual void drawDecoration(QPainter* painter, const QStyleOptionViewItem& opt,
+	const QRect& rect, const QPixmap& pixmap) const;
+    virtual void drawFocus(QPainter* painter, const QStyleOptionViewItem& opt,
+	const QRect& rect) const;
+    virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
+	const QModelIndex& index) const;
+    // Apply item margins
+    void applyMargins(QRect& dest, const QRect& src, bool inc) const;
+
+    bool m_drawFocus;                    // Draw focus
+    int m_roleDisplayText;               // Item display role to handle
+    int m_roleImage;                     // Item role containing image file name
+    int m_roleBackground;                // Item background role to handle
+    int m_roleMargins;                   // Item internal margins role to handle
+    int m_roleQtDrawItems;               // Item draw extra role to handle
+    QStringList m_columnsStr;            // Column names this delegate should be set for
+    QStringList m_editableColsStr;       // List of editable column names
+    QList<int> m_editableCols;           // List of editable columns
+    QList<int> m_columns;                // List of editable columns
+};
+
+
+/**
+ * This class implements a custom item delegate used to display HTML texts
+ * @short Custom HTML item delegate
+ */
+class QtHtmlItemDelegate : public QtItemDelegate
+{
+    YCLASS(QtHtmlItemDelegate,QtItemDelegate)
+    Q_CLASSINFO("QtHtmlItemDelegate","Yate")
+    Q_OBJECT
+public:
+    QtHtmlItemDelegate(QObject* parent, const NamedList& params = NamedList::empty())
+	: QtItemDelegate(parent,params)
+	{}
+protected:
+    virtual void drawDisplay(QPainter* painter, const QStyleOptionViewItem& opt,
+	const QRect& rect, const QString& text) const;
+};
+
+}; // anonymous namespace
+
+#endif // __CUSTOMTREE_H
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/updater.cpp b/modules/qt5/updater.cpp
new file mode 100644
index 0000000..9aa6ea0
--- /dev/null
+++ b/modules/qt5/updater.cpp
@@ -0,0 +1,511 @@
+/**
+ * updater.cpp
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Auto updater logic and downloader for Qt-5 clients.
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#include "updater.h"
+
+#include <unistd.h>
+#include <stdio.h>
+
+#include <QUrl>
+#include <QFile>
+#include <QFileInfo>
+#include <QProcess>
+#include <QNetworkAccessManager>
+#include <QNetworkProxy>
+#include <QNetworkReply>
+
+#define MIN_SIZE 1024
+#define MAX_SIZE (16*1024*1024)
+
+#define TMP_EXT ".tmp"
+#ifdef _WINDOWS
+#define EXE_EXT ".exe"
+#else
+#define EXE_EXT ".bin"
+#endif
+
+using namespace TelEngine;
+namespace { // anonymous
+
+/**
+ * UI logic interaction
+ */
+class UpdateLogic : public ClientLogic
+{
+public:
+    enum Policy {
+	Invalid,
+	Never,
+	Check,
+	Download,
+	Install,
+    };
+    inline UpdateLogic(const char* name)
+	: ClientLogic(name,100),
+	  m_policy(Invalid), m_checking(false),
+	  m_checked(false), m_install(false),
+	  m_http(0), m_file(0), m_httpSlots(0), m_canUpdate(true)
+	{ }
+    virtual ~UpdateLogic();
+    inline Policy policy() const
+	{ return static_cast<Policy>(m_policy); }
+    virtual bool initializedClient();
+    virtual void exitingClient();
+    virtual bool action(Window* wnd, const String& name, NamedList* params);
+    virtual bool toggle(Window* wnd, const String& name, bool active);
+    void dataProgress(qint64 done, qint64 total);
+    void requestDone();
+    void endHttp(bool error);
+protected:
+    void setPolicy(int policy, bool save);
+    void startChecking(bool start = true);
+    void finishedChecking();
+    void startDownloading(bool start = true);
+    void finishedDownloading();
+    void startInstalling();
+private:
+    QString filePath(bool temp);
+    bool startHttp(const char* url, const QString& saveAs);
+    void stopHttp();
+    void stopFile();
+    int m_policy;
+    bool m_checking;
+    bool m_checked;
+    bool m_install;
+    String m_url;
+    QNetworkReply* m_http;
+    QFile* m_file;
+    QtUpdateHttp* m_httpSlots;
+    bool m_canUpdate;
+};
+/**
+ * Plugin registration
+ */
+class Updater : public Plugin
+{
+public:
+    Updater();
+    virtual ~Updater();
+    virtual void initialize();
+private:
+    UpdateLogic* m_logic;
+};
+
+static Updater s_plugin;
+
+static const TokenDict s_policies[] = {
+    { "never",    UpdateLogic::Never    },
+    { "check",    UpdateLogic::Check    },
+    { "download", UpdateLogic::Download },
+    { "install",  UpdateLogic::Install  },
+    { 0,          UpdateLogic::Invalid  }
+};
+
+UpdateLogic::~UpdateLogic()
+{
+}
+
+bool UpdateLogic::initializedClient()
+{
+    // Check if the current user can write to install dir
+    // Disable and uncheck all updater UI controls on failure
+    Configuration cfg(Engine::configFile("updater"));
+    m_canUpdate = !File::exists(cfg) || cfg.save();
+    if (!m_canUpdate) {
+	Debug(toString(),DebugInfo,"Disabling updates: the current user can't write to '%s'",
+	    Engine::configPath().c_str());
+	NamedList p("");
+	p.addParam("check:upd_automatic","false");
+	p.addParam("active:upd_automatic","false");
+	p.addParam("active:upd_install","false");
+	p.addParam("active:upd_check","false");
+	p.addParam("active:upd_download","false");
+	for (int i = 0; s_policies[i].token; i++)
+	    p.addParam("active:upd_policy_" + String(s_policies[i].token),"false");
+	if (Client::self())
+	    Client::self()->setParams(&p);
+	return false;
+    }
+
+    int policy = Engine::config().getIntValue("client",toString(),s_policies,Never);
+    policy = Client::s_settings.getIntValue(toString(),"policy",s_policies,policy);
+    setPolicy(policy,false);
+    if (QFile::exists(filePath(false))) {
+	m_install = Client::s_settings.getBoolValue(toString(),"install");
+	if ((m_policy >= Install) && !m_install) {
+	    Debug(toString(),DebugNote,"Deleting old updater file");
+	    QFile::remove(filePath(false));
+	}
+    }
+    Client::self()->setActive("upd_install",m_install);
+    if (m_install && (m_policy >= Install))
+	startInstalling();
+    else if (m_policy >= Check)
+	startChecking();
+    return false;
+}
+
+void UpdateLogic::exitingClient()
+{
+    startDownloading(false);
+    startChecking(false);
+    stopHttp();
+    delete m_httpSlots;
+    m_httpSlots = 0;
+}
+
+bool UpdateLogic::action(Window* wnd, const String& name, NamedList* params)
+{
+    if (!m_canUpdate)
+	return false;
+    if (name == "upd_install")
+	startInstalling();
+    else if (name == "upd_check")
+	startChecking();
+    else if (name == "upd_download")
+	startDownloading();
+    else
+	return false;
+    return true;
+}
+
+bool UpdateLogic::toggle(Window* wnd, const String& name, bool active)
+{
+    if (!m_canUpdate)
+	return false;
+    if (!name.startsWith("upd_"))
+	return false;
+    if (name == "upd_check")
+	startChecking(active);
+    else if (name == "upd_download")
+	startDownloading(active);
+    else if (name == "upd_automatic")
+	setPolicy(active ? Install : Never,true);
+    else if (active) {
+	String tmp = name;
+	if (tmp.startSkip("upd_policy_",false))
+	    setPolicy(lookup(tmp,s_policies,Invalid),true);
+    }
+    return true;
+}
+
+void UpdateLogic::setPolicy(int policy, bool save)
+{
+    if ((policy == Invalid) || (policy == m_policy))
+	return;
+    const char* pol = lookup(policy,s_policies);
+    if (!pol)
+	return;
+    m_policy = policy;
+    if (save) {
+	Client::s_settings.setValue(toString(),"policy",pol);
+	Client::save(Client::s_settings);
+    }
+    if (!Client::self())
+	return;
+    for (policy = Never; policy <= Install; policy++) {
+	String tmp = "upd_policy_";
+	tmp += lookup(policy,s_policies);
+	Client::self()->setCheck(tmp,(policy == m_policy));
+    }
+    Client::self()->setCheck("upd_automatic",(Install == m_policy));
+}
+
+void UpdateLogic::startChecking(bool start)
+{
+    String url = Engine::config().getValue("client","updateurl");
+    Engine::runParams().replaceParams(url);
+    if (url.trimBlanks().null()) {
+	start = false;
+	if (Client::self()) {
+	    Client::self()->setActive("upd_check",false);
+	    Client::self()->setActive("upd_download",false);
+	    Client::self()->setActive("upd_install",false);
+	}
+    }
+    if (start) {
+	Debug(toString(),DebugNote,"Checking new version: %s",url.c_str());
+	m_checked = false;
+	m_checking = true;
+	start = startHttp(url,"");
+	if (Client::self()) {
+	    Client::self()->setActive("upd_download",false);
+	    Client::self()->setSelect("upd_progress","0");
+	    Client::self()->setText("upd_version","");
+	}
+    }
+    else
+	stopHttp();
+    if (Client::self())
+	Client::self()->setCheck("upd_check",start);
+}
+
+void UpdateLogic::startDownloading(bool start)
+{
+    m_checking = false;
+    if (start && m_install) {
+	m_install = false;
+	Client::s_settings.setValue(toString(),"install",String::boolText(false));
+	Client::save(Client::s_settings);
+    }
+    if (start) {
+	Debug(toString(),DebugNote,"Downloading from: %s",m_url.c_str());
+	start = startHttp(m_url,filePath(true));
+    }
+    else {
+	stopHttp();
+	QFile::remove(filePath(true));
+    }
+    if (Client::self()) {
+	Client::self()->setActive("upd_check",!start);
+	Client::self()->setActive("upd_install",m_install);
+	Client::self()->setCheck("upd_download",start);
+	Client::self()->setSelect("upd_progress","0");
+    }
+}
+
+void UpdateLogic::startInstalling()
+{
+    if (!QFile::exists(filePath(false)))
+	return;
+    QString cmd = Engine::config().getValue("client","updatecmd");
+    if (!cmd.isEmpty()) {
+	String tmp = cmd.toUtf8().constData();
+	NamedList params(Engine::runParams());
+	params.setParam("filename",filePath(false).toUtf8().constData());
+	params.replaceParams(tmp);
+	if (tmp.trimBlanks().null())
+	    return;
+	cmd = QString::fromUtf8(tmp.c_str());
+    }
+    else
+	cmd = filePath(false);
+    if (QProcess::startDetached(cmd)) {
+	Debug(toString(),DebugNote,"Executing: %s",cmd.toUtf8().constData());
+	Client::s_settings.setValue(toString(),"install",String::boolText(false));
+	Client::save(Client::s_settings);
+	Engine::halt(0);
+	return;
+    }
+    Debug(toString(),DebugWarn,"Failed to execute: %s",cmd.toUtf8().constData());
+}
+
+void UpdateLogic::finishedChecking()
+{
+    if (Client::self()) {
+	Client::self()->setCheck("upd_check",false);
+	Client::self()->setActive("upd_download",m_checked);
+	Client::self()->setSelect("upd_progress","0");
+    }
+    if (m_checked && (m_policy >= Download))
+	startDownloading();
+}
+
+void UpdateLogic::finishedDownloading()
+{
+    if (Client::self()) {
+	Client::self()->setCheck("upd_download",false);
+	Client::self()->setActive("upd_check",true);
+	Client::self()->setActive("upd_install",m_install);
+	if (!m_install)
+	    Client::self()->setSelect("upd_progress","0");
+    }
+    Client::s_settings.setValue(toString(),"install",String::boolText(m_install));
+    Client::save(Client::s_settings);
+}
+
+QString UpdateLogic::filePath(bool temp)
+{
+    return QString::fromUtf8((Engine::configPath(true) + Engine::pathSeparator() + toString() +
+	(temp ? TMP_EXT : EXE_EXT)));
+}
+
+bool UpdateLogic::startHttp(const char* url, const QString& saveAs)
+{
+    stopHttp();
+    QUrl qurl(QString::fromUtf8(url));
+    if (!qurl.isValid())
+	return false;
+    QFile* file = 0;
+    if (!saveAs.isEmpty()) {
+	QFile::remove(saveAs);
+	file = new QFile(saveAs);
+	if (!(file->open(QIODevice::WriteOnly) &&
+	    file->setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ExeOwner))) {
+	    file->remove();
+	    delete file;
+	    return false;
+	}
+	m_file = file;
+    }
+    if (!m_httpSlots)
+	m_httpSlots = new QtUpdateHttp(this);
+    const char* proxy = Client::s_settings.getValue(toString(),"proxy_host");
+    if (proxy) {
+	QNetworkProxy proxyCfg(QNetworkProxy::DefaultProxy,
+	    proxy,
+	    Client::s_settings.getIntValue(toString(),"proxy_port",8080),
+	    Client::s_settings.getValue(toString(),"proxy_user"),
+	    Client::s_settings.getValue(toString(),"proxy_pass"));
+	m_httpSlots->setProxy(proxyCfg);
+    }
+    QNetworkRequest req(qurl);
+    m_http = m_httpSlots->get(req);
+    return true;
+}
+
+void UpdateLogic::stopHttp()
+{
+    QNetworkReply* http = m_http;
+    m_http = 0;
+    if (http) {
+	http->abort();
+	delete http;
+    }
+    stopFile();
+}
+
+void UpdateLogic::stopFile()
+{
+    QFile* file = m_file;
+    m_file = 0;
+    delete file;
+}
+
+void UpdateLogic::endHttp(bool error)
+{
+    stopFile();
+    if (!m_http)
+	return;
+    if (m_checking) {
+	if (!error) {
+	    QByteArray data = m_http->readAll();
+	    if (data.size() <= 1024) {
+		String str(data.constData());
+		// 1st row is the URL, everything else description
+		int nl = str.find('\n');
+		if (nl > 0) {
+		    int len = (str.at(nl - 1) == '\r') ? (nl - 1) : nl;
+		    URI url(str.substr(0,len));
+		    url.trimBlanks();
+		    if (url.getProtocol() == "http") {
+			m_checked = true;
+			m_url = url;
+			if (Client::self())
+			    Client::self()->setText("upd_version",str.substr(nl+1));
+		    }
+		}
+	    }
+	}
+	finishedChecking();
+    }
+    else {
+	if (!error) {
+	    QFileInfo info(filePath(true));
+	    if ((info.size() >= MIN_SIZE) && (info.size() <= MAX_SIZE)) {
+		QFile::remove(filePath(false));
+		m_install = QFile::rename(filePath(true),filePath(false));
+	    }
+	}
+	QFile::remove(filePath(true));
+	finishedDownloading();
+    }
+}
+
+void UpdateLogic::dataProgress(qint64 done, qint64 total)
+{
+    if (m_http && m_file) {
+	qint64 ready = m_http->bytesAvailable();
+	while (ready >= 1024) {
+	    char buf[1024];
+	    qint64 got = m_http->read(buf,std::min<qint64>(ready,sizeof(buf)));
+	    if (got <= 0)
+		break;
+	    m_file->write(buf,got);
+	    ready -= got;
+	}
+    }
+    int percent = 0;
+    if (done)
+	percent = (done <= total) ? (int)((done * 100) / total) : 50;
+    if (!Client::self())
+	return;
+    Client::self()->setSelect("upd_progress",String(percent));
+}
+
+void UpdateLogic::requestDone()
+{
+    if (m_http && m_file) {
+	QByteArray buf = m_http->readAll();
+	m_file->write(buf);
+    }
+    endHttp(m_http ? (QNetworkReply::NoError != m_http->error()) : true);
+}
+
+
+QNetworkReply* QtUpdateHttp::get(const QNetworkRequest& request)
+{
+    QNetworkReply* reply = QNetworkAccessManager::get(request);
+    if (reply) {
+	connect(reply,&QNetworkReply::downloadProgress,this,&QtUpdateHttp::dataProgress);
+	connect(reply,&QNetworkReply::finished,this,&QtUpdateHttp::requestDone);
+    }
+    return reply;
+}
+
+void QtUpdateHttp::dataProgress(qint64 done, qint64 total)
+{
+    if (m_logic)
+	m_logic->dataProgress(done, total);
+}
+
+void QtUpdateHttp::requestDone()
+{
+    if (m_logic)
+	m_logic->requestDone();
+}
+
+
+Updater::Updater()
+    : Plugin("updater",true), m_logic(0)
+{
+    Output("Loaded module Updater");
+}
+
+Updater::~Updater()
+{
+    Output("Unloading module Updater");
+    TelEngine::destruct(m_logic);
+}
+
+void Updater::initialize()
+{
+    Output("Initializing module Updater");
+    if (m_logic)
+	return;
+    m_logic = new UpdateLogic("updater");
+}
+
+}; // anonymous namespace
+
+#include "updater.moc"
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/updater.h b/modules/qt5/updater.h
new file mode 100644
index 0000000..388569e
--- /dev/null
+++ b/modules/qt5/updater.h
@@ -0,0 +1,79 @@
+/**
+ * updater.h
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Auto updater logic and downloader for Qt-5 clients.
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#ifndef __UPDATER_H
+#define __UPDATER_H
+
+#include <yatecbase.h>
+
+#undef open
+#undef read
+#undef close
+#undef write
+#undef mkdir
+
+#define QT_NO_DEBUG
+#define QT_DLL
+#define QT_GUI_LIB
+#define QT_CORE_LIB
+#define QT_THREAD_SUPPORT
+
+#include <QNetworkAccessManager>
+
+using namespace TelEngine;
+namespace { // anonymous
+
+class UpdateLogic;
+
+/**
+ * Proxy object so HTTP notification slots are created in the GUI thread
+ */
+class QtUpdateHttp : public QNetworkAccessManager
+{
+    Q_CLASSINFO("QtUpdateHttp","Yate")
+    Q_OBJECT
+public:
+    /**
+     * Constructor
+     * @param logic Qt update logic owning this object
+     */
+    inline QtUpdateHttp(UpdateLogic* logic)
+    :  QNetworkAccessManager(),
+	   m_logic(logic)
+	{ }
+    /**
+     * Start an HTTP request and create a corresponding QNetworkReply object
+     * with its signals attached to this object.
+     * @return New QNetworkReply object attached to this object's slots
+     */
+    QNetworkReply* get(const QNetworkRequest& request);
+private slots:
+    void dataProgress(qint64 done, qint64 total);
+    void requestDone();
+private:
+    UpdateLogic* m_logic;
+};
+
+}; // anonymous namespace
+
+#endif /* __UPDATER_H */
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/widgetlist.cpp b/modules/qt5/widgetlist.cpp
new file mode 100644
index 0000000..5e9a3d3
--- /dev/null
+++ b/modules/qt5/widgetlist.cpp
@@ -0,0 +1,691 @@
+/**
+ * widgetlist.cpp
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Custom widget list objects
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2010-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#include "widgetlist.h"
+
+using namespace TelEngine;
+namespace { // anonymous
+
+// The factory
+class WidgetListFactory : public UIFactory
+{
+public:
+    inline WidgetListFactory(const char* name = "WidgetListFactory")
+	: UIFactory(name)
+	{ m_types.append(new String("WidgetList")); }
+    virtual void* create(const String& type, const char* name, NamedList* params = 0);
+};
+
+static const TokenDict s_delItemDict[] = {
+    {"global", WidgetList::DelItemGlobal},
+    {"single", WidgetList::DelItemSingle},
+    {"native", WidgetList::DelItemNative},
+    {0,0}
+};
+
+static WidgetListFactory s_factory;
+
+
+/*
+ * WidgetListTabWidget
+ */
+WidgetListTabWidget::WidgetListTabWidget(WidgetList* parent, const NamedList& params)
+    : QTabWidget(parent)
+{
+    // Configure delete item button
+#if QT_VERSION >= 0x040500
+    if (parent->m_delItemType == WidgetList::DelItemSingle ||
+	parent->m_delItemType == WidgetList::DelItemNative) {
+	// Set closable tabs
+	bool closable = parent->m_delItemType == WidgetList::DelItemNative;
+	setTabsClosable(closable);
+	// Connect close signal if native close is used
+	if (tabsClosable())
+	    QtClient::connectObjects(this,SIGNAL(tabCloseRequested(int)),parent,SLOT(closeItem(int)));
+    }
+#else
+    // Override settings: we don't support close button on tab page
+    if (parent->m_delItemType != WidgetList::DelItemNone)
+	parent->setDelItemType(WidgetList::DelItemGlobal);
+#endif
+    if (parent->m_delItemType == WidgetList::DelItemGlobal)
+	setCloseButton();
+}
+
+// Build and set a close button for a given tab or a global close if index is negative
+void WidgetListTabWidget::setCloseButton(int index)
+{
+    WidgetList* list = static_cast<WidgetList*>(parent());
+    if (!list)
+	return;
+    // Check if we can set a close button
+#if QT_VERSION >= 0x040500
+    if (index >= 0) {
+	if (list->m_delItemType != WidgetList::DelItemSingle || tabsClosable() || !tabBar())
+	    return;
+    }
+    else if (list->m_delItemType != WidgetList::DelItemGlobal)
+	return;
+#else
+    if (index >= 0 || list->m_delItemType != WidgetList::DelItemGlobal)
+	return;
+#endif
+    // Build the button
+    QToolButton* b = new QToolButton(this);
+    b->setProperty("_yate_noautoconnect",QVariant(true));
+    if (index >= 0) {
+#if QT_VERSION >= 0x040500
+	QWidget* w = widget(index);
+	String item;
+	QtUIWidget::getListItemIdProp(w,item);
+	QtUIWidget::setListItemProp(b,QtClient::setUtf8(item));
+	tabBar()->setTabButton(index,QTabBar::RightSide,b);
+#else
+	delete b;
+	return;
+#endif
+    }
+    else
+	setCornerWidget(b,Qt::TopRightCorner);
+    list->applyDelItemProps(b);
+    QtClient::connectObjects(b,SIGNAL(clicked()),list,SLOT(closeItem()));
+}
+
+// Set tab close button if needed
+void WidgetListTabWidget::tabInserted(int index)
+{
+#if QT_VERSION >= 0x040500
+    if (!tabsClosable())
+	setCloseButton(index);
+#endif
+    QTabWidget::tabInserted(index);
+}
+
+// Tab removed. Notify the parent
+void WidgetListTabWidget::tabRemoved(int index)
+{
+    WidgetList* list = static_cast<WidgetList*>(parent());
+    if (list)
+	list->itemRemoved(index);
+}
+
+/*
+ * WidgetListStackedWidget
+ */
+WidgetListStackedWidget::WidgetListStackedWidget(WidgetList* parent, const NamedList& params)
+    : QStackedWidget(parent)
+{
+}
+
+/*
+ * WidgetList
+ */
+// Constructor
+WidgetList::WidgetList(const char* name, const NamedList& params, QWidget* parent)
+    : QtCustomWidget(name,parent),
+    m_hideWndWhenEmpty(false),
+    m_tab(0),
+    m_pages(0),
+    m_delItemType(DelItemNone),
+    m_delItemProps("")
+{
+    // Build properties
+    QtClient::buildProps(this,params["buildprops"]);
+    // Retrieve the delete item props
+    updateDelItemProps(params,true);
+    const String& type = params["type"];
+    XDebug(ClientDriver::self(),DebugAll,"WidgetList(%s) type=%s",name,type.c_str());
+    QString wName = buildQChildName(params.getValue("widgetname","widget"));
+    if (type == "tabs") {
+	m_tab = new WidgetListTabWidget(this,params);
+	m_tab->setObjectName(wName);
+	QtClient::setWidget(this,m_tab);
+	QtClient::connectObjects(m_tab,SIGNAL(currentChanged(int)),
+	    this,SLOT(currentChanged(int)));
+    }
+    else if (type == "pages") {
+	QWidget* hdr = 0;
+	const String& header = params["header"];
+	if (header)
+	    hdr = QtWindow::loadUI(Client::s_skinPath + header,this,header);
+	if (hdr)
+	    hdr->setObjectName(QtClient::setUtf8("pages_header"));
+	m_pages = new WidgetListStackedWidget(this,params);
+	m_pages->setObjectName(wName);
+	QVBoxLayout* newLayout = new QVBoxLayout;
+	newLayout->setSpacing(0);
+	newLayout->setContentsMargins(0,0,0,0);
+	if (hdr)
+	    newLayout->addWidget(hdr);
+	newLayout->addWidget(m_pages);
+	QLayout* l = layout();
+	if (l)
+	    delete l;
+	setLayout(newLayout);
+	QtClient::connectObjects(m_pages,SIGNAL(currentChanged(int)),
+	    this,SLOT(currentChanged(int)));
+	QtClient::connectObjects(m_pages,SIGNAL(widgetRemoved(int)),
+	    this,SLOT(itemRemoved(int)));
+    }
+    // Set navigation
+    QtUIWidget::initNavigation(params);
+    setParams(params);
+}
+
+// Find an item widget by index
+QWidget* WidgetList::findItemByIndex(int index)
+{
+    QWidget* w = 0;
+    if (m_tab)
+	w = m_tab->widget(index);
+    else if (m_pages)
+	w = m_pages->widget(index);
+    return w;
+}
+
+// Set widget parameters
+bool WidgetList::setParams(const NamedList& params)
+{
+    bool ok = QtUIWidget::setParams(params);
+    ok = QtUIWidget::setParams(this,params) && ok;
+    updateDelItemProps(params);
+    return ok;
+}
+
+// Get widget's items
+bool WidgetList::getOptions(NamedList& items)
+{
+    QList<QObject*> list = getContainerItems();
+    for (int i = 0; i < list.size(); i++)
+	if (list[i]->isWidgetType()) {
+	    String id;
+	    getListItemIdProp(list[i],id);
+	    items.addParam(id,"");
+	}
+    return true;
+}
+
+// Retrieve item parameters
+bool WidgetList::getTableRow(const String& item, NamedList* data)
+{
+    QWidget* w = findItem(item);
+    DDebug(ClientDriver::self(),DebugAll,"WidgetList(%s)::getTableRow(%s,%p) found=%p",
+	name().c_str(),item.c_str(),data,w);
+    if (!w)
+	return false;
+    if (data)
+	getParams(w,*data);
+    return true;
+}
+
+// Add an item
+bool WidgetList::addTableRow(const String& item, const NamedList* data, bool atStart)
+{
+    DDebug(ClientDriver::self(),DebugAll,"WidgetList(%s)::addTableRow(%s,%p,%u)",
+	name().c_str(),item.c_str(),data,atStart);
+    QWidget* parent = 0;
+    if (item) {
+	if (m_tab)
+	    parent = m_tab;
+	else
+	    parent = m_pages;
+    }
+    if (!parent)
+	return false;
+    const String& type = data ? (*data)["item_type"] : String::empty();
+    QWidget* w = loadWidgetType(parent,item,type);
+    if (!w)
+	return false;
+    QtUIWidgetItemProps* p = QtUIWidget::getItemProps(type);
+    if (p && p->m_styleSheet)
+	applyWidgetStyle(w,p->m_styleSheet);
+    if (addItem(w,atStart))
+	setTableRow(item,data);
+    return w != 0;
+}
+
+// Add or set one or more table row(s)
+bool WidgetList::updateTableRows(const NamedList* data, bool atStart)
+{
+    DDebug(ClientDriver::self(),DebugAll,"WidgetList(%s)::updateTableRows(%p,%u)",
+	name().c_str(),data,atStart);
+    // Save the hide empty window flag
+    bool oldHide = m_hideWndWhenEmpty;
+    String oldWHide = m_hideWidgetWhenEmpty;
+    m_hideWndWhenEmpty = false;
+    m_hideWidgetWhenEmpty = "";
+    bool ok = true;
+    unsigned int n = data->length();
+    for (unsigned int i = 0; i < n; i++) {
+	if (Client::exiting())
+	    break;
+	// Get item and the list of parameters
+	NamedString* ns = data->getParam(i);
+	if (!ns)
+	    continue;
+	// Delete ?
+	if (ns->null()) {
+	    ok = delTableRow(ns->name()) && ok;
+	    continue;
+	}
+	// Set existing item or add a new one
+	if (getTableRow(ns->name()))
+	    ok = setTableRow(ns->name(),YOBJECT(NamedList,ns)) && ok;
+	else if (ns->toBoolean())
+	    ok = addTableRow(ns->name(),YOBJECT(NamedList,ns),atStart) && ok;
+	else
+	    ok = false;
+    }
+    m_hideWndWhenEmpty = oldHide;
+    m_hideWidgetWhenEmpty = oldWHide;
+    QtUIWidget::updateNavigation();
+    hideEmpty();
+    return ok;
+}
+
+// Delete an item
+bool WidgetList::delTableRow(const String& item)
+{
+    QWidget* w = findItem(item);
+    DDebug(ClientDriver::self(),DebugAll,"WidgetList(%s)::delTableRow(%s) found=%p",
+	name().c_str(),item.c_str(),w);
+    if (!w)
+	return false;
+    QtClient::deleteLater(w);
+    QtUIWidget::updateNavigation();
+    hideEmpty();
+    return true;
+}
+
+// Set existing item parameters
+bool WidgetList::setTableRow(const String& item, const NamedList* data)
+{
+    QWidget* w = findItem(item);
+    DDebug(ClientDriver::self(),DebugAll,"WidgetList(%s)::setTableRow(%s,%p) wid=%p",
+	name().c_str(),item.c_str(),data,w);
+    if (!w)
+	return false;
+    if (data) {
+	if (m_tab) {
+	    // Hook some parameters to set them in tab
+	    String* name = m_itemTextParam ? data->getParam(m_itemTextParam) : 0;
+	    if (name)
+		m_tab->setTabText(m_tab->indexOf(w),QtClient::setUtf8(*name));
+	    if (m_itemImgParam) {
+		String* tmp = data->getParam("image:" + m_itemImgParam);
+		if (tmp)
+		    m_tab->setTabIcon(m_tab->indexOf(w),QIcon(QtClient::setUtf8(*tmp)));
+	    }
+	}
+	QtUIWidget::setParams(w,*data);
+    }
+    return true;
+}
+
+// Delete all items
+bool WidgetList::clearTable()
+{
+    if (m_tab || m_pages) {
+	QList<QObject*> list = getContainerItems();
+	for (int i = 0; i < list.size(); i++)
+	    if (list[i]->isWidgetType())
+		QtClient::deleteLater(list[i]);
+    }
+    else
+	return false;
+    QtUIWidget::updateNavigation();
+    hideEmpty();
+    return true;
+}
+
+// Select (set active) an item
+bool WidgetList::setSelect(const String& item)
+{
+    QWidget* w = findItem(item);
+    if (!w)
+	return false;
+    if (m_tab)
+	m_tab->setCurrentWidget(w);
+    else if (m_pages)
+	m_pages->setCurrentWidget(w);
+    else
+	return false;
+    QtUIWidget::updateNavigation();
+    return true;
+}
+
+// Retrieve the selected (active) item
+bool WidgetList::getSelect(String& item)
+{
+    QWidget* w = selectedItem();
+    if (w)
+	QtUIWidget::getListItemIdProp(w,item);
+    DDebug(ClientDriver::self(),DebugAll,"WidgetList(%s)::getSelect() '%s' wid=%p",
+	name().c_str(),item.c_str(),w);
+    return w != 0;
+}
+
+// Retrieve a QObject list containing container items
+QList<QObject*> WidgetList::getContainerItems()
+{
+    QList<QObject*> list;
+    if (m_tab) {
+	int n = m_tab->count();
+	for (int i = 0; i < n; i++) {
+	    QWidget* w = m_tab->widget(i);
+	    if (w)
+		list.append(static_cast<QObject*>(w));
+	}
+    }
+    else if (m_pages) {
+	int n = m_pages->count();
+	for (int i = 0; i < n; i++) {
+	    QWidget* w = m_pages->widget(i);
+	    if (w)
+		list.append(static_cast<QObject*>(w));
+	}
+    }
+    return list;
+}
+
+// Select an item by its index
+bool WidgetList::setSelectIndex(int index)
+{
+    if (index < 0 || index >= itemCount())
+	return false;
+    QWidget* w = findItemByIndex(index);
+    String item;
+    if (w)
+	QtUIWidget::getListItemIdProp(w,item);
+    return item ? setSelect(item) : false;
+}
+
+// Retrieve the 0 based index of the current item
+int WidgetList::currentItemIndex()
+{
+    if (m_tab)
+	return m_tab->currentIndex();
+    if (m_pages)
+	return m_pages->currentIndex();
+    return -1;
+}
+
+// Retrieve the number of items in container
+int WidgetList::itemCount()
+{
+    if (m_tab)
+	return m_tab->count();
+    if (m_pages)
+	return m_pages->count();
+    return -1;
+}
+
+// Set _yate_hidewndwhenempty property value. Apply it if changed
+void WidgetList::setHideWndWhenEmpty(bool value)
+{
+    if (m_hideWndWhenEmpty == value)
+	return;
+    m_hideWndWhenEmpty = value;
+    hideEmpty();
+}
+
+// Set _yate_hidewidgetwhenempty property value. Apply it if changed
+void WidgetList::setHideWidgetWhenEmpty(QString value)
+{
+    String s;
+    QtClient::getUtf8(s,value);
+    if (m_hideWidgetWhenEmpty == s)
+	return;
+    m_hideWidgetWhenEmpty = s;
+    hideEmpty();
+}
+
+// Start/stop item flash
+void WidgetList::setFlashItem(QString value)
+{
+    if (!m_tab)
+	return;
+    String on;
+    String item;
+    int pos = value.indexOf(':');
+    if (pos > 0) {
+	QtClient::getUtf8(on,value.left(pos));
+	QtClient::getUtf8(item,value.right(value.length() - pos - 1));
+    }
+    else
+	return;
+    QWidget* w = findItem(item);
+    if (!w)
+	return;
+    int index = m_tab->indexOf(w);
+    if (on.toBoolean())
+	m_tab->setTabTextColor(index,QColor("green"));
+    else
+	m_tab->setTabTextColor(index,QColor("black"));
+}
+
+// Handle selection changes
+void WidgetList::currentChanged(int index)
+{
+    String item;
+    if (index >= 0 && index < itemCount()) {
+	QWidget* w = findItemByIndex(index);
+	if (w)
+	    QtUIWidget::getListItemIdProp(w,item);
+	// Avoid notifying no selection
+	if (!item)
+	    return;
+    }
+    QtWindow* wnd = item ? QtClient::parentWindow(this) : 0;
+    if (wnd)
+	Client::self()->select(wnd,name(),item);
+}
+
+// Item removed slot. Notify the client when empty
+void WidgetList::itemRemoved(int index)
+{
+    if (itemCount())
+	return;
+    QtWindow* wnd = QtClient::parentWindow(this);
+    if (wnd)
+	Client::self()->select(wnd,name(),String::empty());
+}
+
+// Handle current item close action
+void WidgetList::closeItem(int index)
+{
+    if (!m_delItemActionPrefix)
+	return;
+    String item;
+    if (index < 0) {
+	if (m_delItemType == DelItemSingle)
+	    QtUIWidget::getListItemProp(sender(),item);
+	else if (m_delItemType == DelItemGlobal)
+	    getSelect(item);
+    }
+    else if (m_delItemType == DelItemNative) {
+	// Signalled by tab native close button
+	QWidget* w = findItemByIndex(index);
+	if (w)
+	    QtUIWidget::getListItemIdProp(w,item);
+    }
+    XDebug(ClientDriver::self(),DebugAll,
+	"WidgetList(%s)::closeItem(%d) sender (%p,%s) found=%s",
+	name().c_str(),index,sender(),YQT_OBJECT_NAME(sender()),item.c_str());
+    QtWindow* wnd = item ? QtClient::parentWindow(this) : 0;
+    if (wnd)
+	Client::self()->action(wnd,m_delItemActionPrefix + item);
+}
+
+// Handle children events
+bool WidgetList::eventFilter(QObject* watched, QEvent* event)
+{
+    if (!Client::valid())
+	return QtCustomWidget::eventFilter(watched,event);
+    if (event->type() == QEvent::KeyPress) {
+	if (m_wndEvHooked) {
+	    QtWindow* wnd = qobject_cast<QtWindow*>(watched);
+	    if (wnd && wnd == getWindow()) {
+		QString child;
+		QWidget* sel = selectedItem();
+		if (sel && buildQChildNameProp(child,sel,"_yate_keypress_redirect") &&
+		    QtClient::sendEvent(*event,sel,child)) {
+		    QWidget* wid = sel->findChild<QWidget*>(child);
+		    if (wid)
+			wid->setFocus();
+		    return true;
+		}
+		return QtCustomWidget::eventFilter(watched,event);
+	    }
+	}
+	bool filter = false;
+	if (!filterKeyEvent(watched,static_cast<QKeyEvent*>(event),filter))
+	    return QtCustomWidget::eventFilter(watched,event);
+	return filter;
+    }
+    return QtCustomWidget::eventFilter(watched,event);
+}
+
+// Hide the parent window if the container is empty
+void WidgetList::hideEmpty()
+{
+    if (itemCount() || !Client::valid() || !(m_hideWndWhenEmpty || m_hideWidgetWhenEmpty))
+	return;
+    QtWindow* wnd = QtClient::parentWindow(this);
+    if (!wnd)
+	return;
+    if (m_hideWndWhenEmpty)
+	Client::self()->setVisible(wnd->id(),false);
+    if (m_hideWidgetWhenEmpty)
+	wnd->setShow(m_hideWidgetWhenEmpty,false);
+}
+
+// Insert/add a widget item
+bool WidgetList::addItem(QWidget*& w, bool atStart)
+{
+    if (!w)
+	return false;
+    int index = atStart ? 0 : itemCount();
+    if (m_tab)
+	m_tab->insertTab(index,w,QString());
+    else if (m_pages)
+	m_pages->insertWidget(index,w);
+    else {
+	QtClient::deleteLater(w);
+	w = 0;
+    }
+    if (w)
+	QtUIWidget::updateNavigation();
+    return w != 0;
+}
+
+// Retrieve the selected item widget
+QWidget* WidgetList::selectedItem()
+{
+    if (m_tab)
+	return m_tab->currentWidget();
+    if (m_pages)
+	return m_pages->currentWidget();
+    return 0;
+}
+
+// Set delete item type
+void WidgetList::setDelItemType(int type)
+{
+    if (type == m_delItemType)
+	return;
+    m_delItemType = type;
+    XDebug(ClientDriver::self(),DebugAll,"WidgetList(%s)::setDelItemType(%d = %s)",
+	name().c_str(),type,lookup(type,s_delItemDict));
+}
+
+// Retrieve delete item object properties
+void WidgetList::updateDelItemProps(const NamedList& params, bool first)
+{
+    static const String s_delItemProp = "delete_item_property:";
+    if (first) {
+	m_delItemActionPrefix = params.getValue("delete_item_action");
+	if (m_delItemActionPrefix) {
+	    setDelItemType(params.getIntValue("delete_item_type",s_delItemDict,DelItemNone));
+	    if (m_delItemType != DelItemNone)
+		m_delItemActionPrefix << ":" << this->name() << ":";
+	    else
+		m_delItemActionPrefix.clear();
+	}
+    }
+    if (m_delItemType == DelItemNone)
+	return;
+    unsigned int n = params.length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* ns = params.getParam(i);
+	if (!(ns && ns->name().startsWith(s_delItemProp)))
+	    continue;
+	String prop = ns->name().substr(s_delItemProp.length());
+	if (!prop)
+	    continue;
+	m_delItemProps.setParam(prop,*ns);
+	// TODO: Apply the property to all delete item objects if changed
+    }
+}
+
+// Apply delete item object properties
+void WidgetList::applyDelItemProps(QObject* obj)
+{
+    if (!obj)
+	return;
+    unsigned int n = m_delItemProps.length();
+    for (unsigned int i = 0; i < n; i++) {
+	NamedString* ns = m_delItemProps.getParam(i);
+	if (!ns)
+	    continue;
+	DDebug(ClientDriver::self(),DebugAll,
+	    "WidgetList(%s)::applyDelItemProps() %s=%s",
+	    name().c_str(),ns->name().c_str(),ns->c_str());
+	QtClient::setProperty(obj,ns->name(),*ns);
+    }
+}
+
+/*
+ * WidgetListFactory
+ */
+// Build objects
+void* WidgetListFactory::create(const String& type, const char* name, NamedList* params)
+{
+    if (!params)
+	return 0;
+    QWidget* parentWidget = 0;
+    String* wndname = params->getParam("parentwindow");
+    if (!TelEngine::null(wndname)) {
+	String* wName = params->getParam("parentwidget");
+	QtWindow* wnd = static_cast<QtWindow*>(Client::self()->getWindow(*wndname));
+	if (wnd && !TelEngine::null(wName))
+	    parentWidget = wnd->findChild<QWidget*>(QtClient::setUtf8(*wName));
+    }
+    if (type == "WidgetList")
+	return new WidgetList(name,*params,parentWidget);
+    return 0;
+}
+
+}; // anonymous namespace
+
+#include "widgetlist.moc"
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/modules/qt5/widgetlist.h b/modules/qt5/widgetlist.h
new file mode 100644
index 0000000..9d65b82
--- /dev/null
+++ b/modules/qt5/widgetlist.h
@@ -0,0 +1,430 @@
+/**
+ * widgetlist.h
+ * This file is part of the YATE Project http://YATE.null.ro
+ *
+ * Custom widget list objects
+ *
+ * Yet Another Telephony Engine - a fully featured software PBX and IVR
+ * Copyright (C) 2004-2020 Null Team
+ *
+ * This software is distributed under multiple licenses;
+ * see the COPYING file in the main directory for licensing
+ * information for this specific distribution.
+ *
+ * This use of this software may be subject to additional restrictions.
+ * See the LEGAL file in the main directory for details.
+ *
+ * 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.
+ */
+
+#ifndef __WIDGETLIST_H
+#define __WIDGETLIST_H
+
+#include "qt5client.h"
+
+using namespace TelEngine;
+namespace { // anonymous
+
+class WidgetListTabWidget;               // A tab widget client of a widget list
+class WidgetListStackedWidget;           // A stacked widget client of a widget list
+class WidgetList;                        // A widget list
+
+class WidgetListTabWidget : public QTabWidget
+{
+public:
+    /**
+     * Constructor
+     * @param parent WidgetList parent
+     */
+    WidgetListTabWidget(WidgetList* parent, const NamedList& params);
+
+    /**
+     * Set tab text color
+     * @param index Tab index
+     * @param color Tab text color
+     */
+    inline void setTabTextColor(int index, QColor color) {
+	    QTabBar* bar = tabBar();
+	    if (bar)
+		bar->setTabTextColor(index,color);
+	}
+
+    /**
+     * Retrieve the tab text color
+     * @param index Tab index
+     * @return Text color of the given index
+     */
+    inline QColor tabTextColor(int index) {
+	    QTabBar* bar = tabBar();
+	    return bar ? bar->tabTextColor(index) : QColor();
+	}
+
+protected:
+    /**
+     * Build and set a close button for a given tab or a global close if index is negative
+     * Connect the button to parent's slot.
+     * This method is called from tabInserted() with non negative index
+     */
+    void setCloseButton(int index = -1);
+
+    /**
+     * Tab inserted. Set tab close button if needed
+     */
+    virtual void tabInserted(int index);
+
+    /**
+     * Tab removed. Notify the parent
+     */
+    virtual void tabRemoved(int index);
+};
+
+class WidgetListStackedWidget : public QStackedWidget
+{
+public:
+    /**
+     * Constructor
+     * @param parent WidgetList parent
+     */
+    WidgetListStackedWidget(WidgetList* parent, const NamedList& params);
+};
+
+/**
+ * This class holds a basic widget list container
+ * @short A widget list
+ */
+class WidgetList : public QtCustomWidget
+{
+    friend class WidgetListTabWidget;
+    YCLASS(WidgetList,QtCustomWidget)
+    Q_CLASSINFO("WidgetList","Yate")
+    Q_OBJECT
+    Q_PROPERTY(bool _yate_hidewndwhenempty READ hideWndWhenEmpty WRITE setHideWndWhenEmpty(bool))
+    Q_PROPERTY(QString _yate_hidewidgetwhenempty READ hideWidgetWhenEmpty WRITE setHideWidgetWhenEmpty(QString))
+    Q_PROPERTY(QString _yate_itemui READ itemUi WRITE setItemUi(QString))
+    Q_PROPERTY(QString _yate_itemstyle READ itemStyle WRITE setItemStyle(QString))
+    Q_PROPERTY(QString _yate_itemtextparam READ itemTextParam WRITE setItemTextParam(QString))
+    Q_PROPERTY(QString _yate_itemimageparam READ itemImageParam WRITE setItemImageParam(QString))
+    Q_PROPERTY(QString _yate_flashitem READ flashItem WRITE setFlashItem(QString))
+public:
+    /**
+     * Delete item button type
+     */
+    enum DelItem {
+	DelItemNone = 0,                 // No delete item button
+	DelItemGlobal,                   // Global (delete selected) button
+	DelItemSingle,                   // Delete button on each item
+	DelItemNative,                   // Delete button on each item: use native if available
+    };
+
+    /**
+     * Constructor
+     * @param name Object name
+     * @param params Object parameters
+     * @param parent Optional parent
+     */
+    WidgetList(const char* name, const NamedList& params, QWidget* parent);
+
+    /**
+     * Find an item widget by index
+     * @param index Item index
+     * @return QWidget pointer or 0
+     */
+    QWidget* findItemByIndex(int index);
+
+    /**
+     * Set widget parameters
+     * @param params Parameter list
+     * @return True on success
+     */
+    virtual bool setParams(const NamedList& params);
+
+    /**
+     * Get widget's items
+     * @param items List to fill with widget's items
+     * @return True
+     */
+    virtual bool getOptions(NamedList& items);
+
+    /**
+     * Retrieve item parameters
+     * @param item Item id
+     * @param data List to be filled with parameters
+     * @return True on success
+     */
+    virtual bool getTableRow(const String& item, NamedList* data = 0);
+
+    /**
+     * Add a new item
+     * @param item Item id
+     * @param data Item parameters
+     * @param asStart True to insert at start, false to append
+     * @return True on success
+     */
+    virtual bool addTableRow(const String& item, const NamedList* data = 0,
+	bool atStart = false);
+
+    /**
+     * Add/set/delete one or more item(s)
+     * @param data The list of items to add/set/delete
+     * @param atStart True to add new items at start, false to add them to the end
+     * @return True if the operation was successfull
+     */
+    virtual bool updateTableRows(const NamedList* data, bool atStart = false);
+
+    /**
+     * Delete an item
+     * @param item Item id
+     * @return True on success
+     */
+    virtual bool delTableRow(const String& item);
+
+    /**
+     * Set existing item parameters
+     * @param item Item id
+     * @param data Item parameters
+     * @return True on success
+     */
+    virtual bool setTableRow(const String& item, const NamedList* data);
+
+    /**
+     * Delete all items
+     * @return True on success
+     */
+    virtual bool clearTable();
+
+    /**
+     * Select (set active) an item
+     * @param item Item id
+     * @return True on success
+     */
+    virtual bool setSelect(const String& item);
+
+    /**
+     * Retrieve the selected (active) item
+     * @param item Item id
+     * @return True on success
+     */
+    virtual bool getSelect(String& item);
+
+    /**
+     * Retrieve a QObject list containing tree item widgets
+     * @return The list of container item widgets
+     */
+    virtual QList<QObject*> getContainerItems();
+
+    /**
+     * Select an item by its index
+     * @param index Item index to select
+     * @return True on success
+     */
+    virtual bool setSelectIndex(int index);
+
+    /**
+     * Retrieve the 0 based index of the current item
+     * @return The index of the current item (-1 on error or container empty)
+     */
+    virtual int currentItemIndex();
+
+    /**
+     * Retrieve the number of items in container
+     * @return The number of items in container (-1 on error)
+     */
+    virtual int itemCount();
+
+    /**
+     * Retrieve _yate_hidewndwhenempty property value
+     * @return _yate_hidewndwhenempty property value
+     */
+    bool hideWndWhenEmpty()
+	{ return m_hideWndWhenEmpty; }
+
+    /**
+     * Set _yate_hidewndwhenempty property value. Apply it if changed
+     * @param value The new value of _yate_hidewndwhenempty property
+     */
+    void setHideWndWhenEmpty(bool value);
+
+    /**
+     * Retrieve _yate_hidewidgetwhenempty property value
+     * @return _yate_hidewidgetwhenempty property value
+     */
+    QString hideWidgetWhenEmpty()
+	{ return QtClient::setUtf8(m_hideWidgetWhenEmpty); }
+
+    /**
+     * Set _yate_hidewidgetwhenempty property value. Apply it if changed
+     * @param value The new value of _yate_hidewidgetwhenempty property
+     */
+    void setHideWidgetWhenEmpty(QString value);
+
+    /**
+     * Read _yate_itemui property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemUi()
+	{ return QString(); }
+
+    /**
+     * Set an item props ui
+     * @param value Item props ui. Format [type:]ui_name
+     */
+    void setItemUi(QString value) {
+	    String tmp;
+	    QtUIWidgetItemProps* p = getItemProps(value,tmp);
+	    p->m_ui = tmp;
+	}
+
+    /**
+     * Read _yate_itemstyle property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString itemStyle()
+	{ return QString(); }
+
+    /**
+     * Set an item props style sheet
+     * @param value Item props style sheet. Format [type:]stylesheet
+     */
+    void setItemStyle(QString value) {
+	    String tmp;
+	    QtUIWidgetItemProps* p = getItemProps(value,tmp);
+	    p->m_styleSheet = tmp;
+	}
+
+    /**
+     * Retrieve _yate_itemtextparam property value
+     * @return The value of _yate_itemtextparam property
+     */
+    QString itemTextParam()
+	{ return QtClient::setUtf8(m_itemTextParam); }
+
+    /**
+     * Set _yate_itemtextparam property value
+     * @param value The new value of _yate_itemtextparam property
+     */
+    void setItemTextParam(QString value)
+	{ QtClient::getUtf8(m_itemTextParam,value); }
+
+    /**
+     * Retrieve _yate_itemimageparam property value
+     * @return The value of _yate_itemimageparam property
+     */
+    QString itemImageParam()
+	{ return QtClient::setUtf8(m_itemImgParam); }
+
+    /**
+     * Set _yate_itemimageparam property value
+     * @param value The new value of _yate_itemimageparam property
+     */
+    void setItemImageParam(QString value)
+	{ QtClient::getUtf8(m_itemImgParam,value); }
+
+    /**
+     * Read _yate_flashitem property accessor: does nothing
+     * This method is here to stop MOC compiler complaining about missing READ accessor function
+     */
+    QString flashItem()
+	{ return QString(); }
+
+    /**
+     * Start/stop item flash
+     * @param value Item value. Format bool_value:item_id
+     */
+    void setFlashItem(QString value);
+
+public slots:
+    /**
+     * Handle item children actions
+     */
+    void itemChildAction()
+	{ onAction(sender()); }
+
+    /**
+     * Handle item children toggles
+     */
+    void itemChildToggle(bool on)
+	{ onToggle(sender(),on); }
+
+    /**
+     * Handle selection changes
+     */
+    void currentChanged(int index);
+
+    /**
+     * Item removed slot. Notify the client when empty
+     */
+    void itemRemoved(int index);
+
+    /**
+     * Handle item children select
+     */
+    void itemChildSelect()
+	{ onSelect(sender()); }
+
+    /**
+     * Handle item close action
+     */
+    void closeItem(int index = -1);
+
+protected:
+    /**
+     * Handle children events
+     */
+    virtual bool eventFilter(QObject* watched, QEvent* event);
+
+    /**
+     * Hide the parent window or widget if the container is empty
+     */
+    void hideEmpty();
+
+    /**
+     * Insert/add a widget item
+     * @param w Widget to append or insert (it will be deleted and reset on failure)
+     * @param atStart True to insert, false to add
+     * @return True on success
+     */
+    bool addItem(QWidget*& w, bool atStart);
+
+    /**
+     * Retrieve the selected item widget
+     * @return QWidget pointer or 0
+     */
+    QWidget* selectedItem();
+
+    /**
+     * Set delete item type
+     * @param type The new delete item type
+     */
+    void setDelItemType(int type);
+
+    /**
+     * Retrieve delete item object properties
+     * @param params Parameter list
+     * @param first True if called from constructor: update delete item type also
+     */
+    void updateDelItemProps(const NamedList& params, bool first = false);
+
+    /**
+     * Apply delete item object properties
+     * @param obj The object
+     */
+    void applyDelItemProps(QObject* obj);
+
+    bool m_hideWndWhenEmpty;             // Hide the parent window when the container is empty
+    String m_hideWidgetWhenEmpty;        // Widget to hide when the container is empty
+    WidgetListTabWidget* m_tab;          // Tab widget if used
+    WidgetListStackedWidget* m_pages;    // Stacked widget if used
+    int m_delItemType;                   // Delete item type
+    NamedList m_delItemProps;            // Delete item widget properties
+    String m_delItemActionPrefix;        // Delete item action prefix
+    String m_itemTextParam;              // Hook this parameter to set item text
+    String m_itemImgParam;               // Hook this parameter to set item image
+};
+
+}; // anonymous namespace
+
+#endif // __WIDGETLIST_H
+
+/* vi: set ts=8 sw=4 sts=4 noet: */
diff --git a/share/skins/default/qt5client.rc b/share/skins/default/qt5client.rc
new file mode 100644
index 0000000..59811da
--- /dev/null
+++ b/share/skins/default/qt5client.rc
@@ -0,0 +1,170 @@
+; If you want to create windows which have as a parent another window, in the section for each child window
+; add the parameter "parent=" with the name of the parent window.
+; ATTENTION! Keep in mind that the parent window should be created before the child window.
+; This means that here you should list the windows in a order similar to the example bellow:
+;
+; [parentwindow_1]
+; description=...
+; [childwindow_1]
+; description=...
+; parent=parentwindow_1
+; [childwindow_2]
+; description=...
+; parent=parentwindow_1
+;
+; [parentwindow_2]
+; description=...
+; [childwindow_3]
+; description=...
+; parent=parentwindow_2
+
+
+[mainwindow]
+enabled=true
+visible=true
+mainwindow=true
+description=qt5client.ui
+
+[settings]
+enabled=yes
+description=settings.ui
+
+[events]
+enabled=true
+description=events.ui
+
+[help]
+enabled=true
+description=help.ui
+
+[confirm]
+enabled=true
+description=confirm.ui
+
+[message]
+enabled=true
+description=message.ui
+
+[input]
+enabled=false
+description=input.ui
+
+[inputpwd]
+enabled=false
+save=false
+description=inputpwd.ui
+
+[inputacccred]
+enabled=false
+save=false
+description=inputacccred.ui
+
+[account]
+enabled=true
+description=account.ui
+
+[accountlist]
+enabled=true
+description=accountlist.ui
+
+[accountwizard]
+enabled=true
+description=accountwizard.ui
+
+[addrbook]
+enabled=true
+description=addrbook.ui
+
+[chat]
+enabled=no
+save=false
+description=chat.ui
+
+[dockedchat]
+enabled=yes
+description=dockedchat.ui
+
+[mucs]
+enabled=yes
+description=mucs.ui
+
+[mucchat]
+enabled=no
+description=mucchat.ui
+
+[mucinvite]
+enabled=yes
+visible=false
+description=mucinvite.ui
+
+[mucprivchat]
+enabled=no
+description=mucprivchat.ui
+
+[joinmucwizard]
+enabled=true
+savealias=false
+description=joinmucwizard.ui
+
+[contactedit]
+enabled=no
+save=false
+description=contactedit.ui
+
+[chatroomedit]
+enabled=no
+save=false
+description=chatroomedit.ui
+
+[contactinfo]
+enabled=no
+save=false
+description=contactinfo.ui
+
+[contactfs]
+enabled=no
+save=false
+description=contactfs.ui
+
+[contactfsd]
+enabled=no
+save=false
+description=contactfsd.ui
+
+[messages_header]
+enabled=no
+description=messages_header.ui
+
+[messages_generic]
+enabled=no
+description=messages_generic.ui
+
+[messages_okrejignore]
+enabled=no
+description=messages_okrejignore.ui
+
+[messages_loginfail]
+enabled=no
+description=messages_loginfail.ui
+
+[fileprogress]
+enabled=yes
+description=fileprogress.ui
+
+[fileprogress_item]
+enabled=no
+description=fileprogress_item.ui
+
+[archive]
+enabled=yes
+visible=false
+description=archive.ui
+
+[busy]
+enabled=no
+description=busy.ui
+
+[notification]
+enabled=yes
+save=false
+description=notification.ui
diff --git a/share/skins/default/qt5client.ui b/share/skins/default/qt5client.ui
new file mode 100644
index 0000000..6c66c45
--- /dev/null
+++ b/share/skins/default/qt5client.ui
@@ -0,0 +1,3184 @@
+<ui version="4.0" >
+ <class>mainwindow</class>
+ <widget class="QWidget" name="mainwindow" >
+  <property name="geometry" >
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>340</width>
+    <height>520</height>
+   </rect>
+  </property>
+  <property name="sizePolicy" >
+   <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="minimumSize" >
+   <size>
+    <width>300</width>
+    <height>520</height>
+   </size>
+  </property>
+  <property name="focusPolicy" >
+   <enum>Qt::StrongFocus</enum>
+  </property>
+  <property name="windowTitle" >
+   <string comment="VoIP Client" >Yate Client</string>
+  </property>
+  <property name="windowIcon" >
+   <iconset>null_team-32.png</iconset>
+  </property>
+  <property name="toolTip" >
+   <string>Yate Client - Telephony Client</string>
+  </property>
+  <property name="styleSheet" >
+   <string>QWidget#mainwindow {
+  background:#f7f5fd;
+}
+
+</string>
+  </property>
+  <property name="_yate_hideonclose" stdset="0" >
+   <bool>true</bool>
+  </property>
+  <property name="_yate_window" stdset="0" >
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" >
+   <property name="spacing" >
+    <number>0</number>
+   </property>
+   <property name="leftMargin" >
+    <number>2</number>
+   </property>
+   <property name="topMargin" >
+    <number>0</number>
+   </property>
+   <property name="rightMargin" >
+    <number>2</number>
+   </property>
+   <property name="bottomMargin" >
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QMenuBar" name="menubar" >
+     <property name="sizePolicy" >
+      <sizepolicy vsizetype="Minimum" hsizetype="Expanding" >
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <widget class="QMenu" name="menuYate" >
+      <property name="title" >
+       <string>&amp;Yate</string>
+      </property>
+      <property name="separatorsCollapsible" >
+       <bool>true</bool>
+      </property>
+      <addaction name="separator" />
+      <addaction name="setStatusOnline" />
+      <addaction name="joinmuc_wizard" />
+      <addaction name="separator" />
+      <addaction name="separator" />
+      <addaction name="action_show_archive" />
+      <addaction name="separator" />
+      <addaction name="acc_new" />
+      <addaction name="acc_new_wizard" />
+      <addaction name="separator" />
+      <addaction name="separator" />
+      <addaction name="advanced_mode" />
+      <addaction name="separator" />
+      <addaction name="quit" />
+     </widget>
+     <widget class="QMenu" name="menuSettings" >
+      <property name="title" >
+       <string>&amp;Settings</string>
+      </property>
+      <property name="separatorsCollapsible" >
+       <bool>true</bool>
+      </property>
+      <addaction name="action_show_settings" />
+      <addaction name="action_show_accountlist" />
+      <addaction name="toggle_show_events" />
+      <addaction name="action_show_fileprogress" />
+      <addaction name="messages_show" />
+     </widget>
+     <widget class="QMenu" name="menuStatus" >
+      <property name="title" >
+       <string>S&amp;tatus</string>
+      </property>
+      <addaction name="setStatusOnline" />
+      <addaction name="setStatusBusy" />
+      <addaction name="setStatusAway" />
+      <addaction name="setStatusXa" />
+      <addaction name="setStatusDnd" />
+      <addaction name="separator" />
+      <addaction name="setStatusOffline" />
+     </widget>
+     <widget class="QMenu" name="menuFriends" >
+      <property name="title" >
+       <string>&amp;Friends</string>
+      </property>
+      <property name="_yate_menuNoCopy" stdset="0" >
+       <bool>true</bool>
+      </property>
+      <widget class="QMenu" name="menuSubscription" >
+       <property name="title" >
+        <string>Subscription</string>
+       </property>
+       <addaction name="chatcontact_subscribe" />
+       <addaction name="chatcontact_unsubscribe" />
+       <addaction name="chatcontact_unsubscribed" />
+      </widget>
+      <addaction name="chatcontact_new" />
+      <addaction name="chatroom_new" />
+      <addaction name="separator" />
+      <addaction name="chatcontact_chat" />
+      <addaction name="chatcontact_call" />
+      <addaction name="send_file" />
+      <addaction name="share_file" />
+      <addaction name="shared_file" />
+      <addaction name="chatcontact_showlog" />
+      <addaction name="chatcontact_info" />
+      <addaction name="chatcontact_edit" />
+      <addaction name="chatcontact_del" />
+      <addaction name="separator" />
+      <addaction name="menuSubscription" />
+      <addaction name="separator" />
+      <addaction name="chatcontact_showoffline" />
+      <addaction name="chatcontact_flatlist" />
+     </widget>
+     <addaction name="menuYate" />
+     <addaction name="menuSettings" />
+     <addaction name="menuFriends" />
+     <addaction name="menuStatus" />
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_7" >
+     <property name="sizePolicy" >
+      <sizepolicy vsizetype="Fixed" hsizetype="Preferred" >
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize" >
+      <size>
+       <width>0</width>
+       <height>6</height>
+      </size>
+     </property>
+     <property name="maximumSize" >
+      <size>
+       <width>16777215</width>
+       <height>6</height>
+      </size>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QTabWidget" name="mainwindowTabs" >
+     <property name="minimumSize" >
+      <size>
+       <width>0</width>
+       <height>200</height>
+      </size>
+     </property>
+     <property name="styleSheet" >
+      <string>QTabWidget::pane {
+  border: 0px solid #717fa0;
+  border-top: 1px solid #717fa0;
+}
+</string>
+     </property>
+     <property name="tabShape" >
+      <enum>QTabWidget::Rounded</enum>
+     </property>
+     <property name="currentIndex" >
+      <number>1</number>
+     </property>
+     <property name="iconSize" >
+      <size>
+       <width>20</width>
+       <height>20</height>
+      </size>
+     </property>
+     <widget class="QWidget" name="tabChat" >
+      <attribute name="title" >
+       <string>Chat</string>
+      </attribute>
+      <attribute name="icon" >
+       <iconset>chat_tab.png</iconset>
+      </attribute>
+      <layout class="QVBoxLayout" >
+       <property name="leftMargin" >
+        <number>0</number>
+       </property>
+       <property name="topMargin" >
+        <number>0</number>
+       </property>
+       <property name="rightMargin" >
+        <number>0</number>
+       </property>
+       <property name="bottomMargin" >
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QFrame" name="frame_chat_contacts" >
+         <property name="sizePolicy" >
+          <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="styleSheet" >
+          <string>QFrame#frame_chat_contacts {
+  border: 1px solid #717fa0;
+  border-top: 0px solid #717fa0;
+}
+</string>
+         </property>
+         <property name="frameShape" >
+          <enum>QFrame::StyledPanel</enum>
+         </property>
+         <property name="_yate_uiwidget" stdset="0" >
+          <bool>true</bool>
+         </property>
+         <property name="_yate_uiwidget_name" stdset="0" >
+          <string>chat_contacts</string>
+         </property>
+         <property name="_yate_uiwidget_class" stdset="0" >
+          <string>ContactList</string>
+         </property>
+         <property name="_yate_uiwidget_params" stdset="0" >
+          <stringlist>
+           <string>columns=name</string>
+           <string>htmldelegate=name</string>
+           <string>delegateparam.name.drawfocus=false</string>
+           <string>property:_yate_save_props=_yate_flatlist,_yate_showofflinecontacts,_yate_hideemptygroups,_yate_itemsexpstatus</string>
+           <string>property:_yate_flatlist=false</string>
+           <string>property:_yate_showofflinecontacts=true</string>
+           <string>property:_yate_hideemptygroups=true</string>
+           <string>property:_yate_sorting=name,true</string>
+           <string>property:_yate_horizontalheader=false</string>
+           <string>property:itemsExpandable=true</string>
+           <string>property:autoExpand=true</string>
+           <string>property:allColumnsShowFocus=false</string>
+           <string>property:indentation=0</string>
+           <string>property:_yate_nogroup_caption=Not set</string>
+           <string>property:_yate_comparecase=false</string>
+           <string>property:_yate_itemheight=contact:40</string>
+           <string>property:_yate_itemheight=group:22</string>
+           <string>property:_yate_itemheight=chatroom:40</string>
+           <string>property:_yate_itemstyle=contact:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" font-size:10px; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:13px;">${name}&lt;/span>&lt;br>${status_text}&lt;/br>&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itemstyle=group:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" font-size:10px; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:14px;">${name}${statistics}&lt;/span>&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itemstyle=chatroom:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" font-size:10px; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:13px;">${name}&lt;/span>&lt;br>${status_text}&lt;/br>&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itemselectedstyle=contact:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" color:white; font-size:10px; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:13px;">${name}&lt;/span>&lt;br>${status_text}&lt;/br>&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itemselectedstyle=group:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" font-size:10px; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:14px;">${name}${statistics}&lt;/span>&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itemselectedstyle=chatroom:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" color:white; font-size:10px; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:13px;">${name}&lt;/span>&lt;br>${status_text}&lt;/br>&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itembackground=group:color:white</string>
+           <string>property:_yate_itemstatewidget=group:name</string>
+           <string>property:_yate_itemexpandedimage=group:expanded.png</string>
+           <string>property:_yate_itemcollapsedimage=group:collapsed.png</string>
+           <string>property:_yate_itemstatstemplate=group: (${online}/${total})</string>
+           <string>property:_yate_itemtooltip=contact:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" font-family:'Arial'; font-size:10pt; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:14pt; font-weight:600;">${name}&lt;/span>&lt;/p>&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${status_text}&lt;/p>&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${contact}&lt;/p>&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Account: ${account}&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itemtooltip=group:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" font-family:'Arial'; font-size:10pt; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:14pt; font-weight:600;">${name}&lt;/span>&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itemtooltip=chatroom:&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">\np, li { white-space: pre-wrap; }\n&lt;/style>&lt;/head>&lt;body style=" font-family:'Arial'; font-size:10pt; font-weight:400; font-style:normal;">&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;span style=" font-size:14pt; font-weight:600;">${name}&lt;/span>&lt;/p>&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${status_text}&lt;/p>&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${contact}&lt;/p>&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Account: ${account}&lt;/p>&lt;/body>&lt;/html></string>
+           <string>property:_yate_itemmargins=contact:20</string>
+           <string>property:_yate_itemmargins=chatroom:20</string>
+           <string>property:_yate_itempaintbutton=contact:share_file</string>
+           <string>property:_yate_itempaintbuttonparam=contact:share_file:_yate_size:20</string>
+           <string>property:_yate_itempaintbuttonparam=contact:share_file:_yate_iconsize:20</string>
+           <string>property:_yate_itempaintbuttonparam=contact:share_file:_yate_normal_icon:sharefile_20.png</string>
+           <string>property:_yate_itempaintbuttonparam=contact:share_file:_yate_pressed_icon:sharefile_pressed_20.png</string>
+           <string>property:_yate_itempaintbuttonparam=contact:share_file:_yate_hover_icon:sharefile_hover_20.png</string>
+           <string>property:_yate_itempaintbutton=contact:shared_file</string>
+           <string>property:_yate_itempaintbuttonparam=contact:shared_file:_yate_size:20</string>
+           <string>property:_yate_itempaintbuttonparam=contact:shared_file:_yate_iconsize:20</string>
+           <string>property:_yate_itempaintbuttonparam=contact:shared_file:_yate_normal_icon:sharedfile_20.png</string>
+           <string>property:_yate_itempaintbuttonparam=contact:shared_file:_yate_pressed_icon:sharedfile_pressed_20.png</string>
+           <string>property:_yate_itempaintbuttonparam=contact:shared_file:_yate_hover_icon:sharedfile_hover_20.png</string>
+          </stringlist>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabTelephony" >
+      <property name="font" >
+       <font/>
+      </property>
+      <attribute name="title" >
+       <string>Telephony</string>
+      </attribute>
+      <attribute name="icon" >
+       <iconset>phone_tab.png</iconset>
+      </attribute>
+      <layout class="QVBoxLayout" >
+       <property name="spacing" >
+        <number>0</number>
+       </property>
+       <property name="leftMargin" >
+        <number>0</number>
+       </property>
+       <property name="topMargin" >
+        <number>0</number>
+       </property>
+       <property name="rightMargin" >
+        <number>0</number>
+       </property>
+       <property name="bottomMargin" >
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QFrame" name="frameButtons" >
+         <property name="sizePolicy" >
+          <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="minimumSize" >
+          <size>
+           <width>16</width>
+           <height>50</height>
+          </size>
+         </property>
+         <property name="maximumSize" >
+          <size>
+           <width>16777215</width>
+           <height>50</height>
+          </size>
+         </property>
+         <property name="styleSheet" >
+          <string>QFrame#frameButtons {
+  border: 1px solid #717fa0;
+  border-top: 0px;
+  border-bottom: 0px;
+}</string>
+         </property>
+         <layout class="QHBoxLayout" >
+          <property name="spacing" >
+           <number>1</number>
+          </property>
+          <property name="leftMargin" >
+           <number>4</number>
+          </property>
+          <property name="topMargin" >
+           <number>4</number>
+          </property>
+          <property name="rightMargin" >
+           <number>4</number>
+          </property>
+          <property name="bottomMargin" >
+           <number>0</number>
+          </property>
+          <item>
+           <widget class="QToolButton" name="ctrlCalls" >
+            <property name="minimumSize" >
+             <size>
+              <width>40</width>
+              <height>46</height>
+             </size>
+            </property>
+            <property name="maximumSize" >
+             <size>
+              <width>40</width>
+              <height>46</height>
+             </size>
+            </property>
+            <property name="toolTip" >
+             <string>Calls</string>
+            </property>
+            <property name="styleSheet" >
+             <string>QToolButton {
+  border: 0px solid #717fa0;
+  border-radius: 0px;
+  font-size: 11px;
+  background: transparent;
+}
+QToolButton:checked {
+  padding-top: 0px;
+  padding-left: 0px;
+}
+</string>
+            </property>
+            <property name="text" >
+             <string>Calls</string>
+            </property>
+            <property name="icon" >
+             <iconset>calls_tab.png</iconset>
+            </property>
+            <property name="iconSize" >
+             <size>
+              <width>26</width>
+              <height>26</height>
+             </size>
+            </property>
+            <property name="checkable" >
+             <bool>true</bool>
+            </property>
+            <property name="checked" >
+             <bool>true</bool>
+            </property>
+            <property name="autoExclusive" >
+             <bool>true</bool>
+            </property>
+            <property name="toolButtonStyle" >
+             <enum>Qt::ToolButtonTextUnderIcon</enum>
+            </property>
+            <property name="autoRaise" >
+             <bool>true</bool>
+            </property>
+            <property name="_yate_identity" stdset="0" >
+             <string>selectitem:framePages:PageCalls</string>
+            </property>
+            <property name="_yate_normal_icon" stdset="0" >
+             <string>calls_tab.png</string>
+            </property>
+            <property name="_yate_pressed_icon" stdset="0" >
+             <string>calls_tab_pressed.png</string>
+            </property>
+            <property name="_yate_hover_icon" stdset="0" >
+             <string>calls_tab_hover.png</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QToolButton" name="ctrlCDR" >
+            <property name="minimumSize" >
+             <size>
+              <width>46</width>
+              <height>46</height>
+             </size>
+            </property>
+            <property name="maximumSize" >
+             <size>
+              <width>46</width>
+              <height>46</height>
+             </size>
+            </property>
+            <property name="toolTip" >
+             <string>History</string>
+            </property>
+            <property name="styleSheet" >
+             <string>QToolButton {
+  border: 0px solid #717fa0;
+  border-radius: 0px;
+  font-size: 11px;
+  background: transparent;
+}
+QToolButton:checked {
+  padding-top: 0px;
+  padding-left: 0px;
+}
+</string>
+            </property>
+            <property name="text" >
+             <string>History</string>
+            </property>
+            <property name="icon" >
+             <iconset>cdr_tab.png</iconset>
+            </property>
+            <property name="iconSize" >
+             <size>
+              <width>26</width>
+              <height>26</height>
+             </size>
+            </property>
+            <property name="checkable" >
+             <bool>true</bool>
+            </property>
+            <property name="checked" >
+             <bool>false</bool>
+            </property>
+            <property name="autoExclusive" >
+             <bool>true</bool>
+            </property>
+            <property name="toolButtonStyle" >
+             <enum>Qt::ToolButtonTextUnderIcon</enum>
+            </property>
+            <property name="autoRaise" >
+             <bool>true</bool>
+            </property>
+            <property name="_yate_identity" stdset="0" >
+             <string>selectitem:framePages:PageCDR</string>
+            </property>
+            <property name="_yate_normal_icon" stdset="0" >
+             <string>cdr_tab.png</string>
+            </property>
+            <property name="_yate_pressed_icon" stdset="0" >
+             <string>cdr_tab_pressed.png</string>
+            </property>
+            <property name="_yate_hover_icon" stdset="0" >
+             <string>cdr_tab_hover.png</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QToolButton" name="ctrlContacts" >
+            <property name="minimumSize" >
+             <size>
+              <width>50</width>
+              <height>46</height>
+             </size>
+            </property>
+            <property name="maximumSize" >
+             <size>
+              <width>50</width>
+              <height>46</height>
+             </size>
+            </property>
+            <property name="toolTip" >
+             <string>Address Book</string>
+            </property>
+            <property name="styleSheet" >
+             <string>QToolButton {
+  border: 0px solid #717fa0;
+  border-radius: 0px;
+  font-size: 11px;
+  background: transparent;
+}
+QToolButton:checked {
+  padding-top: 0px;
+  padding-left: 0px;
+}
+</string>
+            </property>
+            <property name="text" >
+             <string>Contacts</string>
+            </property>
+            <property name="icon" >
+             <iconset>contacts_tab.png</iconset>
+            </property>
+            <property name="iconSize" >
+             <size>
+              <width>26</width>
+              <height>26</height>
+             </size>
+            </property>
+            <property name="checkable" >
+             <bool>true</bool>
+            </property>
+            <property name="checked" >
+             <bool>false</bool>
+            </property>
+            <property name="autoExclusive" >
+             <bool>true</bool>
+            </property>
+            <property name="toolButtonStyle" >
+             <enum>Qt::ToolButtonTextUnderIcon</enum>
+            </property>
+            <property name="autoRaise" >
+             <bool>true</bool>
+            </property>
+            <property name="_yate_identity" stdset="0" >
+             <string>selectitem:framePages:PageContacts</string>
+            </property>
+            <property name="_yate_normal_icon" stdset="0" >
+             <string>contacts_tab.png</string>
+            </property>
+            <property name="_yate_pressed_icon" stdset="0" >
+             <string>contacts_tab_pressed.png</string>
+            </property>
+            <property name="_yate_hover_icon" stdset="0" >
+             <string>contacts_tab_hover.png</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer>
+            <property name="orientation" >
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" >
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QStackedWidget" name="framePages" >
+         <property name="sizePolicy" >
+          <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+           <horstretch>0</horstretch>
+           <verstretch>1</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="minimumSize" >
+          <size>
+           <width>0</width>
+           <height>200</height>
+          </size>
+         </property>
+         <property name="currentIndex" >
+          <number>0</number>
+         </property>
+         <widget class="QWidget" name="PageCalls" >
+          <property name="sizePolicy" >
+           <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <layout class="QVBoxLayout" >
+           <property name="spacing" >
+            <number>0</number>
+           </property>
+           <property name="leftMargin" >
+            <number>0</number>
+           </property>
+           <property name="topMargin" >
+            <number>0</number>
+           </property>
+           <property name="rightMargin" >
+            <number>0</number>
+           </property>
+           <property name="bottomMargin" >
+            <number>0</number>
+           </property>
+           <item>
+            <widget class="QFrame" name="framePageCalls" >
+             <property name="sizePolicy" >
+              <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+               <horstretch>0</horstretch>
+               <verstretch>0</verstretch>
+              </sizepolicy>
+             </property>
+             <layout class="QVBoxLayout" >
+              <property name="spacing" >
+               <number>0</number>
+              </property>
+              <property name="leftMargin" >
+               <number>0</number>
+              </property>
+              <property name="topMargin" >
+               <number>0</number>
+              </property>
+              <property name="rightMargin" >
+               <number>0</number>
+              </property>
+              <property name="bottomMargin" >
+               <number>0</number>
+              </property>
+              <item>
+               <widget class="QFrame" name="frame_3" >
+                <property name="sizePolicy" >
+                 <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                  <horstretch>0</horstretch>
+                  <verstretch>0</verstretch>
+                 </sizepolicy>
+                </property>
+                <property name="minimumSize" >
+                 <size>
+                  <width>16</width>
+                  <height>12</height>
+                 </size>
+                </property>
+                <property name="maximumSize" >
+                 <size>
+                  <width>16777215</width>
+                  <height>12</height>
+                 </size>
+                </property>
+                <property name="styleSheet" >
+                 <string>  border: 1px solid #717fa0;
+  border-bottom: 0px;
+  border-top: 0px;
+</string>
+                </property>
+                <layout class="QHBoxLayout" >
+                 <property name="spacing" >
+                  <number>0</number>
+                 </property>
+                 <property name="leftMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="topMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="rightMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="bottomMargin" >
+                  <number>0</number>
+                 </property>
+                 <item>
+                  <widget class="QLabel" name="label_8" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Fixed" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>15</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>15</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;
+border-bottom: 1px solid #717fa0;
+</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QLabel" name="label_9" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Fixed" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>18</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>18</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;</string>
+                   </property>
+                   <property name="text" >
+                    <string/>
+                   </property>
+                   <property name="pixmap" >
+                    <pixmap>pointer.png</pixmap>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QLabel" name="label_10" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;
+border-bottom: 1px solid #717fa0;
+</string>
+                   </property>
+                  </widget>
+                 </item>
+                </layout>
+               </widget>
+              </item>
+              <item>
+               <widget class="QFrame" name="frame" >
+                <property name="styleSheet" >
+                 <string>QFrame#frame {
+  border: 1px solid #717fa0;
+  border-top: 0px;
+}</string>
+                </property>
+                <layout class="QVBoxLayout" >
+                 <property name="spacing" >
+                  <number>6</number>
+                 </property>
+                 <property name="leftMargin" >
+                  <number>8</number>
+                 </property>
+                 <property name="topMargin" >
+                  <number>4</number>
+                 </property>
+                 <property name="rightMargin" >
+                  <number>8</number>
+                 </property>
+                 <property name="bottomMargin" >
+                  <number>6</number>
+                 </property>
+                 <item>
+                  <layout class="QHBoxLayout" >
+                   <property name="spacing" >
+                    <number>2</number>
+                   </property>
+                   <item>
+                    <widget class="QFrame" name="frame_call_protocol" >
+                     <property name="sizePolicy" >
+                      <sizepolicy vsizetype="Minimum" hsizetype="Fixed" >
+                       <horstretch>0</horstretch>
+                       <verstretch>0</verstretch>
+                      </sizepolicy>
+                     </property>
+                     <property name="minimumSize" >
+                      <size>
+                       <width>80</width>
+                       <height>16</height>
+                      </size>
+                     </property>
+                     <property name="maximumSize" >
+                      <size>
+                       <width>80</width>
+                       <height>16777215</height>
+                      </size>
+                     </property>
+                     <layout class="QVBoxLayout" >
+                      <property name="spacing" >
+                       <number>0</number>
+                      </property>
+                      <property name="leftMargin" >
+                       <number>0</number>
+                      </property>
+                      <property name="topMargin" >
+                       <number>0</number>
+                      </property>
+                      <property name="rightMargin" >
+                       <number>0</number>
+                      </property>
+                      <property name="bottomMargin" >
+                       <number>0</number>
+                      </property>
+                      <item>
+                       <widget class="QLabel" name="protocolLabel" >
+                        <property name="sizePolicy" >
+                         <sizepolicy vsizetype="Preferred" hsizetype="Fixed" >
+                          <horstretch>0</horstretch>
+                          <verstretch>0</verstretch>
+                         </sizepolicy>
+                        </property>
+                        <property name="minimumSize" >
+                         <size>
+                          <width>80</width>
+                          <height>0</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>80</width>
+                          <height>16777215</height>
+                         </size>
+                        </property>
+                        <property name="text" >
+                         <string>Protocol</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item>
+                       <widget class="QComboBox" name="protocol" >
+                        <property name="sizePolicy" >
+                         <sizepolicy vsizetype="Fixed" hsizetype="Fixed" >
+                          <horstretch>0</horstretch>
+                          <verstretch>0</verstretch>
+                         </sizepolicy>
+                        </property>
+                        <property name="minimumSize" >
+                         <size>
+                          <width>80</width>
+                          <height>25</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>80</width>
+                          <height>25</height>
+                         </size>
+                        </property>
+                        <property name="toolTip" >
+                         <string>VoIP protocol used to make a direct call</string>
+                        </property>
+                       </widget>
+                      </item>
+                     </layout>
+                    </widget>
+                   </item>
+                   <item>
+                    <widget class="QFrame" name="frame_call_account" >
+                     <property name="sizePolicy" >
+                      <sizepolicy vsizetype="Minimum" hsizetype="Expanding" >
+                       <horstretch>0</horstretch>
+                       <verstretch>0</verstretch>
+                      </sizepolicy>
+                     </property>
+                     <layout class="QVBoxLayout" >
+                      <property name="spacing" >
+                       <number>0</number>
+                      </property>
+                      <property name="leftMargin" >
+                       <number>0</number>
+                      </property>
+                      <property name="topMargin" >
+                       <number>0</number>
+                      </property>
+                      <property name="rightMargin" >
+                       <number>0</number>
+                      </property>
+                      <property name="bottomMargin" >
+                       <number>0</number>
+                      </property>
+                      <item>
+                       <widget class="QLabel" name="accountLabel" >
+                        <property name="sizePolicy" >
+                         <sizepolicy vsizetype="Preferred" hsizetype="Expanding" >
+                          <horstretch>0</horstretch>
+                          <verstretch>0</verstretch>
+                         </sizepolicy>
+                        </property>
+                        <property name="text" >
+                         <string>Account</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item>
+                       <widget class="QComboBox" name="account" >
+                        <property name="sizePolicy" >
+                         <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                          <horstretch>0</horstretch>
+                          <verstretch>0</verstretch>
+                         </sizepolicy>
+                        </property>
+                        <property name="minimumSize" >
+                         <size>
+                          <width>0</width>
+                          <height>25</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>16777215</width>
+                          <height>25</height>
+                         </size>
+                        </property>
+                        <property name="toolTip" >
+                         <string>Account used to call through a server</string>
+                        </property>
+                       </widget>
+                      </item>
+                     </layout>
+                    </widget>
+                   </item>
+                  </layout>
+                 </item>
+                 <item>
+                  <layout class="QVBoxLayout" >
+                   <property name="spacing" >
+                    <number>0</number>
+                   </property>
+                   <item>
+                    <layout class="QHBoxLayout" >
+                     <property name="spacing" >
+                      <number>2</number>
+                     </property>
+                     <property name="leftMargin" >
+                      <number>0</number>
+                     </property>
+                     <property name="topMargin" >
+                      <number>0</number>
+                     </property>
+                     <property name="rightMargin" >
+                      <number>0</number>
+                     </property>
+                     <property name="bottomMargin" >
+                      <number>0</number>
+                     </property>
+                     <item>
+                      <widget class="QToolButton" name="display_keypad" >
+                       <property name="minimumSize" >
+                        <size>
+                         <width>24</width>
+                         <height>24</height>
+                        </size>
+                       </property>
+                       <property name="maximumSize" >
+                        <size>
+                         <width>24</width>
+                         <height>24</height>
+                        </size>
+                       </property>
+                       <property name="toolTip" >
+                        <string>Show or hide the keypad</string>
+                       </property>
+                       <property name="icon" >
+                        <iconset>dialpad_20.png</iconset>
+                       </property>
+                       <property name="iconSize" >
+                        <size>
+                         <width>20</width>
+                         <height>20</height>
+                        </size>
+                       </property>
+                       <property name="checkable" >
+                        <bool>true</bool>
+                       </property>
+                       <property name="_yate_normal_icon" stdset="0" >
+                        <string>dialpad_20.png</string>
+                       </property>
+                       <property name="_yate_hover_icon" stdset="0" >
+                        <string>dialpad_20_hover.png</string>
+                       </property>
+                       <property name="_yate_pressed_icon" stdset="0" >
+                        <string>dialpad_20_pressed.png</string>
+                       </property>
+                      </widget>
+                     </item>
+                     <item>
+                      <widget class="QFrame" name="frame_5" >
+                       <property name="sizePolicy" >
+                        <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                         <horstretch>0</horstretch>
+                         <verstretch>0</verstretch>
+                        </sizepolicy>
+                       </property>
+                       <property name="minimumSize" >
+                        <size>
+                         <width>16</width>
+                         <height>24</height>
+                        </size>
+                       </property>
+                       <property name="maximumSize" >
+                        <size>
+                         <width>16777215</width>
+                         <height>24</height>
+                        </size>
+                       </property>
+                       <layout class="QHBoxLayout" >
+                        <property name="leftMargin" >
+                         <number>0</number>
+                        </property>
+                        <property name="topMargin" >
+                         <number>0</number>
+                        </property>
+                        <property name="rightMargin" >
+                         <number>0</number>
+                        </property>
+                        <property name="bottomMargin" >
+                         <number>0</number>
+                        </property>
+                        <item>
+                         <widget class="QComboBox" name="callto" >
+                          <property name="sizePolicy" >
+                           <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                            <horstretch>0</horstretch>
+                            <verstretch>0</verstretch>
+                           </sizepolicy>
+                          </property>
+                          <property name="minimumSize" >
+                           <size>
+                            <width>0</width>
+                            <height>24</height>
+                           </size>
+                          </property>
+                          <property name="maximumSize" >
+                           <size>
+                            <width>16777215</width>
+                            <height>24</height>
+                           </size>
+                          </property>
+                          <property name="focusPolicy" >
+                           <enum>Qt::WheelFocus</enum>
+                          </property>
+                          <property name="acceptDrops" >
+                           <bool>true</bool>
+                          </property>
+                          <property name="editable" >
+                           <bool>true</bool>
+                          </property>
+                          <property name="maxCount" >
+                           <number>20</number>
+                          </property>
+                          <property name="dynamicActionEnter" stdset="0" >
+                           <string>call</string>
+                          </property>
+                          <property name="dynamicActionReturn" stdset="0" >
+                           <string>call</string>
+                          </property>
+                          <property name="_yate_filterevents" stdset="0" >
+                           <bool>true</bool>
+                          </property>
+                          <property name="dynamicActionReturnFilter" stdset="0" >
+                           <bool>true</bool>
+                          </property>
+                          <property name="dynamicActionEnterFilter" stdset="0" >
+                           <bool>true</bool>
+                          </property>
+                          <property name="_yate_textchangednotify" stdset="0" >
+                           <bool>true</bool>
+                          </property>
+                         </widget>
+                        </item>
+                       </layout>
+                      </widget>
+                     </item>
+                     <item>
+                      <widget class="QToolButton" name="call" >
+                       <property name="sizePolicy" >
+                        <sizepolicy vsizetype="Fixed" hsizetype="Minimum" >
+                         <horstretch>0</horstretch>
+                         <verstretch>0</verstretch>
+                        </sizepolicy>
+                       </property>
+                       <property name="minimumSize" >
+                        <size>
+                         <width>0</width>
+                         <height>24</height>
+                        </size>
+                       </property>
+                       <property name="maximumSize" >
+                        <size>
+                         <width>16777215</width>
+                         <height>24</height>
+                        </size>
+                       </property>
+                       <property name="toolTip" >
+                        <string>Make a new call</string>
+                       </property>
+                       <property name="text" >
+                        <string>Call</string>
+                       </property>
+                       <property name="icon" >
+                        <iconset>call.png</iconset>
+                       </property>
+                       <property name="iconSize" >
+                        <size>
+                         <width>20</width>
+                         <height>20</height>
+                        </size>
+                       </property>
+                       <property name="toolButtonStyle" >
+                        <enum>Qt::ToolButtonTextBesideIcon</enum>
+                       </property>
+                       <property name="_yate_hover_icon" stdset="0" >
+                        <string>call_hover.png</string>
+                       </property>
+                       <property name="_yate_normal_icon" stdset="0" >
+                        <string>call.png</string>
+                       </property>
+                       <property name="_yate_pressed_icon" stdset="0" >
+                        <string>call_pressed.png</string>
+                       </property>
+                      </widget>
+                     </item>
+                    </layout>
+                   </item>
+                   <item>
+                    <widget class="QLabel" name="callto_hint" >
+                     <property name="sizePolicy" >
+                      <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                       <horstretch>0</horstretch>
+                       <verstretch>0</verstretch>
+                      </sizepolicy>
+                     </property>
+                     <property name="styleSheet" >
+                      <string>font-size: 10px;</string>
+                     </property>
+                     <property name="text" >
+                      <string/>
+                     </property>
+                    </widget>
+                   </item>
+                  </layout>
+                 </item>
+                 <item>
+                  <widget class="QFrame" name="keypad" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Preferred" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>100</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>100</height>
+                    </size>
+                   </property>
+                   <layout class="QHBoxLayout" >
+                    <property name="topMargin" >
+                     <number>2</number>
+                    </property>
+                    <property name="bottomMargin" >
+                     <number>2</number>
+                    </property>
+                    <item>
+                     <layout class="QGridLayout" >
+                      <property name="horizontalSpacing" >
+                       <number>1</number>
+                      </property>
+                      <property name="verticalSpacing" >
+                       <number>1</number>
+                      </property>
+                      <item row="0" column="0" >
+                       <widget class="QToolButton" name="digit_1" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit1.png);}
+QToolButton::hover {image:url(digit1_hover.png);}
+QToolButton::hover:pressed {image:url(digit1_pressed.png);}
+QToolButton:pressed {image:url(digit1.png);}
+QToolButton:!enabled {image:url(digit1.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:1</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="0" column="1" >
+                       <widget class="QToolButton" name="digit_2" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit2.png);}
+QToolButton::hover {image:url(digit2_hover.png);}
+QToolButton::hover:pressed {image:url(digit2_pressed.png);}
+QToolButton:pressed {image:url(digit2.png);}
+QToolButton:!enabled {image:url(digit2.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:2</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="0" column="2" >
+                       <widget class="QToolButton" name="digit_3" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit3.png);}
+QToolButton::hover {image:url(digit3_hover.png);}
+QToolButton::hover:pressed {image:url(digit3_pressed.png);}
+QToolButton:pressed {image:url(digit3.png);}
+QToolButton:!enabled {image:url(digit3.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:3</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="1" column="0" >
+                       <widget class="QToolButton" name="digit_4" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit4.png);}
+QToolButton::hover {image:url(digit4_hover.png);}
+QToolButton::hover:pressed {image:url(digit4_pressed.png);}
+QToolButton:pressed {image:url(digit4.png);}
+QToolButton:!enabled {image:url(digit4.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:4</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="1" column="1" >
+                       <widget class="QToolButton" name="digit_5" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit5.png);}
+QToolButton::hover {image:url(digit5_hover.png);}
+QToolButton::hover:pressed {image:url(digit5_pressed.png);}
+QToolButton:pressed {image:url(digit5.png);}
+QToolButton:!enabled {image:url(digit5.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:5</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="1" column="2" >
+                       <widget class="QToolButton" name="digit_6" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit6.png);}
+QToolButton::hover {image:url(digit6_hover.png);}
+QToolButton::hover:pressed {image:url(digit6_pressed.png);}
+QToolButton:pressed {image:url(digit6.png);}
+QToolButton:!enabled {image:url(digit6.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:6</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="2" column="0" >
+                       <widget class="QToolButton" name="digit_7" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit7.png);}
+QToolButton::hover {image:url(digit7_hover.png);}
+QToolButton::hover:pressed {image:url(digit7_pressed.png);}
+QToolButton:pressed {image:url(digit7.png);}
+QToolButton:!enabled {image:url(digit7.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:7</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="2" column="1" >
+                       <widget class="QToolButton" name="digit_8" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit8.png);}
+QToolButton::hover {image:url(digit8_hover.png);}
+QToolButton::hover:pressed {image:url(digit8_pressed.png);}
+QToolButton:pressed {image:url(digit8.png);}
+QToolButton:!enabled {image:url(digit8.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:8</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="2" column="2" >
+                       <widget class="QToolButton" name="digit_9" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit9.png);}
+QToolButton::hover {image:url(digit9_hover.png);}
+QToolButton::hover:pressed {image:url(digit9_pressed.png);}
+QToolButton:pressed {image:url(digit9.png);}
+QToolButton:!enabled {image:url(digit9.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:9</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="3" column="0" >
+                       <widget class="QToolButton" name="digit_star" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digitstar.png);}
+QToolButton::hover {image:url(digitstar_hover.png);}
+QToolButton::hover:pressed {image:url(digitstar_pressed.png);}
+QToolButton:pressed {image:url(digitstar.png);}
+QToolButton:!enabled {image:url(digitstar.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:*</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="3" column="1" >
+                       <widget class="QToolButton" name="digit_0" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digit0.png);}
+QToolButton::hover {image:url(digit0_hover.png);}
+QToolButton::hover:pressed {image:url(digit0_pressed.png);}
+QToolButton:pressed {image:url(digit0.png);}
+QToolButton:!enabled {image:url(digit0.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:0</string>
+                        </property>
+                       </widget>
+                      </item>
+                      <item row="3" column="2" >
+                       <widget class="QToolButton" name="digit_pound" >
+                        <property name="minimumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="maximumSize" >
+                         <size>
+                          <width>42</width>
+                          <height>23</height>
+                         </size>
+                        </property>
+                        <property name="styleSheet" >
+                         <string>QToolButton {border: 0px; image:url(digitpound.png);}
+QToolButton::hover {image:url(digitpound_hover.png);}
+QToolButton::hover:pressed {image:url(digitpound_pressed.png);}
+QToolButton:pressed {image:url(digitpound.png);}
+QToolButton:!enabled {image:url(digitpound.png);}
+</string>
+                        </property>
+                        <property name="_yate_identity" stdset="0" >
+                         <string>digit:#</string>
+                        </property>
+                       </widget>
+                      </item>
+                     </layout>
+                    </item>
+                   </layout>
+                  </widget>
+                 </item>
+                </layout>
+               </widget>
+              </item>
+              <item>
+               <widget class="QFrame" name="frame_7" >
+                <property name="sizePolicy" >
+                 <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                  <horstretch>0</horstretch>
+                  <verstretch>0</verstretch>
+                 </sizepolicy>
+                </property>
+                <property name="minimumSize" >
+                 <size>
+                  <width>16</width>
+                  <height>8</height>
+                 </size>
+                </property>
+                <property name="maximumSize" >
+                 <size>
+                  <width>16777215</width>
+                  <height>8</height>
+                 </size>
+                </property>
+                <property name="styleSheet" >
+                 <string>QFrame {
+  background:#f7f5fd;
+}
+</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QFrame" name="frame_channels" >
+                <property name="sizePolicy" >
+                 <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+                  <horstretch>0</horstretch>
+                  <verstretch>0</verstretch>
+                 </sizepolicy>
+                </property>
+                <property name="styleSheet" >
+                 <string>QFrame#frame_channels{
+  border: 1px solid #717fa0;
+}
+</string>
+                </property>
+                <property name="_yate_uiwidget" stdset="0" >
+                 <bool>true</bool>
+                </property>
+                <property name="_yate_uiwidget_name" stdset="0" >
+                 <string>channels</string>
+                </property>
+                <property name="_yate_uiwidget_class" stdset="0" >
+                 <string>QtCustomTree</string>
+                </property>
+                <property name="_yate_uiwidget_params" stdset="0" >
+                 <stringlist>
+                  <string>vertical_scroll_policy=pixel</string>
+                  <string>property:_yate_horizontalheader=false</string>
+                  <string>property:itemsExpandable=false</string>
+                  <string>property:rootIsDecorated=false</string>
+                  <string>property:allColumnsShowFocus=false</string>
+                  <string>property:styleSheet=QTreeWidget {background: #f3faff;}</string>
+                  <string>property:_yate_itemui=channel_item.ui</string>
+                  <string>property:_yate_itemstyle=:QWidget#${name}{background: #f3faff;} QFrame#${name}_frame{background: #ffffff; border: 1px solid #717fa0;}</string>
+                  <string>property:_yate_itemheight=86</string>
+                 </stringlist>
+                </property>
+               </widget>
+              </item>
+             </layout>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+         <widget class="QWidget" name="PageCDR" >
+          <property name="sizePolicy" >
+           <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <layout class="QVBoxLayout" >
+           <property name="spacing" >
+            <number>0</number>
+           </property>
+           <property name="leftMargin" >
+            <number>0</number>
+           </property>
+           <property name="topMargin" >
+            <number>0</number>
+           </property>
+           <property name="rightMargin" >
+            <number>0</number>
+           </property>
+           <property name="bottomMargin" >
+            <number>0</number>
+           </property>
+           <item>
+            <widget class="QFrame" name="frame_page_log" >
+             <property name="styleSheet" >
+              <string>QFrame#frame_page_log {
+  border: 0px solid #717fa0;
+  border-top: 0px solid #717fa0;
+}
+</string>
+             </property>
+             <property name="frameShape" >
+              <enum>QFrame::StyledPanel</enum>
+             </property>
+             <layout class="QVBoxLayout" >
+              <property name="spacing" >
+               <number>0</number>
+              </property>
+              <property name="leftMargin" >
+               <number>0</number>
+              </property>
+              <property name="topMargin" >
+               <number>0</number>
+              </property>
+              <property name="rightMargin" >
+               <number>0</number>
+              </property>
+              <property name="bottomMargin" >
+               <number>0</number>
+              </property>
+              <item>
+               <widget class="QFrame" name="frame_2" >
+                <property name="sizePolicy" >
+                 <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                  <horstretch>0</horstretch>
+                  <verstretch>0</verstretch>
+                 </sizepolicy>
+                </property>
+                <property name="minimumSize" >
+                 <size>
+                  <width>16</width>
+                  <height>12</height>
+                 </size>
+                </property>
+                <property name="maximumSize" >
+                 <size>
+                  <width>16777215</width>
+                  <height>12</height>
+                 </size>
+                </property>
+                <property name="styleSheet" >
+                 <string>  border: 1px solid #717fa0;
+  border-bottom: 0px;
+  border-top: 0px;
+</string>
+                </property>
+                <layout class="QHBoxLayout" >
+                 <property name="spacing" >
+                  <number>0</number>
+                 </property>
+                 <property name="leftMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="topMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="rightMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="bottomMargin" >
+                  <number>0</number>
+                 </property>
+                 <item>
+                  <widget class="QLabel" name="label" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Fixed" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>60</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>60</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;
+border-bottom: 1px solid #717fa0;
+</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QLabel" name="label_3" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Fixed" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>18</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>18</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;</string>
+                   </property>
+                   <property name="text" >
+                    <string/>
+                   </property>
+                   <property name="pixmap" >
+                    <pixmap>pointer.png</pixmap>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QLabel" name="label_4" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;
+border-bottom: 1px solid #717fa0;
+</string>
+                   </property>
+                  </widget>
+                 </item>
+                </layout>
+               </widget>
+              </item>
+              <item>
+               <widget class="QFrame" name="frame_log" >
+                <property name="sizePolicy" >
+                 <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+                  <horstretch>0</horstretch>
+                  <verstretch>0</verstretch>
+                 </sizepolicy>
+                </property>
+                <property name="styleSheet" >
+                 <string>QFrame#frame_log {
+  border: 1px solid #717fa0;
+  border-bottom: 0px solid #717fa0;
+}
+</string>
+                </property>
+                <property name="_yate_uiwidget" stdset="0" >
+                 <bool>true</bool>
+                </property>
+                <property name="_yate_uiwidget_name" stdset="0" >
+                 <string>log</string>
+                </property>
+                <property name="_yate_uiwidget_class" stdset="0" >
+                 <string>QtCustomTree</string>
+                </property>
+                <property name="_yate_uiwidget_params" stdset="0" >
+                 <stringlist>
+                  <string>property:_yate_save_props=_yate_sorting,_yate_col_widths</string>
+                  <string>property:tabKeyNavigation=false</string>
+                  <string>property:sortingEnabled=true</string>
+                  <string>property:allColumnsShowFocus=true</string>
+                  <string>property:_yate_horizontalheader=true</string>
+                  <string>property:_yate_notifyonenterpressed=true</string>
+                  <string>property:_yate_notifyitemchanged=true</string>
+                  <string>property:_yate_itemheight=0</string>
+                  <string>property:_yate_sorting=time,false</string>
+                  <string>property:_yate_itemheight=20</string>
+                  <string>columns=Enabled,Party,Time,Duration</string>
+                  <string>columns.check=enabled</string>
+                  <string>columns.width=30</string>
+                  <string>columns.resize=fixed</string>
+                  <string>columns.allowemptytitle=enabled</string>
+                  <string>griddraw_right_color=#e3e6e9</string>
+                  <string>griddraw_bottom_color=#a9c2c2</string>
+                 </stringlist>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QFrame" name="frame_log_buttons" >
+                <property name="styleSheet" >
+                 <string>QFrame#frame_log_buttons {
+  border: 1px solid #717fa0;
+}
+</string>
+                </property>
+                <property name="frameShape" >
+                 <enum>QFrame::StyledPanel</enum>
+                </property>
+                <layout class="QHBoxLayout" >
+                 <property name="spacing" >
+                  <number>4</number>
+                 </property>
+                 <property name="leftMargin" >
+                  <number>4</number>
+                 </property>
+                 <item>
+                  <widget class="QToolButton" name="log_call" >
+                   <property name="enabled" >
+                    <bool>false</bool>
+                   </property>
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Minimum" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="toolTip" >
+                    <string>Call back this number</string>
+                   </property>
+                   <property name="text" >
+                    <string>Call</string>
+                   </property>
+                   <property name="icon" >
+                    <iconset>call.png</iconset>
+                   </property>
+                   <property name="toolButtonStyle" >
+                    <enum>Qt::ToolButtonTextBesideIcon</enum>
+                   </property>
+                   <property name="_yate_normal_icon" stdset="0" >
+                    <string>call.png</string>
+                   </property>
+                   <property name="_yate_pressed_icon" stdset="0" >
+                    <string>call_pressed.png</string>
+                   </property>
+                   <property name="_yate_hover_icon" stdset="0" >
+                    <string>call_hover.png</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QToolButton" name="log_clear" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Minimum" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="toolTip" >
+                    <string>Clear all calls log</string>
+                   </property>
+                   <property name="text" >
+                    <string>Clear</string>
+                   </property>
+                   <property name="icon" >
+                    <iconset>clear.png</iconset>
+                   </property>
+                   <property name="toolButtonStyle" >
+                    <enum>Qt::ToolButtonTextBesideIcon</enum>
+                   </property>
+                   <property name="_yate_identity" stdset="0" >
+                    <string>clear:log:</string>
+                   </property>
+                   <property name="_yate_normal_icon" stdset="0" >
+                    <string>clear.png</string>
+                   </property>
+                   <property name="_yate_pressed_icon" stdset="0" >
+                    <string>clear_pressed.png</string>
+                   </property>
+                   <property name="_yate_hover_icon" stdset="0" >
+                    <string>clear_hover.png</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QToolButton" name="log_del" >
+                   <property name="enabled" >
+                    <bool>false</bool>
+                   </property>
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Minimum" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="toolTip" >
+                    <string>Delete the selected call log</string>
+                   </property>
+                   <property name="text" >
+                    <string>Delete</string>
+                   </property>
+                   <property name="icon" >
+                    <iconset>delete.png</iconset>
+                   </property>
+                   <property name="toolButtonStyle" >
+                    <enum>Qt::ToolButtonTextBesideIcon</enum>
+                   </property>
+                   <property name="_yate_identity" stdset="0" >
+                    <string>deletecheckeditems:log:</string>
+                   </property>
+                   <property name="_yate_normal_icon" stdset="0" >
+                    <string>delete.png</string>
+                   </property>
+                   <property name="_yate_pressed_icon" stdset="0" >
+                    <string>delete_pressed.png</string>
+                   </property>
+                   <property name="_yate_hover_icon" stdset="0" >
+                    <string>delete_hover.png</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QToolButton" name="log_contact" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Minimum" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="toolTip" >
+                    <string>Turn this number into a contact</string>
+                   </property>
+                   <property name="text" >
+                    <string>Contact</string>
+                   </property>
+                   <property name="icon" >
+                    <iconset>add.png</iconset>
+                   </property>
+                   <property name="toolButtonStyle" >
+                    <enum>Qt::ToolButtonTextBesideIcon</enum>
+                   </property>
+                   <property name="_yate_normal_icon" stdset="0" >
+                    <string>add.png</string>
+                   </property>
+                   <property name="_yate_pressed_icon" stdset="0" >
+                    <string>add_pressed.png</string>
+                   </property>
+                   <property name="_yate_hover_icon" stdset="0" >
+                    <string>add_hover.png</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <spacer>
+                   <property name="orientation" >
+                    <enum>Qt::Horizontal</enum>
+                   </property>
+                   <property name="sizeHint" >
+                    <size>
+                     <width>40</width>
+                     <height>20</height>
+                    </size>
+                   </property>
+                  </spacer>
+                 </item>
+                </layout>
+               </widget>
+              </item>
+             </layout>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+         <widget class="QWidget" name="PageContacts" >
+          <property name="sizePolicy" >
+           <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <layout class="QVBoxLayout" >
+           <property name="spacing" >
+            <number>0</number>
+           </property>
+           <property name="leftMargin" >
+            <number>0</number>
+           </property>
+           <property name="topMargin" >
+            <number>0</number>
+           </property>
+           <property name="rightMargin" >
+            <number>0</number>
+           </property>
+           <property name="bottomMargin" >
+            <number>0</number>
+           </property>
+           <item>
+            <widget class="QFrame" name="frame_page_contacts" >
+             <property name="styleSheet" >
+              <string>QFrame#frame_page_contacts {
+  border: 0px solid #717fa0;
+  border-left: 1px solid #717fa0;
+  border-right: 1px solid #717fa0;
+}
+</string>
+             </property>
+             <property name="frameShape" >
+              <enum>QFrame::StyledPanel</enum>
+             </property>
+             <layout class="QVBoxLayout" >
+              <property name="spacing" >
+               <number>0</number>
+              </property>
+              <property name="leftMargin" >
+               <number>0</number>
+              </property>
+              <property name="topMargin" >
+               <number>0</number>
+              </property>
+              <property name="rightMargin" >
+               <number>0</number>
+              </property>
+              <property name="bottomMargin" >
+               <number>0</number>
+              </property>
+              <item>
+               <widget class="QFrame" name="frame_4" >
+                <property name="sizePolicy" >
+                 <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                  <horstretch>0</horstretch>
+                  <verstretch>0</verstretch>
+                 </sizepolicy>
+                </property>
+                <property name="minimumSize" >
+                 <size>
+                  <width>16</width>
+                  <height>12</height>
+                 </size>
+                </property>
+                <property name="maximumSize" >
+                 <size>
+                  <width>16777215</width>
+                  <height>12</height>
+                 </size>
+                </property>
+                <property name="styleSheet" >
+                 <string>border: 0px solid #717fa0;
+</string>
+                </property>
+                <layout class="QHBoxLayout" >
+                 <property name="spacing" >
+                  <number>0</number>
+                 </property>
+                 <property name="leftMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="topMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="rightMargin" >
+                  <number>0</number>
+                 </property>
+                 <property name="bottomMargin" >
+                  <number>0</number>
+                 </property>
+                 <item>
+                  <widget class="QLabel" name="label_2" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Fixed" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>106</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>106</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;
+border-bottom: 1px solid #717fa0;
+</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QLabel" name="label_5" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Fixed" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>18</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>18</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;</string>
+                   </property>
+                   <property name="text" >
+                    <string/>
+                   </property>
+                   <property name="pixmap" >
+                    <pixmap>pointer.png</pixmap>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QLabel" name="label_6" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>8</height>
+                    </size>
+                   </property>
+                   <property name="styleSheet" >
+                    <string>border: 0px;
+border-bottom: 1px solid #717fa0;
+</string>
+                   </property>
+                  </widget>
+                 </item>
+                </layout>
+               </widget>
+              </item>
+              <item>
+               <layout class="QHBoxLayout" >
+                <property name="spacing" >
+                 <number>4</number>
+                </property>
+                <property name="leftMargin" >
+                 <number>6</number>
+                </property>
+                <property name="topMargin" >
+                 <number>6</number>
+                </property>
+                <property name="rightMargin" >
+                 <number>6</number>
+                </property>
+                <property name="bottomMargin" >
+                 <number>6</number>
+                </property>
+                <item>
+                 <widget class="QToolButton" name="abk_new" >
+                  <property name="sizePolicy" >
+                   <sizepolicy vsizetype="Fixed" hsizetype="Fixed" >
+                    <horstretch>0</horstretch>
+                    <verstretch>0</verstretch>
+                   </sizepolicy>
+                  </property>
+                  <property name="minimumSize" >
+                   <size>
+                    <width>25</width>
+                    <height>25</height>
+                   </size>
+                  </property>
+                  <property name="maximumSize" >
+                   <size>
+                    <width>25</width>
+                    <height>25</height>
+                   </size>
+                  </property>
+                  <property name="toolTip" >
+                   <string>Create new contact</string>
+                  </property>
+                  <property name="text" >
+                   <string>New</string>
+                  </property>
+                  <property name="icon" >
+                   <iconset>add.png</iconset>
+                  </property>
+                  <property name="toolButtonStyle" >
+                   <enum>Qt::ToolButtonIconOnly</enum>
+                  </property>
+                  <property name="_yate_normal_icon" stdset="0" >
+                   <string>add.png</string>
+                  </property>
+                  <property name="_yate_pressed_icon" stdset="0" >
+                   <string>add_pressed.png</string>
+                  </property>
+                  <property name="_yate_hover_icon" stdset="0" >
+                   <string>add_hover.png</string>
+                  </property>
+                 </widget>
+                </item>
+                <item>
+                 <widget class="QFrame" name="frame_6" >
+                  <property name="sizePolicy" >
+                   <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+                    <horstretch>0</horstretch>
+                    <verstretch>0</verstretch>
+                   </sizepolicy>
+                  </property>
+                  <property name="minimumSize" >
+                   <size>
+                    <width>16</width>
+                    <height>25</height>
+                   </size>
+                  </property>
+                  <property name="maximumSize" >
+                   <size>
+                    <width>16777215</width>
+                    <height>25</height>
+                   </size>
+                  </property>
+                  <layout class="QHBoxLayout" >
+                   <property name="spacing" >
+                    <number>0</number>
+                   </property>
+                   <property name="leftMargin" >
+                    <number>0</number>
+                   </property>
+                   <property name="topMargin" >
+                    <number>0</number>
+                   </property>
+                   <property name="rightMargin" >
+                    <number>0</number>
+                   </property>
+                   <property name="bottomMargin" >
+                    <number>0</number>
+                   </property>
+                   <item>
+                    <widget class="QLineEdit" name="search_contact" >
+                     <property name="minimumSize" >
+                      <size>
+                       <width>0</width>
+                       <height>25</height>
+                      </size>
+                     </property>
+                     <property name="maximumSize" >
+                      <size>
+                       <width>16777215</width>
+                       <height>25</height>
+                      </size>
+                     </property>
+                     <property name="styleSheet" >
+                      <string>QLineEdit {
+  background-image: url(search.png);
+  background-repeat: no-repeat;
+  background-position: left;
+  padding-left: 20px;
+}
+</string>
+                     </property>
+                     <property name="_yate_textchangednotify" stdset="0" >
+                      <bool>true</bool>
+                     </property>
+                    </widget>
+                   </item>
+                  </layout>
+                 </widget>
+                </item>
+               </layout>
+              </item>
+              <item>
+               <widget class="QFrame" name="frame_contacts" >
+                <property name="sizePolicy" >
+                 <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
+                  <horstretch>0</horstretch>
+                  <verstretch>0</verstretch>
+                 </sizepolicy>
+                </property>
+                <property name="styleSheet" >
+                 <string>QFrame#frame_contacts {
+  border: 0px;
+  border-top: 1px solid #717fa0;
+  border-bottom: 1px solid #717fa0;
+}
+</string>
+                </property>
+                <property name="_yate_uiwidget" stdset="0" >
+                 <bool>true</bool>
+                </property>
+                <property name="_yate_uiwidget_name" stdset="0" >
+                 <string>contacts</string>
+                </property>
+                <property name="_yate_uiwidget_class" stdset="0" >
+                 <string>QtCustomTree</string>
+                </property>
+                <property name="_yate_uiwidget_params" stdset="0" >
+                 <stringlist>
+                  <string>property:_yate_save_props=_yate_sorting,_yate_col_widths</string>
+                  <string>property:tabKeyNavigation=false</string>
+                  <string>property:sortingEnabled=true</string>
+                  <string>property:allColumnsShowFocus=true</string>
+                  <string>property:_yate_horizontalheader=true</string>
+                  <string>property:_yate_notifyonenterpressed=true</string>
+                  <string>property:_yate_notifyitemchanged=true</string>
+                  <string>property:_yate_itemheight=0</string>
+                  <string>property:_yate_sorting=name,true</string>
+                  <string>property:_yate_itemheight=20</string>
+                  <string>columns=Enabled,Name,Number/URI</string>
+                  <string>columns.check=enabled</string>
+                  <string>columns.width=30</string>
+                  <string>columns.resize=fixed</string>
+                  <string>columns.allowemptytitle=enabled</string>
+                  <string>griddraw_right_color=#e3e6e9</string>
+                  <string>griddraw_bottom_color=#a9c2c2</string>
+                 </stringlist>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QFrame" name="frame_contacts_buttons" >
+                <property name="styleSheet" >
+                 <string>QFrame#frame_contacts_buttons {
+  border: 0px solid #717fa0;
+  border-bottom: 1px solid #717fa0;
+}
+</string>
+                </property>
+                <property name="frameShape" >
+                 <enum>QFrame::StyledPanel</enum>
+                </property>
+                <layout class="QHBoxLayout" >
+                 <property name="spacing" >
+                  <number>4</number>
+                 </property>
+                 <property name="leftMargin" >
+                  <number>4</number>
+                 </property>
+                 <item>
+                  <widget class="QToolButton" name="abk_call" >
+                   <property name="enabled" >
+                    <bool>false</bool>
+                   </property>
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Minimum" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="toolTip" >
+                    <string>Call this contact</string>
+                   </property>
+                   <property name="text" >
+                    <string>Call</string>
+                   </property>
+                   <property name="icon" >
+                    <iconset>call.png</iconset>
+                   </property>
+                   <property name="toolButtonStyle" >
+                    <enum>Qt::ToolButtonTextBesideIcon</enum>
+                   </property>
+                   <property name="_yate_normal_icon" stdset="0" >
+                    <string>call.png</string>
+                   </property>
+                   <property name="_yate_pressed_icon" stdset="0" >
+                    <string>call_pressed.png</string>
+                   </property>
+                   <property name="_yate_hover_icon" stdset="0" >
+                    <string>call_hover.png</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QToolButton" name="abk_del" >
+                   <property name="enabled" >
+                    <bool>false</bool>
+                   </property>
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Minimum" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="toolTip" >
+                    <string>Delete contact(s)</string>
+                   </property>
+                   <property name="text" >
+                    <string>Delete</string>
+                   </property>
+                   <property name="icon" >
+                    <iconset>delete.png</iconset>
+                   </property>
+                   <property name="toolButtonStyle" >
+                    <enum>Qt::ToolButtonTextBesideIcon</enum>
+                   </property>
+                   <property name="_yate_identity" stdset="0" >
+                    <string>deletecheckeditems:contacts:</string>
+                   </property>
+                   <property name="_yate_normal_icon" stdset="0" >
+                    <string>delete.png</string>
+                   </property>
+                   <property name="_yate_pressed_icon" stdset="0" >
+                    <string>delete_pressed.png</string>
+                   </property>
+                   <property name="_yate_hover_icon" stdset="0" >
+                    <string>delete_hover.png</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QToolButton" name="abk_edit" >
+                   <property name="sizePolicy" >
+                    <sizepolicy vsizetype="Fixed" hsizetype="Minimum" >
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize" >
+                    <size>
+                     <width>0</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize" >
+                    <size>
+                     <width>16777215</width>
+                     <height>25</height>
+                    </size>
+                   </property>
+                   <property name="toolTip" >
+                    <string>Edit this contact</string>
+                   </property>
+                   <property name="text" >
+                    <string>Edit</string>
+                   </property>
+                   <property name="icon" >
+                    <iconset>edit.png</iconset>
+                   </property>
+                   <property name="toolButtonStyle" >
+                    <enum>Qt::ToolButtonTextBesideIcon</enum>
+                   </property>
+                   <property name="_yate_normal_icon" stdset="0" >
+                    <string>edit.png</string>
+                   </property>
+                   <property name="_yate_pressed_icon" stdset="0" >
+                    <string>edit_pressed.png</string>
+                   </property>
+                   <property name="_yate_hover_icon" stdset="0" >
+                    <string>edit_hover.png</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <spacer>
+                   <property name="orientation" >
+                    <enum>Qt::Horizontal</enum>
+                   </property>
+                   <property name="sizeHint" >
+                    <size>
+                     <width>40</width>
+                     <height>20</height>
+                    </size>
+                   </property>
+                  </spacer>
+                 </item>
+                </layout>
+               </widget>
+              </item>
+             </layout>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QFrame" name="frame_messages" >
+     <property name="sizePolicy" >
+      <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize" >
+      <size>
+       <width>16</width>
+       <height>110</height>
+      </size>
+     </property>
+     <property name="maximumSize" >
+      <size>
+       <width>16777215</width>
+       <height>110</height>
+      </size>
+     </property>
+     <property name="styleSheet" >
+      <string>QScrollBar {
+  border: 1px solid #717fa0;
+  background: white;
+}
+QScrollBar::handle {
+  background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #f6e8e8);
+  border: 1px solid #717fa0;
+}
+QScrollBar:vertical {
+  width: 14px;
+  margin: 16px 0px 16px 0px;
+  border-top: 0px;
+  border-bottom: 0px;
+}
+QScrollBar:horizontal {
+  height: 14px;
+  margin: 0px 16px 0px 16px;
+  border-left: 0px;
+  border-right: 0px;
+}
+QScrollBar::add-line {
+  height: 14px;
+  width: 14px;
+  border: 1px solid #717fa0;
+  subcontrol-origin: margin;
+  background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #f6e8e8);
+}
+QScrollBar::sub-line {
+  height: 14px;
+  width: 14px;
+  border: 1px solid #717fa0;
+  subcontrol-origin: margin;
+  background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #f6e8e8);
+}
+QScrollBar::handle:vertical { border-left: 0px; border-right: 0px; }
+QScrollBar::handle:horizontal { border-top: 0px; border-bottom: 0px; }
+QScrollBar::add-line:vertical { subcontrol-position: bottom; image: url(scroll_down.png); height: 14px; }
+QScrollBar::sub-line:vertical { subcontrol-position: top; image: url(scroll_up.png); height: 14px; }
+QScrollBar::add-line:horizontal { subcontrol-position: right; image: url(scroll_right.png); width: 14px; }
+QScrollBar::sub-line:horizontal { subcontrol-position: left; image: url(scroll_left.png); width: 14px; }
+QScrollBar::up-arrow:pressed { border: 1px solid #717fa0; }
+QScrollBar::down-arrow:pressed { border: 1px solid #717fa0; }
+QScrollBar::left-arrow:pressed { border: 1px solid #717fa0; }
+QScrollBar::right-arrow:pressed { border: 1px solid #717fa0; }
+
+
+QLabel {
+  font-size: 11px;
+}
+QTextEdit {
+  border: 0px;
+  font-size: 11px;
+}
+</string>
+     </property>
+     <property name="_yate_uiwidget" stdset="0" >
+      <bool>true</bool>
+     </property>
+     <property name="_yate_uiwidget_name" stdset="0" >
+      <string>messages</string>
+     </property>
+     <property name="_yate_uiwidget_class" stdset="0" >
+      <string>WidgetList</string>
+     </property>
+     <property name="_yate_uiwidget_params" stdset="0" >
+      <stringlist>
+       <string>type=pages</string>
+       <string>header=messages_header.ui</string>
+       <string>navigate_prev=messages_prev</string>
+       <string>navigate_next=messages_next</string>
+       <string>navigate_info=messages_counter</string>
+       <string>navigate_info_format=${index}/${count}</string>
+       <string>navigate_title=messages_title</string>
+       <string>delete_item_action=deleteitem</string>
+       <string>property:_yate_hidewndwhenempty=false</string>
+       <string>property:_yate_hidewidgetwhenempty=frame_messages</string>
+       <string>property:_yate_itemui=generic:messages_generic.ui</string>
+       <string>property:_yate_itemui=subscription:messages_okrejignore.ui</string>
+       <string>property:_yate_itemui=mucinvite:messages_okrejignore.ui</string>
+       <string>property:_yate_itemui=loginfail:messages_loginfail.ui</string>
+       <string>property:_yate_itemui=incomingcall:messages_okrejignore.ui</string>
+       <string>property:_yate_itemui=incomingfile:messages_okrejignore.ui</string>
+       <string>property:_yate_itemui=rosterreqfail:messages_generic.ui</string>
+       <string>property:_yate_itemui=noaudio:messages_generic.ui</string>
+       <string>property:_yate_itemui=contactupdatefail:messages_generic.ui</string>
+       <string>property:_yate_itemui=contactremovefail:messages_generic.ui</string>
+      </stringlist>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QFrame" name="frameStatus" >
+     <property name="sizePolicy" >
+      <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize" >
+      <size>
+       <width>16</width>
+       <height>32</height>
+      </size>
+     </property>
+     <property name="maximumSize" >
+      <size>
+       <width>16777215</width>
+       <height>32</height>
+      </size>
+     </property>
+     <layout class="QHBoxLayout" >
+      <property name="spacing" >
+       <number>4</number>
+      </property>
+      <property name="leftMargin" >
+       <number>2</number>
+      </property>
+      <property name="topMargin" >
+       <number>4</number>
+      </property>
+      <property name="rightMargin" >
+       <number>2</number>
+      </property>
+      <property name="bottomMargin" >
+       <number>4</number>
+      </property>
+      <item>
+       <widget class="QToolButton" name="global_account_status" >
+        <property name="sizePolicy" >
+         <sizepolicy vsizetype="Expanding" hsizetype="Fixed" >
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize" >
+         <size>
+          <width>26</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize" >
+         <size>
+          <width>26</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="icon" >
+         <iconset>status_offline.png</iconset>
+        </property>
+        <property name="popupMode" >
+         <enum>QToolButton::InstantPopup</enum>
+        </property>
+        <property name="autoRaise" >
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QLabel" name="status" >
+        <property name="sizePolicy" >
+         <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize" >
+         <size>
+          <width>0</width>
+          <height>25</height>
+         </size>
+        </property>
+        <property name="text" >
+         <string>Yate </string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QToolButton" name="toggle_show_help" >
+        <property name="sizePolicy" >
+         <sizepolicy vsizetype="Expanding" hsizetype="Fixed" >
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize" >
+         <size>
+          <width>26</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize" >
+         <size>
+          <width>26</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="toolTip" >
+         <string>Show or hide the Help window</string>
+        </property>
+        <property name="icon" >
+         <iconset>quest.png</iconset>
+        </property>
+        <property name="checkable" >
+         <bool>true</bool>
+        </property>
+        <property name="_yate_identity" stdset="0" >
+         <string>showwindow:help:0</string>
+        </property>
+        <property name="_yate_normal_icon" stdset="0" >
+         <string>quest.png</string>
+        </property>
+        <property name="_yate_pressed_icon" stdset="0" >
+         <string>quest_pressed.png</string>
+        </property>
+        <property name="_yate_hover_icon" stdset="0" >
+         <string>quest_hover.png</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+  <action name="quit" >
+   <property name="icon" >
+    <iconset>quit.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Quit</string>
+   </property>
+   <property name="menuRole" >
+    <enum>QAction::QuitRole</enum>
+   </property>
+  </action>
+  <action name="acc_new" >
+   <property name="icon" >
+    <iconset>addaccount_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Add account</string>
+   </property>
+  </action>
+  <action name="action_show_accountlist" >
+   <property name="icon" >
+    <iconset>user_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Accounts</string>
+   </property>
+  </action>
+  <action name="action_show_settings" >
+   <property name="icon" >
+    <iconset>configure_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Options</string>
+   </property>
+   <property name="menuRole" >
+    <enum>QAction::PreferencesRole</enum>
+   </property>
+  </action>
+  <action name="chatcontact_chat" >
+   <property name="icon" >
+    <iconset>chat_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Chat</string>
+   </property>
+  </action>
+  <action name="chatcontact_subscribe" >
+   <property name="text" >
+    <string>Request subscription</string>
+   </property>
+  </action>
+  <action name="chatcontact_unsubscribe" >
+   <property name="text" >
+    <string>Request subscription removal</string>
+   </property>
+  </action>
+  <action name="chatcontact_unsubscribed" >
+   <property name="text" >
+    <string>Remove subscription</string>
+   </property>
+  </action>
+  <action name="chatcontact_call" >
+   <property name="icon" >
+    <iconset>phone_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Call</string>
+   </property>
+  </action>
+  <action name="chatcontact_new" >
+   <property name="icon" >
+    <iconset>add_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Add</string>
+   </property>
+  </action>
+  <action name="chatcontact_edit" >
+   <property name="icon" >
+    <iconset>edit_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Edit</string>
+   </property>
+   <property name="toolTip" >
+    <string>Edit contact</string>
+   </property>
+  </action>
+  <action name="chatcontact_del" >
+   <property name="icon" >
+    <iconset>delete_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Remove</string>
+   </property>
+   <property name="toolTip" >
+    <string>Remove contact</string>
+   </property>
+  </action>
+  <action name="chatcontact_showoffline" >
+   <property name="checkable" >
+    <bool>true</bool>
+   </property>
+   <property name="checked" >
+    <bool>true</bool>
+   </property>
+   <property name="text" >
+    <string>Show offline friends</string>
+   </property>
+   <property name="_yate_identity" stdset="0" >
+    <string>setparams:property:chat_contacts:_yate_showofflinecontacts</string>
+   </property>
+  </action>
+  <action name="chatcontact_flatlist" >
+   <property name="checkable" >
+    <bool>true</bool>
+   </property>
+   <property name="text" >
+    <string>Flat list</string>
+   </property>
+   <property name="_yate_identity" stdset="0" >
+    <string>setparams:property:chat_contacts:_yate_flatlist</string>
+   </property>
+  </action>
+  <action name="setStatusOnline" >
+   <property name="icon" >
+    <iconset>status_online_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Online</string>
+   </property>
+  </action>
+  <action name="setStatusBusy" >
+   <property name="icon" >
+    <iconset>status_busy_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Busy</string>
+   </property>
+  </action>
+  <action name="setStatusAway" >
+   <property name="icon" >
+    <iconset>status_away_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Away</string>
+   </property>
+  </action>
+  <action name="setStatusXa" >
+   <property name="icon" >
+    <iconset>status_xa_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Extended away</string>
+   </property>
+  </action>
+  <action name="setStatusDnd" >
+   <property name="icon" >
+    <iconset>status_dnd_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Do Not Disturb</string>
+   </property>
+  </action>
+  <action name="setStatusOffline" >
+   <property name="icon" >
+    <iconset>status_offline_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Offline</string>
+   </property>
+  </action>
+  <action name="joinmuc_wizard" >
+   <property name="icon" >
+    <iconset>muc_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Join chat room</string>
+   </property>
+  </action>
+  <action name="acc_new_wizard" >
+   <property name="icon" >
+    <iconset>addaccountwiz_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Add account wizard</string>
+   </property>
+  </action>
+  <action name="messages_show" >
+   <property name="icon" >
+    <iconset>notif_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Show notifications</string>
+   </property>
+   <property name="_yate_menuNoCopy" stdset="0" >
+    <bool>true</bool>
+   </property>
+  </action>
+  <action name="chatcontact_info" >
+   <property name="icon" >
+    <iconset>info_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Info</string>
+   </property>
+  </action>
+  <action name="toggle_show_events" >
+   <property name="checkable" >
+    <bool>true</bool>
+   </property>
+   <property name="icon" >
+    <iconset>events_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Debug window</string>
+   </property>
+   <property name="_yate_identity" stdset="0" >
+    <string>showwindow:events</string>
+   </property>
+  </action>
+  <action name="chatcontact_hideemptygroups" >
+   <property name="checkable" >
+    <bool>true</bool>
+   </property>
+   <property name="checked" >
+    <bool>true</bool>
+   </property>
+   <property name="text" >
+    <string>Hide empty groups</string>
+   </property>
+   <property name="_yate_identity" stdset="0" >
+    <string>setparams:property:chat_contacts:_yate_hideemptygroups</string>
+   </property>
+  </action>
+  <action name="advanced_mode" >
+   <property name="checkable" >
+    <bool>true</bool>
+   </property>
+   <property name="checked" >
+    <bool>true</bool>
+   </property>
+   <property name="text" >
+    <string>Advanced mode</string>
+   </property>
+  </action>
+  <action name="send_file" >
+   <property name="icon" >
+    <iconset>sendfile_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Send file</string>
+   </property>
+  </action>
+  <action name="action_show_fileprogress" >
+   <property name="icon" >
+    <iconset>file_trans_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>File transfer</string>
+   </property>
+  </action>
+  <action name="action_show_archive" >
+   <property name="icon" >
+    <iconset>archive_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Archive</string>
+   </property>
+  </action>
+  <action name="chatcontact_showlog" >
+   <property name="icon" >
+    <iconset>archive.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Show log</string>
+   </property>
+  </action>
+  <action name="chatroom_new" >
+   <property name="icon" >
+    <iconset>addchatroom_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Add chat room</string>
+   </property>
+  </action>
+  <action name="share_file" >
+   <property name="icon" >
+    <iconset>sharefile_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Share Files</string>
+   </property>
+  </action>
+  <action name="shared_file" >
+   <property name="icon" >
+    <iconset>sharedfile_menu.png</iconset>
+   </property>
+   <property name="text" >
+    <string>Shared Files</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/yate-config.in b/yate-config.in
index ec66c69..7a82be6 100644
--- a/yate-config.in
+++ b/yate-config.in
@@ -7,7 +7,7 @@
 # Take a look at the source file yate-config.sh instead.
 #
 # Yet Another Telephony Engine - a fully featured software PBX and IVR
-# Copyright (C) 2005-2014 Null Team
+# Copyright (C) 2005-2020 Null Team
 #
 # This software is distributed under multiple licenses;
 # see the COPYING file in the main directory for licensing
@@ -134,29 +134,29 @@ while [ "$#" != 0 ]; do
 	--param=HAVE_MALLINFO)
 	    echo "@HAVE_MALLINFO@"
 	    ;;
-	--param=QT4_STATIC_MODULES)
-	    echo "@QT4_STATIC_MODULES@"
+	--param=QT5_STATIC_MODULES)
+	    echo "@QT5_STATIC_MODULES@"
 	    ;;
-	--param=QT4_VER)
-	    echo "@QT4_VER@"
+	--param=QT5_VER)
+	    echo "@QT5_VER@"
 	    ;;
-	--param=QT4_MOC)
-	    echo "@QT4_MOC@"
+	--param=QT5_MOC)
+	    echo "@QT5_MOC@"
 	    ;;
-	--param=QT4_LIB_NET)
-	    echo "@QT4_LIB_NET@"
+	--param=QT5_LIB_NET)
+	    echo "@QT5_LIB_NET@"
 	    ;;
-	--param=QT4_INC_NET)
-	    echo "@QT4_INC_NET@"
+	--param=QT5_INC_NET)
+	    echo "@QT5_INC_NET@"
 	    ;;
-	--param=QT4_LIB)
-	    echo "@QT4_LIB@"
+	--param=QT5_LIB)
+	    echo "@QT5_LIB@"
 	    ;;
-	--param=QT4_INC)
-	    echo "@QT4_INC@"
+	--param=QT5_INC)
+	    echo "@QT5_INC@"
 	    ;;
-	--param=HAVE_QT4)
-	    echo "@HAVE_QT4@"
+	--param=HAVE_QT5)
+	    echo "@HAVE_QT5@"
 	    ;;
 	--param=LIBUSB_LIB)
 	    echo "@LIBUSB_LIB@"
-- 
2.30.2

