diff options
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | TODO.org | 12 | ||||
| -rw-r--r-- | assets/javascript/finder.js | 132 | ||||
| -rw-r--r-- | init.lua | 5 | ||||
| -rw-r--r-- | lua/null-browser/extras/hints.lua | 65 | ||||
| -rw-r--r-- | src/events/KeyPressedEvent.hpp | 25 | ||||
| -rw-r--r-- | src/widgets/BrowserApp.cpp | 2 | ||||
| -rw-r--r-- | src/widgets/BrowserWindow.cpp | 12 | ||||
| -rw-r--r-- | src/widgets/BrowserWindow.hpp | 2 |
9 files changed, 251 insertions, 5 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 08bf560..4bf1144 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,5 +103,6 @@ install(TARGETS ${PROJECT} LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}") install(DIRECTORY lua/ DESTINATION lua) +install(DIRECTORY assets/ DESTINATION assets) # file(COPY "${CMAKE_SOURCE_DIR}/lua" DESTINATION "${CMAKE_INSTALL_PREFIX}") @@ -4,13 +4,20 @@ - [X] Create view with html (web.view.set_html()) - [X] Expose JS <-> lua in page (web.view.expose()) - [X] Tests for api -- [ ] Run JS in page (web.view.eval_js()) +- [X] Run JS in page (web.view.run_js()) +- [X] KeyPressed event +- [X] f-key navigation +- [ ] On finder stop, exit find mode - [ ] web.decorations.*.set_size() -- [ ] f-key navigation +- [ ] web.keymap.configure_mode(modename, { passthrough = false }) +- [ ] web.view.reload +- [ ] Load assets directory into build - [ ] Fullscreen - [ ] Zoom in/out - [ ] Use table for all internals api options? - [ ] Generate docs for api +- [ ] Show number of tabs in statusline +- [ ] web.view.reload() ** Bugs - [ ] Managing focus in decorations? @@ -20,6 +27,7 @@ - [ ] web.view apis in `-e` flag from "clients" (non-servers calls) don't work ** Next +- [ ] Granular updates in statusline plugin instead of set_html multiple times - [ ] Permission management (list/allow/deny) lua api - [ ] Tests for window - [ ] More tests for stack diff --git a/assets/javascript/finder.js b/assets/javascript/finder.js new file mode 100644 index 0000000..925a135 --- /dev/null +++ b/assets/javascript/finder.js @@ -0,0 +1,132 @@ +(() => { + /** @typedef {{ key: string; elem: HTMLElement; labelElem: HTMLElement | null }} Match */ + + const addRoot = () => { + const root = document.createElement('div'); + document.body.appendChild(root); + return root; + } + + /** + * @param match {Match} + * @return {Promise<HTMLElement | null>} + */ + const createLabelElem = (match) => new Promise((resolve, reject) => { + const label = Object.assign(document.createElement('div'), { + className: '__nullbrowser_findlabel', + textContent: match.key, + }); + requestAnimationFrame(() => { + const rect = match.elem.getBoundingClientRect(); + if (rect.top < 0 | rect.top > window.innerHeight) return reject(null); + if (rect.left < 0 | rect.left > window.innerWidth) return reject(null); + + resolve(label); + requestAnimationFrame(() => { + Object.assign(label.style, { + position: 'fixed', + zIndex: 999999999, // maybe a few more 9s will do + top: `${rect.top}px`, + left: `${rect.left}px`, + backgroundColor: 'yellow', + border: '1px solid #000', + boxShadow: '0 0 3px #555', + color: '#222', + padding: '1px 2px', + fontSize: '10px', + fontWeight: 'bold', + fontFamily: 'monospace', + }); + }); + }); + }); + + const finder = { + /** @type {string} */ + keys: '', + /** @type {Array<Match>} */ + matches: [], + /** @type {HTMLElement | null} */ + labelsRoot: null, + /** @type {boolean} */ + openInNewView: false, + + stop() { + finder.matches = []; + finder.keys = ''; + [...(finder.labelsRoot?.children ?? [])].forEach(child => { + child.remove(); + }); + }, + + /** + * @param selector {string} + * @param new_view {boolean} + */ + start(selector, new_view) { + finder.stop() + finder.labelsRoot ??= addRoot(); + finder.createMatches(selector); + finder.openInNewView = new_view; + }, + + /** @param char {string} */ + filterOutByKey(char) { + if (!/[0-9]+/.test(char)) return; // NOTE: temporary + + finder.keys = finder.keys + char; + finder.matches = finder.matches.filter(m => { + const isMatch = m.key.startsWith(finder.keys); + if (!isMatch) { + m.labelElem?.remove(); + return false; + } + return true; + }); + if (finder.matches.length <= 1) { + const match = finder.matches[0] + finder.stop(); + if (match) { + if (match.elem?.href) { + if (finder.openInNewView) { + window.open(match.elem?.href) + } else { + location.href = match.elem?.href + } + } else { + match.elem?.click() + } + } + } + }, + + /** @param selector {string} */ + createMatches(selector) { + finder.matches = []; + const elements = document.querySelectorAll(selector); + + const matches = [...elements].map(async (elem, index) => { + /** @type {Match} */ + const match = { + elem, + key: `${index}`.padStart(`${elements.length}`.length, '0'), + labelElem: null, + } + match.labelElem = await createLabelElem(match); + return match + }); + + Promise.allSettled(matches).then(matches => { + matches.forEach(result => { + if (result.status !== 'fulfilled') return; + if (result.value.labelElem) + finder.labelsRoot.appendChild(result.value.labelElem) + finder.matches.push(result.value) + }); + }); + }, + }; + + window._nullbrowser ||= {}; + window._nullbrowser.finder ||= finder; +})(); @@ -62,4 +62,9 @@ require 'null-browser.extras.statusline'.init { decoration = web.decorations.bottom, } +local hints = require 'null-browser.extras.hints' +hints.init() +web.keymap.set('n', 'f', function() hints.start('a[href], button', false) end) +web.keymap.set('n', '<s-f>', function() hints.start('a[href], button', true) end) + print('ending...') diff --git a/lua/null-browser/extras/hints.lua b/lua/null-browser/extras/hints.lua new file mode 100644 index 0000000..f2ee21b --- /dev/null +++ b/lua/null-browser/extras/hints.lua @@ -0,0 +1,65 @@ +local hints = { + config = { + mode = 'f', + }, +} + +local js_setup_code = '' + +function hints.init(on_ready) + hints.load_finder_js(function() + web.keymap.set(hints.config.mode, '<Esc>', function() + hints.stop() + end) + + web.event.add_listener('KeyPressed', { + callback = function(event) + if web.keymap.get_mode() == hints.config.mode then + hints.filter_key(event.key) + end + end, + }) + + if type(on_ready) == 'function' then on_ready() end + end) +end + +function hints.start(selector, new_view) + local open_in_new_view = new_view and 'true' or 'false' + web.view.run_js( + js_setup_code .. + ";_nullbrowser.finder.start('" .. + selector .. + "', " .. + open_in_new_view .. + ")" + ) + web.schedule(function() + -- Trigger f mode after tick to avoid the trigger key (f) to get captured in event + web.keymap.set_mode(hints.config.mode) + end) +end + +function hints.filter_key(key) + web.view.run_js("_nullbrowser.finder.filterOutByKey('" .. key .. "')") +end + +function hints.stop() + web.keymap.set_mode('n') + web.view.run_js('_nullbrowser.finder.stop()') +end + +function hints.load_finder_js(on_ready) + web.uv.fs_open('./assets/javascript/finder.js', 'r', 438, function(err, file) + if err then return end + if not file then return {} end + local stat = assert(web.uv.fs_fstat(file)) + local js = assert(web.uv.fs_read(file, stat.size)) + assert(web.uv.fs_close(file)) + js_setup_code = js + + on_ready() + end) +end + +return hints diff --git a/src/events/KeyPressedEvent.hpp b/src/events/KeyPressedEvent.hpp new file mode 100644 index 0000000..8c7ded1 --- /dev/null +++ b/src/events/KeyPressedEvent.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include <QtCore> +#include <lua.hpp> +#include <qevent.h> + +#include "events/Event.hpp" + +class KeyPressedEvent : public Event { +public: + const QString key; + + KeyPressedEvent(QString key) : key(std::move(key)) { kind = "KeyPressed"; } + + static KeyPressedEvent *from_qkeyevent(QKeyEvent *qevent) { + // TODO: Using with modifiers + auto *event = new KeyPressedEvent(qevent->text()); + return event; + } + + void lua_push(lua_State *state) const override { + Event::lua_push(state); + SET_FIELD("key", string, key.toStdString().c_str()) + } +}; diff --git a/src/widgets/BrowserApp.cpp b/src/widgets/BrowserApp.cpp index d6f274b..c3c2313 100644 --- a/src/widgets/BrowserApp.cpp +++ b/src/widgets/BrowserApp.cpp @@ -84,7 +84,7 @@ bool BrowserApp::eventFilter(QObject *target, QEvent *event) { if (auto *target_widget = dynamic_cast<QWidget *>(target); win->isAncestorOf(target_widget)) { auto *key_event = static_cast<QKeyEvent *>(event); - const bool should_skip = win->on_window_key_event(key_event); + const bool should_skip = win->on_window_key_event(target, key_event); return should_skip; } } diff --git a/src/widgets/BrowserWindow.cpp b/src/widgets/BrowserWindow.cpp index 1971b9c..7852a66 100644 --- a/src/widgets/BrowserWindow.cpp +++ b/src/widgets/BrowserWindow.cpp @@ -4,9 +4,13 @@ #include <QStackedLayout> #include <QVBoxLayout> #include <QtCore> +#include <qcoreevent.h> #include "Configuration.hpp" +#include "WindowActionRouter.hpp" +#include "events/KeyPressedEvent.hpp" #include "keymap/KeymapEvaluator.hpp" +#include "widgets/BrowserApp.hpp" #include "widgets/Decorations.hpp" #include "widgets/WebViewStack.hpp" @@ -52,10 +56,16 @@ BrowserWindow::BrowserWindow(const Configuration &configuration, QWebEngineProfi &BrowserWindow::close_window_requested); } -bool BrowserWindow::on_window_key_event(QKeyEvent *event) { +bool BrowserWindow::on_window_key_event(QObject *target, QKeyEvent *event) { auto &keymap_evaluator = KeymapEvaluator::instance(); const bool should_skip = keymap_evaluator.evaluate(event->modifiers(), (Qt::Key)event->key()); + // TODO: Fix this logic to find the right "target" for event + if (event->type() == QEvent::KeyPress && dynamic_cast<WebView *>(target->parent())) { + auto *lua_event = KeyPressedEvent::from_qkeyevent(event); + WindowActionRouter::instance().dispatch_event(lua_event); + } + return should_skip; } diff --git a/src/widgets/BrowserWindow.hpp b/src/widgets/BrowserWindow.hpp index 2ea7b88..bc6cea1 100644 --- a/src/widgets/BrowserWindow.hpp +++ b/src/widgets/BrowserWindow.hpp @@ -41,7 +41,7 @@ public: void run_javascript(const QString &js_code, WebViewId webview_id); void expose_rpc_function(const QString &name, const RpcFunc &action, WebViewId webview_id); - bool on_window_key_event(QKeyEvent *event); + bool on_window_key_event(QObject *target, QKeyEvent *event); void closeEvent(QCloseEvent * /*event*/) override { emit closed(); }; |
