From 212259aae34027525d3393ce6086968bd28e14a8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 1 Dec 2023 18:47:24 +0400 Subject: [PATCH] Handle shortcuts in IV. --- Telegram/Resources/iv_html/page.js | 188 ++++++++++++---------- Telegram/SourceFiles/core/application.cpp | 15 +- Telegram/SourceFiles/iv/iv_controller.cpp | 69 +++++++- Telegram/SourceFiles/iv/iv_controller.h | 22 ++- Telegram/SourceFiles/iv/iv_instance.cpp | 53 ++++++ Telegram/SourceFiles/iv/iv_instance.h | 3 + Telegram/cmake/td_iv.cmake | 6 + 7 files changed, 261 insertions(+), 95 deletions(-) diff --git a/Telegram/Resources/iv_html/page.js b/Telegram/Resources/iv_html/page.js index d821a1e43..8486e2cc7 100644 --- a/Telegram/Resources/iv_html/page.js +++ b/Telegram/Resources/iv_html/page.js @@ -1,86 +1,112 @@ var IV = { - sendPostMessage: function(data) { - try { - window.parent.postMessage(JSON.stringify(data), window.parentOrigin); - } catch(e) {} - }, - frameClickHandler: function(e) { - var target = e.target, href; - do { - if (target.tagName == 'SUMMARY') return; - if (target.tagName == 'DETAILS') return; - if (target.tagName == 'LABEL') return; - if (target.tagName == 'AUDIO') return; - if (target.tagName == 'A') break; - } while (target = target.parentNode); - if (target && target.hasAttribute('href')) { - var base_loc = document.createElement('A'); - base_loc.href = window.currentUrl; - if (base_loc.origin != target.origin || - base_loc.pathname != target.pathname || - base_loc.search != target.search) { - IV.sendPostMessage({event: 'link_click', url: target.href}); - } - } - e.preventDefault(); - }, - postMessageHandler: function(event) { - if (event.source !== window.parent || - event.origin != window.parentOrigin) { - return; - } - try { - var data = JSON.parse(event.data); - } catch(e) { - var data = {}; - } - }, - slideshowSlide: function(el, next) { - var dir = window.getComputedStyle(el, null).direction || 'ltr'; - var marginProp = dir == 'rtl' ? 'marginRight' : 'marginLeft'; - if (next) { - var s = el.previousSibling.s; - s.value = (+s.value + 1 == s.length) ? 0 : +s.value + 1; - s.forEach(function(el){ el.checked && el.parentNode.scrollIntoView && el.parentNode.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'}); }); - el.firstChild.style[marginProp] = (-100 * s.value) + '%'; - } else { - el.form.nextSibling.firstChild.style[marginProp] = (-100 * el.value) + '%'; - } - return false; - }, - initPreBlocks: function() { - if (!hljs) return; - var pres = document.getElementsByTagName('pre'); - for (var i = 0; i < pres.length; i++) { - if (pres[i].hasAttribute('data-language')) { - hljs.highlightBlock(pres[i]); - } - } - }, - initEmbedBlocks: function() { - var iframes = document.getElementsByTagName('iframe'); - for (var i = 0; i < iframes.length; i++) { - (function(iframe) { - window.addEventListener('message', function(event) { - if (event.source !== iframe.contentWindow || - event.origin != window.origin) { - return; - } - try { - var data = JSON.parse(event.data); - } catch(e) { - var data = {}; - } - if (data.eventType == 'resize_frame') { - if (data.eventData.height) { - iframe.style.height = data.eventData.height + 'px'; - } - } - }, false); - })(iframes[i]); - } - } + notify: function(message) { + if (window.external && window.external.invoke) { + window.external.invoke(JSON.stringify(message)); + } + }, + frameClickHandler: function(e) { + var target = e.target, href; + do { + if (target.tagName == 'SUMMARY') return; + if (target.tagName == 'DETAILS') return; + if (target.tagName == 'LABEL') return; + if (target.tagName == 'AUDIO') return; + if (target.tagName == 'A') break; + } while (target = target.parentNode); + if (target && target.hasAttribute('href')) { + var base_loc = document.createElement('A'); + base_loc.href = window.currentUrl; + if (base_loc.origin != target.origin || + base_loc.pathname != target.pathname || + base_loc.search != target.search) { + IV.notify({ event: 'link_click', url: target.href }); + } + } + e.preventDefault(); + }, + frameKeyDown: function (e) { + let keyW = (e.key === 'w') + || (e.code === 'KeyW') + || (e.keyCode === 87); + let keyQ = (e.key === 'q') + || (e.code === 'KeyQ') + || (e.keyCode === 81); + let keyM = (e.key === 'm') + || (e.code === 'KeyM') + || (e.keyCode === 77); + if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM)) { + e.preventDefault(); + IV.notify({ + event: 'keydown', + modifier: e.ctrlKey ? 'ctrl' : 'cmd', + key: keyW ? 'w' : keyQ ? 'q' : 'm', + }); + } else if (e.key === 'Escape' || e.keyCode === 27) { + e.preventDefault(); + IV.notify({ + event: 'keydown', + key: 'escape', + }); + } + }, + postMessageHandler: function(event) { + if (event.source !== window.parent || + event.origin != window.parentOrigin) { + return; + } + try { + var data = JSON.parse(event.data); + } catch(e) { + var data = {}; + } + }, + slideshowSlide: function(el, next) { + var dir = window.getComputedStyle(el, null).direction || 'ltr'; + var marginProp = dir == 'rtl' ? 'marginRight' : 'marginLeft'; + if (next) { + var s = el.previousSibling.s; + s.value = (+s.value + 1 == s.length) ? 0 : +s.value + 1; + s.forEach(function(el){ el.checked && el.parentNode.scrollIntoView && el.parentNode.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'}); }); + el.firstChild.style[marginProp] = (-100 * s.value) + '%'; + } else { + el.form.nextSibling.firstChild.style[marginProp] = (-100 * el.value) + '%'; + } + return false; + }, + initPreBlocks: function() { + if (!hljs) return; + var pres = document.getElementsByTagName('pre'); + for (var i = 0; i < pres.length; i++) { + if (pres[i].hasAttribute('data-language')) { + hljs.highlightBlock(pres[i]); + } + } + }, + initEmbedBlocks: function() { + var iframes = document.getElementsByTagName('iframe'); + for (var i = 0; i < iframes.length; i++) { + (function(iframe) { + window.addEventListener('message', function(event) { + if (event.source !== iframe.contentWindow || + event.origin != window.origin) { + return; + } + try { + var data = JSON.parse(event.data); + } catch(e) { + var data = {}; + } + if (data.eventType == 'resize_frame') { + if (data.eventData.height) { + iframe.style.height = data.eventData.height + 'px'; + } + } + }, false); + })(iframes[i]); + } + } }; document.onclick = IV.frameClickHandler; +document.onkeydown = IV.frameKeyDown; window.onmessage = IV.postMessageHandler; diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 8fe1e75c7..06691b5c9 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -1547,12 +1547,12 @@ bool Application::closeActiveWindow() { if (_mediaView && _mediaView->isActive()) { _mediaView->close(); return true; - } else if (!calls().closeCurrentActiveCall()) { - if (const auto window = activeWindow()) { - if (window->widget()->isActive()) { - window->close(); - return true; - } + } else if (_iv->closeActive() || calls().closeCurrentActiveCall()) { + return true; + } else if (const auto window = activeWindow()) { + if (window->widget()->isActive()) { + window->close(); + return true; } } return false; @@ -1562,7 +1562,8 @@ bool Application::minimizeActiveWindow() { if (_mediaView && _mediaView->isActive()) { _mediaView->minimize(); return true; - } else if (calls().minimizeCurrentActiveCall()) { + } else if (_iv->minimizeActive() + || calls().minimizeCurrentActiveCall()) { return true; } else { if (const auto window = activeWindow()) { diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index 6f9069dfd..709461eaa 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -7,14 +7,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "iv/iv_controller.h" +#include "base/platform/base_platform_info.h" #include "iv/iv_data.h" #include "ui/widgets/rp_window.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_embed.h" #include "webview/webview_interface.h" +#include "styles/palette.h" #include +#include +#include +#include #include +#include namespace Iv { @@ -47,22 +53,56 @@ void Controller::show(const QString &dataPath, Prepared page) { _webview = nullptr; }); if (!raw->widget()) { - _webview = nullptr; - _window = nullptr; + _events.fire(Event::Close); return; } + window->events( + ) | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::Close) { + close(); + } else if (e->type() == QEvent::KeyPress) { + const auto event = static_cast(e.get()); + if (event->key() == Qt::Key_Escape) { + escape(); + } + } + }, window->lifetime()); raw->widget()->show(); container->geometryValue( ) | rpl::start_with_next([=](QRect geometry) { raw->widget()->setGeometry(geometry); - }, _lifetime); + }, container->lifetime()); + + container->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(container).fillRect(clip, st::windowBg); + }, container->lifetime()); raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) { return true; }); raw->setNavigationDoneHandler([=](bool success) { }); + raw->setMessageHandler([=](const QJsonDocument &message) { + crl::on_main(_window.get(), [=] { + const auto object = message.object(); + const auto event = object.value("event").toString(); + if (event == u"keydown"_q) { + const auto key = object.value("key").toString(); + const auto modifier = object.value("modifier").toString(); + const auto ctrl = Platform::IsMac() ? u"cmd"_q : u"ctrl"_q; + if (key == u"escape"_q) { + escape(); + } else if (key == u"w"_q && modifier == ctrl) { + close(); + } else if (key == u"m"_q && modifier == ctrl) { + minimize(); + } else if (key == u"q"_q && modifier == ctrl) { + quit(); + } + } + }); + }); raw->setDataRequestHandler([=](Webview::DataRequest request) { if (!request.id.starts_with("iv/")) { _dataRequests.fire(std::move(request)); @@ -104,8 +144,27 @@ void Controller::show(const QString &dataPath, Prepared page) { window->show(); } -rpl::producer Controller::dataRequests() const { - return _dataRequests.events(); +bool Controller::active() const { + return _window && _window->isActiveWindow(); +} + +void Controller::minimize() { + if (_window) { + _window->setWindowState(_window->windowState() + | Qt::WindowMinimized); + } +} + +void Controller::escape() { + close(); +} + +void Controller::close() { + _events.fire(Event::Close); +} + +void Controller::quit() { + _events.fire(Event::Quit); } rpl::lifetime &Controller::lifetime() { diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h index b35dfc00a..7e623f7ce 100644 --- a/Telegram/SourceFiles/iv/iv_controller.h +++ b/Telegram/SourceFiles/iv/iv_controller.h @@ -25,16 +25,34 @@ public: Controller(); ~Controller(); - void show(const QString &dataPath, Prepared page); + enum class Event { + Close, + Quit, + }; - [[nodiscard]] rpl::producer dataRequests() const; + void show(const QString &dataPath, Prepared page); + [[nodiscard]] bool active() const; + void minimize(); + + [[nodiscard]] rpl::producer dataRequests() const { + return _dataRequests.events(); + } + + [[nodiscard]] rpl::producer events() const { + return _events.events(); + } [[nodiscard]] rpl::lifetime &lifetime(); private: + void escape(); + void close(); + void quit(); + std::unique_ptr _window; std::unique_ptr _webview; rpl::event_stream _dataRequests; + rpl::event_stream _events; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index 27f3dcfb2..4a1accd7e 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/iv_instance.h" #include "core/file_utilities.h" +#include "core/shortcuts.h" #include "data/data_cloud_file.h" #include "data/data_document.h" #include "data/data_file_origin.h" @@ -63,6 +64,17 @@ public: not_null data) const; [[nodiscard]] bool showingFrom(not_null session) const; [[nodiscard]] bool activeFor(not_null session) const; + [[nodiscard]] bool active() const; + + void minimize(); + + [[nodiscard]] rpl::producer events() const { + return _events.events(); + } + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } private: struct MapPreview { @@ -131,6 +143,8 @@ private: std::vector _resources; int _resource = -1; + rpl::event_stream _events; + rpl::lifetime _lifetime; }; @@ -358,6 +372,10 @@ void Shown::writeEmbed(QString id, QString hash) { void Shown::showWindowed(Prepared result) { _controller = std::make_unique(); + + _controller->events( + ) | rpl::start_to_stream(_events, _controller->lifetime()); + _controller->dataRequests( ) | rpl::start_with_next([=](Webview::DataRequest request) { const auto requested = QString::fromStdString(request.id); @@ -372,6 +390,7 @@ void Shown::showWindowed(Prepared result) { sendEmbed(id.mid(5).toUtf8(), std::move(request)); } }, _controller->lifetime()); + const auto domain = &_session->domain(); _controller->show(domain->local().webviewDataPath(), std::move(result)); } @@ -628,6 +647,16 @@ bool Shown::activeFor(not_null session) const { return showingFrom(session) && _controller; } +bool Shown::active() const { + return _controller && _controller->active(); +} + +void Shown::minimize() { + if (_controller) { + _controller->minimize(); + } +} + Instance::Instance() = default; Instance::~Instance() = default; @@ -641,6 +670,14 @@ void Instance::show( return; } _shown = std::make_unique(show, data, local); + _shown->events() | rpl::start_with_next([=](Controller::Event event) { + if (event == Controller::Event::Close) { + _shown = nullptr; + } else if (event == Controller::Event::Quit) { + Shortcuts::Launch(Shortcuts::Command::Quit); + } + }, _shown->lifetime()); + if (!_tracking.contains(session)) { _tracking.emplace(session); session->lifetime().add([=] { @@ -656,6 +693,22 @@ bool Instance::hasActiveWindow(not_null session) const { return _shown && _shown->activeFor(session); } +bool Instance::closeActive() { + if (!_shown || !_shown->active()) { + return false; + } + _shown = nullptr; + return true; +} + +bool Instance::minimizeActive() { + if (!_shown || !_shown->active()) { + return false; + } + _shown->minimize(); + return true; +} + void Instance::closeAll() { _shown = nullptr; } diff --git a/Telegram/SourceFiles/iv/iv_instance.h b/Telegram/SourceFiles/iv/iv_instance.h index 7a1b21f62..566876303 100644 --- a/Telegram/SourceFiles/iv/iv_instance.h +++ b/Telegram/SourceFiles/iv/iv_instance.h @@ -30,6 +30,9 @@ public: [[nodiscard]] bool hasActiveWindow( not_null session) const; + bool closeActive(); + bool minimizeActive(); + void closeAll(); [[nodiscard]] rpl::lifetime &lifetime(); diff --git a/Telegram/cmake/td_iv.cmake b/Telegram/cmake/td_iv.cmake index 64fe487ce..c15db5afc 100644 --- a/Telegram/cmake/td_iv.cmake +++ b/Telegram/cmake/td_iv.cmake @@ -20,6 +20,12 @@ PRIVATE iv/iv_prepare.h ) +nice_target_sources(td_iv ${res_loc} +PRIVATE + iv_html/page.css + iv_html/page.js +) + target_include_directories(td_iv PUBLIC ${src_loc}