From 83553f76a3a9a26bf760b249eb0d6e19f4297feb Mon Sep 17 00:00:00 2001 From: Octavian 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 +#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 + +#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(w); + else if (w->inherits("QAction")) + m_action = static_cast(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(what); + if (!m_widget) { + m_action = parent->findChild(what); + if (!m_action) + m_object = parent->findChild(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(m_widget); } + inline QCheckBox* check() + { return static_cast(m_widget); } + inline QTableWidget* table() + { return static_cast(m_widget); } + inline QListWidget* list() + { return static_cast(m_widget); } + inline QComboBox* combo() + { return static_cast(m_widget); } + inline QTabWidget* tab() + { return static_cast(m_widget); } + inline QStackedWidget* stackWidget() + { return static_cast(m_widget); } + inline QTextEdit* textEdit() + { return static_cast(m_widget); } + inline QLabel* label() + { return static_cast(m_widget); } + inline QLineEdit* lineEdit() + { return static_cast(m_widget); } + inline QAbstractButton* abstractButton() + { return static_cast(m_widget); } + inline QSlider* slider() + { return static_cast(m_widget); } + inline QProgressBar* progressBar() + { return static_cast(m_widget); } + inline QSpinBox* spinBox() + { return static_cast(m_widget); } + inline QCalendarWidget* calendar() + { return static_cast(m_widget); } + inline QSplitter* splitter() + { return static_cast(m_widget); } + inline QtTable* customTable() + { return qobject_cast(m_widget); } + inline QtTree* customTree() + { return qobject_cast(m_widget); } + inline QtCustomWidget* customWidget() + { return qobject_cast(m_widget); } + inline QtCustomObject* customObject() + { return qobject_cast(m_object); } + inline UIWidget* uiWidget() { + switch (type()) { + case CustomTable: + return static_cast(customTable()); + case CustomWidget: + return static_cast(customWidget()); + case CustomObject: + return static_cast(customObject()); + case CustomTree: + return static_cast(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(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 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(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(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 buildIntList(const String& buf, int len = 0) +{ + QList 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(QtClient::setUtf8(name)); + if (!m_table) + return; + init(tmp); +} + +TableWidget::TableWidget(QtWidget& table, bool tmp) + : m_table(static_cast((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(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 trayIcons = findChildren(); + 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 tables = findChildren(); + for (int i = 0; i < tables.size(); i++) { + if (qobject_cast(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 spl = findChildren(); + 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 child = findChildren(); + 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(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(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(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(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(target); + QMenu* mOwner = !mbOwner ? qobject_cast(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 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(target); + if (tb) + tb->setMenu(menu); + else { + QPushButton* pb = qobject_cast(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(QtClient::setUtf8(owner)); + if (!parent) + return false; + } + QMenu* menu = parent->findChild(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(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(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(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,¶ms); + } + 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(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,¶ms); +} + +// Choose file window was cancelled +void QtWindow::chooseFileRejected() +{ + QFileDialog* dlg = qobject_cast(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(obj); + if (combo) + return QtClient::connectObjects(combo,SIGNAL(editTextChanged(const QString&)), + this,SLOT(textChanged(const QString&))); + QLineEdit* lineEdit = qobject_cast(obj); + if (lineEdit) + return QtClient::connectObjects(lineEdit,SIGNAL(textChanged(const QString&)), + this,SLOT(textChanged(const QString&))); + QTextEdit* textEdit = qobject_cast(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(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(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((*list)[0]); + int col = tmp ? tmp->toInteger(-1) : -1; + if (col >= 0) { + tmp = static_cast((*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 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(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(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(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 d = findChildren(); + 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(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 frm = findChildren(); + 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,¶ms); + if (!obj) + continue; + QWidget* wid = qobject_cast(obj); + if (wid) + QtClient::setWidget(frm[i],wid); + else { + obj->setParent(frm[i]); + QtCustomObject* customObj = qobject_cast(obj); + if (customObj) + customObj->parentChanged(); + } + } + + // Add the first menubar to layout + QList menuBars = findChildren(); + 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 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 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(sect->c_str()),*sect); + } + + // Process "_yate_setaction" property for our children + QtClient::setAction(this); + + // Connect actions' signal + QList actions = findChildren(); + 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 combos = findChildren(); + 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 buttons = findChildren(); + for(int i = 0; i < buttons.size(); i++) + if (QtClient::autoConnect(buttons[i])) + connectButton(buttons[i]); + + // Connect group boxes signals + QList grp = findChildren(); + 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 sliders = findChildren(); + for (int i = 0; i < sliders.size(); i++) + QtClient::connectObjects(sliders[i],SIGNAL(valueChanged(int)),this,SLOT(selectionChanged())); + + // Connect calendar widget signals + QList cals = findChildren(); + for (int i = 0; i < cals.size(); i++) + QtClient::connectObjects(cals[i],SIGNAL(selectionChanged()),this,SLOT(selectionChanged())); + + // Connect list boxes signals + QList lists = findChildren(); + 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 tabs = findChildren(); + for (int i = 0; i < tabs.size(); i++) + QtClient::connectObjects(tabs[i],SIGNAL(currentChanged(int)),this,SLOT(selectionChanged())); + + // Connect stacked widget signals + QList sw = findChildren(); + for (int i = 0; i < sw.size(); i++) + QtClient::connectObjects(sw[i],SIGNAL(currentChanged(int)),this,SLOT(selectionChanged())); + + // Connect line edit signals + QList le = findChildren(); + for (int i = 0; i < le.size(); i++) + connectTextChanged(le[i]); + + // Connect text edit signals + QList te = findChildren(); + 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 tables = findChildren(); + for (int i = 0; i < tables.size(); i++) { + bool nonCustom = (0 == qobject_cast(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 w = findChildren(); + w.append(this); + for (int i = 0; i < w.size(); i++) { + QList 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"); + qRegisterMetaType("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(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 buttons = widget->findChildren(); + 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 actions = widget->findChildren(); + 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 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(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(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(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(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 frm = parent->findChildren(); + 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(parent->window()); + if (w) + params.setParam("parentwindow",w->id()); + getUtf8(params,"parentwidget",frm[i]->objectName(),true); + QObject* obj = (QObject*)UIFactory::build(type,name,¶ms); + if (!obj) + continue; + QWidget* wid = qobject_cast(obj); + if (wid) + QtClient::setWidget(frm[i],wid); + else { + obj->setParent(frm[i]); + QtCustomObject* customObj = qobject_cast(obj); + if (customObj) + customObj->parentChanged(); + } + } +} + +// Associate actions to buttons with '_yate_setaction' property set +void QtClient::setAction(QWidget* parent) +{ + if (!parent) + return; + QList tb = parent->findChildren(); + for (int i = 0; i < tb.size(); i++) { + QVariant var = tb[i]->property("_yate_setaction"); + if (var.toString().isEmpty()) + continue; + QAction* a = parent->findChild(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(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(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 list = menu->findChildren(); + 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 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(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(obj); + if (b) + b->setIcon(img); + else { + QMenu* m = qobject_cast(obj); + if (m) + m->setIcon(img); + else + return false; + } + } + return true; + } + QAction* a = qobject_cast(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(setUtf8(action)); + if (!parent) + return true; + // Avoid notifying a disabled action + bool ok = true; + if (parent->isWidgetType()) + ok = (qobject_cast(parent))->isEnabled(); + else { + QAction* a = qobject_cast(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(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(o->get())->c_str())); + TelEngine::destruct(list); + return l; +} + +// Split a string. Returns a list of int values +QList QtClient::str2IntList(const String& str, int defVal, bool emptyOk) +{ + QList 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 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(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(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(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 > 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 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 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 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(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(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(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(tmp); + ok = c && QtClient::setImage(c,*data,*ns) && ok; + } + } + else if (n == s_setmenu) + buildWidgetItemMenu(qobject_cast(parent),YOBJECT(NamedList,ns),cName); + else if (n == s_height) { + QString tmp = buildQChildName(pName,cName); + QWidget* w = qobject_cast(parent)->findChild(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(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(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(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(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(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(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(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(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(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 list = menu->findChildren(); + 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(target); + if (mOwner) + mOwner->insertMenu(0,menu); + else { + QToolButton* tb = qobject_cast(target); + if (tb) + tb->setMenu(menu); + else { + QPushButton* pb = qobject_cast(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(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(QtClient::setUtf8(name)); + if (!child) + return false; + QAbstractButton* b = 0; + QAction* a = 0; + if (child->isWidgetType()) + b = qobject_cast(child); + else + a = qobject_cast(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,¶ms); +} + +// 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 c = w->findChildren(); + 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(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(c[i]); + if (gb) { + if (gb->isCheckable()) + QtClient::connectObjects(gb,SIGNAL(toggled(bool)),qObj,toggleSlot); + continue; + } + // Connect combo boxes + QComboBox* combo = qobject_cast(c[i]); + if (combo) { + QtClient::connectObjects(combo,SIGNAL(activated(int)),qObj,selectSlot); + continue; + } + // Connect list boxes + QListWidget* lst = qobject_cast(c[i]); + if (lst) { + QtClient::connectObjects(lst,SIGNAL(currentRowChanged(int)),qObj,selectSlot); + continue; + } + // Connect sliders + QSlider* sld = qobject_cast(c[i]); + if (sld) { + QtClient::connectObjects(sld,SIGNAL(valueChanged(int)),qObj,selectSlot); + continue; + } + continue; + } + // Connect actions signals + QAction* a = qobject_cast(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(n); + if (act) { + if (act->isWidgetType()) + ok = (qobject_cast(act))->isEnabled(); + else { + QAction* a = qobject_cast(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(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 c = w->findChildren(); + for (int i = 0; i < c.size(); i++) { + QLabel* l = qobject_cast(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 + +#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 +#include +#include + +#define QT_NO_DEBUG +#define QT_DLL +#define QT_GUI_LIB +#define QT_CORE_LIB +#define QT_THREAD_SUPPORT + +#include +#include +#include + +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 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 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 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(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(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(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(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 getContainerItems() + { return QList(); } + + /** + * 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(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(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(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(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( + 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(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(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(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,¶ms); + 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(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(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(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 + +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(param->getObject(YATOM("NamedList"))); + if (menu) { + // Get parent window receiving menu events + QtWindow* wnd = static_cast(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(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(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(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(Client::self()->getWindow(*wndname)); + if (wnd && !TelEngine::null(wName)) + parentWidget = wnd->findChild(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 + +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) ? "
" : "\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(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("${url-display}"); + 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(Client::self()->getWindow(*wndname)); + if (wnd && !TelEngine::null(wName)) + parentWidget = wnd->findChild(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 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 +#include +#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& 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 ${$} 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 items) const +{ + if (!m_urlBuilder) + return 0; + int n = items.size(); + if (n < 1) + return 0; + QList urls; + for (int i = 0; i < n; i++) { + QtTreeItem* it = static_cast(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(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,¶ms); + setDragEnabled(true); + } + if (drop) { + m_drop = new QtListDrop(this,¶ms); + 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(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 dlgs = QtItemDelegate::buildDelegates(this,params,&s_delegateCommon); + QStringList cNames; + for (int i = 0; i < dlgs.size(); i++) { + QtItemDelegate* dlg = qobject_cast(dlgs[i]); + if (!dlg) { + delete dlgs[i]; + continue; + } + if (cNames.size() < 1) + cNames = columnIDs(); + dlg->updateColumns(cNames); + QList& 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(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 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 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(list[0]))->id(); + return ok; +} + +// Retrieve multiple selection +bool QtCustomTree::getSelect(NamedList& items) +{ + QList 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 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(sel[i]),true); + } + if (nSel <= 0) + onSelect(this,&(String::empty())); + else if (nSel == 1) + onSelect(this,&(static_cast(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 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 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(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 QtCustomTree::getContainerItems() +{ + QList list; + QList items = findItems(); + for (int i = 0; i < items.size(); i++) { + QWidget* w = itemWidget(items[i],0); + if (w) + list.append(static_cast(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(start) : invisibleRootItem(); + if (!root) + return 0; + int n = root->childCount(); + for (int i = 0; i < n; i++) { + QtTreeItem* item = static_cast(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 QtCustomTree::findItems(bool recursive, QtTreeItem* start) +{ + QList list; + QTreeWidgetItem* root = start ? static_cast(start) : invisibleRootItem(); + if (!root) + return list; + int n = root->childCount(); + for (int i = 0; i < n; i++) { + QtTreeItem* item = static_cast(root->child(i)); + if (!item) + continue; + list.append(item); + if (recursive) { + QList tmp = findItems(true,item); + list += tmp; + } + } + return list; +} + +// Find all tree items having a given id +QList QtCustomTree::findItems(const String& id, QtTreeItem* start, + bool includeStart, bool recursive) +{ + QList list; + if (start && includeStart && id == start->id()) + list.append(start); + QTreeWidgetItem* root = start ? static_cast(start) : invisibleRootItem(); + if (!root) + return list; + int n = root->childCount(); + for (int i = 0; i < n; i++) { + QtTreeItem* item = static_cast(root->child(i)); + if (!item) + continue; + if (id == item->id()) + list.append(item); + if (recursive) { + QList tmp = findItems(id,item,false,true); + list += tmp; + } + } + return list; +} + +// Find all tree items having a given type +QList QtCustomTree::findItems(int type, QtTreeItem* start, + bool includeStart, bool recursive) +{ + QList list; + if (start && includeStart && type == start->type()) + list.append(start); + QTreeWidgetItem* root = start ? static_cast(start) : invisibleRootItem(); + if (!root) + return list; + int n = root->childCount(); + for (int i = 0; i < n; i++) { + QtTreeItem* item = static_cast(root->child(i)); + if (!item) + continue; + if (type == item->type()) + list.append(item); + if (recursive) { + QList tmp = findItems(type,item,false,true); + list += tmp; + } + } + return list; +} + +// Find all tree items from model +QList QtCustomTree::findItems(QModelIndexList list) +{ + QList 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(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(start) : invisibleRootItem(); + if (!root) + return; + int n = root->childCount(); + for (int i = 0; i < n; i++) { + QtTreeItem* item = static_cast(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(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 list, int pos, QtTreeItem* parent) +{ + SafeInt safeChg(&m_changing); + QTreeWidgetItem* root = parent ? static_cast(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(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(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(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(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(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(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(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 items) +{ + for (int i = 0; i < items.size(); i++) + dest.addParam(static_cast(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(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(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 = (¶ms == &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(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(item); + } + else if (recursive) + root = invisibleRootItem(); + int nc = root ? root->childCount() : 0; + for (int i = 0; i < nc; i++) { + QtTreeItem* it = static_cast(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(parent)); + } + TelEngine::destruct(it); + if (setSelTimer) + *setSelTimer = sel; + else if (sel) + startSelectTriggerTimer(); +} + +// Remove a list of items +void QtCustomTree::removeItems(QList items) +{ + bool setSelTimer = false; + for (int i = 0; i < items.size(); i++) { + bool sel = false; + removeItem(static_cast(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(e); + } + else if (e->type() == QEvent::DragEnter) { + if (!m_drop->start(*static_cast(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(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(e); + move = static_cast(e); + } + else if (e->type() == QEvent::Drop) { + if (!m_drop->started()) + return false; + } + else + return false; + int acceptDrop = QtDrop::None; + QtTreeItem* it = static_cast(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(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 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 c = findItems(TypeContact,grp,true,false); + QList r = findItems(TypeChatRoom,grp,true,false); + total = c.size() + r.size(); + online = 0; + for (int i = 0; i < c.size(); i++) + if (!(static_cast(c[i]))->offline()) + online++; + for (int j = 0; j < r.size(); j++) + if (!(static_cast(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 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* list) +{ + QList local; + if (!list) + list = &local; + *list = findItems(id); + for (int i = 0; i < list->size(); i++) { + QtTreeItem* it = static_cast((*list)[i]); + if (isContactType(it->type()) && it->id() == id) + return static_cast(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 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(c[j]); + if (cc && cc->id() == (static_cast(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 list = findItems(TypeContact); + for (int i = 0; i < list.size(); i++) { + ContactItem* c = static_cast(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& 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(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(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 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(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(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(&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(&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 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(cil.m_groups[i]); + if (cil.m_contacts[i].size()) { + ContactItem* item = static_cast((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(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(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& list) +{ + if (!list.size()) + return; + SafeInt safeChg(&m_changing); + if (m_sortKey == "name") { + QVector v(list.size()); + for (int i = 0; i < list.size(); i++) { + v[i].first = list[i]; + v[i].second = (static_cast(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(m_groups[i]); + if (item->id() == id) + return i; + } + int pos = m_groups.size(); + if (pos && id != s_noGroupId && + (static_cast(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(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:

${name}

"); + setItemStyle("file:

${name}

"); + setItemStyle("drive:

${name}

"); + } + 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& items, ObjList* list, + const String& nameParam, int iconCol) +{ + if (!list) + return; + for (ObjList* o = list->skipNull(); o; o = o->skipNext()) { + FileItem* f = static_cast(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 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(list[i]); + updateItem(*it,*it); + } +} + +// Sort a list of items +void FileListTree::sortItems(QList& list, int type) +{ + if (type != TypeDir && type != TypeFile) + return; + QVector v(list.size()); + for (int i = 0; i < list.size(); i++) { + v[i].first = list[i]; + v[i].second = QtClient::setUtf8(static_cast(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(sender()); + if (th != m_dirListThread) + return; + DirListThread* t = static_cast(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(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(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(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(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& 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(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(); + extraPaint = static_cast((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 QtItemDelegate::buildDelegates(QObject* parent, const NamedList& params, + const NamedList* common, const String& prefix) +{ + QList 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(),¶ms); + if (!obj) + return 0; + QAbstractItemDelegate* d = qobject_cast(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(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(Client::self()->getWindow(*wndname)); + if (wnd && !TelEngine::null(wName)) + parentWidget = wnd->findChild(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 QtTreeItemList; +typedef QPair QtTreeItemKey; +typedef QPair 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 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 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 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 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 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 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 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 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(item),column); } + + /** + * Catch item expanded signal + * @param item The item + */ + void itemExpandedSlot(QTreeWidgetItem* item) + { onItemExpandedChanged(static_cast(item)); } + + /** + * Catch item collapsed signal + * @param item The item + */ + void itemCollapsedSlot(QTreeWidgetItem* item) + { onItemExpandedChanged(static_cast(item)); } + + /** + * Catch item changed signal + */ + void itemChangedSlot(QTreeWidgetItem* item, int column) + { onItemChanged(static_cast(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 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 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 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* 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& 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& 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 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 m_groups; + QList 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& 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& 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 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 m_editableCols; // List of editable columns + QList 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#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(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(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 + +#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 + +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(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(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 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 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 WidgetList::getContainerItems() +{ + QList 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(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(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(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(child); + if (wid) + wid->setFocus(); + return true; + } + return QtCustomWidget::eventFilter(watched,event); + } + } + bool filter = false; + if (!filterKeyEvent(watched,static_cast(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(Client::self()->getWindow(*wndname)); + if (wnd && !TelEngine::null(wName)) + parentWidget = wnd->findChild(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 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 @@ + + mainwindow + + + + 0 + 0 + 340 + 520 + + + + + 0 + 0 + + + + + 300 + 520 + + + + Qt::StrongFocus + + + Yate Client + + + null_team-32.png + + + Yate Client - Telephony Client + + + QWidget#mainwindow { + background:#f7f5fd; +} + + + + + true + + + true + + + + 0 + + + 2 + + + 0 + + + 2 + + + 0 + + + + + + 0 + 0 + + + + + &Yate + + + true + + + + + + + + + + + + + + + + + + + &Settings + + + true + + + + + + + + + + S&tatus + + + + + + + + + + + + &Friends + + + true + + + + Subscription + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 6 + + + + + 16777215 + 6 + + + + + + + + + 0 + 200 + + + + QTabWidget::pane { + border: 0px solid #717fa0; + border-top: 1px solid #717fa0; +} + + + + QTabWidget::Rounded + + + 1 + + + + 20 + 20 + + + + + Chat + + + chat_tab.png + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + QFrame#frame_chat_contacts { + border: 1px solid #717fa0; + border-top: 0px solid #717fa0; +} + + + + QFrame::StyledPanel + + + true + + + chat_contacts + + + ContactList + + + + columns=name + htmldelegate=name + delegateparam.name.drawfocus=false + property:_yate_save_props=_yate_flatlist,_yate_showofflinecontacts,_yate_hideemptygroups,_yate_itemsexpstatus + property:_yate_flatlist=false + property:_yate_showofflinecontacts=true + property:_yate_hideemptygroups=true + property:_yate_sorting=name,true + property:_yate_horizontalheader=false + property:itemsExpandable=true + property:autoExpand=true + property:allColumnsShowFocus=false + property:indentation=0 + property:_yate_nogroup_caption=Not set + property:_yate_comparecase=false + property:_yate_itemheight=contact:40 + property:_yate_itemheight=group:22 + property:_yate_itemheight=chatroom:40 + property:_yate_itemstyle=contact:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-size:10px; 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;"><span style=" font-size:13px;">${name}</span><br>${status_text}</br></p></body></html> + property:_yate_itemstyle=group:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-size:10px; 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;"><span style=" font-size:14px;">${name}${statistics}</span></p></body></html> + property:_yate_itemstyle=chatroom:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-size:10px; 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;"><span style=" font-size:13px;">${name}</span><br>${status_text}</br></p></body></html> + property:_yate_itemselectedstyle=contact:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" color:white; font-size:10px; 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;"><span style=" font-size:13px;">${name}</span><br>${status_text}</br></p></body></html> + property:_yate_itemselectedstyle=group:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-size:10px; 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;"><span style=" font-size:14px;">${name}${statistics}</span></p></body></html> + property:_yate_itemselectedstyle=chatroom:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" color:white; font-size:10px; 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;"><span style=" font-size:13px;">${name}</span><br>${status_text}</br></p></body></html> + property:_yate_itembackground=group:color:white + property:_yate_itemstatewidget=group:name + property:_yate_itemexpandedimage=group:expanded.png + property:_yate_itemcollapsedimage=group:collapsed.png + property:_yate_itemstatstemplate=group: (${online}/${total}) + property:_yate_itemtooltip=contact:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:'Arial'; font-size:10pt; 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;"><span style=" font-size:14pt; font-weight:600;">${name}</span></p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${status_text}</p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${contact}</p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Account: ${account}</p></body></html> + property:_yate_itemtooltip=group:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:'Arial'; font-size:10pt; 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;"><span style=" font-size:14pt; font-weight:600;">${name}</span></p></body></html> + property:_yate_itemtooltip=chatroom:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:'Arial'; font-size:10pt; 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;"><span style=" font-size:14pt; font-weight:600;">${name}</span></p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${status_text}</p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${contact}</p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Account: ${account}</p></body></html> + property:_yate_itemmargins=contact:20 + property:_yate_itemmargins=chatroom:20 + property:_yate_itempaintbutton=contact:share_file + property:_yate_itempaintbuttonparam=contact:share_file:_yate_size:20 + property:_yate_itempaintbuttonparam=contact:share_file:_yate_iconsize:20 + property:_yate_itempaintbuttonparam=contact:share_file:_yate_normal_icon:sharefile_20.png + property:_yate_itempaintbuttonparam=contact:share_file:_yate_pressed_icon:sharefile_pressed_20.png + property:_yate_itempaintbuttonparam=contact:share_file:_yate_hover_icon:sharefile_hover_20.png + property:_yate_itempaintbutton=contact:shared_file + property:_yate_itempaintbuttonparam=contact:shared_file:_yate_size:20 + property:_yate_itempaintbuttonparam=contact:shared_file:_yate_iconsize:20 + property:_yate_itempaintbuttonparam=contact:shared_file:_yate_normal_icon:sharedfile_20.png + property:_yate_itempaintbuttonparam=contact:shared_file:_yate_pressed_icon:sharedfile_pressed_20.png + property:_yate_itempaintbuttonparam=contact:shared_file:_yate_hover_icon:sharedfile_hover_20.png + + + + + + + + + + + + Telephony + + + phone_tab.png + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16 + 50 + + + + + 16777215 + 50 + + + + QFrame#frameButtons { + border: 1px solid #717fa0; + border-top: 0px; + border-bottom: 0px; +} + + + + 1 + + + 4 + + + 4 + + + 4 + + + 0 + + + + + + 40 + 46 + + + + + 40 + 46 + + + + Calls + + + QToolButton { + border: 0px solid #717fa0; + border-radius: 0px; + font-size: 11px; + background: transparent; +} +QToolButton:checked { + padding-top: 0px; + padding-left: 0px; +} + + + + Calls + + + calls_tab.png + + + + 26 + 26 + + + + true + + + true + + + true + + + Qt::ToolButtonTextUnderIcon + + + true + + + selectitem:framePages:PageCalls + + + calls_tab.png + + + calls_tab_pressed.png + + + calls_tab_hover.png + + + + + + + + 46 + 46 + + + + + 46 + 46 + + + + History + + + QToolButton { + border: 0px solid #717fa0; + border-radius: 0px; + font-size: 11px; + background: transparent; +} +QToolButton:checked { + padding-top: 0px; + padding-left: 0px; +} + + + + History + + + cdr_tab.png + + + + 26 + 26 + + + + true + + + false + + + true + + + Qt::ToolButtonTextUnderIcon + + + true + + + selectitem:framePages:PageCDR + + + cdr_tab.png + + + cdr_tab_pressed.png + + + cdr_tab_hover.png + + + + + + + + 50 + 46 + + + + + 50 + 46 + + + + Address Book + + + QToolButton { + border: 0px solid #717fa0; + border-radius: 0px; + font-size: 11px; + background: transparent; +} +QToolButton:checked { + padding-top: 0px; + padding-left: 0px; +} + + + + Contacts + + + contacts_tab.png + + + + 26 + 26 + + + + true + + + false + + + true + + + Qt::ToolButtonTextUnderIcon + + + true + + + selectitem:framePages:PageContacts + + + contacts_tab.png + + + contacts_tab_pressed.png + + + contacts_tab_hover.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + 1 + + + + + 0 + 200 + + + + 0 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16 + 12 + + + + + 16777215 + 12 + + + + border: 1px solid #717fa0; + border-bottom: 0px; + border-top: 0px; + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 15 + 8 + + + + + 15 + 8 + + + + border: 0px; +border-bottom: 1px solid #717fa0; + + + + + + + + + 0 + 0 + + + + + 18 + 8 + + + + + 18 + 8 + + + + border: 0px; + + + + + + pointer.png + + + + + + + + 0 + 0 + + + + + 0 + 8 + + + + + 16777215 + 8 + + + + border: 0px; +border-bottom: 1px solid #717fa0; + + + + + + + + + + + QFrame#frame { + border: 1px solid #717fa0; + border-top: 0px; +} + + + + 6 + + + 8 + + + 4 + + + 8 + + + 6 + + + + + 2 + + + + + + 0 + 0 + + + + + 80 + 16 + + + + + 80 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 80 + 0 + + + + + 80 + 16777215 + + + + Protocol + + + + + + + + 0 + 0 + + + + + 80 + 25 + + + + + 80 + 25 + + + + VoIP protocol used to make a direct call + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Account + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Account used to call through a server + + + + + + + + + + + + 0 + + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 24 + 24 + + + + + 24 + 24 + + + + Show or hide the keypad + + + dialpad_20.png + + + + 20 + 20 + + + + true + + + dialpad_20.png + + + dialpad_20_hover.png + + + dialpad_20_pressed.png + + + + + + + + 0 + 0 + + + + + 16 + 24 + + + + + 16777215 + 24 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + Qt::WheelFocus + + + true + + + true + + + 20 + + + call + + + call + + + true + + + true + + + true + + + true + + + + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + Make a new call + + + Call + + + call.png + + + + 20 + 20 + + + + Qt::ToolButtonTextBesideIcon + + + call_hover.png + + + call.png + + + call_pressed.png + + + + + + + + + + 0 + 0 + + + + font-size: 10px; + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 100 + + + + + 16777215 + 100 + + + + + 2 + + + 2 + + + + + 1 + + + 1 + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:1 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:2 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:3 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:4 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:5 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:6 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:7 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:8 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:9 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:* + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:0 + + + + + + + + 42 + 23 + + + + + 42 + 23 + + + + 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);} + + + + digit:# + + + + + + + + + + + + + + + + 0 + 0 + + + + + 16 + 8 + + + + + 16777215 + 8 + + + + QFrame { + background:#f7f5fd; +} + + + + + + + + + 0 + 0 + + + + QFrame#frame_channels{ + border: 1px solid #717fa0; +} + + + + true + + + channels + + + QtCustomTree + + + + vertical_scroll_policy=pixel + property:_yate_horizontalheader=false + property:itemsExpandable=false + property:rootIsDecorated=false + property:allColumnsShowFocus=false + property:styleSheet=QTreeWidget {background: #f3faff;} + property:_yate_itemui=channel_item.ui + property:_yate_itemstyle=:QWidget#${name}{background: #f3faff;} QFrame#${name}_frame{background: #ffffff; border: 1px solid #717fa0;} + property:_yate_itemheight=86 + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame#frame_page_log { + border: 0px solid #717fa0; + border-top: 0px solid #717fa0; +} + + + + QFrame::StyledPanel + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16 + 12 + + + + + 16777215 + 12 + + + + border: 1px solid #717fa0; + border-bottom: 0px; + border-top: 0px; + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 60 + 8 + + + + + 60 + 8 + + + + border: 0px; +border-bottom: 1px solid #717fa0; + + + + + + + + + 0 + 0 + + + + + 18 + 8 + + + + + 18 + 8 + + + + border: 0px; + + + + + + pointer.png + + + + + + + + 0 + 0 + + + + + 0 + 8 + + + + + 16777215 + 8 + + + + border: 0px; +border-bottom: 1px solid #717fa0; + + + + + + + + + + + + 0 + 0 + + + + QFrame#frame_log { + border: 1px solid #717fa0; + border-bottom: 0px solid #717fa0; +} + + + + true + + + log + + + QtCustomTree + + + + property:_yate_save_props=_yate_sorting,_yate_col_widths + property:tabKeyNavigation=false + property:sortingEnabled=true + property:allColumnsShowFocus=true + property:_yate_horizontalheader=true + property:_yate_notifyonenterpressed=true + property:_yate_notifyitemchanged=true + property:_yate_itemheight=0 + property:_yate_sorting=time,false + property:_yate_itemheight=20 + columns=Enabled,Party,Time,Duration + columns.check=enabled + columns.width=30 + columns.resize=fixed + columns.allowemptytitle=enabled + griddraw_right_color=#e3e6e9 + griddraw_bottom_color=#a9c2c2 + + + + + + + + QFrame#frame_log_buttons { + border: 1px solid #717fa0; +} + + + + QFrame::StyledPanel + + + + 4 + + + 4 + + + + + false + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Call back this number + + + Call + + + call.png + + + Qt::ToolButtonTextBesideIcon + + + call.png + + + call_pressed.png + + + call_hover.png + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Clear all calls log + + + Clear + + + clear.png + + + Qt::ToolButtonTextBesideIcon + + + clear:log: + + + clear.png + + + clear_pressed.png + + + clear_hover.png + + + + + + + false + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Delete the selected call log + + + Delete + + + delete.png + + + Qt::ToolButtonTextBesideIcon + + + deletecheckeditems:log: + + + delete.png + + + delete_pressed.png + + + delete_hover.png + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Turn this number into a contact + + + Contact + + + add.png + + + Qt::ToolButtonTextBesideIcon + + + add.png + + + add_pressed.png + + + add_hover.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame#frame_page_contacts { + border: 0px solid #717fa0; + border-left: 1px solid #717fa0; + border-right: 1px solid #717fa0; +} + + + + QFrame::StyledPanel + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16 + 12 + + + + + 16777215 + 12 + + + + border: 0px solid #717fa0; + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 106 + 8 + + + + + 106 + 8 + + + + border: 0px; +border-bottom: 1px solid #717fa0; + + + + + + + + + 0 + 0 + + + + + 18 + 8 + + + + + 18 + 8 + + + + border: 0px; + + + + + + pointer.png + + + + + + + + 0 + 0 + + + + + 0 + 8 + + + + + 16777215 + 8 + + + + border: 0px; +border-bottom: 1px solid #717fa0; + + + + + + + + + + + 4 + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 0 + 0 + + + + + 25 + 25 + + + + + 25 + 25 + + + + Create new contact + + + New + + + add.png + + + Qt::ToolButtonIconOnly + + + add.png + + + add_pressed.png + + + add_hover.png + + + + + + + + 0 + 0 + + + + + 16 + 25 + + + + + 16777215 + 25 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + QLineEdit { + background-image: url(search.png); + background-repeat: no-repeat; + background-position: left; + padding-left: 20px; +} + + + + true + + + + + + + + + + + + + 0 + 0 + + + + QFrame#frame_contacts { + border: 0px; + border-top: 1px solid #717fa0; + border-bottom: 1px solid #717fa0; +} + + + + true + + + contacts + + + QtCustomTree + + + + property:_yate_save_props=_yate_sorting,_yate_col_widths + property:tabKeyNavigation=false + property:sortingEnabled=true + property:allColumnsShowFocus=true + property:_yate_horizontalheader=true + property:_yate_notifyonenterpressed=true + property:_yate_notifyitemchanged=true + property:_yate_itemheight=0 + property:_yate_sorting=name,true + property:_yate_itemheight=20 + columns=Enabled,Name,Number/URI + columns.check=enabled + columns.width=30 + columns.resize=fixed + columns.allowemptytitle=enabled + griddraw_right_color=#e3e6e9 + griddraw_bottom_color=#a9c2c2 + + + + + + + + QFrame#frame_contacts_buttons { + border: 0px solid #717fa0; + border-bottom: 1px solid #717fa0; +} + + + + QFrame::StyledPanel + + + + 4 + + + 4 + + + + + false + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Call this contact + + + Call + + + call.png + + + Qt::ToolButtonTextBesideIcon + + + call.png + + + call_pressed.png + + + call_hover.png + + + + + + + false + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Delete contact(s) + + + Delete + + + delete.png + + + Qt::ToolButtonTextBesideIcon + + + deletecheckeditems:contacts: + + + delete.png + + + delete_pressed.png + + + delete_hover.png + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + Edit this contact + + + Edit + + + edit.png + + + Qt::ToolButtonTextBesideIcon + + + edit.png + + + edit_pressed.png + + + edit_hover.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 16 + 110 + + + + + 16777215 + 110 + + + + 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; +} + + + + true + + + messages + + + WidgetList + + + + type=pages + header=messages_header.ui + navigate_prev=messages_prev + navigate_next=messages_next + navigate_info=messages_counter + navigate_info_format=${index}/${count} + navigate_title=messages_title + delete_item_action=deleteitem + property:_yate_hidewndwhenempty=false + property:_yate_hidewidgetwhenempty=frame_messages + property:_yate_itemui=generic:messages_generic.ui + property:_yate_itemui=subscription:messages_okrejignore.ui + property:_yate_itemui=mucinvite:messages_okrejignore.ui + property:_yate_itemui=loginfail:messages_loginfail.ui + property:_yate_itemui=incomingcall:messages_okrejignore.ui + property:_yate_itemui=incomingfile:messages_okrejignore.ui + property:_yate_itemui=rosterreqfail:messages_generic.ui + property:_yate_itemui=noaudio:messages_generic.ui + property:_yate_itemui=contactupdatefail:messages_generic.ui + property:_yate_itemui=contactremovefail:messages_generic.ui + + + + + + + + + 0 + 0 + + + + + 16 + 32 + + + + + 16777215 + 32 + + + + + 4 + + + 2 + + + 4 + + + 2 + + + 4 + + + + + + 0 + 0 + + + + + 26 + 0 + + + + + 26 + 16777215 + + + + status_offline.png + + + QToolButton::InstantPopup + + + true + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Yate + + + + + + + + 0 + 0 + + + + + 26 + 0 + + + + + 26 + 16777215 + + + + Show or hide the Help window + + + quest.png + + + true + + + showwindow:help:0 + + + quest.png + + + quest_pressed.png + + + quest_hover.png + + + + + + + + + + quit.png + + + Quit + + + QAction::QuitRole + + + + + addaccount_menu.png + + + Add account + + + + + user_menu.png + + + Accounts + + + + + configure_menu.png + + + Options + + + QAction::PreferencesRole + + + + + chat_menu.png + + + Chat + + + + + Request subscription + + + + + Request subscription removal + + + + + Remove subscription + + + + + phone_menu.png + + + Call + + + + + add_menu.png + + + Add + + + + + edit_menu.png + + + Edit + + + Edit contact + + + + + delete_menu.png + + + Remove + + + Remove contact + + + + + true + + + true + + + Show offline friends + + + setparams:property:chat_contacts:_yate_showofflinecontacts + + + + + true + + + Flat list + + + setparams:property:chat_contacts:_yate_flatlist + + + + + status_online_menu.png + + + Online + + + + + status_busy_menu.png + + + Busy + + + + + status_away_menu.png + + + Away + + + + + status_xa_menu.png + + + Extended away + + + + + status_dnd_menu.png + + + Do Not Disturb + + + + + status_offline_menu.png + + + Offline + + + + + muc_menu.png + + + Join chat room + + + + + addaccountwiz_menu.png + + + Add account wizard + + + + + notif_menu.png + + + Show notifications + + + true + + + + + info_menu.png + + + Info + + + + + true + + + events_menu.png + + + Debug window + + + showwindow:events + + + + + true + + + true + + + Hide empty groups + + + setparams:property:chat_contacts:_yate_hideemptygroups + + + + + true + + + true + + + Advanced mode + + + + + sendfile_menu.png + + + Send file + + + + + file_trans_menu.png + + + File transfer + + + + + archive_menu.png + + + Archive + + + + + archive.png + + + Show log + + + + + addchatroom_menu.png + + + Add chat room + + + + + sharefile_menu.png + + + Share Files + + + + + sharedfile_menu.png + + + Shared Files + + + + + + 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