From f5064bb5ddb57c9f8a66d3fbf89c1317f6ebd31b Mon Sep 17 00:00:00 2001 From: SimplestStudio Date: Thu, 5 Sep 2024 13:25:37 +0300 Subject: [PATCH] [win] add feature: offer associations --- win-linux/defaults.pri | 2 + .../src/cascapplicationmanagerwrapper.cpp | 5 + win-linux/src/cascapplicationmanagerwrapper.h | 1 + win-linux/src/defines.h | 1 + win-linux/src/platform_win/association.cpp | 365 ++++++++++++++++++ win-linux/src/platform_win/association.h | 54 +++ 6 files changed, 428 insertions(+) create mode 100644 win-linux/src/platform_win/association.cpp create mode 100644 win-linux/src/platform_win/association.h diff --git a/win-linux/defaults.pri b/win-linux/defaults.pri index 642252b1d..aee1f99b9 100644 --- a/win-linux/defaults.pri +++ b/win-linux/defaults.pri @@ -250,6 +250,7 @@ core_windows { HEADERS += $$PWD/src/windows/platform_win/cwindowplatform.h \ $$PWD/src/windows/platform_win/caption.h \ $$PWD/src/platform_win/singleapplication.h \ + $$PWD/src/platform_win/association.h \ $$PWD/src/platform_win/filechooser.h \ $$PWD/src/platform_win/printdialog.h \ $$PWD/src/platform_win/message.h \ @@ -257,6 +258,7 @@ core_windows { SOURCES += $$PWD/src/windows/platform_win/cwindowplatform.cpp \ $$PWD/src/platform_win/singleapplication.cpp \ + $$PWD/src/platform_win/association.cpp \ $$PWD/src/platform_win/filechooser.cpp \ $$PWD/src/platform_win/printdialog.cpp \ $$PWD/src/platform_win/message.cpp diff --git a/win-linux/src/cascapplicationmanagerwrapper.cpp b/win-linux/src/cascapplicationmanagerwrapper.cpp index f3cbee5c2..0f4695764 100644 --- a/win-linux/src/cascapplicationmanagerwrapper.cpp +++ b/win-linux/src/cascapplicationmanagerwrapper.cpp @@ -33,6 +33,7 @@ # include # include # include "platform_win/singleapplication.h" +# include "platform_win/association.h" #else # include # include "platform_linux/singleapplication.h" @@ -1081,6 +1082,10 @@ void CAscApplicationManagerWrapper::onDocumentReady(int uid) }); } #endif + +#ifdef _WIN32 + Association::instance().chekForAssociations(uid); +#endif } void CAscApplicationManagerWrapper::startApp() diff --git a/win-linux/src/cascapplicationmanagerwrapper.h b/win-linux/src/cascapplicationmanagerwrapper.h index f8c969d18..07243c062 100644 --- a/win-linux/src/cascapplicationmanagerwrapper.h +++ b/win-linux/src/cascapplicationmanagerwrapper.h @@ -217,6 +217,7 @@ public: void OnEvent(NSEditorApi::CAscCefMenuEvent *); bool event(QEvent *event); private: + friend class Association; friend class CAscApplicationManagerWrapper_Private; std::unique_ptr m_private; diff --git a/win-linux/src/defines.h b/win-linux/src/defines.h index 20716d020..d307ec532 100644 --- a/win-linux/src/defines.h +++ b/win-linux/src/defines.h @@ -50,6 +50,7 @@ # define APP_MUTEX_NAME "asc:editors" #else # define APP_DATA_PATH "/ONLYOFFICE/DesktopEditors" +# define APP_REG_NAME "ONLYOFFICE Editors" # define REG_GROUP_KEY "ONLYOFFICE" # define APP_MUTEX_NAME "TEAMLAB" #endif diff --git a/win-linux/src/platform_win/association.cpp b/win-linux/src/platform_win/association.cpp new file mode 100644 index 000000000..af1b1430b --- /dev/null +++ b/win-linux/src/platform_win/association.cpp @@ -0,0 +1,365 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2019 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha + * street, Riga, Latvia, EU, LV-1050. + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * Pursuant to Section 7(b) of the License you must retain the original Product + * logo when distributing the program. Pursuant to Section 7(e) we decline to + * grant you any rights under trademark law for use of our trademarks. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +#include "association.h" +#include "utils.h" +#include "defines.h" +#include "components/cmessage.h" +#include "cascapplicationmanagerwrapper.h" +#include +#include +#include +#include +#include + +#define DAY_TO_SEC 24*3600 + + +#ifdef __OS_WIN_XP +// static void regValue(HKEY rootKey, LPCWSTR ext, std::wstring &value) +// { +// HKEY hKey; +// if (RegOpenKeyEx(rootKey, L"SOFTWARE\\Classes", 0, KEY_READ, &hKey) == ERROR_SUCCESS) { +// DWORD type = REG_SZ, cbData = 0; +// if (SHGetValue(hKey, ext, L"", &type, NULL, &cbData) == ERROR_SUCCESS) { +// wchar_t *pvData = (wchar_t*)malloc(cbData); +// if (SHGetValue(hKey, ext, L"", &type, (void*)pvData, &cbData) == ERROR_SUCCESS) +// value = pvData; +// free(pvData); +// } +// RegCloseKey(hKey); +// } +// } +#endif + +class Association::AssociationPrivate +{ +public: + AssociationPrivate(); + ~AssociationPrivate(); + + // bool isFirstRun(); + bool isFormatAssociated(const wchar_t*); + void tryProposeAssociation(QWidget *parent, const std::wstring &fileExt = L""); + void associate(const std::vector &unassocFileExts); + + time_t m_lastCheck = 0; + bool m_ignoreAssocMsg; + std::unordered_map m_extMap; + class DialogSchedule; + DialogSchedule *m_pDialogSchedule; +}; + +class Association::AssociationPrivate::DialogSchedule : public QObject +{ +public: + DialogSchedule(AssociationPrivate *owner); + void addToSchedule(const std::wstring &fileExt); + +private: + AssociationPrivate *m_owner; + QTimer *m_timer; + std::unordered_set m_unique_ext; +}; + +Association::AssociationPrivate::DialogSchedule::DialogSchedule(AssociationPrivate *owner) : + QObject(), + m_owner(owner) +{ + m_timer = new QTimer(this); + m_timer->setInterval(3000); + m_timer->setSingleShot(false); + connect(m_timer, &QTimer::timeout, this, [=] { + QWidget *wnd = WindowHelper::currentTopWindow(); + if (wnd && !m_unique_ext.empty()) { + std::wstring fileExt; + if (m_unique_ext.size() == 1) { + if (*m_unique_ext.begin() != L".all") + fileExt = *m_unique_ext.begin(); + } else + if (m_unique_ext.size() == 2 && m_unique_ext.find(L".all") != m_unique_ext.end()) { + for (const auto &ext : m_unique_ext) { + if (ext != L".all") { + fileExt = ext; + break; + } + } + } + + QTimer::singleShot(0, this, [=]() { + m_owner->tryProposeAssociation(wnd, fileExt); + }); + m_unique_ext.clear(); + m_timer->stop(); + } + }); +} + +void Association::AssociationPrivate::DialogSchedule::addToSchedule(const std::wstring &fileExt) +{ + m_unique_ext.insert(fileExt); + if (!m_timer->isActive()) + m_timer->start(); +} + +Association::AssociationPrivate::AssociationPrivate() : m_pDialogSchedule(new DialogSchedule(this)) +{ + GET_REGISTRY_USER(reg_user) + m_ignoreAssocMsg = reg_user.value("ignoreAssocMsg", false).toBool() || IsPackage(Portable); + if (!m_ignoreAssocMsg) + m_lastCheck = time_t(reg_user.value("lastAssocCheck", 0).toLongLong()); + + m_extMap = { + {L".doc", L"ASC.Document.1"}, + {L".docx", L"ASC.Document.12"}, + {L".xls", L"ASC.Sheet.1"}, + {L".xlsx", L"ASC.Sheet.12"}, + {L".ppt", L"ASC.Show.1"}, + {L".pptx", L"ASC.Show.12"}, + {L".pps", L"ASC.SlideShow.1"}, + {L".ppsx", L"ASC.SlideShow.12"}, + {L".odt", L"ASC.Document.2"}, + {L".ods", L"ASC.Sheet.2"}, + {L".odp", L"ASC.Show.2"}, + {L".rtf", L"ASC.Rtf"}, + {L".csv", L"ASC.Csv"}, + {L".pdf", L"ASC.Pdf"}, + {L".djvu", L"ASC.DjVu"}, + {L".xps", L"ASC.Xps"}, + {L".pot", L"ASC.Pot"}, + {L".pptm", L"ASC.Pptm"}, + {L".epub", L"ASC.Epub"}, + {L".fb2", L"ASC.Fb2"}, + {L".dotx", L"ASC.Dotx"}, + {L".oxps", L"ASC.Oxps"}, + {L".xlsb", L"ASC.Xlsb"}, + {L".docxf", L"ASC.Docxf"}, + }; +} + +Association::AssociationPrivate::~AssociationPrivate() +{ + delete m_pDialogSchedule, m_pDialogSchedule = nullptr; +} + +// bool Association::AssociationPrivate::isFirstRun() +// { +// GET_REGISTRY_USER(reg_user); +// if (!reg_user.contains("hasRunBefore")) { +// reg_user.setValue("hasRunBefore", true); +// return true; +// } +// return false; +// } + +bool Association::AssociationPrivate::isFormatAssociated(const wchar_t *fileExt) +{ + DWORD bufSize = 0; + HRESULT hr = AssocQueryString(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME, fileExt, NULL, NULL, &bufSize); + if (hr == S_FALSE) { + std::wstring buf(bufSize - 1, '\0'); + hr = AssocQueryString(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME, fileExt, NULL, &buf[0], &bufSize); + if (SUCCEEDED(hr)) + return buf.find(TEXT(APP_REG_NAME)) != std::wstring::npos; + } + return false; +} + +void Association::AssociationPrivate::tryProposeAssociation(QWidget *parent, const std::wstring &fileExt) +{ + if (m_ignoreAssocMsg) + return; + + m_lastCheck = time(nullptr); + GET_REGISTRY_USER(reg_user); + reg_user.setValue("lastAssocCheck", static_cast(m_lastCheck)); + + std::vector unassocFileExts; + if (fileExt.empty()) { + for (auto it = m_extMap.begin(); it != m_extMap.end(); ++it) { + if (!isFormatAssociated(it->first.c_str())) + unassocFileExts.push_back(it->first); + } + } else + if (!isFormatAssociated(fileExt.c_str())) + unassocFileExts.push_back(fileExt); + + if (!unassocFileExts.empty()) { + QString msg = unassocFileExts.size() == 1 ? QObject::tr("Do you want to make %1 your default application for extention: %2?") + .arg(QString(WINDOW_NAME), QString::fromStdWString(unassocFileExts[0])) : + QObject::tr("Do you want to make %1 your default application for all supported extentions?") + .arg(QString(WINDOW_NAME)); + int res = CMessage::showMessage(parent, msg, MsgType::MSG_INFO, MsgBtns::mbYesDefNo, &m_ignoreAssocMsg, QObject::tr("Do not show this message again")); + if (m_ignoreAssocMsg) { + GET_REGISTRY_USER(reg_user) + reg_user.setValue("ignoreAssocMsg", true); + } + + if (res == MODAL_RESULT_YES) { + if (Utils::getWinVersion() >= Utils::WinVer::Win10) { + ShellExecute(NULL, L"open", L"ms-settings:defaultapps", NULL, NULL, SW_SHOWNORMAL); + + } else + if (Utils::getWinVersion() >= Utils::WinVer::Win8) { +#ifndef __OS_WIN_XP + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (SUCCEEDED(hr)) { + IApplicationAssociationRegistrationUI *ar; + HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistrationUI, 0, CLSCTX_INPROC_SERVER, + IID_IApplicationAssociationRegistrationUI, (void**)&ar); + if (SUCCEEDED(hr)) { + ar->LaunchAdvancedAssociationUI(TEXT(APP_REG_NAME)); + ar->Release(); + } + CoUninitialize(); + } +#endif + } else { + associate(unassocFileExts); + } + } + } +} + +void Association::AssociationPrivate::associate(const std::vector &unassocFileExts) +{ +#ifdef __OS_WIN_XP + for (const auto &ext : unassocFileExts) { + if (m_extMap.find(ext) != m_extMap.end()) { + std::wstring progId = m_extMap[ext]; + // std::wstring progId1, progId2; + // regValue(HKEY_LOCAL_MACHINE, ext.c_str(), progId1); + // regValue(HKEY_CURRENT_USER, ext.c_str(), progId2); + // if ((!progId2.empty() && progId2 != progId) || (!progId1.empty() && progId1 != progId)) { + HKEY hKey; + std::wstring userChoise = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\" + ext; + if (RegOpenKeyEx(HKEY_CURRENT_USER, userChoise.c_str(), 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS) { + if (RegDeleteKey(hKey, L"UserChoice") == ERROR_SUCCESS) { + HKEY hKeyUser; + std::wstring path = L"SOFTWARE\\Classes\\" + ext; + if (RegOpenKeyEx(HKEY_CURRENT_USER, path.c_str(), 0, KEY_ALL_ACCESS, &hKeyUser) == ERROR_SUCCESS) { + SHSetValue(hKeyUser, L"", 0, REG_SZ, (const BYTE*)progId.c_str(), (DWORD)(progId.length() + 1) * sizeof(WCHAR)); + RegCloseKey(hKeyUser); + } + } + RegCloseKey(hKey); + } + // } + } + } + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); +#else + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (SUCCEEDED(hr)) { + IApplicationAssociationRegistration *pAr; + HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistration, NULL, CLSCTX_INPROC, IID_IApplicationAssociationRegistration, (void**)&pAr); + if (SUCCEEDED(hr)) { + for (const auto &ext : unassocFileExts) { + if (m_extMap.find(ext) != m_extMap.end()) + pAr->SetAppAsDefault(TEXT(APP_REG_NAME), ext.c_str(), AT_FILEEXTENSION); + } + pAr->Release(); + } + CoUninitialize(); + } +#endif +} + +Association::Association() : pimpl(new AssociationPrivate) +{ + +} + +Association::~Association() +{ + delete pimpl, pimpl = nullptr; +} + +Association& Association::instance() +{ + static Association inst; + return inst; +} + +void Association::chekForAssociations(int uid) +{ + if (pimpl->m_ignoreAssocMsg || (time(nullptr) - pimpl->m_lastCheck) < DAY_TO_SEC) + return; + + if (uid < 0/*Association::isFirstRun()*/) { + pimpl->m_pDialogSchedule->addToSchedule(L".all"); + } else + /*if (uid > -1)*/ { + CAscTabData *data = nullptr; + AscAppManager &app = AscAppManager::getInstance(); + if (CEditorWindow *editor = app.editorWindowFromViewId(uid)) { + data = editor->mainView()->data(); + } else + if (app.mainWindow() && app.mainWindow()->holdView(uid)) { + int indx = app.mainWindow()->tabWidget()->tabIndexByView(uid); + if (indx > -1) + data = app.mainWindow()->tabWidget()->panel(indx)->data(); + } + + if (data) { + std::wstring fileExt; + if (data->isLocal() && !data->url().empty()) { + QFileInfo inf(QString::fromStdWString(data->url())); + fileExt = inf.completeSuffix().toStdWString(); + if (!fileExt.empty()) + fileExt.insert(fileExt.begin(), L'.'); + } + + if (fileExt.empty()) { + switch (data->contentType()) { + case AscEditorType::etDocument: + fileExt = L".docx"; + break; + case AscEditorType::etPresentation: + fileExt = L".pptx"; + break; + case AscEditorType::etSpreadsheet: + fileExt = L".xlsx"; + break; + case AscEditorType::etPdf: + fileExt = L".pdf"; + break; + default: + break; + } + } + + if (!fileExt.empty() && pimpl->m_extMap.find(fileExt) != pimpl->m_extMap.end()) + pimpl->m_pDialogSchedule->addToSchedule(fileExt); + } + } +} diff --git a/win-linux/src/platform_win/association.h b/win-linux/src/platform_win/association.h new file mode 100644 index 000000000..2506ac380 --- /dev/null +++ b/win-linux/src/platform_win/association.h @@ -0,0 +1,54 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2019 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha + * street, Riga, Latvia, EU, LV-1050. + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * Pursuant to Section 7(b) of the License you must retain the original Product + * logo when distributing the program. Pursuant to Section 7(e) we decline to + * grant you any rights under trademark law for use of our trademarks. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +#ifndef ASSOCIATION_H +#define ASSOCIATION_H + + +class Association +{ +public: + Association(const Association&) = delete; + Association& operator=(const Association&) = delete; + static Association& instance(); + + void chekForAssociations(int uid); + +private: + Association(); + ~Association(); + + class AssociationPrivate; + AssociationPrivate *pimpl; +}; + +#endif // ASSOCIATION_H