Handle shortcuts in IV.

This commit is contained in:
John Preston 2023-12-01 18:47:24 +04:00
parent 6d733bb566
commit 212259aae3
7 changed files with 261 additions and 95 deletions

View file

@ -1,86 +1,112 @@
var IV = { var IV = {
sendPostMessage: function(data) { notify: function(message) {
try { if (window.external && window.external.invoke) {
window.parent.postMessage(JSON.stringify(data), window.parentOrigin); window.external.invoke(JSON.stringify(message));
} catch(e) {} }
}, },
frameClickHandler: function(e) { frameClickHandler: function(e) {
var target = e.target, href; var target = e.target, href;
do { do {
if (target.tagName == 'SUMMARY') return; if (target.tagName == 'SUMMARY') return;
if (target.tagName == 'DETAILS') return; if (target.tagName == 'DETAILS') return;
if (target.tagName == 'LABEL') return; if (target.tagName == 'LABEL') return;
if (target.tagName == 'AUDIO') return; if (target.tagName == 'AUDIO') return;
if (target.tagName == 'A') break; if (target.tagName == 'A') break;
} while (target = target.parentNode); } while (target = target.parentNode);
if (target && target.hasAttribute('href')) { if (target && target.hasAttribute('href')) {
var base_loc = document.createElement('A'); var base_loc = document.createElement('A');
base_loc.href = window.currentUrl; base_loc.href = window.currentUrl;
if (base_loc.origin != target.origin || if (base_loc.origin != target.origin ||
base_loc.pathname != target.pathname || base_loc.pathname != target.pathname ||
base_loc.search != target.search) { base_loc.search != target.search) {
IV.sendPostMessage({event: 'link_click', url: target.href}); IV.notify({ event: 'link_click', url: target.href });
} }
} }
e.preventDefault(); e.preventDefault();
}, },
postMessageHandler: function(event) { frameKeyDown: function (e) {
if (event.source !== window.parent || let keyW = (e.key === 'w')
event.origin != window.parentOrigin) { || (e.code === 'KeyW')
return; || (e.keyCode === 87);
} let keyQ = (e.key === 'q')
try { || (e.code === 'KeyQ')
var data = JSON.parse(event.data); || (e.keyCode === 81);
} catch(e) { let keyM = (e.key === 'm')
var data = {}; || (e.code === 'KeyM')
} || (e.keyCode === 77);
}, if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM)) {
slideshowSlide: function(el, next) { e.preventDefault();
var dir = window.getComputedStyle(el, null).direction || 'ltr'; IV.notify({
var marginProp = dir == 'rtl' ? 'marginRight' : 'marginLeft'; event: 'keydown',
if (next) { modifier: e.ctrlKey ? 'ctrl' : 'cmd',
var s = el.previousSibling.s; key: keyW ? 'w' : keyQ ? 'q' : 'm',
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'}); }); } else if (e.key === 'Escape' || e.keyCode === 27) {
el.firstChild.style[marginProp] = (-100 * s.value) + '%'; e.preventDefault();
} else { IV.notify({
el.form.nextSibling.firstChild.style[marginProp] = (-100 * el.value) + '%'; event: 'keydown',
} key: 'escape',
return false; });
}, }
initPreBlocks: function() { },
if (!hljs) return; postMessageHandler: function(event) {
var pres = document.getElementsByTagName('pre'); if (event.source !== window.parent ||
for (var i = 0; i < pres.length; i++) { event.origin != window.parentOrigin) {
if (pres[i].hasAttribute('data-language')) { return;
hljs.highlightBlock(pres[i]); }
} try {
} var data = JSON.parse(event.data);
}, } catch(e) {
initEmbedBlocks: function() { var data = {};
var iframes = document.getElementsByTagName('iframe'); }
for (var i = 0; i < iframes.length; i++) { },
(function(iframe) { slideshowSlide: function(el, next) {
window.addEventListener('message', function(event) { var dir = window.getComputedStyle(el, null).direction || 'ltr';
if (event.source !== iframe.contentWindow || var marginProp = dir == 'rtl' ? 'marginRight' : 'marginLeft';
event.origin != window.origin) { if (next) {
return; var s = el.previousSibling.s;
} s.value = (+s.value + 1 == s.length) ? 0 : +s.value + 1;
try { s.forEach(function(el){ el.checked && el.parentNode.scrollIntoView && el.parentNode.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'}); });
var data = JSON.parse(event.data); el.firstChild.style[marginProp] = (-100 * s.value) + '%';
} catch(e) { } else {
var data = {}; el.form.nextSibling.firstChild.style[marginProp] = (-100 * el.value) + '%';
} }
if (data.eventType == 'resize_frame') { return false;
if (data.eventData.height) { },
iframe.style.height = data.eventData.height + 'px'; initPreBlocks: function() {
} if (!hljs) return;
} var pres = document.getElementsByTagName('pre');
}, false); for (var i = 0; i < pres.length; i++) {
})(iframes[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.onclick = IV.frameClickHandler;
document.onkeydown = IV.frameKeyDown;
window.onmessage = IV.postMessageHandler; window.onmessage = IV.postMessageHandler;

View file

@ -1547,12 +1547,12 @@ bool Application::closeActiveWindow() {
if (_mediaView && _mediaView->isActive()) { if (_mediaView && _mediaView->isActive()) {
_mediaView->close(); _mediaView->close();
return true; return true;
} else if (!calls().closeCurrentActiveCall()) { } else if (_iv->closeActive() || calls().closeCurrentActiveCall()) {
if (const auto window = activeWindow()) { return true;
if (window->widget()->isActive()) { } else if (const auto window = activeWindow()) {
window->close(); if (window->widget()->isActive()) {
return true; window->close();
} return true;
} }
} }
return false; return false;
@ -1562,7 +1562,8 @@ bool Application::minimizeActiveWindow() {
if (_mediaView && _mediaView->isActive()) { if (_mediaView && _mediaView->isActive()) {
_mediaView->minimize(); _mediaView->minimize();
return true; return true;
} else if (calls().minimizeCurrentActiveCall()) { } else if (_iv->minimizeActive()
|| calls().minimizeCurrentActiveCall()) {
return true; return true;
} else { } else {
if (const auto window = activeWindow()) { if (const auto window = activeWindow()) {

View file

@ -7,14 +7,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "iv/iv_controller.h" #include "iv/iv_controller.h"
#include "base/platform/base_platform_info.h"
#include "iv/iv_data.h" #include "iv/iv_data.h"
#include "ui/widgets/rp_window.h" #include "ui/widgets/rp_window.h"
#include "webview/webview_data_stream_memory.h" #include "webview/webview_data_stream_memory.h"
#include "webview/webview_embed.h" #include "webview/webview_embed.h"
#include "webview/webview_interface.h" #include "webview/webview_interface.h"
#include "styles/palette.h"
#include <QtCore/QRegularExpression> #include <QtCore/QRegularExpression>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
#include <QtCore/QFile> #include <QtCore/QFile>
#include <QtGui/QPainter>
namespace Iv { namespace Iv {
@ -47,22 +53,56 @@ void Controller::show(const QString &dataPath, Prepared page) {
_webview = nullptr; _webview = nullptr;
}); });
if (!raw->widget()) { if (!raw->widget()) {
_webview = nullptr; _events.fire(Event::Close);
_window = nullptr;
return; return;
} }
window->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::Close) {
close();
} else if (e->type() == QEvent::KeyPress) {
const auto event = static_cast<QKeyEvent*>(e.get());
if (event->key() == Qt::Key_Escape) {
escape();
}
}
}, window->lifetime());
raw->widget()->show(); raw->widget()->show();
container->geometryValue( container->geometryValue(
) | rpl::start_with_next([=](QRect geometry) { ) | rpl::start_with_next([=](QRect geometry) {
raw->widget()->setGeometry(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) { raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
return true; return true;
}); });
raw->setNavigationDoneHandler([=](bool success) { 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) { raw->setDataRequestHandler([=](Webview::DataRequest request) {
if (!request.id.starts_with("iv/")) { if (!request.id.starts_with("iv/")) {
_dataRequests.fire(std::move(request)); _dataRequests.fire(std::move(request));
@ -104,8 +144,27 @@ void Controller::show(const QString &dataPath, Prepared page) {
window->show(); window->show();
} }
rpl::producer<Webview::DataRequest> Controller::dataRequests() const { bool Controller::active() const {
return _dataRequests.events(); 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() { rpl::lifetime &Controller::lifetime() {

View file

@ -25,16 +25,34 @@ public:
Controller(); Controller();
~Controller(); ~Controller();
void show(const QString &dataPath, Prepared page); enum class Event {
Close,
Quit,
};
[[nodiscard]] rpl::producer<Webview::DataRequest> dataRequests() const; void show(const QString &dataPath, Prepared page);
[[nodiscard]] bool active() const;
void minimize();
[[nodiscard]] rpl::producer<Webview::DataRequest> dataRequests() const {
return _dataRequests.events();
}
[[nodiscard]] rpl::producer<Event> events() const {
return _events.events();
}
[[nodiscard]] rpl::lifetime &lifetime(); [[nodiscard]] rpl::lifetime &lifetime();
private: private:
void escape();
void close();
void quit();
std::unique_ptr<Ui::RpWindow> _window; std::unique_ptr<Ui::RpWindow> _window;
std::unique_ptr<Webview::Window> _webview; std::unique_ptr<Webview::Window> _webview;
rpl::event_stream<Webview::DataRequest> _dataRequests; rpl::event_stream<Webview::DataRequest> _dataRequests;
rpl::event_stream<Event> _events;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
}; };

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "iv/iv_instance.h" #include "iv/iv_instance.h"
#include "core/file_utilities.h" #include "core/file_utilities.h"
#include "core/shortcuts.h"
#include "data/data_cloud_file.h" #include "data/data_cloud_file.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_file_origin.h" #include "data/data_file_origin.h"
@ -63,6 +64,17 @@ public:
not_null<Data*> data) const; not_null<Data*> data) const;
[[nodiscard]] bool showingFrom(not_null<Main::Session*> session) const; [[nodiscard]] bool showingFrom(not_null<Main::Session*> session) const;
[[nodiscard]] bool activeFor(not_null<Main::Session*> session) const; [[nodiscard]] bool activeFor(not_null<Main::Session*> session) const;
[[nodiscard]] bool active() const;
void minimize();
[[nodiscard]] rpl::producer<Controller::Event> events() const {
return _events.events();
}
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private: private:
struct MapPreview { struct MapPreview {
@ -131,6 +143,8 @@ private:
std::vector<QByteArray> _resources; std::vector<QByteArray> _resources;
int _resource = -1; int _resource = -1;
rpl::event_stream<Controller::Event> _events;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
}; };
@ -358,6 +372,10 @@ void Shown::writeEmbed(QString id, QString hash) {
void Shown::showWindowed(Prepared result) { void Shown::showWindowed(Prepared result) {
_controller = std::make_unique<Controller>(); _controller = std::make_unique<Controller>();
_controller->events(
) | rpl::start_to_stream(_events, _controller->lifetime());
_controller->dataRequests( _controller->dataRequests(
) | rpl::start_with_next([=](Webview::DataRequest request) { ) | rpl::start_with_next([=](Webview::DataRequest request) {
const auto requested = QString::fromStdString(request.id); const auto requested = QString::fromStdString(request.id);
@ -372,6 +390,7 @@ void Shown::showWindowed(Prepared result) {
sendEmbed(id.mid(5).toUtf8(), std::move(request)); sendEmbed(id.mid(5).toUtf8(), std::move(request));
} }
}, _controller->lifetime()); }, _controller->lifetime());
const auto domain = &_session->domain(); const auto domain = &_session->domain();
_controller->show(domain->local().webviewDataPath(), std::move(result)); _controller->show(domain->local().webviewDataPath(), std::move(result));
} }
@ -628,6 +647,16 @@ bool Shown::activeFor(not_null<Main::Session*> session) const {
return showingFrom(session) && _controller; 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;
Instance::~Instance() = default; Instance::~Instance() = default;
@ -641,6 +670,14 @@ void Instance::show(
return; return;
} }
_shown = std::make_unique<Shown>(show, data, local); _shown = std::make_unique<Shown>(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)) { if (!_tracking.contains(session)) {
_tracking.emplace(session); _tracking.emplace(session);
session->lifetime().add([=] { session->lifetime().add([=] {
@ -656,6 +693,22 @@ bool Instance::hasActiveWindow(not_null<Main::Session*> session) const {
return _shown && _shown->activeFor(session); 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() { void Instance::closeAll() {
_shown = nullptr; _shown = nullptr;
} }

View file

@ -30,6 +30,9 @@ public:
[[nodiscard]] bool hasActiveWindow( [[nodiscard]] bool hasActiveWindow(
not_null<Main::Session*> session) const; not_null<Main::Session*> session) const;
bool closeActive();
bool minimizeActive();
void closeAll(); void closeAll();
[[nodiscard]] rpl::lifetime &lifetime(); [[nodiscard]] rpl::lifetime &lifetime();

View file

@ -20,6 +20,12 @@ PRIVATE
iv/iv_prepare.h 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 target_include_directories(td_iv
PUBLIC PUBLIC
${src_loc} ${src_loc}