diff options
40 files changed, 1341 insertions, 2 deletions
diff --git a/cui/Library_cui.mk b/cui/Library_cui.mk index cb26653b395d..5918be60972c 100644 --- a/cui/Library_cui.mk +++ b/cui/Library_cui.mk @@ -188,6 +188,7 @@ $(eval $(call gb_Library_add_exception_objects,cui,\ cui/source/options/optgenrl \ cui/source/options/opthtml \ cui/source/options/optlanguagetool \ + cui/source/options/optdeepl \ cui/source/options/optinet2 \ cui/source/options/optjava \ cui/source/options/optjsearch \ diff --git a/cui/UIConfig_cui.mk b/cui/UIConfig_cui.mk index cdedcc15a857..89c8869ade36 100644 --- a/cui/UIConfig_cui.mk +++ b/cui/UIConfig_cui.mk @@ -144,6 +144,7 @@ $(eval $(call gb_UIConfig_add_uifiles,cui,\ cui/uiconfig/ui/optgeneralpage \ cui/uiconfig/ui/opthtmlpage \ cui/uiconfig/ui/langtoolconfigpage \ + cui/uiconfig/ui/deepltabpage \ cui/uiconfig/ui/optionsdialog \ cui/uiconfig/ui/optjsearchpage \ cui/uiconfig/ui/optlanguagespage \ diff --git a/cui/inc/treeopt.hrc b/cui/inc/treeopt.hrc index 8a56a6bde4bf..cdd7b582da89 100644 --- a/cui/inc/treeopt.hrc +++ b/cui/inc/treeopt.hrc @@ -56,7 +56,8 @@ const std::pair<TranslateId, sal_uInt16> SID_LANGUAGE_OPTIONS_RES[] = { NC_("SID_LANGUAGE_OPTIONS_RES", "Searching in Japanese"), RID_SVXPAGE_JSEARCH_OPTIONS }, { NC_("SID_LANGUAGE_OPTIONS_RES", "Asian Layout"), RID_SVXPAGE_ASIAN_LAYOUT }, { NC_("SID_LANGUAGE_OPTIONS_RES", "Complex Text Layout"), RID_SVXPAGE_OPTIONS_CTL }, - { NC_("SID_LANGUAGE_OPTIONS_RES", "LanguageTool Server"), RID_SVXPAGE_LANGTOOL_OPTIONS } + { NC_("SID_LANGUAGE_OPTIONS_RES", "LanguageTool Server"), RID_SVXPAGE_LANGTOOL_OPTIONS }, + { NC_("SID_LANGUAGE_OPTIONS_RES", "DeepL Server"), RID_SVXPAGE_DEEPL_OPTIONS } }; const std::pair<TranslateId, sal_uInt16> SID_INET_DLG_RES[] = diff --git a/cui/source/options/optdeepl.cxx b/cui/source/options/optdeepl.cxx new file mode 100644 index 000000000000..b219845f07c7 --- /dev/null +++ b/cui/source/options/optdeepl.cxx @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "optdeepl.hxx" +#include <svtools/deeplcfg.hxx> + +OptDeeplTabPage::OptDeeplTabPage(weld::Container* pPage, weld::DialogController* pController, + const SfxItemSet& rSet) + : SfxTabPage(pPage, pController, "cui/ui/deepltabpage.ui", "OptDeeplPage", &rSet) + , m_xAPIUrl(m_xBuilder->weld_entry("apiurl")) + , m_xAuthKey(m_xBuilder->weld_entry("authkey")) +{ +} + +OptDeeplTabPage::~OptDeeplTabPage() {} + +void OptDeeplTabPage::Reset(const SfxItemSet*) +{ + SvxDeeplOptions& rDeeplOptions = SvxDeeplOptions::Get(); + m_xAPIUrl->set_text(rDeeplOptions.getAPIUrl()); + m_xAuthKey->set_text(rDeeplOptions.getAuthKey()); +} + +bool OptDeeplTabPage::FillItemSet(SfxItemSet*) +{ + SvxDeeplOptions& rDeeplOptions = SvxDeeplOptions::Get(); + rDeeplOptions.setAPIUrl(m_xAPIUrl->get_text()); + rDeeplOptions.setAuthKey(m_xAuthKey->get_text()); + return false; +} + +std::unique_ptr<SfxTabPage> OptDeeplTabPage::Create(weld::Container* pPage, + weld::DialogController* pController, + const SfxItemSet* rAttrSet) +{ + return std::make_unique<OptDeeplTabPage>(pPage, pController, *rAttrSet); +} diff --git a/cui/source/options/optdeepl.hxx b/cui/source/options/optdeepl.hxx new file mode 100644 index 000000000000..3258f67fe8af --- /dev/null +++ b/cui/source/options/optdeepl.hxx @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once +#include <sfx2/tabdlg.hxx> + +class OptDeeplTabPage : public SfxTabPage +{ +public: + OptDeeplTabPage(weld::Container* pPage, weld::DialogController* pController, + const SfxItemSet& rSet); + virtual ~OptDeeplTabPage() override; + static std::unique_ptr<SfxTabPage> + Create(weld::Container* pPage, weld::DialogController* pController, const SfxItemSet* rAttrSet); + + virtual bool FillItemSet(SfxItemSet* rSet) override; + virtual void Reset(const SfxItemSet* rSet) override; + +private: + std::unique_ptr<weld::Entry> m_xAPIUrl; + std::unique_ptr<weld::Entry> m_xAuthKey; +}; diff --git a/cui/source/options/treeopt.cxx b/cui/source/options/treeopt.cxx index 1089114e5ddc..805ddfc48317 100644 --- a/cui/source/options/treeopt.cxx +++ b/cui/source/options/treeopt.cxx @@ -63,6 +63,7 @@ #include <treeopt.hxx> #include "optbasic.hxx" #include "optlanguagetool.hxx" +#include "optdeepl.hxx" #include <com/sun/star/awt/XContainerWindowEventHandler.hpp> #include <com/sun/star/awt/ContainerWindowProvider.hpp> @@ -298,6 +299,7 @@ static std::unique_ptr<SfxTabPage> CreateGeneralTabPage(sal_uInt16 nId, weld::Co case RID_SVXPAGE_ACCESSIBILITYCONFIG: fnCreate = &SvxAccessibilityOptionsTabPage::Create; break; case RID_SVXPAGE_OPTIONS_CTL: fnCreate = &SvxCTLOptionsPage::Create ; break; case RID_SVXPAGE_LANGTOOL_OPTIONS: fnCreate = &OptLanguageToolTabPage::Create ; break; + case RID_SVXPAGE_DEEPL_OPTIONS: fnCreate = &OptDeeplTabPage::Create ; break; case RID_SVXPAGE_OPTIONS_JAVA: fnCreate = &SvxJavaOptionsPage::Create ; break; #if HAVE_FEATURE_OPENCL case RID_SVXPAGE_OPENCL: fnCreate = &SvxOpenCLTabPage::Create ; break; diff --git a/cui/uiconfig/ui/deepltabpage.ui b/cui/uiconfig/ui/deepltabpage.ui new file mode 100644 index 000000000000..1694bc5a09c8 --- /dev/null +++ b/cui/uiconfig/ui/deepltabpage.ui @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.2 --> +<interface domain="cui"> + <requires lib="gtk+" version="3.20"/> + <object class="GtkBox" id="OptDeeplPage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkGrid" id="grid1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_homogeneous">True</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="xpad">5</property> + <property name="ypad">5</property> + <property name="label" translatable="yes" context="deepltabpage|label1">DeepL API Options</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">grid1</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLinkButton" id="privacy"> + <property name="label" translatable="yes" context="deepltabpage|privacy">Please read the privacy policy</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="relief">none</property> + <property name="uri">https://www.deepl.com/privacy/</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="grid2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">5</property> + <property name="margin_end">5</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="row_spacing">6</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="margin_start">5</property> + <property name="label" translatable="yes" context="deepltabpage|privacy">API URL:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">apiurl</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="margin_end">5</property> + <property name="label" translatable="yes" context="deepltabpage|label3">Auth Key:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">authkey</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="apiurl"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="authkey"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> +</interface> diff --git a/desktop/source/lib/init.cxx b/desktop/source/lib/init.cxx index 2be264d39a33..388ca174c23b 100644 --- a/desktop/source/lib/init.cxx +++ b/desktop/source/lib/init.cxx @@ -128,6 +128,7 @@ #include <tools/json_writer.hxx> #include <svtools/ctrltool.hxx> #include <svtools/langtab.hxx> +#include <svtools/deeplcfg.hxx> #include <vcl/fontcharmap.hxx> #ifdef IOS #include <vcl/sysdata.hxx> @@ -6778,6 +6779,28 @@ void setCertificateDir() } } +void setDeeplConfig() +{ + const char* pAPIUrlString = ::getenv("DEEPL_API_URL"); + const char* pAuthKeyString = ::getenv("DEEPL_AUTH_KEY"); + if (pAPIUrlString && pAuthKeyString) + { + OUString aAPIUrl = OStringToOUString(pAPIUrlString, RTL_TEXTENCODING_UTF8); + OUString aAuthKey = OStringToOUString(pAuthKeyString, RTL_TEXTENCODING_UTF8); + try + { + SvxDeeplOptions& rDeeplOptions = SvxDeeplOptions::Get(); + rDeeplOptions.setAPIUrl(aAPIUrl); + rDeeplOptions.setAuthKey(aAuthKey); + } + catch(uno::Exception const& rException) + { + SAL_WARN("lok", "Failed to set Deepl API settings: " << rException.Message); + } + } +} + + } static int lo_initialize(LibreOfficeKit* pThis, const char* pAppPath, const char* pUserProfileUrl) @@ -7092,6 +7115,7 @@ static int lo_initialize(LibreOfficeKit* pThis, const char* pAppPath, const char #endif setCertificateDir(); + setDeeplConfig(); if (bNotebookbar) { diff --git a/include/linguistic/translate.hxx b/include/linguistic/translate.hxx new file mode 100644 index 000000000000..985930c74d83 --- /dev/null +++ b/include/linguistic/translate.hxx @@ -0,0 +1,9 @@ +#pragma once +#include <linguistic/lngdllapi.h> +#include <rtl/string.hxx> + +namespace linguistic +{ +LNG_DLLPUBLIC OString Translate(const OString& rTargetLang, const OString& rAPIUrl, + const OString& rAuthKey, const OString& rData); +} // namespace diff --git a/include/sfx2/pageids.hxx b/include/sfx2/pageids.hxx index 9371848b784a..1464ec40c55a 100644 --- a/include/sfx2/pageids.hxx +++ b/include/sfx2/pageids.hxx @@ -57,6 +57,7 @@ #define RID_SVXPAGE_COLORCONFIG (RID_SVX_START + 249) #define RID_SVXPAGE_BASICIDE_OPTIONS (RID_SVX_START + 209) #define RID_SVXPAGE_LANGTOOL_OPTIONS (RID_SVX_START + 210) +#define RID_SVXPAGE_DEEPL_OPTIONS (RID_SVX_START + 211) // Resource-Id's ------------------------------------------------------------ diff --git a/include/sfx2/sfxsids.hrc b/include/sfx2/sfxsids.hrc index e37c7894e574..8333ce792d9d 100644 --- a/include/sfx2/sfxsids.hrc +++ b/include/sfx2/sfxsids.hrc @@ -224,6 +224,7 @@ class SvxZoomItem; #define SID_VIEW_DATA_SOURCE_BROWSER (SID_SFX_START + 1660) #define SID_UNPACK (SID_SFX_START + 1662) // (SID_SFX_START + 1663) used further down +#define SID_ATTR_TARGETLANG_STR (SID_SFX_START + 1664) // FREE #define SID_OUTPUTSTREAM TypedWhichId<SfxUnoAnyItem>(SID_SFX_START + 1666) #define SID_IMAGE_ORIENTATION (SID_SFX_START + 1667) diff --git a/include/svtools/deeplcfg.hxx b/include/svtools/deeplcfg.hxx new file mode 100644 index 000000000000..a943d72d4d7e --- /dev/null +++ b/include/svtools/deeplcfg.hxx @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once +#include <unotools/configitem.hxx> +#include <com/sun/star/uno/Sequence.hxx> +#include <svtools/svtdllapi.h> + +using namespace utl; +using namespace com::sun::star::uno; + +struct DeeplOptions_Impl; + +class SVT_DLLPUBLIC SvxDeeplOptions final : public utl::ConfigItem +{ +public: + SvxDeeplOptions(); + virtual ~SvxDeeplOptions() override; + + virtual void Notify(const css::uno::Sequence<OUString>& _rPropertyNames) override; + static SvxDeeplOptions& Get(); + + const OUString& getAPIUrl() const; + void setAPIUrl(const OUString& rVal); + + const OUString& getAuthKey() const; + void setAuthKey(const OUString& rVal); + +private: + std::unique_ptr<DeeplOptions_Impl> pImpl; + void Load(const css::uno::Sequence<OUString>& rPropertyNames); + virtual void ImplCommit() override; + static const Sequence<OUString>& GetPropertyNames(); +}; diff --git a/include/svx/svxids.hrc b/include/svx/svxids.hrc index 96122a66bc10..20a2a29ac716 100644 --- a/include/svx/svxids.hrc +++ b/include/svx/svxids.hrc @@ -587,7 +587,7 @@ class XFillGradientItem; #define SID_FM_FILECONTROL ( SID_SVX_START + 605 ) //( SID_SVX_START + 606 ) is used by SID_DRAWTBX_REDACTED_EXPORT #define SID_FM_NAVIGATIONBAR ( SID_SVX_START + 607 ) -//FREE +#define SID_FM_TRANSLATE ( SID_SVX_START + 608 ) //FREE #define SID_FM_DELETEROWS ( SID_SVX_START + 610 ) //FREE diff --git a/include/vcl/htmltransferable.hxx b/include/vcl/htmltransferable.hxx new file mode 100644 index 000000000000..2576c7e31bb4 --- /dev/null +++ b/include/vcl/htmltransferable.hxx @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <com/sun/star/datatransfer/XTransferable.hpp> +#include <cppuhelper/weak.hxx> +#include <rtl/ustring.hxx> +#include <vcl/dllapi.h> + +namespace vcl::unohelper +{ +// Helper class for passing HTML string as XTransferable to TransferableDataHelper object +class VCL_DLLPUBLIC HtmlTransferable final : public css::datatransfer::XTransferable, + public ::cppu::OWeakObject +{ +private: + OString data; + +public: + HtmlTransferable(OString sData); + virtual ~HtmlTransferable() override; + + // css::uno::XInterface + css::uno::Any SAL_CALL queryInterface(const css::uno::Type& rType) override; + void SAL_CALL acquire() noexcept override { OWeakObject::acquire(); } + void SAL_CALL release() noexcept override { OWeakObject::release(); } + + // css::datatransfer::XTransferable + css::uno::Any SAL_CALL getTransferData(const css::datatransfer::DataFlavor& aFlavor) override; + css::uno::Sequence<css::datatransfer::DataFlavor> SAL_CALL getTransferDataFlavors() override; + sal_Bool SAL_CALL isDataFlavorSupported(const css::datatransfer::DataFlavor& aFlavor) override; +}; + +} // namespace vcl::unohelper diff --git a/linguistic/Library_lng.mk b/linguistic/Library_lng.mk index cff45e3a68ac..6ac44d7770b8 100644 --- a/linguistic/Library_lng.mk +++ b/linguistic/Library_lng.mk @@ -51,6 +51,7 @@ $(eval $(call gb_Library_use_externals,lng,\ boost_headers \ icuuc \ icu_headers \ + curl \ )) $(eval $(call gb_Library_add_exception_objects,lng,\ @@ -72,6 +73,7 @@ $(eval $(call gb_Library_add_exception_objects,lng,\ linguistic/source/spelldsp \ linguistic/source/spelldta \ linguistic/source/thesdsp \ + linguistic/source/translate \ )) # vim: set noet sw=4 ts=4: diff --git a/linguistic/source/translate.cxx b/linguistic/source/translate.cxx new file mode 100644 index 000000000000..5337c33153e1 --- /dev/null +++ b/linguistic/source/translate.cxx @@ -0,0 +1,71 @@ +#include <linguistic/translate.hxx> +#include <sal/log.hxx> +#include <curl/curl.h> +#include <sal/log.hxx> +#include <rtl/string.h> +#include <boost/property_tree/ptree.hpp> +#include <boost/property_tree/json_parser.hpp> +#include <vcl/htmltransferable.hxx> +#include <tools/long.hxx> + +namespace linguistic +{ +OString Translate(const OString& rTargetLang, const OString& rAPIUrl, const OString& rAuthKey, + const OString& rData) +{ + constexpr tools::Long CURL_TIMEOUT = 10L; + + std::unique_ptr<CURL, std::function<void(CURL*)>> curl(curl_easy_init(), + [](CURL* p) { curl_easy_cleanup(p); }); + curl_easy_setopt(curl.get(), CURLOPT_URL, rAPIUrl.getStr()); + curl_easy_setopt(curl.get(), CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, CURL_TIMEOUT); + + std::string response_body; + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, + +[](void* buffer, size_t size, size_t nmemb, void* userp) -> size_t { + if (!userp) + return 0; + std::string* response = static_cast<std::string*>(userp); + size_t real_size = size * nmemb; + response->append(static_cast<char*>(buffer), real_size); + return real_size; + }); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, static_cast<void*>(&response_body)); + OString aLang(curl_easy_escape(curl.get(), rTargetLang.getStr(), rTargetLang.getLength())); + OString aAuthKey(curl_easy_escape(curl.get(), rAuthKey.getStr(), rAuthKey.getLength())); + OString aData(curl_easy_escape(curl.get(), rData.getStr(), rData.getLength())); + OString aPostData("auth_key=" + aAuthKey + "&target_lang=" + aLang + "&text=" + aData); + + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, aPostData.getStr()); + CURLcode cc = curl_easy_perform(curl.get()); + if (cc != CURLE_OK) + { + SAL_WARN("linguistic", + "Translate: CURL perform returned with error: " << static_cast<sal_Int32>(cc)); + return {}; + } + tools::Long nStatusCode; + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &nStatusCode); + if (nStatusCode != 200) + { + SAL_WARN("linguistic", + "Translate: CURL request returned with status code: " << nStatusCode); + return {}; + } + // parse the response + boost::property_tree::ptree root; + std::stringstream aStream(response_body.data()); + boost::property_tree::read_json(aStream, root); + boost::property_tree::ptree& translations = root.get_child("translations"); + size_t size = translations.size(); + if (size <= 0) + { + SAL_WARN("linguistic", "Translate: API did not return any translations"); + } + // take the first one + const boost::property_tree::ptree& translation = translations.begin()->second; + const std::string text = translation.get<std::string>("text"); + return OString(text); +} +} diff --git a/officecfg/registry/data/org/openoffice/Office/UI/WriterCommands.xcu b/officecfg/registry/data/org/openoffice/Office/UI/WriterCommands.xcu index 9513c20cd5d1..1c20f951bf1d 100644 --- a/officecfg/registry/data/org/openoffice/Office/UI/WriterCommands.xcu +++ b/officecfg/registry/data/org/openoffice/Office/UI/WriterCommands.xcu @@ -1349,6 +1349,14 @@ <value>1</value> </prop> </node> + <node oor:name=".uno:Translate" oor:op="replace"> + <prop oor:name="Label" oor:type="xs:string"> + <value xml:lang="en-US">Translate...</value> + </prop> + <prop oor:name="Properties" oor:type="xs:int"> + <value>1</value> + </prop> + </node> <node oor:name=".uno:FormatColumns" oor:op="replace"> <prop oor:name="Label" oor:type="xs:string"> <value xml:lang="en-US">Co~lumns...</value> diff --git a/officecfg/registry/schema/org/openoffice/Office/Linguistic.xcs b/officecfg/registry/schema/org/openoffice/Office/Linguistic.xcs index fe9365d036dc..2ecc4ae34804 100644 --- a/officecfg/registry/schema/org/openoffice/Office/Linguistic.xcs +++ b/officecfg/registry/schema/org/openoffice/Office/Linguistic.xcs @@ -408,6 +408,28 @@ </prop> </group> </group> + <group oor:name="Translation"> + <info> + <desc>Contains translation relevant settings.</desc> + </info> + <group oor:name="Deepl"> + <info> + <desc>Contains DeepL API relevant settings.</desc> + </info> + <prop oor:name="ApiURL" oor:type="xs:string"> + <info> + <desc>Deepl Translator API URL</desc> + <label>URL for the Deepl translator api</label> + </info> + </prop> + <prop oor:name="AuthKey" oor:type="xs:string"> + <info> + <desc>Deepl Translator API URL Authkey</desc> + <label>Auth key for the Deepl translator api</label> + </info> + </prop> + </group> + </group> <group oor:name="Hyphenation"> <info> <desc>Contains hyphenation relevant settings.</desc> diff --git a/svtools/Library_svt.mk b/svtools/Library_svt.mk index bc7d07b3bd48..a2bc1aad7ccf 100644 --- a/svtools/Library_svt.mk +++ b/svtools/Library_svt.mk @@ -84,6 +84,7 @@ $(eval $(call gb_Library_add_exception_objects,svt,\ svtools/source/config/fontsubstconfig \ svtools/source/config/htmlcfg \ svtools/source/config/languagetoolcfg \ + svtools/source/config/deeplcfg \ svtools/source/config/itemholder2 \ svtools/source/config/miscopt \ svtools/source/config/slidesorterbaropt \ diff --git a/svtools/source/config/deeplcfg.cxx b/svtools/source/config/deeplcfg.cxx new file mode 100644 index 000000000000..3b022b70804b --- /dev/null +++ b/svtools/source/config/deeplcfg.cxx @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/log.hxx> +#include <sal/config.h> +#include <svtools/deeplcfg.hxx> +#include <com/sun/star/uno/Sequence.hxx> +#include <tools/debug.hxx> + +using namespace utl; +using namespace com::sun::star::uno; + +struct DeeplOptions_Impl +{ + OUString sAPIUrl; + OUString sAuthKey; +}; + +const Sequence<OUString>& SvxDeeplOptions::GetPropertyNames() +{ + static Sequence<OUString> const aNames{ + "Deepl/ApiURL", + "Deepl/AuthKey", + }; + return aNames; +} + +const OUString& SvxDeeplOptions::getAPIUrl() const { return pImpl->sAPIUrl; } + +void SvxDeeplOptions::setAPIUrl(const OUString& rVal) +{ + pImpl->sAPIUrl = rVal; + SetModified(); +} + +const OUString& SvxDeeplOptions::getAuthKey() const { return pImpl->sAuthKey; } + +void SvxDeeplOptions::setAuthKey(const OUString& rVal) +{ + pImpl->sAuthKey = rVal; + SetModified(); +} + +namespace +{ +class theSvxDeeplOptions : public rtl::Static<SvxDeeplOptions, theSvxDeeplOptions> +{ +}; +} + +SvxDeeplOptions& SvxDeeplOptions::Get() { return theSvxDeeplOptions::get(); } + +SvxDeeplOptions::SvxDeeplOptions() + : ConfigItem("Office.Linguistic/Translation") + , pImpl(new DeeplOptions_Impl) +{ + Load(GetPropertyNames()); +} + +SvxDeeplOptions::~SvxDeeplOptions() {} +void SvxDeeplOptions::Notify(const css::uno::Sequence<OUString>&) { Load(GetPropertyNames()); } + +void SvxDeeplOptions::Load(const css::uno::Sequence<OUString>& aNames) +{ + Sequence<Any> aValues = GetProperties(aNames); + const Any* pValues = aValues.getConstArray(); + DBG_ASSERT(aValues.getLength() == aNames.getLength(), "GetProperties failed"); + if (aValues.getLength() != aNames.getLength()) + return; + for (int nProp = 0; nProp < aNames.getLength(); nProp++) + { + if (!pValues[nProp].hasValue()) + continue; + switch (nProp) + { + case 0: + pValues[nProp] >>= pImpl->sAPIUrl; + break; + case 1: + pValues[nProp] >>= pImpl->sAuthKey; + break; + default: + break; + } + } +} + +void SvxDeeplOptions::ImplCommit() +{ + const Sequence<OUString>& aNames = GetPropertyNames(); + Sequence<Any> aValues(aNames.getLength()); + Any* pValues = aValues.getArray(); + for (int nProp = 0; nProp < aNames.getLength(); nProp++) + { + switch (nProp) + { + case 0: + pValues[nProp] <<= pImpl->sAPIUrl; + break; + case 1: + pValues[nProp] <<= pImpl->sAuthKey; + break; + default: + break; + } + } + PutProperties(aNames, aValues); +}
\ No newline at end of file diff --git a/svx/sdi/svx.sdi b/svx/sdi/svx.sdi index 0336b6c2d101..c53fefd67d39 100644 --- a/svx/sdi/svx.sdi +++ b/svx/sdi/svx.sdi @@ -1596,6 +1596,23 @@ SfxBoolItem NavigationBar SID_FM_NAVIGATIONBAR GroupId = SfxGroupId::Controls; ] +SfxBoolItem Translate SID_FM_TRANSLATE +(SfxStringItem TargetLang SID_ATTR_TARGETLANG_STR) +[ + AutoUpdate = FALSE, + FastCall = TRUE, + ReadOnlyDoc = FALSE, + Toggle = FALSE, + Container = FALSE, + RecordAbsolute = FALSE, + RecordPerSet; + + AccelConfig = FALSE, + MenuConfig = FALSE, + ToolBoxConfig = FALSE, + GroupId = SfxGroupId::Format; +] + SfxBoolItem Combobox SID_INSERT_COMBOBOX diff --git a/sw/Library_sw.mk b/sw/Library_sw.mk index 22694869fb54..d5c59fa4cc4a 100644 --- a/sw/Library_sw.mk +++ b/sw/Library_sw.mk @@ -694,6 +694,7 @@ $(eval $(call gb_Library_add_exception_objects,sw,\ sw/source/uibase/shells/grfsh \ sw/source/uibase/shells/grfshex \ sw/source/uibase/shells/langhelper \ + sw/source/uibase/shells/translatehelper \ sw/source/uibase/shells/listsh \ sw/source/uibase/shells/mediash \ sw/source/uibase/shells/navsh \ diff --git a/sw/Library_swui.mk b/sw/Library_swui.mk index 9187d3c209a1..8e9222d97cc1 100644 --- a/sw/Library_swui.mk +++ b/sw/Library_swui.mk @@ -79,6 +79,7 @@ $(eval $(call gb_Library_use_libraries,swui,\ vcl \ drawinglayercore \ drawinglayer \ + lng \ )) $(eval $(call gb_Library_add_exception_objects,swui,\ @@ -152,6 +153,7 @@ $(eval $(call gb_Library_add_exception_objects,swui,\ sw/source/ui/misc/pgfnote \ sw/source/ui/misc/pggrid \ sw/source/ui/misc/srtdlg \ + sw/source/ui/misc/translatelangselect \ sw/source/ui/misc/swmodalredlineacceptdlg \ sw/source/ui/misc/titlepage \ sw/source/ui/table/colwd \ diff --git a/sw/UIConfig_swriter.mk b/sw/UIConfig_swriter.mk index f4e003e37243..02ea3ded32d0 100644 --- a/sw/UIConfig_swriter.mk +++ b/sw/UIConfig_swriter.mk @@ -188,6 +188,7 @@ $(eval $(call gb_UIConfig_add_uifiles,modules/swriter,\ sw/uiconfig/swriter/ui/insertautotextdialog \ sw/uiconfig/swriter/ui/insertbookmark \ sw/uiconfig/swriter/ui/insertbreak \ + sw/uiconfig/swriter/ui/translationdialog \ sw/uiconfig/swriter/ui/insertcaption \ sw/uiconfig/swriter/ui/insertdbcolumnsdialog \ sw/uiconfig/swriter/ui/insertfootnote \ diff --git a/sw/inc/strings.hrc b/sw/inc/strings.hrc index 81a54280d92c..4f2dd39ff86b 100644 --- a/sw/inc/strings.hrc +++ b/sw/inc/strings.hrc @@ -299,6 +299,7 @@ #define STR_DELETE_NOTE_AUTHOR NC_("STR_DELETE_NOTE_AUTHOR", "Delete ~All Comments by $1") #define STR_HIDE_NOTE_AUTHOR NC_("STR_HIDE_NOTE_AUTHOR", "H~ide All Comments by $1") #define STR_OUTLINE_NUMBERING NC_("STR_OUTLINE_NUMBERING", "Chapter Numbering") +#define STR_STATSTR_SWTRANSLATE NC_("STR_STATSTR_SWTRANSLATE", "Translating document...") /* To translators: $1 == will be replaced by STR_WORDCOUNT_WORDARG, and $2 by STR_WORDCOUNT_COLARG e.g. Selected: 1 word, 2 characters */ #define STR_WORDCOUNT NC_("STR_WORDCOUNT", "Selected: $1, $2") diff --git a/sw/inc/swabstdlg.hxx b/sw/inc/swabstdlg.hxx index 5c5f2bc2cae7..02b38a71d5ee 100644 --- a/sw/inc/swabstdlg.hxx +++ b/sw/inc/swabstdlg.hxx @@ -383,6 +383,18 @@ public: virtual sal_uInt16 GetRestartPage() const = 0; }; +class SwLanguageListItem; + +class AbstractSwTranslateLangSelectDlg +{ +protected: + virtual ~AbstractSwTranslateLangSelectDlg() = default; +public: + virtual std::shared_ptr<weld::DialogController> getDialogController() = 0; + virtual std::optional<SwLanguageListItem> GetSelectedLanguage() = 0; +}; + + class SwAbstractDialogFactory { public: @@ -406,6 +418,7 @@ public: CreateSwContentControlListItemDlg(weld::Window* pParent, SwContentControlListItem& rItem) = 0; virtual std::shared_ptr<AbstractSwBreakDlg> CreateSwBreakDlg(weld::Window *pParent, SwWrtShell &rSh) = 0; + virtual std::shared_ptr<AbstractSwTranslateLangSelectDlg> CreateSwTranslateLangSelectDlg(weld::Window *pParent, SwWrtShell &rSh) = 0; virtual VclPtr<VclAbstractDialog> CreateSwChangeDBDlg(SwView& rVw) = 0; virtual VclPtr<SfxAbstractTabDialog> CreateSwCharDlg(weld::Window* pParent, SwView& pVw, const SfxItemSet& rCoreSet, SwCharDlgMode nDialogMode, const OUString* pFormatStr = nullptr) = 0; diff --git a/sw/sdi/_textsh.sdi b/sw/sdi/_textsh.sdi index 05f6af09f823..6830a248a24b 100644 --- a/sw/sdi/_textsh.sdi +++ b/sw/sdi/_textsh.sdi @@ -1814,5 +1814,11 @@ interface BaseText StateMethod = GetState ; ] + SID_FM_TRANSLATE + [ + ExecMethod = Execute ; + StateMethod = GetState ; + ] + } // end of interface text diff --git a/sw/source/ui/dialog/swdlgfact.cxx b/sw/source/ui/dialog/swdlgfact.cxx index 1aa438f01af6..6e9ebc873ab7 100644 --- a/sw/source/ui/dialog/swdlgfact.cxx +++ b/sw/source/ui/dialog/swdlgfact.cxx @@ -89,6 +89,7 @@ #include <uiborder.hxx> #include <mmresultdialogs.hxx> #include <formatlinebreak.hxx> +#include <translatelangselect.hxx> using namespace ::com::sun::star; using namespace css::frame; @@ -796,6 +797,12 @@ sal_uInt16 AbstractMailMergeWizard_Impl::GetRestartPage() const return m_xDlg->GetRestartPage(); } +std::optional<SwLanguageListItem> AbstractSwTranslateLangSelectDlg_Impl::GetSelectedLanguage() +{ + SwTranslateLangSelectDlg* pDlg = dynamic_cast<SwTranslateLangSelectDlg*>(m_xDlg.get()); + return pDlg->GetSelectedLanguage(); +} + VclPtr<AbstractSwInsertAbstractDlg> SwAbstractDialogFactory_Impl::CreateSwInsertAbstractDlg(weld::Window* pParent) { return VclPtr<AbstractSwInsertAbstractDlg_Impl>::Create(std::make_unique<SwInsertAbstractDlg>(pParent)); @@ -855,6 +862,11 @@ std::shared_ptr<AbstractSwBreakDlg> SwAbstractDialogFactory_Impl::CreateSwBreakD return std::make_shared<AbstractSwBreakDlg_Impl>(std::make_unique<SwBreakDlg>(pParent, rSh)); } +std::shared_ptr<AbstractSwTranslateLangSelectDlg> SwAbstractDialogFactory_Impl::CreateSwTranslateLangSelectDlg(weld::Window* pParent, SwWrtShell &rSh) +{ + return std::make_shared<AbstractSwTranslateLangSelectDlg_Impl>(std::make_unique<SwTranslateLangSelectDlg>(pParent, rSh)); +} + VclPtr<VclAbstractDialog> SwAbstractDialogFactory_Impl::CreateSwChangeDBDlg(SwView& rVw) { #if HAVE_FEATURE_DBCONNECTIVITY && !ENABLE_FUZZERS diff --git a/sw/source/ui/dialog/swdlgfact.hxx b/sw/source/ui/dialog/swdlgfact.hxx index c127566ea10a..f97ff430c294 100644 --- a/sw/source/ui/dialog/swdlgfact.hxx +++ b/sw/source/ui/dialog/swdlgfact.hxx @@ -188,6 +188,19 @@ public: virtual std::shared_ptr<weld::DialogController> getDialogController() override { return m_xDlg; } }; +class AbstractSwTranslateLangSelectDlg_Impl : public AbstractSwTranslateLangSelectDlg +{ + std::shared_ptr<weld::DialogController> m_xDlg; +public: + explicit AbstractSwTranslateLangSelectDlg_Impl(std::shared_ptr<weld::DialogController> p) + : m_xDlg(std::move(p)) + { + } + + virtual std::shared_ptr<weld::DialogController> getDialogController() override { return m_xDlg; } + virtual std::optional<SwLanguageListItem> GetSelectedLanguage() override; +}; + class AbstractSwTableWidthDlg_Impl : public VclAbstractDialog { std::unique_ptr<SwTableWidthDlg> m_xDlg; @@ -684,6 +697,7 @@ public: SwContentControlListItem& rItem) override; virtual std::shared_ptr<AbstractSwBreakDlg> CreateSwBreakDlg(weld::Window *pParent, SwWrtShell &rSh) override; + virtual std::shared_ptr<AbstractSwTranslateLangSelectDlg> CreateSwTranslateLangSelectDlg(weld::Window *pParent, SwWrtShell &rSh) override; virtual VclPtr<VclAbstractDialog> CreateSwChangeDBDlg(SwView& rVw) override; virtual VclPtr<SfxAbstractTabDialog> CreateSwCharDlg(weld::Window* pParent, SwView& pVw, const SfxItemSet& rCoreSet, SwCharDlgMode nDialogMode, const OUString* pFormatStr = nullptr) override; diff --git a/sw/source/ui/misc/translatelangselect.cxx b/sw/source/ui/misc/translatelangselect.cxx new file mode 100644 index 000000000000..7e6d02cf94fb --- /dev/null +++ b/sw/source/ui/misc/translatelangselect.cxx @@ -0,0 +1,153 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/svapp.hxx> +#include <osl/diagnose.h> +#include <uitool.hxx> +#include <swtypes.hxx> +#include <wrtsh.hxx> +#include <view.hxx> +#include <viewopt.hxx> +#include <translatelangselect.hxx> +#include <pagedesc.hxx> +#include <poolfmt.hxx> +#include <sal/log.hxx> +#include <ndtxt.hxx> +#include <shellio.hxx> +#include <svtools/deeplcfg.hxx> +#include <vcl/idle.hxx> +#include <mdiexp.hxx> +#include <strings.hrc> +#include <com/sun/star/task/XStatusIndicator.hpp> +#include <sfx2/viewfrm.hxx> +#include <com/sun/star/task/XStatusIndicatorFactory.hpp> +#include <linguistic/translate.hxx> + +int SwTranslateLangSelectDlg::selectedLangIdx = -1; +SwTranslateLangSelectDlg::SwTranslateLangSelectDlg(weld::Window* pParent, SwWrtShell& rSh) + : GenericDialogController(pParent, "modules/swriter/ui/translationdialog.ui", + "LanguageSelectDialog") + , rWrtSh(rSh) + , m_xLanguageListBox(m_xBuilder->weld_combo_box("combobox1")) + , m_xBtnCancel(m_xBuilder->weld_button("cancel")) + , m_xBtnTranslate(m_xBuilder->weld_button("translate")) + , m_xLanguageVec({ + SwLanguageListItem("BG", "Bulgarian"), + SwLanguageListItem("CS", "Czech"), + SwLanguageListItem("DA", "Danish"), + SwLanguageListItem("DE", "German"), + SwLanguageListItem("EL", "Greek"), + SwLanguageListItem("EN-GB", "English (British)"), + SwLanguageListItem("EN-US", "English (American)"), + SwLanguageListItem("ET", "Estonian"), + SwLanguageListItem("FI", "Finnish"), + SwLanguageListItem("FR", "French"), + SwLanguageListItem("HU", "Hungarian"), + SwLanguageListItem("ID", "Indonesian"), + SwLanguageListItem("IT", "Italian"), + SwLanguageListItem("JA", "Japanese"), + SwLanguageListItem("LT", "Lithuanian"), + SwLanguageListItem("LV", "Dutch"), + SwLanguageListItem("PL", "Polish"), + SwLanguageListItem("PT-BR", "Portuguese (Brazilian)"), + SwLanguageListItem("PT-PT", "Portuguese (European)"), + SwLanguageListItem("RO", "Romanian"), + SwLanguageListItem("RU", "Russian"), + SwLanguageListItem("SK", "Slovak"), + SwLanguageListItem("SL", "Slovenian"), + SwLanguageListItem("SV", "Swedish"), + SwLanguageListItem("TR", "Turkish"), + SwLanguageListItem("ZH", "Chinese (simplified)"), + }) + , m_bTranslationStarted(false) + , m_bCancelTranslation(false) +{ + m_xLanguageListBox->connect_changed(LINK(this, SwTranslateLangSelectDlg, LangSelectHdl)); + m_xBtnCancel->connect_clicked(LINK(this, SwTranslateLangSelectDlg, LangSelectCancelHdl)); + m_xBtnTranslate->connect_clicked(LINK(this, SwTranslateLangSelectDlg, LangSelectTranslateHdl)); + + for (const auto& item : m_xLanguageVec) + { + m_xLanguageListBox->append_text(OStringToOUString(item.getName(), RTL_TEXTENCODING_UTF8)); + } + + if (SwTranslateLangSelectDlg::selectedLangIdx != -1) + { + m_xLanguageListBox->set_active(SwTranslateLangSelectDlg::selectedLangIdx); + } +} + +std::optional<SwLanguageListItem> SwTranslateLangSelectDlg::GetSelectedLanguage() +{ + if (SwTranslateLangSelectDlg::selectedLangIdx != -1) + { + return m_xLanguageVec.at(SwTranslateLangSelectDlg::selectedLangIdx); + } + + return {}; +} + +IMPL_STATIC_LINK(SwTranslateLangSelectDlg, LangSelectHdl, weld::ComboBox&, rBox, void) +{ + const auto selected = rBox.get_active(); + SwTranslateLangSelectDlg::selectedLangIdx = selected; +} + +IMPL_LINK_NOARG(SwTranslateLangSelectDlg, LangSelectCancelHdl, weld::Button&, void) +{ + // stop translation first + if (m_bTranslationStarted) + m_bCancelTranslation = true; + else + m_xDialog->response(RET_CANCEL); +} + +IMPL_LINK_NOARG(SwTranslateLangSelectDlg, LangSelectTranslateHdl, weld::Button&, void) +{ + if (SwTranslateLangSelectDlg::selectedLangIdx == -1) + { + m_xDialog->response(RET_CANCEL); + return; + } + + SvxDeeplOptions& rDeeplOptions = SvxDeeplOptions::Get(); + if (rDeeplOptions.getAPIUrl().isEmpty() || rDeeplOptions.getAuthKey().isEmpty()) + { + SAL_WARN("sw.ui", "SwTranslateLangSelectDlg: API options are not set"); + m_xDialog->response(RET_CANCEL); + return; + } + + const OString aAPIUrl + = OUStringToOString(rtl::Concat2View(rDeeplOptions.getAPIUrl() + "?tag_handling=html"), + RTL_TEXTENCODING_UTF8) + .trim(); + const OString aAuthKey + = OUStringToOString(rDeeplOptions.getAuthKey(), RTL_TEXTENCODING_UTF8).trim(); + const auto aTargetLang + = m_xLanguageVec.at(SwTranslateLangSelectDlg::selectedLangIdx).getLanguage(); + + m_bTranslationStarted = true; + + SwTranslateHelper::TranslateAPIConfig aConfig({ aAPIUrl, aAuthKey, aTargetLang }); + SwTranslateHelper::TranslateDocumentCancellable(rWrtSh, aConfig, m_bCancelTranslation); + m_xDialog->response(RET_OK); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/uibase/inc/translatehelper.hxx b/sw/source/uibase/inc/translatehelper.hxx new file mode 100644 index 000000000000..906b527647b6 --- /dev/null +++ b/sw/source/uibase/inc/translatehelper.hxx @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once +#include <swtypes.hxx> + +class SwWrtShell; +class SwPaM; +class SwNode; +class SwTextNode; + +namespace SwTranslateHelper +{ +struct SW_DLLPUBLIC TranslateAPIConfig final +{ + const OString m_xAPIUrl; + const OString m_xAuthKey; + const OString m_xTargetLanguage; +}; +SW_DLLPUBLIC OString ExportPaMToHTML(SwPaM* pCursor, bool bReplacePTag); +SW_DLLPUBLIC void PasteHTMLToPaM(SwWrtShell& rWrtSh, SwPaM* pCursor, const OString& rData, + bool bSetSelection); +SW_DLLPUBLIC void TranslateDocument(SwWrtShell& rWrtSh, const TranslateAPIConfig& rConfig); +SW_DLLPUBLIC void TranslateDocumentCancellable(SwWrtShell& rWrtSh, + const TranslateAPIConfig& rConfig, + bool& rCancelTranslation); +} diff --git a/sw/source/uibase/inc/translatelangselect.hxx b/sw/source/uibase/inc/translatelangselect.hxx new file mode 100644 index 000000000000..4190aafac442 --- /dev/null +++ b/sw/source/uibase/inc/translatelangselect.hxx @@ -0,0 +1,69 @@ + +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once +#include <vcl/weld.hxx> +#include <rtl/string.h> +#include <vector> +#include <optional> +#include "translatehelper.hxx" + +class SwWrtShell; + +// SwLanguageListItem Helper class for displaying available languages with their names and tags on the listbox. +class SwLanguageListItem final +{ +public: + SwLanguageListItem(const OString& sLanguage, const OString& sName) + : m_sLanguage(sLanguage) + , m_sName(sName) + { + } + const OString& getLanguage() const { return m_sLanguage; } + const OString& getName() const { return m_sName; } + +private: + const OString m_sLanguage; + const OString m_sName; +}; + +// SwTranslateLangSelectDlg Language selection dialog for translation API. +class SwTranslateLangSelectDlg final : public weld::GenericDialogController +{ +public: + static int selectedLangIdx; + SwTranslateLangSelectDlg(weld::Window* pParent, SwWrtShell& rSh); + std::optional<SwLanguageListItem> GetSelectedLanguage(); + +private: + SwWrtShell& rWrtSh; + std::unique_ptr<weld::ComboBox> m_xLanguageListBox; + std::unique_ptr<weld::Button> m_xBtnCancel; + std::unique_ptr<weld::Button> m_xBtnTranslate; + std::vector<SwLanguageListItem> m_xLanguageVec; + + bool m_bTranslationStarted; + bool m_bCancelTranslation; + + DECL_STATIC_LINK(SwTranslateLangSelectDlg, LangSelectHdl, weld::ComboBox&, void); + DECL_LINK(LangSelectCancelHdl, weld::Button&, void); + DECL_LINK(LangSelectTranslateHdl, weld::Button&, void); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/uibase/shells/textsh1.cxx b/sw/source/uibase/shells/textsh1.cxx index b7cfc8d9f7b5..da69a34ef75d 100644 --- a/sw/source/uibase/shells/textsh1.cxx +++ b/sw/source/uibase/shells/textsh1.cxx @@ -103,6 +103,9 @@ #include <bookmark.hxx> #include <linguistic/misc.hxx> #include <authfld.hxx> +#include <translatelangselect.hxx> +#include <svtools/deeplcfg.hxx> +#include <translatehelper.hxx> using namespace ::com::sun::star; using namespace com::sun::star::beans; @@ -1489,6 +1492,32 @@ void SwTextShell::Execute(SfxRequest &rReq) } } break; + case SID_FM_TRANSLATE: + { + const SfxPoolItem* pTargetLangStringItem = nullptr; + if (pArgs && SfxItemState::SET == pArgs->GetItemState(SID_ATTR_TARGETLANG_STR, false, &pTargetLangStringItem)) + { + SvxDeeplOptions& rDeeplOptions = SvxDeeplOptions::Get(); + if (rDeeplOptions.getAPIUrl().isEmpty() || rDeeplOptions.getAuthKey().isEmpty()) + { + SAL_WARN("sw.ui", "SID_FM_TRANSLATE: API options are not set"); + break; + } + const OString aAPIUrl = OUStringToOString(rtl::Concat2View(rDeeplOptions.getAPIUrl() + "?tag_handling=html"), RTL_TEXTENCODING_UTF8).trim(); + const OString aAuthKey = OUStringToOString(rDeeplOptions.getAuthKey(), RTL_TEXTENCODING_UTF8).trim(); + OString aTargetLang = OUStringToOString(static_cast<const SfxStringItem*>(pTargetLangStringItem)->GetValue(), RTL_TEXTENCODING_UTF8); + SwTranslateHelper::TranslateAPIConfig aConfig({aAPIUrl, aAuthKey, aTargetLang}); + SwTranslateHelper::TranslateDocument(rWrtSh, aConfig); + } + else + { + SwAbstractDialogFactory* pFact = SwAbstractDialogFactory::Create(); + std::shared_ptr<AbstractSwTranslateLangSelectDlg> pAbstractDialog(pFact->CreateSwTranslateLangSelectDlg(GetView().GetFrameWeld(), rWrtSh)); + std::shared_ptr<weld::DialogController> pDialogController(pAbstractDialog->getDialogController()); + weld::DialogController::runAsync(pDialogController, [] (sal_Int32 /*nResult*/) { }); + } + } + break; case SID_SPELLCHECK_IGNORE: { SwPaM *pPaM = rWrtSh.GetCursor(); diff --git a/sw/source/uibase/shells/translatehelper.cxx b/sw/source/uibase/shells/translatehelper.cxx new file mode 100644 index 000000000000..92f09913730a --- /dev/null +++ b/sw/source/uibase/shells/translatehelper.cxx @@ -0,0 +1,208 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#include <wrtsh.hxx> +#include <pam.hxx> +#include <node.hxx> +#include <ndtxt.hxx> +#include <translatehelper.hxx> +#include <sal/log.hxx> +#include <rtl/string.h> +#include <shellio.hxx> +#include <vcl/scheduler.hxx> +#include <vcl/svapp.hxx> +#include <boost/property_tree/ptree.hpp> +#include <boost/property_tree/json_parser.hpp> +#include <vcl/htmltransferable.hxx> +#include <vcl/transfer.hxx> +#include <swdtflvr.hxx> +#include <linguistic/translate.hxx> +#include <com/sun/star/task/XStatusIndicator.hpp> +#include <sfx2/viewfrm.hxx> +#include <com/sun/star/task/XStatusIndicatorFactory.hpp> +#include <strings.hrc> + +namespace SwTranslateHelper +{ +OString ExportPaMToHTML(SwPaM* pCursor, bool bReplacePTag) +{ + SolarMutexGuard gMutex; + OString aResult; + WriterRef xWrt; + GetHTMLWriter(u"NoLineLimit,SkipHeaderFooter", OUString(), xWrt); + if (pCursor != nullptr) + { + SvMemoryStream aMemoryStream; + SwWriter aWriter(aMemoryStream, *pCursor); + ErrCode nError = aWriter.Write(xWrt); + if (nError.IsError()) + { + SAL_WARN("sw.ui", "ExportPaMToHTML: failed to export selection to HTML"); + return {}; + } + aResult + = OString(static_cast<const char*>(aMemoryStream.GetData()), aMemoryStream.GetSize()); + if (bReplacePTag) + { + aResult = aResult.replaceAll("<p", "<span"); + aResult = aResult.replaceAll("</p>", "</span>"); + } + return aResult; + } + return {}; +} + +void PasteHTMLToPaM(SwWrtShell& rWrtSh, SwPaM* pCursor, const OString& rData, bool bSetSelection) +{ + SolarMutexGuard gMutex; + rtl::Reference<vcl::unohelper::HtmlTransferable> pHtmlTransferable + = new vcl::unohelper::HtmlTransferable(rData); + if (pHtmlTransferable.is()) + { + TransferableDataHelper aDataHelper(pHtmlTransferable); + if (aDataHelper.GetXTransferable().is() + && SwTransferable::IsPasteSpecial(rWrtSh, aDataHelper)) + { + if (bSetSelection) + { + rWrtSh.SetSelection(*pCursor); + } + SwTransferable::Paste(rWrtSh, aDataHelper); + rWrtSh.KillSelection(nullptr, false); + } + } +} + +void TranslateDocument(SwWrtShell& rWrtSh, const TranslateAPIConfig& rConfig) +{ + bool bCancel = false; + TranslateDocumentCancellable(rWrtSh, rConfig, bCancel); +} + +void TranslateDocumentCancellable(SwWrtShell& rWrtSh, const TranslateAPIConfig& rConfig, + bool& rCancelTranslation) +{ + auto m_pCurrentPam = rWrtSh.GetCursor(); + bool bHasSelection = rWrtSh.HasSelection(); + + if (bHasSelection) + { + // iteration will start top to bottom + if (m_pCurrentPam->GetPoint()->nNode > m_pCurrentPam->GetMark()->nNode) + m_pCurrentPam->Exchange(); + } + + auto const& pNodes = rWrtSh.GetNodes(); + SwPosition aPoint = *m_pCurrentPam->GetPoint(); + SwPosition aMark = *m_pCurrentPam->GetMark(); + auto startNode = bHasSelection ? aPoint.nNode.GetIndex() : SwNodeOffset(0); + auto endNode = bHasSelection ? aMark.nNode.GetIndex() : pNodes.Count() - 1; + + sal_Int32 nCount(0); + sal_Int32 nProgress(0); + + for (SwNodeOffset n(startNode); n <= endNode; ++n) + { + if (pNodes[n] && pNodes[n]->IsTextNode()) + { + if (pNodes[n]->GetTextNode()->GetText().isEmpty()) + continue; + nCount++; + } + } + + SfxViewFrame* pFrame = SfxViewFrame::Current(); + uno::Reference<frame::XFrame> xFrame = pFrame->GetFrame().GetFrameInterface(); + uno::Reference<task::XStatusIndicatorFactory> xProgressFactory(xFrame, uno::UNO_QUERY); + uno::Reference<task::XStatusIndicator> xStatusIndicator; + + if (xProgressFactory.is()) + { + xStatusIndicator = xProgressFactory->createStatusIndicator(); + } + + if (xStatusIndicator.is()) + xStatusIndicator->start(SwResId(STR_STATSTR_SWTRANSLATE), nCount); + + for (SwNodeOffset n(startNode); n <= endNode; ++n) + { + if (rCancelTranslation) + break; + + if (n >= rWrtSh.GetNodes().Count()) + break; + + if (!pNodes[n]) + break; + + SwNode* pNode = pNodes[n]; + if (pNode->IsTextNode()) + { + if (pNode->GetTextNode()->GetText().isEmpty()) + continue; + + auto cursor + = Writer::NewUnoCursor(*rWrtSh.GetDoc(), pNode->GetIndex(), pNode->GetIndex()); + + // set edges (start, end) for nodes inside the selection. + if (bHasSelection) + { + if (startNode == endNode) + { + cursor->SetMark(); + cursor->GetPoint()->nContent = aPoint.nContent; + cursor->GetMark()->nContent = aMark.nContent; + } + else if (n == startNode) + { + cursor->SetMark(); + cursor->GetPoint()->nContent = std::min(aPoint.nContent, aMark.nContent); + } + else if (n == endNode) + { + cursor->SetMark(); + cursor->GetMark()->nContent = aMark.nContent; + cursor->GetPoint()->nContent = 0; + } + } + + const auto aOut = SwTranslateHelper::ExportPaMToHTML(cursor.get(), true); + const auto aTranslatedOut = linguistic::Translate( + rConfig.m_xTargetLanguage, rConfig.m_xAPIUrl, rConfig.m_xAuthKey, aOut); + SwTranslateHelper::PasteHTMLToPaM(rWrtSh, cursor.get(), aTranslatedOut, true); + + if (xStatusIndicator.is()) + xStatusIndicator->setValue((100 * ++nProgress) / nCount); + + Idle aIdle("ProgressBar::SetValue aIdle"); + aIdle.SetPriority(TaskPriority::POST_PAINT); + aIdle.Start(); + + rWrtSh.LockView(true); + while (aIdle.IsActive() && !Application::IsQuit()) + { + Application::Yield(); + } + rWrtSh.LockView(false); + } + } + + if (xStatusIndicator.is()) + xStatusIndicator->end(); +} +} diff --git a/sw/uiconfig/sglobal/menubar/menubar.xml b/sw/uiconfig/sglobal/menubar/menubar.xml index 6246d44392df..4e305c1410f4 100644 --- a/sw/uiconfig/sglobal/menubar/menubar.xml +++ b/sw/uiconfig/sglobal/menubar/menubar.xml @@ -720,6 +720,8 @@ <menu:menuitem menu:id=".uno:WordCountDialog" menu:style="text"/> <menu:menuitem menu:id=".uno:AccessibilityCheck"/> <menu:menuseparator/> + <menu:menuitem menu:id=".uno:Translate" menu:style="text"/> + <menu:menuseparator/> <menu:menu menu:id=".uno:AutoFormatMenu"> <menu:menupopup> <menu:menuitem menu:id=".uno:OnlineAutoFormat"/> diff --git a/sw/uiconfig/swriter/menubar/menubar.xml b/sw/uiconfig/swriter/menubar/menubar.xml index 45a7c3b10b53..06823b5beacc 100644 --- a/sw/uiconfig/swriter/menubar/menubar.xml +++ b/sw/uiconfig/swriter/menubar/menubar.xml @@ -745,6 +745,8 @@ <menu:menuitem menu:id=".uno:WordCountDialog"/> <menu:menuitem menu:id=".uno:AccessibilityCheck"/> <menu:menuseparator/> + <menu:menuitem menu:id=".uno:Translate"/> + <menu:menuseparator/> <menu:menu menu:id=".uno:AutoFormatMenu"> <menu:menupopup> <menu:menuitem menu:id=".uno:OnlineAutoFormat"/> diff --git a/sw/uiconfig/swriter/ui/translationdialog.ui b/sw/uiconfig/swriter/ui/translationdialog.ui new file mode 100644 index 000000000000..a6aff92ab00c --- /dev/null +++ b/sw/uiconfig/swriter/ui/translationdialog.ui @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.2 --> +<interface domain="sw"> + <requires lib="gtk+" version="3.20"/> + <object class="GtkDialog" id="LanguageSelectDialog"> + <property name="can_focus">False</property> + <property name="border_width">6</property> + <property name="title" translatable="yes" context="LanguageSelectDialog">Language Selection</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="default_width">0</property> + <property name="default_height">0</property> + <property name="type_hint">dialog</property> + <child type="titlebar"> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="action-area1"> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="cancel"> + <property name="label">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="translate"> + <property name="label">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="grid1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">5</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes" context="LanguageSelectDialog">Select the target language for translation</property> + <property name="mnemonic_widget">combobox1</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="combobox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">cancel</action-widget> + <action-widget response="-5">translate</action-widget> + </action-widgets> + </object> +</interface> diff --git a/vcl/Library_vcl.mk b/vcl/Library_vcl.mk index a432ceb09146..4b1751005bf0 100644 --- a/vcl/Library_vcl.mk +++ b/vcl/Library_vcl.mk @@ -412,6 +412,7 @@ $(eval $(call gb_Library_add_exception_objects,vcl,\ vcl/source/app/svmain \ vcl/source/app/timer \ vcl/source/app/unohelp2 \ + vcl/source/app/htmltransferable \ vcl/source/app/unohelp \ vcl/source/app/vclevent \ vcl/source/app/watchdog \ diff --git a/vcl/jsdialog/enabled.cxx b/vcl/jsdialog/enabled.cxx index 6dbc32cdaf2f..f260691a64d7 100644 --- a/vcl/jsdialog/enabled.cxx +++ b/vcl/jsdialog/enabled.cxx @@ -57,6 +57,7 @@ bool isBuilderEnabled(std::u16string_view rUIFile, bool bMobile) || rUIFile == u"xmlsec/ui/digitalsignaturesdialog.ui" || rUIFile == u"xmlsec/ui/viewcertdialog.ui" || rUIFile == u"xmlsec/ui/certgeneral.ui" || rUIFile == u"xmlsec/ui/certpage.ui" || rUIFile == u"svx/ui/accessibilitycheckdialog.ui" + || rUIFile == u"modules/swriter/ui/translationdialog.ui" || rUIFile == u"svx/ui/accessibilitycheckentry.ui") { return true; diff --git a/vcl/source/app/htmltransferable.cxx b/vcl/source/app/htmltransferable.cxx new file mode 100644 index 000000000000..24f65fe929b1 --- /dev/null +++ b/vcl/source/app/htmltransferable.cxx @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/htmltransferable.hxx> +#include <sot/exchange.hxx> +#include <sot/formats.hxx> +#include <vcl/svapp.hxx> +#include <com/sun/star/datatransfer/UnsupportedFlavorException.hpp> +#include <com/sun/star/datatransfer/clipboard/XClipboard.hpp> +#include <com/sun/star/datatransfer/clipboard/XFlushableClipboard.hpp> +#include <cppuhelper/queryinterface.hxx> +#include <boost/property_tree/json_parser.hpp> + +using namespace ::com::sun::star; + +namespace vcl::unohelper +{ +HtmlTransferable::HtmlTransferable(OString sData) + : data(sData) +{ +} + +HtmlTransferable::~HtmlTransferable() {} + +// css::uno::XInterface +uno::Any HtmlTransferable::queryInterface(const uno::Type& rType) +{ + uno::Any aRet = ::cppu::queryInterface(rType, static_cast<datatransfer::XTransferable*>(this)); + return (aRet.hasValue() ? aRet : OWeakObject::queryInterface(rType)); +} + +// css::datatransfer::XTransferable +uno::Any HtmlTransferable::getTransferData(const datatransfer::DataFlavor& rFlavor) +{ + SotClipboardFormatId nT = SotExchange::GetFormat(rFlavor); + if (nT != SotClipboardFormatId::HTML) + { + throw datatransfer::UnsupportedFlavorException(); + } + size_t size = data.getLength(); + uno::Sequence<sal_Int8> sData(size); + std::memcpy(sData.getArray(), data.getStr(), size); + return uno::Any(sData); +} + +uno::Sequence<datatransfer::DataFlavor> HtmlTransferable::getTransferDataFlavors() +{ + uno::Sequence<datatransfer::DataFlavor> aDataFlavors(1); + auto ref = aDataFlavors.getArray()[0]; + ref.MimeType = "text/html"; + ref.DataType = cppu::UnoType<uno::Sequence<sal_Int8>>::get(); + SotExchange::GetFormatDataFlavor(SotClipboardFormatId::HTML, aDataFlavors.getArray()[0]); + return aDataFlavors; +} + +sal_Bool HtmlTransferable::isDataFlavorSupported(const datatransfer::DataFlavor& rFlavor) +{ + SotClipboardFormatId nT = SotExchange::GetFormat(rFlavor); + return (nT == SotClipboardFormatId::HTML); +} + +} // namespace vcl::unohelper |