From 16bbfb5a5181d769704b7315cd44621a1c8044dc Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 6 Nov 2025 03:05:54 -0700 Subject: Patched clientswitcher --- psi-plus/patches/clientswitcher-fix.patch | 2459 +++++++++++++++++++++++++++++ psi-plus/template | 3 +- 2 files changed, 2461 insertions(+), 1 deletion(-) create mode 100644 psi-plus/patches/clientswitcher-fix.patch (limited to 'psi-plus') diff --git a/psi-plus/patches/clientswitcher-fix.patch b/psi-plus/patches/clientswitcher-fix.patch new file mode 100644 index 0000000..952a940 --- /dev/null +++ b/psi-plus/patches/clientswitcher-fix.patch @@ -0,0 +1,2459 @@ +diff -Nru a/plugins/generic/clientswitcherplugin/accountsettings.cpp b/plugins/generic/clientswitcherplugin/accountsettings.cpp +--- a/plugins/generic/clientswitcherplugin/accountsettings.cpp 2025-08-26 14:50:07.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/accountsettings.cpp 2020-07-10 11:50:10.000000000 +0000 +@@ -38,8 +38,9 @@ + + bool AccountSettings::isEmpty() + { +- if (response_mode == RespAllow && !lock_time_requ) { +- if (os_name.isNull() && client_name.isEmpty() && caps_node.isEmpty()) { ++ if (!enable_contacts && !enable_conferences && response_mode == RespAllow && !lock_time_requ ++ && show_requ_mode == LogNever && log_mode == LogNever) { ++ if (os_name.isEmpty() && client_name.isEmpty() && caps_node.isEmpty() && caps_version.isEmpty()) { + return true; + } + } +@@ -77,6 +78,10 @@ + QString param_value = param_list.join("="); + if (param_name == "acc_id") { + account_id = stripSlashes(param_value); ++ } else if (param_name == "en_cnt") { ++ enable_contacts = (param_value == "true") ? true : false; ++ } else if (param_name == "en_cnf") { ++ enable_conferences = (param_value == "true") ? true : false; + } else if (param_name == "l_req") { + if (param_value == "true") + response_mode = RespNotImpl; +@@ -86,16 +91,30 @@ + response_mode = RespAllow; + } else if (param_name == "l_treq") { + lock_time_requ = (param_value == "true") ? true : false; ++ } else if (param_name == "s_req") { ++ show_requ_mode = LogNever; ++ if (param_value == "true" || param_value == "if-repl") { ++ show_requ_mode = LogIfReplace; ++ } else if (param_value == "always") { ++ show_requ_mode = LogAlways; ++ } + } else if (param_name == "os_nm") { + os_name = stripSlashes(param_value); +- } else if (param_name == "os_ver") { +- os_version = stripSlashes(param_value); + } else if (param_name == "cl_nm") { + client_name = stripSlashes(param_value); + } else if (param_name == "cl_ver") { + client_version = stripSlashes(param_value); + } else if (param_name == "cp_nd") { + caps_node = stripSlashes(param_value); ++ } else if (param_name == "cp_ver") { ++ caps_version = stripSlashes(param_value); ++ } else if (param_name == "log") { ++ log_mode = LogNever; ++ if (param_value == "true" || param_value == "if-repl") { ++ log_mode = LogIfReplace; ++ } else if (param_value == "always") { ++ log_mode = LogAlways; ++ } + } + } + } +@@ -104,6 +123,8 @@ + QString AccountSettings::toString() + { + QString s_res = "acc_id=" + addSlashes(account_id); ++ s_res.append(";en_cnt=").append((enable_contacts) ? "true" : "false"); ++ s_res.append(";en_cnf=").append((enable_conferences) ? "true" : "false"); + QString str1; + if (response_mode == RespNotImpl) + str1 = "true"; +@@ -113,29 +134,44 @@ + str1 = "false"; + s_res.append(";l_req=").append(str1); + s_res.append(";l_treq=").append((lock_time_requ) ? "true" : "false"); +- if (!os_name.isNull()) +- s_res.append(";os_nm=").append(addSlashes(os_name)); +- if (!os_version.isNull()) +- s_res.append(";os_ver=").append(addSlashes(os_version)); +- if (!client_name.isNull()) +- s_res.append(";cl_nm=").append(addSlashes(client_name)); +- if (!client_version.isNull()) +- s_res.append(";cl_ver=").append(addSlashes(client_version)); +- if (!caps_node.isNull()) +- s_res.append(";cp_nd=").append(addSlashes(caps_node)); ++ if (show_requ_mode == LogIfReplace) { ++ str1 = "if-repl"; ++ } else if (show_requ_mode == LogAlways) { ++ str1 = "always"; ++ } else { ++ str1 = "never"; ++ } ++ s_res.append(";s_req=").append(str1); ++ s_res.append(";os_nm=").append(addSlashes(os_name)); ++ s_res.append(";cl_nm=").append(addSlashes(client_name)); ++ s_res.append(";cl_ver=").append(addSlashes(client_version)); ++ s_res.append(";cp_nd=").append(addSlashes(caps_node)); ++ s_res.append(";cp_ver=").append(addSlashes(caps_version)); ++ if (log_mode == LogIfReplace) { ++ str1 = "if-repl"; ++ } else if (log_mode == LogAlways) { ++ str1 = "always"; ++ } else { ++ str1 = "never"; ++ } ++ s_res.append(";log=").append(str1); + return s_res; + } + + void AccountSettings::init() + { +- account_id = ""; +- response_mode = RespAllow; +- lock_time_requ = false; +- os_name = ""; +- os_version = ""; +- client_name = ""; +- client_version = ""; +- caps_node = ""; ++ account_id = ""; ++ enable_contacts = false; ++ enable_conferences = false; ++ response_mode = RespAllow; ++ lock_time_requ = false; ++ show_requ_mode = LogNever; ++ os_name = ""; ++ client_name = ""; ++ client_version = ""; ++ caps_node = ""; ++ caps_version = ""; ++ log_mode = LogNever; + } + + QString AccountSettings::addSlashes(QString &str) +diff -Nru a/plugins/generic/clientswitcherplugin/accountsettings.h b/plugins/generic/clientswitcherplugin/accountsettings.h +--- a/plugins/generic/clientswitcherplugin/accountsettings.h 2025-08-26 14:50:07.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/accountsettings.h 2020-07-10 11:50:10.000000000 +0000 +@@ -31,6 +31,7 @@ + + public: + enum { RespAllow = 0, RespNotImpl = 1, RespIgnore = 2 }; // как номер индекса в combobox ++ enum { LogNever = 0, LogIfReplace = 1, LogAlways = 2 }; + + AccountSettings(); + AccountSettings(const QString &); +@@ -41,13 +42,17 @@ + QString toString(); + //-- + QString account_id; ++ bool enable_contacts; ++ bool enable_conferences; + int response_mode; + bool lock_time_requ; ++ int show_requ_mode; + QString os_name; +- QString os_version; + QString client_name; + QString client_version; + QString caps_node; ++ QString caps_version; ++ int log_mode; + + private: + //-- +diff -Nru a/plugins/generic/clientswitcherplugin/changelog.txt b/plugins/generic/clientswitcherplugin/changelog.txt +--- a/plugins/generic/clientswitcherplugin/changelog.txt 2025-08-26 14:50:07.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/changelog.txt 2020-07-10 11:50:10.000000000 +0000 +@@ -1,11 +1,3 @@ +-2024-03-19 +-v0.2 +- + added initial Qt6 support +- +-2020-07-22 +-v0.1 +- * The plugin was rewritten to reuse caps generation of iris +- + 2013-09-02 + v0.0.18 - taurus + * Использовать слово Groupchat вместо Conference +diff -Nru a/plugins/generic/clientswitcherplugin/clientswitcherplugin.cpp b/plugins/generic/clientswitcherplugin/clientswitcherplugin.cpp +--- a/plugins/generic/clientswitcherplugin/clientswitcherplugin.cpp 2025-08-26 14:50:07.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/clientswitcherplugin.cpp 2020-07-10 11:50:10.000000000 +0000 +@@ -22,21 +22,27 @@ + * + */ + +-// #include +-// #include ++//#include ++//#include + #include + + #include "clientswitcherplugin.h" ++#include "viewer.h" + + #define constPluginShortName "clientswitcher" + #define constPluginName "Client Switcher Plugin" + #define constForAllAcc "for_all_acc" + #define constAccSettingList "accsettlist" ++#define constShowLogHeight "showlogheight" ++#define constShowLogWidth "showlogwidth" ++#define constLastLogItem "lastlogview" ++#define constPopupDuration "popupduration" + + ClientSwitcherPlugin::ClientSwitcherPlugin() : +- psiOptions(nullptr), psiInfo(nullptr), psiAccount(nullptr), psiAccountCtl(nullptr), enabled(false), +- for_all_acc(false), def_os_name(""), def_client_name(""), def_client_version(""), def_caps_node(""), +- def_caps_version("") ++ sender_(nullptr), psiOptions(nullptr), psiPopup(nullptr), psiInfo(nullptr), psiAccount(nullptr), ++ psiAccountCtl(nullptr), psiContactInfo(nullptr), psiIcon(nullptr), enabled(false), for_all_acc(false), ++ def_os_name(""), def_client_name(""), def_client_version(""), def_caps_node(""), def_caps_version(""), ++ heightLogsView(500), widthLogsView(600), lastLogItem(""), popupId(0) + + { + settingsList.clear(); +@@ -56,86 +62,73 @@ + if (!psiOptions) + return false; + enabled = true; +- +- os_presets = { { "Windows", "95" }, +- { "Windows", "98" }, +- { "Windows", "ME" }, +- { "Windows", "2000" }, +- { "Windows", "XP" }, +- { "Windows", "2003" }, +- { "Windows", "2008" }, +- { "Windows", "Vista" }, +- { "Windows", "7" }, +- { "Windows", "10" }, +- { "Arch" }, +- { "Debian GNU/Linux", "6.0.1 (squeeze)" }, +- { "Ubuntu", "10.04.2 LTS" }, +- { "RFRemix", "14.1 (Laughlin)" }, +- { "openSUSE", "11.4" }, +- { "Gentoo Base System", "2.0.3" }, +- { "Mac OS X" }, +- { "Mac OS X", "10.6" }, +- { "Android OS", "2.3.6 (build XXLB1)" }, +- { "Plan9" }, +- { "Solaris" }, +- { "FreeBSD" }, +- { "NetBSD" }, +- { "OpenBSD" }, +- { "Nokia5130c-2", "07.91" }, +- { "SonyEricssonW580i", "R8BE001" } }; +- ++ os_presets.clear(); ++ os_presets << OsStruct("Windows 98") << OsStruct("Windows ME") << OsStruct("Windows 2000"); ++ os_presets << OsStruct("Windows XP") << OsStruct("Windows Server 2003") << OsStruct("Windows Server 2008"); ++ os_presets << OsStruct("Windows Vista") << OsStruct("Windows 7, 32-bit") << OsStruct("Windows 7, 64-bit"); ++ os_presets << OsStruct("Arch Linux") << OsStruct("Debian GNU/Linux 6.0.1 (squeeze)"); ++ os_presets << OsStruct("Ubuntu 10.04.2 LTS") << OsStruct("RFRemix release 14.1 (Laughlin)"); ++ os_presets << OsStruct("openSUSE 11.4") << OsStruct("Gentoo Base System release 2.0.3"); ++ os_presets << OsStruct("Mac OS X") << OsStruct("Mac OS X 10.6"); ++ os_presets << OsStruct("Android OS 2.3.6 (build XXLB1)"); ++ os_presets << OsStruct("Plan9") << OsStruct("Solaris"); ++ os_presets << OsStruct("FreeBSD") << OsStruct("NetBSD") << OsStruct("OpenBSD"); ++ os_presets << OsStruct("Nokia5130c-2/07.91") << OsStruct("SonyEricssonW580i/R8BE001"); + client_presets.clear(); +- client_presets << ClientStruct("Bombus", "0.7.1429M-Zlib", "http://bombus-im.org/java"); +- client_presets << ClientStruct("Gajim", "0.12.5", "https://gajim.org"); +- client_presets << ClientStruct("Mcabber", "0.9.10", "http://mcabber.com/caps"); +- client_presets << ClientStruct("Miranda", "0.9.16.0", "http://miranda-im.org/caps"); +- client_presets << ClientStruct("Pidgin", "2.8.0", "http://pidgin.im"); +- client_presets << ClientStruct("Psi", "0.14", "https://psi-im.org/caps"); +- client_presets << ClientStruct("QIP Infium", "9034", "http://qip.ru/caps"); +- client_presets << ClientStruct("qutIM", "0.2", "http://qutim.org"); +- client_presets << ClientStruct("Swift", "1.0", "http://swift.im"); +- client_presets << ClientStruct("Talisman", "0.1.1.15", "http://jabrvista.net.ru"); +- client_presets << ClientStruct("Tkabber", "0.11.1", "http://tkabber.jabber.ru"); +- client_presets << ClientStruct("Talkonaut", "1.0.0.84", "http://www.google.com/xmpp/client/caps"); +- client_presets << ClientStruct(QString::fromUtf8("Я.Онлайн"), "3.2.0.8873", "yandex-webchat"); ++ client_presets << ClientStruct("Bombus", "0.7.1429M-Zlib", "http://bombus-im.org/java", "0.7.1429M-Zlib"); ++ client_presets << ClientStruct("Gajim", "0.12.5", "https://gajim.org", "0.12.5"); ++ client_presets << ClientStruct("Mcabber", "0.9.10", "http://mcabber.com/caps", "0.9.10"); ++ client_presets << ClientStruct("Miranda", "0.9.16.0", "http://miranda-im.org/caps", "0.9.16.0"); ++ client_presets << ClientStruct("Pidgin", "2.8.0", "http://pidgin.im", "2.8.0"); ++ client_presets << ClientStruct("Psi", "0.14", "https://psi-im.org/caps", "0.14"); ++ client_presets << ClientStruct("QIP Infium", "9034", "http://qip.ru/caps", "9034"); ++ client_presets << ClientStruct("qutIM", "0.2", "http://qutim.org", "0.2"); ++ client_presets << ClientStruct("Swift", "1.0", "http://swift.im", "1.0"); ++ client_presets << ClientStruct("Talisman", "0.1.1.15", "http://jabrvista.net.ru", "0.1.1.15"); ++ client_presets << ClientStruct("Tkabber", "0.11.1", "http://tkabber.jabber.ru", "0.11.1"); ++ client_presets << ClientStruct("Talkonaut", "1.0.0.84", "http://www.google.com/xmpp/client/caps", "1.0.0.84"); ++ client_presets << ClientStruct(QString::fromUtf8("Я.Онлайн"), "3.2.0.8873", "yandex-webchat", "3.2.0.8873"); + // Флаг "для всех аккаунтов" + for_all_acc = psiOptions->getPluginOption(constForAllAcc, QVariant(false)).toBool(); + // Получаем настройки аккаунтов + QStringList sett_list = psiOptions->getPluginOption(constAccSettingList, QVariant()).toStringList(); ++ // Get and register popup option ++ int popup_duration = psiOptions->getPluginOption(constPopupDuration, QVariant(5000)).toInt() / 1000; ++ popupId = psiPopup->registerOption(constPluginName, popup_duration, ++ QLatin1String("plugins.options.clientswitcher.") + constPopupDuration); + // Формируем структуры + int cnt = sett_list.size(); + for (int i = 0; i < cnt; i++) { +- AccountSettings *as = new AccountSettings(sett_list.at(i)); +- if (as) { +- if (as->isValid()) +- settingsList.push_back(as); ++ AccountSettings *ac = new AccountSettings(sett_list.at(i)); ++ if (ac) { ++ if (ac->isValid()) ++ settingsList.push_back(ac); + else +- delete as; ++ delete ac; + } + } ++ // Настройки для просмотра логов ++ logsDir = psiInfo->appCurrentProfileDir(ApplicationInfoAccessingHost::DataLocation) + "/logs/clientswitcher"; ++ QDir dir(logsDir); ++ if (!dir.exists(logsDir)) ++ dir.mkpath(logsDir); ++ logsDir.append("/"); ++ heightLogsView = psiOptions->getPluginOption(constShowLogHeight, QVariant(heightLogsView)).toInt(); ++ widthLogsView = psiOptions->getPluginOption(constShowLogWidth, QVariant(widthLogsView)).toInt(); ++ lastLogItem = psiOptions->getPluginOption(constLastLogItem, QVariant(widthLogsView)).toString(); + + return true; + } + + bool ClientSwitcherPlugin::disable() + { +- if (!enabled) +- return true; +- + while (settingsList.size() != 0) { + AccountSettings *as = settingsList.takeLast(); + if (as) + delete as; + } +- +- for (int i = 0;; i++) { +- QString id = psiAccount->getId(i); +- if (id == "-1") +- break; +- psiAccountCtl->setClientVersionInfo(i, {}); +- } +- + enabled = false; ++ psiPopup->unregisterOption(constPluginName); + return true; + } + +@@ -151,7 +144,7 @@ + ui_options.cb_ospreset->addItem("user defined", "user"); + int cnt = os_presets.size(); + for (int i = 0; i < cnt; i++) { +- ui_options.cb_ospreset->addItem(QString("%1 %2").arg(os_presets.at(i).name, os_presets.at(i).version)); ++ ui_options.cb_ospreset->addItem(os_presets.at(i).name); + } + // Заполняем виджет пресетов клиента + ui_options.cb_clientpreset->addItem("default", "default"); +@@ -160,13 +153,20 @@ + for (int i = 0; i < cnt; i++) { + ui_options.cb_clientpreset->addItem(client_presets.at(i).name); + } +- ++ // Элементы для просмотра логов ++ QDir dir(logsDir); ++ int pos = -1; ++ for (const QString &file : dir.entryList(QDir::Files)) { ++ ui_options.cb_logslist->addItem(file); ++ ++pos; ++ if (file == lastLogItem) ++ ui_options.cb_logslist->setCurrentIndex(pos); ++ } ++ if (pos < 0) ++ ui_options.bt_viewlog->setEnabled(false); + //-- +-#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) + connect(ui_options.cb_allaccounts, &QCheckBox::stateChanged, this, &ClientSwitcherPlugin::enableAccountsList); +-#else +- connect(ui_options.cb_allaccounts, &QCheckBox::checkStateChanged, this, &ClientSwitcherPlugin::enableAccountsList); +-#endif ++ connect(ui_options.bt_viewlog, &QPushButton::released, this, &ClientSwitcherPlugin::viewFromOpt); + // TODO: update after stopping support of Ubuntu Xenial: + connect(ui_options.cb_accounts, SIGNAL(currentIndexChanged(int)), this, SLOT(restoreOptionsAcc(int))); + connect(ui_options.cmb_lockrequ, SIGNAL(currentIndexChanged(int)), this, SLOT(enableMainParams(int))); +@@ -179,11 +179,13 @@ + + void ClientSwitcherPlugin::applyOptions() + { ++ bool caps_updated = false; + // Аккаунт + bool for_all_acc_old = for_all_acc; + for_all_acc = ui_options.cb_allaccounts->isChecked(); +- bool caps_updated = (for_all_acc != for_all_acc_old); +- ++ if (for_all_acc != for_all_acc_old) { ++ caps_updated = true; ++ } + int acc_index = ui_options.cb_accounts->currentIndex(); + if (acc_index == -1 && !for_all_acc) + return; +@@ -196,39 +198,53 @@ + as->account_id = acc_id; + settingsList.push_back(as); + } +- ++ // Подмена/блокировка для контактов ++ bool tmp_flag = ui_options.cb_contactsenable->isChecked(); ++ if (as->enable_contacts != tmp_flag) { ++ as->enable_contacts = tmp_flag; ++ caps_updated = true; ++ } ++ // Подмена/блокировка для конференций ++ tmp_flag = ui_options.cb_conferencesenable->isChecked(); ++ if (as->enable_conferences != tmp_flag) { ++ as->enable_conferences = tmp_flag; ++ caps_updated = true; ++ } + // Блокировка запроса версии + int respMode = ui_options.cmb_lockrequ->currentIndex(); + if (as->response_mode != respMode) { +- caps_updated = (as->response_mode == AccountSettings::RespAllow || respMode == AccountSettings::RespAllow); ++ if (as->response_mode == AccountSettings::RespAllow || respMode == AccountSettings::RespAllow) ++ caps_updated = true; + as->response_mode = respMode; + } + // Блокировка запроса времени +- bool tmp_flag = ui_options.cb_locktimerequ->isChecked(); ++ tmp_flag = ui_options.cb_locktimerequ->isChecked(); + if (as->lock_time_requ != tmp_flag) { + as->lock_time_requ = tmp_flag; + caps_updated = true; + } ++ // Уведомления при запросах версии ++ as->show_requ_mode = ui_options.cmb_showrequ->currentIndex(); ++ // Ведение лога ++ as->log_mode = ui_options.cmb_savetolog->currentIndex(); + // Наименование ОС + if (ui_options.cb_ospreset->currentIndex() == 0) { +- caps_updated = !(as->os_name.isEmpty() && as->os_version.isEmpty()); +- as->os_name = QString(); +- as->os_version = QString(); ++ as->os_name = ""; + } else { +- auto name = ui_options.le_osname->text().trimmed(); +- auto version = ui_options.le_osversion->text().trimmed(); +- caps_updated = !(name == as->os_name && version == as->os_version); +- as->os_name = name; +- as->os_version = version; ++ as->os_name = ui_options.le_osname->text().trimmed(); + } + // Клиент + if (ui_options.cb_clientpreset->currentIndex() == 0) { +- as->client_name = QString(); +- as->client_version = QString(); ++ as->client_name = ""; ++ as->client_version = ""; + if (!as->caps_node.isEmpty()) { +- as->caps_node = QString(); ++ as->caps_node = ""; + caps_updated = true; + } ++ if (!as->caps_version.isEmpty()) { ++ as->caps_version = ""; ++ caps_updated = true; ++ } + } else { + // Название клиента + as->client_name = ui_options.le_clientname->text().trimmed(); +@@ -240,6 +256,12 @@ + as->caps_node = str1; + caps_updated = true; + } ++ // Caps version клиента ++ str1 = ui_options.le_capsversion->text().trimmed(); ++ if (as->caps_version != str1) { ++ as->caps_version = str1; ++ caps_updated = true; ++ } + } + // Сохраняем опции + psiOptions->setPluginOption(constForAllAcc, QVariant(for_all_acc)); +@@ -284,7 +306,7 @@ + QString name = psiAccount->getName(i); + if (name.isEmpty()) + name = "?"; +- ui_options.cb_accounts->addItem(QString("%1 (%2)").arg(name, psiAccount->getJid(i)), QVariant(id)); ++ ui_options.cb_accounts->addItem(QString("%1 (%2)").arg(name).arg(psiAccount->getJid(i)), QVariant(id)); + cnt++; + } + } +@@ -304,8 +326,66 @@ + + void ClientSwitcherPlugin::optionChanged(const QString & /*option*/) { } + ++//-- StanzaSender --------------------------------------------------- ++ ++void ClientSwitcherPlugin::setStanzaSendingHost(StanzaSendingHost *host) { sender_ = host; } ++ + // ----------------------- StanzaFilter ------------------------------ +-bool ClientSwitcherPlugin::incomingStanza(int, const QDomElement &) { return false; } ++bool ClientSwitcherPlugin::incomingStanza(int account, const QDomElement &stanza) ++{ ++ if (!enabled) ++ return false; ++ QString acc_id = (for_all_acc) ? "all" : psiAccount->getId(account); ++ AccountSettings *as = getAccountSetting(acc_id); ++ if (!as) ++ return false; ++ if (!as->enable_contacts && !as->enable_conferences) ++ return false; ++ int respMode = as->response_mode; ++ if (respMode != AccountSettings::RespAllow || as->lock_time_requ || !as->caps_node.isEmpty() ++ || !as->caps_version.isEmpty()) { ++ if (stanza.tagName() == "iq" && stanza.attribute("type") == "get") { ++ const QString s_to = stanza.attribute("from"); ++ if (isSkipStanza(as, account, s_to)) ++ return false; ++ QDomNode s_child = stanza.firstChild(); ++ while (!s_child.isNull()) { ++ const QString xmlns = s_child.toElement().namespaceURI(); ++ if (s_child.toElement().tagName() == "query") { ++ if (xmlns == "http://jabber.org/protocol/disco#info") { ++ QString node = s_child.toElement().attribute("node"); ++ if (!node.isEmpty()) { ++ QString new_node = def_caps_node; ++ QStringList split_node = node.split("#"); ++ if (split_node.size() > 1) { ++ split_node.removeFirst(); ++ QString node_ver = split_node.join("#"); ++ if (node_ver == ((respMode == AccountSettings::RespAllow) ? as->caps_version : "n/a")) { ++ node_ver = def_caps_version; ++ } ++ new_node.append("#" + node_ver); ++ } ++ s_child.toElement().setAttribute("node", new_node); ++ } ++ } else if (xmlns == "jabber:iq:version") { ++ if (respMode == AccountSettings::RespIgnore) { ++ // Showing popup if it is necessary ++ if (as->show_requ_mode == AccountSettings::LogAlways) ++ showPopup(jidToNick(account, s_to)); ++ // Write log if it is necessary ++ if (as->log_mode == AccountSettings::LogAlways) ++ saveToLog(account, s_to, "ignored"); ++ return true; ++ } ++ } ++ } ++ s_child = s_child.nextSibling(); ++ } ++ } ++ } ++ ++ return false; ++} + + bool ClientSwitcherPlugin::outgoingStanza(int account, QDomElement &stanza) + { +@@ -315,15 +395,216 @@ + AccountSettings *as = getAccountSetting(acc_id); + if (!as) + return false; +- ++ if (as->response_mode != AccountSettings::RespAllow || !as->caps_node.isEmpty() || !as->caps_version.isEmpty()) { ++ // --------------- presence --------------- ++ if (stanza.tagName() == "presence") { ++ if (!as->enable_contacts && !as->enable_conferences) ++ return false; ++ // Анализ сообщения о присутствии ++ bool skipFlag = isSkipStanza(as, account, stanza.attribute("to")); ++ if (skipFlag && !as->enable_conferences) // Конференция не определяется при входе в нее, проверим позже ++ return false; ++ short unsigned int found_flag = 0; // Чтобы исключить перебор лишних элементов ++ QDomNode s_child = stanza.firstChild(); ++ QDomNode caps_node; ++ while (!s_child.isNull()) { ++ if (((found_flag & 1) == 0) && s_child.toElement().tagName() == "c") { ++ // Подмена будет отложенной ++ caps_node = s_child; ++ found_flag |= 1; ++ if (found_flag == 3) ++ break; ++ s_child = s_child.nextSibling(); ++ continue; ++ } ++ if (((found_flag & 2) == 0) && s_child.toElement().tagName() == "x") { ++ if (s_child.namespaceURI() == "http://jabber.org/protocol/muc") { ++ found_flag |= 2; ++ if (found_flag == 3) ++ break; ++ } ++ } ++ s_child = s_child.nextSibling(); ++ } ++ if ((found_flag == 1 && as->enable_contacts) || (found_flag == 3 && as->enable_conferences)) { ++ // Подменяем капс ++ if (as->response_mode != AccountSettings::RespNotImpl) { ++ caps_node.toElement().setAttribute("node", as->caps_node); ++ caps_node.toElement().setAttribute("ver", as->caps_version); ++ } else { ++ caps_node.toElement().setAttribute("node", "unknown"); ++ caps_node.toElement().setAttribute("ver", "n/a"); ++ } ++ } ++ return false; ++ } ++ } + if (stanza.tagName() == "iq" && stanza.attribute("type") == "result") { +- // QString s_to = stanza.attribute("to"); ++ bool is_version_query = false; ++ bool is_version_replaced = false; ++ QString s_to = stanza.attribute("to"); + QStringList send_ver_list; +- auto s_child = stanza.firstChildElement(); ++ QDomNode s_child = stanza.firstChild(); ++ int respMode = as->response_mode; + while (!s_child.isNull()) { +- if (s_child.tagName() == "time") { ++ if (s_child.toElement().tagName() == "query") { ++ QString xmlns_str = s_child.namespaceURI(); ++ if (xmlns_str == "http://jabber.org/protocol/disco#info") { ++ // --- Ответ disco ++ if (isSkipStanza(as, account, s_to)) ++ return false; ++ if (respMode == AccountSettings::RespAllow && !as->lock_time_requ && as->caps_node.isEmpty() ++ && as->caps_version.isEmpty()) ++ return false; ++ // Подменяем ноду, если она есть ++ QString node = s_child.toElement().attribute("node"); ++ if (!node.isEmpty()) { ++ QString new_node = (respMode == AccountSettings::RespAllow) ? as->caps_node : "unknown"; ++ QStringList split_node = node.split("#"); ++ if (split_node.size() > 1) { ++ split_node.removeFirst(); ++ QString new_ver = split_node.join("#"); ++ if (new_ver == def_caps_version) ++ new_ver = (respMode == AccountSettings::RespAllow) ? as->caps_version : "n/a"; ++ new_node.append("#" + new_ver); ++ } ++ s_child.toElement().setAttribute("node", new_node); ++ } ++ // Подменяем identity и удаляем feature для версии, если есть блокировка ++ int update = 0; ++ if (respMode == AccountSettings::RespAllow) ++ ++update; ++ if (!as->lock_time_requ) ++ ++update; ++ QDomNode ver_domnode; ++ QDomNode time_domnode; ++ QDomNode q_child = s_child.firstChild(); ++ while (!q_child.isNull()) { ++ QString tag_name = q_child.toElement().tagName(); ++ if (tag_name == "feature") { ++ if (respMode != AccountSettings::RespAllow ++ && q_child.toElement().attribute("var") == "jabber:iq:version") { ++ ver_domnode = q_child; ++ if (++update >= 3) ++ break; ++ } else if (as->lock_time_requ && q_child.toElement().attribute("var") == "urn:xmpp:time") { ++ time_domnode = q_child; ++ if (++update >= 3) ++ break; ++ } ++ } else if (tag_name == "identity") { ++ if (!q_child.toElement().attribute("name").isEmpty()) ++ q_child.toElement().setAttribute( ++ "name", (respMode == AccountSettings::RespAllow) ? as->client_name : "unknown"); ++ if (++update >= 3) ++ break; ++ } ++ q_child = q_child.nextSibling(); ++ } ++ if (!s_child.isNull()) { ++ if (!ver_domnode.isNull()) ++ s_child.removeChild(ver_domnode); ++ if (!time_domnode.isNull()) ++ s_child.removeChild(time_domnode); ++ } ++ } else if (xmlns_str == "jabber:iq:version") { ++ // Ответ version ++ is_version_query = true; ++ bool skip_stanza = isSkipStanza(as, account, s_to); ++ if (skip_stanza && as->log_mode != AccountSettings::LogAlways) ++ break; ++ QDomDocument xmldoc = stanza.ownerDocument(); ++ if (respMode == AccountSettings::RespAllow) { ++ // Подменяем ответ ++ bool f_os_name = false; ++ bool f_client_name = false; ++ bool f_client_ver = false; ++ QDomNode q_child = s_child.firstChild(); ++ while (!q_child.isNull()) { ++ QString tag_name = q_child.toElement().tagName().toLower(); ++ if (tag_name == "os") { ++ QString str1; ++ if (!skip_stanza && !as->os_name.isEmpty()) { ++ str1 = as->os_name; ++ q_child.toElement().replaceChild(xmldoc.createTextNode(str1), q_child.firstChild()); ++ is_version_replaced = true; ++ } else { ++ str1 = q_child.toElement().text(); ++ } ++ send_ver_list.push_back("OS: " + str1); ++ f_os_name = true; ++ } else if (tag_name == "name") { ++ QString str1; ++ if (!skip_stanza && !as->client_name.isEmpty()) { ++ str1 = as->client_name; ++ q_child.toElement().replaceChild(xmldoc.createTextNode(str1), q_child.firstChild()); ++ is_version_replaced = true; ++ } else { ++ str1 = q_child.toElement().text(); ++ } ++ send_ver_list.push_back("Client name: " + str1); ++ f_client_name = true; ++ } else if (tag_name == "version") { ++ QString str1; ++ if (!skip_stanza && !as->caps_version.isEmpty()) { ++ str1 = as->client_version; ++ q_child.toElement().replaceChild(xmldoc.createTextNode(str1), q_child.firstChild()); ++ is_version_replaced = true; ++ } else { ++ str1 = q_child.toElement().text(); ++ } ++ send_ver_list.push_back("Client version: " + str1); ++ f_client_ver = true; ++ } ++ q_child = q_child.nextSibling(); ++ } ++ // Создаем теги, если их нет в ответе ++ QDomDocument doc; ++ if (!f_client_name && !as->client_name.isEmpty()) { ++ doc = s_child.ownerDocument(); ++ QDomElement cl_name = doc.createElement("name"); ++ cl_name.appendChild(doc.createTextNode(as->client_name)); ++ s_child.appendChild(cl_name); ++ } ++ if (!f_client_ver && !as->client_version.isEmpty()) { ++ if (doc.isNull()) ++ doc = s_child.ownerDocument(); ++ QDomElement cl_ver = doc.createElement("version"); ++ cl_ver.appendChild(doc.createTextNode(as->client_version)); ++ s_child.appendChild(cl_ver); ++ } ++ if (!f_os_name && !as->os_name.isEmpty()) { ++ if (doc.isNull()) ++ doc = s_child.ownerDocument(); ++ QDomElement os_name = doc.createElement("os"); ++ os_name.appendChild(doc.createTextNode(as->os_name)); ++ s_child.appendChild(os_name); ++ } ++ } else if (respMode == AccountSettings::RespNotImpl) { ++ // Отклонение запроса, как будто не реализовано ++ stanza.setAttribute("type", "error"); ++ QDomNode q_child = s_child.firstChild(); ++ while (!q_child.isNull()) { ++ s_child.removeChild(q_child); ++ q_child = s_child.firstChild(); ++ } ++ QDomElement err = xmldoc.createElement("error"); ++ err.setAttribute("type", "cancel"); ++ err.setAttribute("code", "501"); ++ stanza.appendChild(err); ++ QDomElement not_imp ++ = xmldoc.createElementNS("urn:ietf:params:xml:ns:xmpp-stanzas", "feature-not-implemented"); ++ err.appendChild(not_imp); ++ send_ver_list.push_back("feature-not-implemented"); ++ } ++ } ++ break; ++ } else if (s_child.toElement().tagName() == "time") { + QString xmlns_str = s_child.namespaceURI(); + if (xmlns_str == "urn:xmpp:time") { ++ // Ответ time ++ if (isSkipStanza(as, account, s_to)) ++ break; + if (as->lock_time_requ) { + QDomDocument xmldoc = stanza.ownerDocument(); + // Отклонение запроса, как будто не реализовано +@@ -343,8 +624,18 @@ + } + } + } +- s_child = s_child.nextSiblingElement(); ++ s_child = s_child.nextSibling(); + } ++ ++ // Showing popup if it is necessary ++ if (is_version_query && as->show_requ_mode != AccountSettings::LogNever ++ && (as->show_requ_mode == AccountSettings::LogAlways || is_version_replaced)) ++ showPopup(jidToNick(account, s_to)); ++ ++ // Write log if it is necessary ++ if (is_version_query && as->log_mode != AccountSettings::LogNever ++ && (as->log_mode == AccountSettings::LogAlways || is_version_replaced)) ++ saveToLog(account, s_to, send_ver_list.join(", ")); + } + return false; + } +@@ -355,6 +646,8 @@ + "You can specify the version of the client and OS or to select them from the preset list.\n"); + } + ++void ClientSwitcherPlugin::setPopupAccessingHost(PopupAccessingHost *host) { psiPopup = host; } ++ + // ----------------------- ApplicationInfoAccessor ------------------------------ + void ClientSwitcherPlugin::setApplicationInfoAccessingHost(ApplicationInfoAccessingHost *host) + { +@@ -366,7 +659,6 @@ + def_caps_node = psiInfo->appCapsNode(); + def_caps_version = psiInfo->appCapsVersion(); + def_os_name = psiInfo->appOsName(); +- def_os_version = psiInfo->appOsVersion(); + } + } + +@@ -374,22 +666,23 @@ + void ClientSwitcherPlugin::setAccountInfoAccessingHost(AccountInfoAccessingHost *host) { psiAccount = host; } + + // ----------------------- PsiAccountController ---------------------------------- +-void ClientSwitcherPlugin::setPsiAccountControllingHost(PsiAccountControllingHost *host) +-{ +- psiAccountCtl = host; ++void ClientSwitcherPlugin::setPsiAccountControllingHost(PsiAccountControllingHost *host) { psiAccountCtl = host; } + +- psiAccountCtl->subscribeBeforeLogin(this, [this](int accountId) { updateInfo(accountId); }); +-} ++// ----------------------- ContactInfoAccessor ----------------------- ++void ClientSwitcherPlugin::setContactInfoAccessingHost(ContactInfoAccessingHost *host) { psiContactInfo = host; } ++ ++// ----------------------- IconFactoryAccessor ----------------------- ++void ClientSwitcherPlugin::setIconFactoryAccessingHost(IconFactoryAccessingHost *host) { psiIcon = host; } + + // ----------------------- Private ------------------------------ + +-int ClientSwitcherPlugin::getOsTemplateIndex(const QString &os_name, const QString &os_version) ++int ClientSwitcherPlugin::getOsTemplateIndex(const QString &os_name) + { + if (os_name.isEmpty()) + return 0; // default + int cnt = os_presets.size(); + for (int i = 0; i < cnt; i++) { +- if (os_name == os_presets.at(i).name && os_version == os_presets.at(i).version) { ++ if (os_name == os_presets.at(i).name) { + i += 2; // Т.к. впереди default и user defined + return i; + } +@@ -397,14 +690,15 @@ + return 1; // user defined + } + +-int ClientSwitcherPlugin::getClientTemplateIndex(const QString &cl_name, const QString &cl_ver, const QString &cp_node) ++int ClientSwitcherPlugin::getClientTemplateIndex(const QString &cl_name, const QString &cl_ver, const QString &cp_node, ++ const QString &cp_ver) + { +- if (cl_name.isEmpty() && cl_ver.isEmpty() && cp_node.isEmpty()) ++ if (cl_name.isEmpty() && cl_ver.isEmpty() && cp_node.isEmpty() && cp_ver.isEmpty()) + return 0; // default + int cnt = client_presets.size(); + for (int i = 0; i < cnt; i++) { + if (cl_name == client_presets.at(i).name && cl_ver == client_presets.at(i).version) { +- if (cp_node == client_presets.at(i).caps_node) { ++ if (cp_node == client_presets.at(i).caps_node && cp_ver == client_presets.at(i).caps_version) { + i += 2; // есть еще default и user defined + return i; + } +@@ -438,28 +732,6 @@ + return nullptr; + } + +-ClientSwitcherPlugin::UpdateStatus ClientSwitcherPlugin::updateInfo(int account) +-{ +- if (!enabled || !psiAccount || !psiAccountCtl) +- return UpdateStatus::Disabled; +- +- QString acc_id = psiAccount->getId(account); +- if (acc_id == "-1" || acc_id.isEmpty()) +- return UpdateStatus::NoAccount; +- +- AccountSettings *as = getAccountSetting(acc_id); +- if (!as || !as->isValid()) +- return UpdateStatus::NoSettings; +- +- QVariantMap info = { { "os-name", as->os_name }, +- { "os-version", as->os_version }, +- { "client-name", as->client_name }, +- { "client-version", as->client_version }, +- { "caps-node", as->caps_node } }; +- psiAccountCtl->setClientVersionInfo(account, info); +- return UpdateStatus::Ok; +-} +- + /** + * Отсылка пресентса с новыми капсами от имени указанного аккаунта + * Если account == -1, то капсы посылаются всем активным аккаунтам +@@ -472,10 +744,10 @@ + if (acc == -1) + acc = 0; + for (;; acc++) { +- auto ret = updateInfo(acc); +- if (ret == UpdateStatus::NoAccount) ++ QString acc_id = psiAccount->getId(acc); ++ if (acc_id == "-1") + break; +- if (ret == UpdateStatus::Ok) { ++ if (!acc_id.isEmpty()) { + QString acc_status = psiAccount->getStatus(acc); + if (!acc_status.isEmpty() && acc_status != "offline" && acc_status != "invisible") { + // Отсылаем тот же статус, а капсы заменим в outgoingStanza() +@@ -487,6 +759,32 @@ + } + } + ++bool ClientSwitcherPlugin::isSkipStanza(AccountSettings *as, const int account, const QString &to) ++{ ++ if (to.isEmpty()) { // Широковещательный ++ if (!as->enable_contacts) ++ return true; ++ } else { // Адресный ++ QString to_jid = to.split("/").takeFirst(); ++ if (!to_jid.contains("@")) { ++ if (as->enable_contacts) { ++ if (!to.contains("/")) ++ return false; // Предполагаем что это сервер ++ return true; // Ошибочный запрос ++ } ++ } ++ if (psiContactInfo->isConference(account, to_jid) || psiContactInfo->isPrivate(account, to)) { ++ // if (to.contains("conference.")) { ++ if (!as->enable_conferences) ++ return true; ++ } else { ++ if (!as->enable_contacts) ++ return true; ++ } ++ } ++ return false; ++} ++ + // ----------------------- Slots ------------------------------ + void ClientSwitcherPlugin::enableAccountsList(int all_acc_mode) + { +@@ -516,23 +814,30 @@ + as->account_id = acc_id; + settingsList.push_back(as); + } ++ // Подмена/блокировка для контактов ++ ui_options.cb_contactsenable->setChecked(as->enable_contacts); ++ // Подмена/блокировка для конференций ++ ui_options.cb_conferencesenable->setChecked(as->enable_conferences); + // Блокировка запроса версии + ui_options.cmb_lockrequ->setCurrentIndex(as->response_mode); + // Блокировка запроса времени + ui_options.cb_locktimerequ->setChecked(as->lock_time_requ); ++ // Уведомления при запросах версии ++ ui_options.cmb_showrequ->setCurrentIndex(as->show_requ_mode); ++ // Ведение лога ++ ui_options.cmb_savetolog->setCurrentIndex(as->log_mode); + // Виджет шаблона ОС +- QString os_name = as->os_name; +- QString os_version = as->os_version; +- int os_templ = getOsTemplateIndex(os_name, os_version); ++ QString os_name = as->os_name; ++ int os_templ = getOsTemplateIndex(os_name); + ui_options.cb_ospreset->setCurrentIndex(os_templ); + // Название ОС + ui_options.le_osname->setText(os_name); +- ui_options.le_osversion->setText(os_version); + // Виджет шаблона клиента + QString cl_name = as->client_name; + QString cl_ver = as->client_version; + QString cp_node = as->caps_node; +- int cl_templ = getClientTemplateIndex(cl_name, cl_ver, cp_node); ++ QString cp_ver = as->caps_version; ++ int cl_templ = getClientTemplateIndex(cl_name, cl_ver, cp_node, cp_ver); + ui_options.cb_clientpreset->setCurrentIndex(cl_templ); + // Название клиента + ui_options.le_clientname->setText(cl_name); +@@ -540,7 +845,10 @@ + ui_options.le_clientversion->setText(cl_ver); + // Caps node клиента + ui_options.le_capsnode->setText(cp_node); ++ // Caps version клиента ++ ui_options.le_capsversion->setText(cp_ver); + // Блокировка/снятие блокировки виджетов ++ ui_options.gb_enablefor->setEnabled(true); + ui_options.cmb_lockrequ->setEnabled(true); + enableMainParams(as->response_mode); + enableOsParams(os_templ); +@@ -549,6 +857,9 @@ + } + } + // Блокировка виджетов при отсутствии валидного акка ++ ui_options.cb_contactsenable->setChecked(false); ++ ui_options.cb_conferencesenable->setChecked(false); ++ ui_options.gb_enablefor->setEnabled(false); + ui_options.cmb_lockrequ->setCurrentIndex(AccountSettings::RespAllow); + ui_options.cmb_lockrequ->setEnabled(false); + ui_options.cb_ospreset->setCurrentIndex(0); +@@ -570,20 +881,16 @@ + { + if (mode == 1) { // user defined + ui_options.le_osname->setEnabled(true); +- ui_options.le_osversion->setEnabled(true); + } else { + if (mode == 0) { // default os + ui_options.le_osname->setText(def_os_name); +- ui_options.le_osversion->setText(def_os_version); + } else { + int pres_index = mode - 2; + if (pres_index >= 0 && pres_index < os_presets.size()) { + ui_options.le_osname->setText(os_presets.at(pres_index).name); +- ui_options.le_osversion->setText(os_presets.at(pres_index).version); + } + } + ui_options.le_osname->setEnabled(false); +- ui_options.le_osversion->setEnabled(false); + } + } + +@@ -606,6 +913,7 @@ + ui_options.le_clientname->setText(client_presets.at(pres_index).name); + ui_options.le_clientversion->setText(client_presets.at(pres_index).version); + ui_options.le_capsnode->setText(client_presets.at(pres_index).caps_node); ++ ui_options.le_capsversion->setText(client_presets.at(pres_index).caps_version); + } + } + ui_options.le_clientname->setEnabled(false); +@@ -614,3 +922,74 @@ + ui_options.le_capsversion->setEnabled(false); + } + } ++ ++void ClientSwitcherPlugin::viewFromOpt() ++{ ++ lastLogItem = ui_options.cb_logslist->currentText(); ++ if (lastLogItem.isEmpty()) ++ return; ++ psiOptions->setPluginOption(constLastLogItem, QVariant(lastLogItem)); ++ showLog(lastLogItem); ++} ++ ++QString ClientSwitcherPlugin::jidToNick(int account, const QString &jid) ++{ ++ QString nick; ++ if (psiContactInfo) ++ nick = psiContactInfo->name(account, jid); ++ if (nick.isEmpty()) ++ nick = jid; ++ return nick; ++} ++ ++void ClientSwitcherPlugin::showPopup(const QString &nick) ++{ ++ int msecs = psiPopup->popupDuration(constPluginName); ++ if (msecs > 0) ++ psiPopup->initPopup(tr("%1 has requested your version").arg(sender_->escape(nick)), constPluginName, ++ "psi/headline", popupId); ++} ++ ++void ClientSwitcherPlugin::showLog(const QString &filename) ++{ ++ QString fullname = logsDir + filename; ++ Viewer *v = new Viewer(fullname, psiIcon); ++ v->resize(widthLogsView, heightLogsView); ++ if (!v->init()) { ++ delete (v); ++ return; ++ } ++ connect(v, &Viewer::onClose, this, &ClientSwitcherPlugin::onCloseView); ++ v->show(); ++} ++ ++void ClientSwitcherPlugin::saveToLog(const int account, const QString &to_jid, const QString &ver_str) ++{ ++ QString acc_jid = psiAccount->getJid(account); ++ if (acc_jid.isEmpty() || acc_jid == "-1") ++ return; ++ QFile file(logsDir + acc_jid.replace("@", "_at_") + ".log"); ++ if (file.open(QIODevice::WriteOnly | QIODevice::Append)) { ++ QString time_str = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); ++ QTextStream out(&file); ++ out.setCodec("UTF-8"); ++ out.setGenerateByteOrderMark(false); ++#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) ++ out << time_str << " " << to_jid << " <-- " << ver_str << Qt::endl; ++#else ++ out << time_str << " " << to_jid << " <-- " << ver_str << endl; ++#endif ++ } ++} ++ ++void ClientSwitcherPlugin::onCloseView(int w, int h) ++{ ++ if (widthLogsView != w) { ++ widthLogsView = w; ++ psiOptions->setPluginOption(constShowLogWidth, QVariant(w)); ++ } ++ if (heightLogsView != h) { ++ heightLogsView = h; ++ psiOptions->setPluginOption(constShowLogHeight, QVariant(h)); ++ } ++} +diff -Nru a/plugins/generic/clientswitcherplugin/clientswitcherplugin.h b/plugins/generic/clientswitcherplugin/clientswitcherplugin.h +--- a/plugins/generic/clientswitcherplugin/clientswitcherplugin.h 2025-08-26 14:50:07.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/clientswitcherplugin.h 2020-07-10 11:50:10.000000000 +0000 +@@ -42,6 +42,8 @@ + #include "optionaccessinghost.h" + #include "optionaccessor.h" + #include "plugininfoprovider.h" ++#include "popupaccessinghost.h" ++#include "popupaccessor.h" + #include "psiaccountcontroller.h" + #include "psiaccountcontrollinghost.h" + #include "psiplugin.h" +@@ -54,17 +56,22 @@ + class ClientSwitcherPlugin : public QObject, + public PsiPlugin, + public OptionAccessor, ++ public StanzaSender, + public StanzaFilter, + public PluginInfoProvider, ++ public PopupAccessor, + public ApplicationInfoAccessor, + public AccountInfoAccessor, +- public PsiAccountController ++ public PsiAccountController, ++ public ContactInfoAccessor, ++ public IconFactoryAccessor + + { + Q_OBJECT + Q_PLUGIN_METADATA(IID "com.psi-plus.ClientSwitcherPlugin" FILE "psiplugin.json") +- Q_INTERFACES(PsiPlugin OptionAccessor StanzaFilter PluginInfoProvider ApplicationInfoAccessor AccountInfoAccessor +- PsiAccountController) ++ Q_INTERFACES( ++ PsiPlugin OptionAccessor StanzaSender StanzaFilter PluginInfoProvider PopupAccessor ApplicationInfoAccessor ++ AccountInfoAccessor PsiAccountController ContactInfoAccessor IconFactoryAccessor) + + public: + ClientSwitcherPlugin(); +@@ -78,59 +85,79 @@ + // OptionAccessor + virtual void setOptionAccessingHost(OptionAccessingHost *); + virtual void optionChanged(const QString &); ++ // StanzaSender ++ virtual void setStanzaSendingHost(StanzaSendingHost *); + // StanzaFilter + virtual bool incomingStanza(int, const QDomElement &); + virtual bool outgoingStanza(int, QDomElement &); + // EventFilter + virtual QString pluginInfo(); ++ // PopupAccessor ++ virtual void setPopupAccessingHost(PopupAccessingHost *); + // ApplicationInfoAccessor + virtual void setApplicationInfoAccessingHost(ApplicationInfoAccessingHost *); + // AccountInfoAccessing + virtual void setAccountInfoAccessingHost(AccountInfoAccessingHost *); + // PsiAccountController + virtual void setPsiAccountControllingHost(PsiAccountControllingHost *); ++ // ContactInfoAccessor ++ virtual void setContactInfoAccessingHost(ContactInfoAccessingHost *); ++ // IconFactoryAccessor ++ virtual void setIconFactoryAccessingHost(IconFactoryAccessingHost *host); + + private: + struct OsStruct { + QString name; +- QString version; +- OsStruct(const QString &n, const QString &v = QString()) : name(n), version(v) { } ++ OsStruct(const QString &n) : name(n) { } + }; + + struct ClientStruct { + QString name; + QString version; + QString caps_node; +- ClientStruct(const QString &n, const QString &v, const QString &cn) : name(n), version(v), caps_node(cn) { } ++ QString caps_version; ++ ClientStruct(const QString &n, const QString &v, const QString &cn, const QString &cv) : ++ name(n), version(v), caps_node(cn), caps_version(cv) ++ { ++ } + }; +- + Ui::OptionsWidget ui_options; +- OptionAccessingHost *psiOptions; ++ StanzaSendingHost * sender_; ++ OptionAccessingHost * psiOptions; ++ PopupAccessingHost * psiPopup; + ApplicationInfoAccessingHost *psiInfo; +- AccountInfoAccessingHost *psiAccount; +- PsiAccountControllingHost *psiAccountCtl; ++ AccountInfoAccessingHost * psiAccount; ++ PsiAccountControllingHost * psiAccountCtl; ++ ContactInfoAccessingHost * psiContactInfo; ++ IconFactoryAccessingHost * psiIcon; + //-- + bool enabled; + bool for_all_acc; + QList settingsList; + //-- + QString def_os_name; +- QString def_os_version; + QString def_client_name; + QString def_client_version; + QString def_caps_node; + QString def_caps_version; + QList os_presets; + QList client_presets; ++ QString logsDir; ++ int heightLogsView; ++ int widthLogsView; ++ QString lastLogItem; ++ int popupId; ++ //-- + +- int getOsTemplateIndex(const QString &, const QString &os_version); +- int getClientTemplateIndex(const QString &, const QString &, const QString &); ++ int getOsTemplateIndex(const QString &); ++ int getClientTemplateIndex(const QString &, const QString &, const QString &, const QString &); + int getAccountById(const QString &); + AccountSettings *getAccountSetting(const QString &); +- +-private: +- enum class UpdateStatus { Ok, Disabled, NoAccount, NoSettings }; +- UpdateStatus updateInfo(int account); ++ bool isSkipStanza(AccountSettings *, const int, const QString &); ++ QString jidToNick(int account, const QString &jid); ++ void showPopup(const QString &nick); ++ void showLog(const QString &filename); ++ void saveToLog(const int, const QString &, const QString &); + + private slots: + void enableAccountsList(int); +@@ -139,6 +166,8 @@ + void enableOsParams(int); + void enableClientParams(int); + void setNewCaps(int); ++ void viewFromOpt(); ++ void onCloseView(int, int); + }; + + #endif +diff -Nru a/plugins/generic/clientswitcherplugin/CMakeLists.txt b/plugins/generic/clientswitcherplugin/CMakeLists.txt +--- a/plugins/generic/clientswitcherplugin/CMakeLists.txt 2025-08-26 14:50:07.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/CMakeLists.txt 2020-07-10 11:50:10.000000000 +0000 +@@ -6,12 +6,9 @@ + + set( PLUGIN clientswitcherplugin ) + project(${PLUGIN} LANGUAGES CXX) +-cmake_minimum_required(VERSION 3.10.0) ++cmake_minimum_required(VERSION 3.1.0) + if(POLICY CMP0071) +- cmake_policy(SET CMP0071 NEW) +-endif() +-if(POLICY CMP0074) +- cmake_policy(SET CMP0074 NEW) ++ cmake_policy(SET CMP0071 OLD) + endif() + set( CMAKE_AUTOMOC TRUE ) + +@@ -24,6 +21,7 @@ + find_package(PsiPluginsApi REQUIRED) + include(${PsiPluginsApi_DIR}/variables.cmake) + include_directories( ++ ${CMAKE_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_LIST_DIR} + ${PsiPluginsApi_INCLUDE_DIR} +@@ -35,20 +33,24 @@ + set( _HDRS + ${PLUGIN}.h + accountsettings.h ++ viewer.h ++ typeaheadfind.h + ) + set( _SRCS + ${PLUGIN}.cpp + accountsettings.cpp ++ viewer.cpp ++ typeaheadfind.cpp + ) + set( _UIS + options.ui + ) +-find_package( Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Widgets Xml REQUIRED ) ++find_package( Qt5 COMPONENTS Widgets Xml REQUIRED ) + set(QT_DEPLIBS +- Qt${QT_DEFAULT_MAJOR_VERSION}::Widgets +- Qt${QT_DEFAULT_MAJOR_VERSION}::Xml ++ Qt5::Widgets ++ Qt5::Xml + ) +-qt_wrap_ui(UIS ${_UIS}) ++qt5_wrap_ui(UIS ${_UIS}) + + if(WIN32) + set(LIB_TYPE "MODULE") +diff -Nru a/plugins/generic/clientswitcherplugin/options.ui b/plugins/generic/clientswitcherplugin/options.ui +--- a/plugins/generic/clientswitcherplugin/options.ui 2025-08-26 14:50:07.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/options.ui 2020-07-10 11:50:10.000000000 +0000 +@@ -13,109 +13,57 @@ + + Form + +- ++ + +- +- +- +- +- Account: +- +- +- +- +- +- +- +- 141 +- 0 +- +- +- +- +- +- +- +- For all accounts +- +- +- +- +- +- +- Qt::Horizontal +- +- +- +- 40 +- 20 +- +- +- +- +- +- +- +- +- +- true ++ ++ ++ ++ 0 ++ 1 ++ ++ ++ ++ 0 + +- +- +- +- 0 +- 0 +- 942 +- 664 +- +- +- ++ ++ ++ General ++ ++ + +- ++ + +- ++ + +- Response mode ++ Account: + + + + +- +- +- +- allow +- +- +- +- +- not implemented +- +- +- +- +- ignore +- +- ++ ++ ++ ++ 141 ++ 0 ++ ++ + + + +- ++ + +- Deny iq time request ++ For all accounts + + + + +- ++ + + Qt::Horizontal + +- +- QSizePolicy::Expanding +- + + +- 18 ++ 40 + 20 + + +@@ -124,133 +72,347 @@ + + + +- +- ++ ++ + true + +- +- OS +- +- +- +- +- +- +- +- OS name +- +- +- +- +- +- +- Template +- +- +- +- +- +- +- +- +- +- +- +- +- OS version +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- Client +- +- +- +- +- +- +- +- Template +- +- +- +- +- +- +- +- +- +- Client name +- +- +- +- +- +- +- +- +- +- Client version +- +- +- +- +- +- +- +- +- +- Caps node +- +- +- +- +- +- +- +- +- +- Caps version +- +- +- +- +- +- +- +- +- ++ ++ ++ ++ 0 ++ 0 ++ 930 ++ 573 ++ ++ ++ ++ ++ ++ ++ ++ ++ Response mode ++ ++ ++ ++ ++ ++ ++ ++ allow ++ ++ ++ ++ ++ not implemented ++ ++ ++ ++ ++ ignore ++ ++ ++ ++ ++ ++ ++ ++ Deny iq time request ++ ++ ++ ++ ++ ++ ++ Qt::Horizontal ++ ++ ++ QSizePolicy::Expanding ++ ++ ++ ++ 18 ++ 20 ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ true ++ ++ ++ OS ++ ++ ++ ++ ++ ++ ++ ++ Template ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ OS name ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Client ++ ++ ++ ++ ++ ++ ++ ++ Template ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Client name ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Client version ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Caps node ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Caps version ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ 0 ++ 0 ++ ++ ++ ++ Enable for: ++ ++ ++ ++ ++ ++ ++ ++ Contacts ++ ++ ++ ++ ++ ++ ++ Qt::Horizontal ++ ++ ++ QSizePolicy::Maximum ++ ++ ++ ++ 30 ++ 20 ++ ++ ++ ++ ++ ++ ++ ++ Groupchats ++ ++ ++ ++ ++ ++ ++ Qt::Horizontal ++ ++ ++ QSizePolicy::Expanding ++ ++ ++ ++ 18 ++ 20 ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Show popup at version iq ++ ++ ++ ++ ++ ++ ++ ++ never ++ ++ ++ ++ ++ if iq replaced ++ ++ ++ ++ ++ always ++ ++ ++ ++ ++ ++ ++ ++ Save queries to log ++ ++ ++ ++ ++ ++ ++ ++ never ++ ++ ++ ++ ++ if iq replaced ++ ++ ++ ++ ++ always ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Qt::Vertical ++ ++ ++ ++ 20 ++ 40 ++ ++ ++ ++ ++ ++ + + ++ ++ ++ ++ ++ Logs ++ ++ + +- +- +- <b>Attention!</b> Thoughtless usage of Client Switcher Plugin may cause to inability of using OMEMO and OpenPGP encryption. Use functions of this plugin very carefully... +- +- +- true +- +- ++ ++ ++ ++ ++ ++ 1 ++ 0 ++ ++ ++ ++ ++ ++ ++ ++ View log ++ ++ ++ ++ + + +- ++ + + Qt::Vertical + + + + 20 +- 40 ++ 471 + + + +@@ -260,6 +422,16 @@ + + + ++ ++ ++ <b>Attention!</b> Thoughtless usage of Client Switcher Plugin may cause to inability of using OMEMO and OpenPGP encryption. Use functions of this plugin very carefully... ++ ++ ++ true ++ ++ ++ ++ + + + <a href="https://psi-plus.com/wiki/en:plugins#client_switcher_plugin">Wiki (Online)</a> +@@ -271,6 +443,27 @@ + + + ++ ++ tabWidget ++ cb_accounts ++ cb_allaccounts ++ scrollArea ++ cmb_lockrequ ++ cb_locktimerequ ++ cb_ospreset ++ le_osname ++ cb_clientpreset ++ le_clientname ++ le_clientversion ++ le_capsnode ++ le_capsversion ++ cb_contactsenable ++ cb_conferencesenable ++ cmb_showrequ ++ cmb_savetolog ++ cb_logslist ++ bt_viewlog ++ + + + +diff -Nru a/plugins/generic/clientswitcherplugin/psiplugin.json b/plugins/generic/clientswitcherplugin/psiplugin.json +--- a/plugins/generic/clientswitcherplugin/psiplugin.json 2025-08-26 14:50:07.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/psiplugin.json 2020-07-10 11:50:10.000000000 +0000 +@@ -2,7 +2,7 @@ + "name": "Client Switcher", + "name:ru": "Подмена клиента", + "shortname": "clientswitcher", +- "version": "0.2", ++ "version": "0.0.18", + "priority": 2, + "icon": "base64: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", + "description": "Hide or replace information about your XMPP client and/or operating system.", +diff -Nru a/plugins/generic/clientswitcherplugin/typeaheadfind.cpp b/plugins/generic/clientswitcherplugin/typeaheadfind.cpp +--- a/plugins/generic/clientswitcherplugin/typeaheadfind.cpp 1970-01-01 00:00:00.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/typeaheadfind.cpp 2020-07-10 11:50:10.000000000 +0000 +@@ -0,0 +1,178 @@ ++/* ++ * typeaheadfind.cpp - plugin ++ * Copyright (C) 2009-2010 Evgeny Khryukin ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License ++ * as published by the Free Software Foundation; either version 2 ++ * of the License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ * ++ */ ++ ++#include "typeaheadfind.h" ++ ++#include ++#include ++#include ++#include ++ ++using namespace ClientSwitcher; ++ ++class TypeAheadFindBar::Private { ++public: ++ void doFind(bool backward = false) ++ { ++ QTextDocument::FindFlags options; ++ if (caseSensitive) ++ options |= QTextDocument::FindCaseSensitively; ++ if (backward) { ++ options |= QTextDocument::FindBackward; ++ QTextCursor cursor = te->textCursor(); ++ cursor.setPosition(cursor.selectionStart()); ++ cursor.movePosition(QTextCursor::Left); ++ te->setTextCursor(cursor); ++ } ++ if (find(text, options)) { ++ le_find->setStyleSheet(""); ++ } else { ++ le_find->setStyleSheet("QLineEdit { background: #ff6666; color: #ffffff }"); ++ } ++ } ++ ++ bool find(const QString &str, QTextDocument::FindFlags options, ++ QTextCursor::MoveOperation start = QTextCursor::NoMove) ++ { ++ Q_UNUSED(str); ++ if (start != QTextCursor::NoMove) { ++ QTextCursor cursor = te->textCursor(); ++ cursor.movePosition(start); ++ te->setTextCursor(cursor); ++ } ++ bool found = te->find(text, options); ++ if (!found) { ++ if (start == QTextCursor::NoMove) ++ return find(text, options, ++ options & QTextDocument::FindBackward ? QTextCursor::End : QTextCursor::Start); ++ return false; ++ } ++ return true; ++ } ++ ++ QString text; ++ bool caseSensitive; ++ QTextEdit * te; ++ QLineEdit * le_find; ++ QPushButton *but_next; ++ QPushButton *but_prev; ++ QPushButton *first_page, *next_page, *last_page, *prev_page; ++ QCheckBox * cb_case; ++}; ++ ++TypeAheadFindBar::TypeAheadFindBar(IconFactoryAccessingHost *IcoHost, QTextEdit *textedit, const QString &title, ++ QWidget *parent) : ++ QToolBar(title, parent), ++ icoHost_(IcoHost) ++{ ++ d = new Private(); ++ d->te = textedit; ++ init(); ++} ++ ++void TypeAheadFindBar::init() ++{ ++ d->caseSensitive = false; ++ d->text = ""; ++ addWidget(new QLabel(tr("Search: "), this)); ++ ++ d->le_find = new QLineEdit(this); ++ d->le_find->setMaximumWidth(128); ++ connect(d->le_find, &QLineEdit::textEdited, this, &TypeAheadFindBar::textChanged); ++ addWidget(d->le_find); ++ ++ d->but_prev = new QPushButton(this); ++ d->but_prev->setFixedSize(25, 25); ++ d->but_prev->setIcon(icoHost_->getIcon("psi/arrowUp")); ++ d->but_prev->setEnabled(false); ++ connect(d->but_prev, &QPushButton::released, this, &TypeAheadFindBar::findPrevious); ++ addWidget(d->but_prev); ++ ++ d->but_next = new QPushButton(this); ++ d->but_next->setFixedSize(25, 25); ++ d->but_next->setIcon(icoHost_->getIcon("psi/arrowDown")); ++ d->but_next->setEnabled(false); ++ connect(d->but_next, &QPushButton::released, this, &TypeAheadFindBar::findNext); ++ addWidget(d->but_next); ++ ++ d->cb_case = new QCheckBox(tr("&Case sensitive"), this); ++ connect(d->cb_case, &QCheckBox::clicked, this, &TypeAheadFindBar::caseToggled); ++ addWidget(d->cb_case); ++ ++ addSeparator(); ++ ++ d->first_page = new QPushButton(this); ++ d->first_page->setToolTip(tr("First page")); ++ connect(d->first_page, &QPushButton::released, this, &TypeAheadFindBar::firstPage); ++ d->first_page->setFixedSize(25, 25); ++ d->first_page->setIcon(icoHost_->getIcon("psi/doubleBackArrow")); ++ addWidget(d->first_page); ++ ++ d->prev_page = new QPushButton(this); ++ d->prev_page->setToolTip(tr("Previous page")); ++ connect(d->prev_page, &QPushButton::released, this, &TypeAheadFindBar::prevPage); ++ d->prev_page->setFixedSize(25, 25); ++ d->prev_page->setIcon(icoHost_->getIcon("psi/arrowLeft")); ++ addWidget(d->prev_page); ++ ++ d->next_page = new QPushButton(this); ++ d->next_page->setToolTip(tr("Next page")); ++ connect(d->next_page, &QPushButton::released, this, &TypeAheadFindBar::nextPage); ++ d->next_page->setFixedSize(25, 25); ++ d->next_page->setIcon(icoHost_->getIcon("psi/arrowRight")); ++ addWidget(d->next_page); ++ ++ d->last_page = new QPushButton(this); ++ d->last_page->setToolTip(tr("Last page")); ++ connect(d->last_page, &QPushButton::released, this, &TypeAheadFindBar::lastPage); ++ d->last_page->setFixedSize(25, 25); ++ d->last_page->setIcon(icoHost_->getIcon("psi/doubleNextArrow")); ++ addWidget(d->last_page); ++} ++ ++TypeAheadFindBar::~TypeAheadFindBar() ++{ ++ delete d; ++ d = nullptr; ++} ++ ++void TypeAheadFindBar::textChanged(const QString &str) ++{ ++ QTextCursor cursor = d->te->textCursor(); ++ if (str.isEmpty()) { ++ d->but_next->setEnabled(false); ++ d->but_prev->setEnabled(false); ++ d->le_find->setStyleSheet(""); ++ cursor.clearSelection(); ++ d->te->setTextCursor(cursor); ++ } else { ++ d->but_next->setEnabled(true); ++ d->but_prev->setEnabled(true); ++ cursor.setPosition(cursor.selectionStart()); ++ d->te->setTextCursor(cursor); ++ d->text = str; ++ d->doFind(); ++ } ++} ++ ++void TypeAheadFindBar::findNext() { d->doFind(); } ++ ++void TypeAheadFindBar::findPrevious() { d->doFind(true); } ++ ++void TypeAheadFindBar::caseToggled() { d->caseSensitive = d->cb_case->checkState(); } +diff -Nru a/plugins/generic/clientswitcherplugin/typeaheadfind.h b/plugins/generic/clientswitcherplugin/typeaheadfind.h +--- a/plugins/generic/clientswitcherplugin/typeaheadfind.h 1970-01-01 00:00:00.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/typeaheadfind.h 2020-07-10 11:50:10.000000000 +0000 +@@ -0,0 +1,58 @@ ++/* ++ * typeaheadfind.h - plugin ++ * Copyright (C) 2009-2010 Evgeny Khryukin ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License ++ * as published by the Free Software Foundation; either version 2 ++ * of the License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ * ++ */ ++ ++#ifndef TYPEAHEADFIND_H ++#define TYPEAHEADFIND_H ++ ++#include ++#include ++ ++#include "iconfactoryaccessinghost.h" ++ ++namespace ClientSwitcher { ++ ++class TypeAheadFindBar : public QToolBar { ++ Q_OBJECT ++public: ++ TypeAheadFindBar(IconFactoryAccessingHost *IcoHost, QTextEdit *textedit, const QString &title, ++ QWidget *parent = nullptr); ++ ++ ~TypeAheadFindBar(); ++ void init(); ++ ++signals: ++ void firstPage(); ++ void lastPage(); ++ void nextPage(); ++ void prevPage(); ++ ++private slots: ++ void textChanged(const QString &); ++ void findNext(); ++ void findPrevious(); ++ void caseToggled(); ++ ++private: ++ class Private; ++ Private * d; ++ IconFactoryAccessingHost *icoHost_; ++}; ++ ++} ++#endif +diff -Nru a/plugins/generic/clientswitcherplugin/viewer.cpp b/plugins/generic/clientswitcherplugin/viewer.cpp +--- a/plugins/generic/clientswitcherplugin/viewer.cpp 1970-01-01 00:00:00.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/viewer.cpp 2020-07-10 11:50:10.000000000 +0000 +@@ -0,0 +1,190 @@ ++/* ++ * viewer.cpp - plugin ++ * Copyright (C) 2009-2010 Evgeny Khryukin ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License ++ * as published by the Free Software Foundation; either version 2 ++ * of the License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ * ++ */ ++ ++#include "viewer.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++Viewer::Viewer(QString filename, IconFactoryAccessingHost *IcoHost, QWidget *parent) : ++ QDialog(parent), icoHost_(IcoHost), fileName_(filename) ++{ ++ setAttribute(Qt::WA_DeleteOnClose); ++ setWindowTitle(filename); ++ setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint ++ | Qt::CustomizeWindowHint); ++ QVBoxLayout *layout = new QVBoxLayout(this); ++ textWid = new QTextEdit(); ++ QPalette pal = textWid->palette(); ++ pal.setColor(QPalette::Inactive, QPalette::Highlight, pal.color(QPalette::Active, QPalette::Highlight)); ++ pal.setColor(QPalette::Inactive, QPalette::HighlightedText, pal.color(QPalette::Active, QPalette::HighlightedText)); ++ textWid->setPalette(pal); ++ layout->addWidget(textWid); ++ findBar = new ClientSwitcher::TypeAheadFindBar(icoHost_, textWid, tr("Find"), this); ++ QPushButton *Close = new QPushButton(icoHost_->getIcon("psi/quit"), tr("Close")); ++ QPushButton *Save = new QPushButton(icoHost_->getIcon("psi/save"), tr("Save Changes")); ++ QPushButton *Delete = new QPushButton(icoHost_->getIcon("psi/remove"), tr("Delete Log")); ++ QPushButton *Update = new QPushButton(icoHost_->getIcon("psi/reload"), tr("Update Log")); ++ QHBoxLayout *butLayout = new QHBoxLayout(); ++ butLayout->addWidget(Delete); ++ butLayout->addStretch(); ++ butLayout->addWidget(Update); ++ butLayout->addWidget(Save); ++ butLayout->addWidget(Close); ++ layout->addWidget(findBar); ++ layout->addLayout(butLayout); ++ connect(Close, &QPushButton::released, this, &Viewer::close); ++ connect(Delete, &QPushButton::released, this, &Viewer::deleteLog); ++ connect(Save, &QPushButton::released, this, &Viewer::saveLog); ++ connect(Update, &QPushButton::released, this, &Viewer::updateLog); ++ ++ connect(findBar, &ClientSwitcher::TypeAheadFindBar::firstPage, this, &Viewer::firstPage); ++ connect(findBar, &ClientSwitcher::TypeAheadFindBar::lastPage, this, &Viewer::lastPage); ++ connect(findBar, &ClientSwitcher::TypeAheadFindBar::prevPage, this, &Viewer::prevPage); ++ connect(findBar, &ClientSwitcher::TypeAheadFindBar::nextPage, this, &Viewer::nextPage); ++} ++ ++void Viewer::closeEvent(QCloseEvent *e) ++{ ++ emit onClose(width(), height()); ++ QDialog::closeEvent(e); ++ e->accept(); ++} ++ ++void Viewer::deleteLog() ++{ ++ int ret = QMessageBox::question(this, tr("Delete log file"), tr("Are you sure?"), QMessageBox::Yes, ++ QMessageBox::Cancel); ++ if (ret == QMessageBox::Cancel) { ++ return; ++ } ++ close(); ++ QFile file(fileName_); ++ if (file.open(QIODevice::ReadWrite)) { ++ file.remove(); ++ } ++} ++ ++void Viewer::saveLog() ++{ ++ QDateTime Modified = QFileInfo(fileName_).lastModified(); ++ if (lastModified_ < Modified) { ++ QMessageBox msgBox; ++ msgBox.setWindowTitle(tr("Save log")); ++ msgBox.setText(tr("New messages has been added to log. If you save your changes, you will lose them")); ++ msgBox.setInformativeText(tr("Do you want to save your changes?")); ++ msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel); ++ msgBox.setDefaultButton(QMessageBox::Cancel); ++ int ret = msgBox.exec(); ++ if (ret == QMessageBox::Cancel) { ++ return; ++ } ++ } else { ++ int ret ++ = QMessageBox::question(this, tr("Save log"), tr("Are you sure?"), QMessageBox::Yes, QMessageBox::Cancel); ++ if (ret == QMessageBox::Cancel) { ++ return; ++ } ++ } ++ QFile file(fileName_); ++ if (file.open(QIODevice::ReadWrite)) { ++ file.remove(); ++ } ++ if (file.open(QIODevice::ReadWrite)) { ++ QTextStream out(&file); ++ out.setCodec("UTF-8"); ++ QString Text = textWid->toPlainText(); ++ pages_.insert(currentPage_, Text); ++ for (int i = 0; i < pages_.size(); i++) { ++ out.setGenerateByteOrderMark(false); ++ out << pages_.value(i); ++ } ++ } ++} ++ ++void Viewer::updateLog() ++{ ++ pages_.clear(); ++ init(); ++} ++ ++bool Viewer::init() ++{ ++ bool b = false; ++ QFile file(fileName_); ++ if (file.open(QIODevice::ReadOnly)) { ++ QString page; ++ int numPage = 0; ++ QTextStream in(&file); ++ in.setCodec("UTF-8"); ++ while (!in.atEnd()) { ++ page = ""; ++ for (int i = 0; i < 500; i++) { ++ if (in.atEnd()) ++ break; ++ page += in.readLine() + "\n"; ++ } ++ pages_.insert(numPage++, page); ++ } ++ currentPage_ = pages_.size() - 1; ++ lastModified_ = QDateTime::currentDateTime(); ++ setPage(); ++ b = true; ++ } ++ return b; ++} ++ ++void Viewer::setPage() ++{ ++ QString text = pages_.value(currentPage_); ++ textWid->setText(text); ++ QTextCursor cur = textWid->textCursor(); ++ cur.setPosition(text.length()); ++ textWid->setTextCursor(cur); ++} ++ ++void Viewer::nextPage() ++{ ++ if (currentPage_ < pages_.size() - 1) ++ currentPage_++; ++ setPage(); ++} ++ ++void Viewer::prevPage() ++{ ++ if (currentPage_ > 0) ++ currentPage_--; ++ setPage(); ++} ++ ++void Viewer::lastPage() ++{ ++ currentPage_ = pages_.size() - 1; ++ setPage(); ++} ++ ++void Viewer::firstPage() ++{ ++ currentPage_ = 0; ++ setPage(); ++} +diff -Nru a/plugins/generic/clientswitcherplugin/viewer.h b/plugins/generic/clientswitcherplugin/viewer.h +--- a/plugins/generic/clientswitcherplugin/viewer.h 1970-01-01 00:00:00.000000000 +0000 ++++ b/plugins/generic/clientswitcherplugin/viewer.h 2020-07-10 11:50:10.000000000 +0000 +@@ -0,0 +1,63 @@ ++/* ++ * viewer.h - plugin ++ * Copyright (C) 2009-2010 Evgeny Khryukin ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License ++ * as published by the Free Software Foundation; either version 2 ++ * of the License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ * ++ */ ++ ++#ifndef VIEWER_H ++#define VIEWER_H ++ ++#include ++#include ++#include ++#include ++ ++#include "iconfactoryaccessinghost.h" ++#include "typeaheadfind.h" ++ ++class Viewer : public QDialog { ++ Q_OBJECT ++public: ++ Viewer(QString filename, IconFactoryAccessingHost *IcoHost, QWidget *parent = nullptr); ++ bool init(); ++ ++private: ++ IconFactoryAccessingHost * icoHost_; ++ QString fileName_; ++ QDateTime lastModified_; ++ QTextEdit * textWid; ++ ClientSwitcher::TypeAheadFindBar *findBar; ++ QMap pages_; ++ int currentPage_; ++ void setPage(); ++ ++private slots: ++ void saveLog(); ++ void updateLog(); ++ void deleteLog(); ++ void nextPage(); ++ void prevPage(); ++ void firstPage(); ++ void lastPage(); ++ ++protected: ++ void closeEvent(QCloseEvent *e); ++ ++signals: ++ void onClose(int, int); ++}; ++ ++#endif // VIEWER_H diff --git a/psi-plus/template b/psi-plus/template index 14990b3..d29ecbf 100644 --- a/psi-plus/template +++ b/psi-plus/template @@ -19,4 +19,5 @@ maintainer="Nathan Smith " license="GPL-2.0-only" homepage="https://psi-plus.com" distfiles="https://github.com/psi-plus/psi-plus-snapshots/archive/refs/tags/${version}.tar.gz" -checksum="add907b7e81f6b9ef0f49d31dee2b9093dd0cf4fb92eaaea231d6656aea83b99" \ No newline at end of file +checksum="add907b7e81f6b9ef0f49d31dee2b9093dd0cf4fb92eaaea231d6656aea83b99" +patch_args="-Np1" \ No newline at end of file -- cgit v1.2.3