diff --git a/docs/articles/ui_html_compatibility.md b/docs/articles/ui_html_compatibility.md index 2f6d0d27b..2e519b93a 100644 --- a/docs/articles/ui_html_compatibility.md +++ b/docs/articles/ui_html_compatibility.md @@ -1005,8 +1005,36 @@ back into the owning `UIWebView`, including relative URL resolution. This means ordinary links can navigate the current web view. +`NavigationRequest` carries the source node, mouse buttons, and keyboard modifiers. Middle click, +the platform modifier plus left click, or an explicit `Target::NewTab` request emits +`Event::OnLinkOpenRequested` on the WebView. Subscribe to this event, open the resolved URI in a +new view, and call `UIWebView::LinkOpenEvent::accept()` to handle it. Unhandled requests navigate +the current view. Applications add their own link menu actions through `OnCreateContextMenu`; +`ContextMenuEvent::getTarget()` identifies the clicked document node. + +`getTitle()` returns the parsed HTML document title. `onTitleChanged()` reports the title after +loading, including an empty title for pages without one. The `ui_html` example uses it for tab labels +and falls back to the host or URL. Tabs cap their width at 200dp and truncate long titles with ellipsis. +The example enables `UITabWidgetSplitter::setShowTabBarWhenSplit(true)` to keep each pane's tab bar +visible while split. This option is disabled by default for other splitter users. + +`` (including `rel="shortcut icon"`) loads the declared favicon through +the document's shared image cache. Relative URLs resolve against the document URL, and SVG icons +are supported. `Event::OnFaviconChanged` carries a `UIWebView::FaviconEvent` with the loaded texture; +an empty icon clears the previous document's favicon. Responses from an older navigation are ignored. +The `ui_html` example displays the texture as a 16dp tab icon. + `UIWebView` maintains its own navigation history. +`UIMarkdownView` uses the same shortcuts through its enclosing `UIScrollView`; disable them on +that scroll view with `setEnableDefaultKeybindings(false)`. + +Default scrolling shortcuts are Space and Page Down (one viewport down), Shift+Space and Page Up +(one viewport up). Keys handled by a focused descendant do not reach the WebView. While text input is active, +Space scrolls only if its committed text reaches the WebView unhandled; custom controls can consume +it through `onTextInput()` like built-in editors. Call `setEnableDefaultKeybindings(false)` to disable scrolling +shortcuts, including the inherited arrow and Home/End bindings. + Anchor interaction also cooperates with document text selection so a completed text drag does not accidentally activate the link. @@ -1074,6 +1102,8 @@ other text-like types -> UIHTMLTextInput Forms collect named control values and can submit navigation requests. +An omitted or empty form `action` submits to the full current document URL, preserving its query. + Supported submission encodings include: ```text diff --git a/docs/articles/ui_inspector.md b/docs/articles/ui_inspector.md index e605f20b1..7ba0fd2ca 100644 --- a/docs/articles/ui_inspector.md +++ b/docs/articles/ui_inspector.md @@ -54,6 +54,7 @@ python3 projects/scripts/eepp-inspect.py focus --scene scene:2 python3 projects/scripts/eepp-inspect.py screenshot --scene scene:2 --format png python3 projects/scripts/eepp-inspect.py screenshot --window win:2 --rect 10 20 400 300 --format webp python3 projects/scripts/eepp-inspect.py click --scene scene:2 '#submit' +python3 projects/scripts/eepp-inspect.py click --button right --scene scene:2 '#content' python3 projects/scripts/eepp-inspect.py key --scene scene:2 Enter python3 projects/scripts/eepp-inspect.py type --scene scene:2 'hello world' python3 projects/scripts/eepp-inspect.py type --target '#search' 'hello world' @@ -114,7 +115,7 @@ All examples below omit `id` where the surrounding explanation is enough; real r | `ui.inspect` | Exactly one of `handle` or `selector` plus optional `scene`; optional `properties`, `maxStringLength:4096` (maximum 65536) | Handle, scene, typed `properties`, `unavailable` | `invalid-widget`, `target-not-found`, `target-ambiguous` | | `ui.focus` | `scene` default | Scene and focused widget summary or `null` | `invalid-scene` | | `ui.screenshot` | Optional `scene` or `window` (mutually exclusive); optional `rect:[x,y,width,height]`; `format:"png"` | Temporary file path, format, window/scene handles, captured rectangle and size | `invalid-window`, `invalid-scene`, `invalid-params`, `screenshot-failed` | -| `input.click` | Exactly one target; optional `button:"left"`, `count:1` (maximum 2) | Target handle, scene, click point | `target-not-interactable`, `permission-denied` | +| `input.click` | Exactly one target; optional `button:"left"`, `"middle"`, or `"right"`, `count:1` (maximum 2) | Target handle, scene, click point | `target-not-interactable`, `permission-denied` | | `input.key` | `scene` default, `key` required, `action:"press"`, `modifiers:[]` | Key and action | `invalid-params`, `permission-denied` | | `input.text` | `scene` default, `text` required (maximum 64 KiB UTF-8) | Character count | `invalid-params`, `permission-denied` | | `watch.subscribe` | One handle or selector; explicit `properties` array; `initial:true` | Subscription and bound target values | `target-not-found`, `property-unknown` | @@ -159,7 +160,20 @@ These compact request/response pairs show each method's envelope. Handle numbers `ui.screenshot` captures the native window containing the selected scene. With no target it captures the default scene's full native window. An explicit `window` captures that whole window; an explicit `scene` crops to its visible world bounds, including a WebView document viewport. `rect` overrides that crop and uses top-left native-window pixel coordinates. It must fit entirely inside the window and have positive width and height. Supported formats are `png`, `jpg`, `bmp`, `tga`, `qoi`, and `webp`; eepp's case-insensitive image extension parser also accepts `jpeg` and `jfif` as aliases for `jpg`. The response uses the canonical extension. The server renders the selected window after the current UI update, saves the image in the OS temporary directory, and returns its absolute `path`; it does not send image bytes over NDJSON. The caller is responsible for deleting the file when finished. The path is useful to a local agent; remote clients must separately retrieve the file, for example over SSH. Screenshot observation is available in read-only mode and can be used inside `session.batch`. -Single-widget methods never pick the first of multiple selector matches: zero matches return `target-not-found`, and multiple matches return `target-ambiguous`. `input.click` uses the window input route and scene hit testing; clipped, covered, invisible, disabled, or off-window targets can return `target-not-interactable`. It does not scroll a target into view. `input.key` accepts names understood by eepp's `Input::getKeyFromName()` plus `Enter` as an alias for `Return`, and modifiers `Ctrl`, `Shift`, `Alt`, `Meta`; action may be `press`, `down`, or `up`. `input.text` sends Unicode text input to the current focus path and does not focus a widget first. Read-only mode omits input capabilities and returns `permission-denied` for these methods. +Single-widget methods never pick the first of multiple selector matches: zero matches return `target-not-found`, and multiple matches return `target-ambiguous`. `input.click` uses the window input route and scene hit testing; clipped, covered, invisible, disabled, or off-window targets can return `target-not-interactable`. It does not scroll a target into view. The default button is `left`; set `button` to `middle` for a middle click or `right` to open a context menu. Other button values return `invalid-params`. `input.key` accepts names understood by eepp's `Input::getKeyFromName()` plus `Enter` as an alias for `Return`, and modifiers `Ctrl`, `Shift`, `Alt`, `Meta`; action may be `press`, `down`, or `up`. `input.text` sends Unicode text input to the current focus path and does not focus a widget first. Read-only mode omits input capabilities and returns `permission-denied` for these methods. + +### Checking context menus + +Right-click a visible target, then query the popup and its items. For a WebView, the HTML target belongs to its document scene, while the popup usually belongs to the parent application scene: + +```sh +python3 projects/scripts/eepp-inspect.py contexts +python3 projects/scripts/eepp-inspect.py click --button right --scene scene:2 '#context-target' +python3 projects/scripts/eepp-inspect.py query --scene scene:1 'popupmenu' +python3 projects/scripts/eepp-inspect.py tree --scene scene:1 --depth 5 +``` + +`input.click` aims at the target's center. Choose a visible child if a large HTML element extends beyond the window; a body whose center is off-screen will return `target-not-interactable`. The command reports the chosen point in window pixels. The same button option works in a raw or batched `input.click` request: `{"method":"input.click","params":{"scene":"scene:2","selector":"#context-target","button":"right"}}`. ## Properties diff --git a/include/eepp/network/cookiemanager.hpp b/include/eepp/network/cookiemanager.hpp index 4afc7d95c..b9149eba4 100644 --- a/include/eepp/network/cookiemanager.hpp +++ b/include/eepp/network/cookiemanager.hpp @@ -25,6 +25,14 @@ class EE_API CookieManager { /** Remove all stored cookies. */ void clear(); + /** Remove cookies stored for exactly this domain. */ + void clearDomain( const std::string& domain ); + + /** Load or save this jar to a versioned file. Loading replaces the current contents. */ + bool loadFromFile( const std::string& path ); + + bool saveToFile( const std::string& path ) const; + /** @return The number of cookie entries across all domains. */ size_t size() const; diff --git a/include/eepp/scene/event.hpp b/include/eepp/scene/event.hpp index 5107673ad..2728c7dd1 100644 --- a/include/eepp/scene/event.hpp +++ b/include/eepp/scene/event.hpp @@ -137,6 +137,8 @@ class EE_API Event { OnHideFindReplace, OnTooltipCreated, MouseWheel, + OnLinkOpenRequested, + OnFaviconChanged, NoEvent = eeINDEX_NOT_FOUND }; diff --git a/include/eepp/ui/tools/uitabwidgetsplitter.hpp b/include/eepp/ui/tools/uitabwidgetsplitter.hpp index a2647f2ea..4569766a4 100644 --- a/include/eepp/ui/tools/uitabwidgetsplitter.hpp +++ b/include/eepp/ui/tools/uitabwidgetsplitter.hpp @@ -120,6 +120,12 @@ class EE_API UITabWidgetSplitter { void setHideTabBarOnSingleTab( bool hideTabBarOnSingleTab ); + /** Whether splitting keeps single-tab bars visible. Disabled by default. + * Explicitly hidden tab bars (setHideTabBar) remain hidden. */ + bool getShowTabBarWhenSplit() const; + + void setShowTabBarWhenSplit( bool showTabBarWhenSplit ); + void setHideTabBar( bool hideTabBar ); bool getVisualSplitting() const; @@ -255,6 +261,7 @@ class EE_API UITabWidgetSplitter { bool mHideTabBar{ false }; bool mHideTabBarOnSingleTab{ true }; bool mVisualSplitting{ true }; + bool mShowTabBarWhenSplit{ false }; Float mVisualSplitEdgePercent{ 0.1 }; Mutex mTabWidgetMutex; std::function mOnTabWidgetCreateCb; @@ -274,6 +281,8 @@ class EE_API UITabWidgetSplitter { void detachWidgetEvents( UIWidget* widget ); + void updateTabBarVisibility(); + void closeAllTabs( std::vector tabs, UITabWidget::FocusTabBehavior focusTabBehavior ); void updateTabWidgetVisualSplitting(); diff --git a/include/eepp/ui/uipopupmenu.hpp b/include/eepp/ui/uipopupmenu.hpp index 1aa8447c2..eddc4b3d5 100644 --- a/include/eepp/ui/uipopupmenu.hpp +++ b/include/eepp/ui/uipopupmenu.hpp @@ -48,13 +48,17 @@ class EE_API UIPopUpMenu : public UIMenu { class EE_API ContextMenuEvent : public MouseEvent { public: ContextMenuEvent( Node* node, UIPopUpMenu* menu, const Uint32& eventType, const Vector2i& pos, - const Uint32& flags ) : - MouseEvent( node, eventType, pos, flags ), menu( menu ) {} + const Uint32& flags, const Node* target = nullptr ) : + MouseEvent( node, eventType, pos, flags ), menu( menu ), target( target ) {} UIPopUpMenu* getMenu() const { return menu; } + /** The widget under the pointer, when the menu creator provides one. */ + const Node* getTarget() const { return target; } + protected: UIPopUpMenu* menu; + const Node* target; }; }} // namespace EE::UI diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index dceb477c3..152536255 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -44,10 +44,16 @@ class UIIcon; class UIRoot; struct NavigationRequest { + enum class Target : Uint8 { Current, NewTab }; + URI uri; std::string method{ "GET" }; std::string body; std::map extraHeaders; + const Node* source{ nullptr }; + Uint32 mouseButtons{ 0 }; + Uint32 modifiers{ 0 }; + Target target{ Target::Current }; }; class EE_API UISceneNode : public SceneNode { @@ -931,9 +937,14 @@ class EE_API UISceneNode : public SceneNode { /** @return The document referer */ URI getReferer() const { return mReferer; }; - const Network::CookieManager& getCookieManager() const { return mCookieManager; } + const CookieManager& getCookieManager() const { return *mCookieManager; } - Network::CookieManager& getCookieManager() { return mCookieManager; } + CookieManager& getCookieManager() { return *mCookieManager; } + + /** Share one cookie jar across document scenes. Passing nullptr creates a fresh private jar. */ + void setCookieManager( std::shared_ptr manager ) { + mCookieManager = manager ? std::move( manager ) : std::make_shared(); + } const WebResourceCachePtr& getWebResourceCache() const { return mWebResourceCache; } @@ -1057,7 +1068,7 @@ class EE_API UISceneNode : public SceneNode { URI mURI; URI mReferer; std::function mNavigationInterceptorCb; - Network::CookieManager mCookieManager; + std::shared_ptr mCookieManager{ std::make_shared() }; /** * @brief Protected constructor. diff --git a/include/eepp/ui/uiscrollview.hpp b/include/eepp/ui/uiscrollview.hpp index 9e207cd28..aaeef1a37 100644 --- a/include/eepp/ui/uiscrollview.hpp +++ b/include/eepp/ui/uiscrollview.hpp @@ -52,6 +52,7 @@ class EE_API UIScrollView : public UITouchDraggableWidget { void setAnchorScroll( bool anchor ); + /** Enable default scrolling shortcuts. Enabled by default. */ void setEnableDefaultKeybindings( bool enable ); bool areDefaultKeybindingsEnabled() const { return mDefaultKeybindings; } @@ -92,6 +93,10 @@ class EE_API UIScrollView : public UITouchDraggableWidget { virtual Uint32 onKeyDown( const KeyEvent& event ); + virtual Uint32 onTextInput( const TextInputEvent& event ); + + void scrollByViewport( Float direction ); + void onValueChangeCb( const Event* Event ); virtual void onScrollViewSizeChange( const Event* Event ); diff --git a/include/eepp/ui/uitextinput.hpp b/include/eepp/ui/uitextinput.hpp index dc23f296a..b11243725 100644 --- a/include/eepp/ui/uitextinput.hpp +++ b/include/eepp/ui/uitextinput.hpp @@ -201,6 +201,8 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { virtual void onFontStyleChanged(); + virtual void onFontColorChanged(); + void onThemeLoaded(); virtual void onCursorPosChange(); diff --git a/include/eepp/ui/uitextview.hpp b/include/eepp/ui/uitextview.hpp index 3012a95ea..62b272107 100644 --- a/include/eepp/ui/uitextview.hpp +++ b/include/eepp/ui/uitextview.hpp @@ -179,6 +179,8 @@ class EE_API UITextView : public UIWidget { virtual void onFontStyleChanged(); + virtual void onFontColorChanged(); + virtual void onAlphaChange(); virtual void onPaddingChange(); diff --git a/include/eepp/ui/uiwebview.hpp b/include/eepp/ui/uiwebview.hpp index a358087b1..7baa57503 100644 --- a/include/eepp/ui/uiwebview.hpp +++ b/include/eepp/ui/uiwebview.hpp @@ -17,6 +17,10 @@ using namespace EE::Network; +namespace EE { namespace Network { +class CookieManager; +}} // namespace EE::Network + namespace EE { namespace UI { class UIHTMLHtml; @@ -26,6 +30,31 @@ class UISceneNode; class EE_API UIWebView : public UIScrollView { public: + /** The icon is empty when the replacement document has no favicon yet. */ + struct FaviconEvent : Scene::Event { + TexturePtr icon; + + FaviconEvent( Node* node, TexturePtr icon = {} ) : + Scene::Event( node, Event::OnFaviconChanged ), icon( std::move( icon ) ) {} + }; + + struct TitleEvent : Scene::Event { + std::string title; + + TitleEvent( Node* node, std::string title ) : + Scene::Event( node, Event::OnTitleChanged ), title( std::move( title ) ) {} + }; + + struct LinkOpenEvent : Scene::Event { + URI uri; + mutable bool handled{ false }; + + LinkOpenEvent( Node* node, URI uri ) : + Scene::Event( node, Event::OnLinkOpenRequested ), uri( std::move( uri ) ) {} + + void accept() const { handled = true; } + }; + struct NavigationEvent : Scene::Event { URI uri; bool success{ false }; @@ -72,6 +101,8 @@ class EE_API UIWebView : public UIScrollView { const URI& getCurrentURI() const; + const std::string& getTitle() const { return mTitle; } + void reload(); UIWidget* getDocumentContainer() const; @@ -82,6 +113,9 @@ class EE_API UIWebView : public UIScrollView { UIWebView* setWebResourceCache( WebResourceCachePtr cache, CachePartitionId partition = 0 ); + /** Share a cookie jar with other WebViews. Set before starting navigation. */ + UIWebView* setCookieManager( std::shared_ptr manager ); + void setStyleSheetDefaultMarker( Uint32 marker ); void setUserAgent( const std::string& userAgent ); @@ -127,6 +161,7 @@ class EE_API UIWebView : public UIScrollView { bool mIsLoading{ false }; Uint64 mNavigationGeneration{ 0 }; std::string mUserAgent; + std::string mTitle; Time mDefaultTimeout{ Seconds( 30 ) }; Time mWebResourceCachePruneElapsed; Uint32 mStyleSheetDefaultMarker{ 0 }; @@ -141,6 +176,7 @@ class EE_API UIWebView : public UIScrollView { virtual void scheduledUpdate( const Time& time ); virtual Uint32 onKeyDown( const KeyEvent& event ); + virtual Uint32 onMessage( const NodeMessage* message ); virtual void onScrollViewSizeChange( const Event* event ); diff --git a/projects/scripts/eepp-inspect.py b/projects/scripts/eepp-inspect.py index 64bdf6e70..1738889aa 100755 --- a/projects/scripts/eepp-inspect.py +++ b/projects/scripts/eepp-inspect.py @@ -110,7 +110,9 @@ def command(args): params["format"] = args.format return "ui.screenshot", params if name == "click": - return "input.click", target(args.target, args.scene) + params = target(args.target, args.scene) + params["button"] = args.button + return "input.click", params if name == "key": params.update(key=args.key, action=args.action, modifiers=args.modifiers) return "input.key", params @@ -168,6 +170,7 @@ def main(): click = sub.add_parser("click") click.add_argument("target") click.add_argument("--scene") + click.add_argument("--button", choices=("left", "middle", "right"), default="left") key = sub.add_parser("key") key.add_argument("key") key.add_argument("--scene") diff --git a/src/eepp/network/cookiemanager.cpp b/src/eepp/network/cookiemanager.cpp index cb285780f..2d3cfe5b1 100644 --- a/src/eepp/network/cookiemanager.cpp +++ b/src/eepp/network/cookiemanager.cpp @@ -1,12 +1,14 @@ #include +#include #include +#include +#include namespace EE { namespace Network { CookieManager::CookieManager() {} -void CookieManager::storeCookies( const std::string& domain, - const Http::Response& response ) { +void CookieManager::storeCookies( const std::string& domain, const Http::Response& response ) { std::string setCookie = response.getField( "set-cookie" ); if ( !setCookie.empty() ) parseSetCookie( domain, setCookie ); @@ -38,6 +40,61 @@ void CookieManager::clear() { mCookies.clear(); } +void CookieManager::clearDomain( const std::string& domain ) { + Lock l( mMutex ); + mCookies.erase( domain ); +} + +bool CookieManager::loadFromFile( const std::string& path ) { + std::ifstream file( path, std::ios::binary ); + if ( !file ) + return false; + std::string version; + if ( !( file >> version ) || version != "eepp-cookies-1" ) + return false; + UnorderedMap> cookies; + std::string domain, name, value; + while ( file >> std::quoted( domain ) ) { + if ( !( file >> std::quoted( name ) >> std::quoted( value ) ) || domain.empty() || + name.empty() ) + return false; + cookies[domain][name] = value; + } + if ( !file.eof() ) + return false; + Lock l( mMutex ); + mCookies = std::move( cookies ); + return true; +} + +bool CookieManager::saveToFile( const std::string& path ) const { + const std::string temporaryPath = path + ".tmp"; + { + std::ofstream file( temporaryPath, std::ios::binary | std::ios::trunc ); + if ( !file ) + return false; + file << "eepp-cookies-1\n"; + { + Lock l( mMutex ); + for ( const auto& domain : mCookies ) { + for ( const auto& cookie : domain.second ) + file << std::quoted( domain.first ) << ' ' << std::quoted( cookie.first ) << ' ' + << std::quoted( cookie.second ) << '\n'; + } + } + file.flush(); + if ( !file ) { + FileSystem::fileRemove( temporaryPath ); + return false; + } + } + if ( !FileSystem::fileMove( temporaryPath, path ) ) { + FileSystem::fileRemove( temporaryPath ); + return false; + } + return true; +} + size_t CookieManager::size() const { Lock l( mMutex ); size_t total = 0; @@ -73,6 +130,7 @@ void CookieManager::parseSetCookie( const std::string& domain, } bool CookieManager::hasCookie( const std::string& domain ) const { + Lock l( mMutex ); return mCookies.find( domain ) != mCookies.end(); } diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index e79b51192..dd6564047 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -1061,6 +1061,7 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr // redirections, send a new request to the redirection location. if ( ( received.getStatus() == Response::MovedPermanently || received.getStatus() == Response::MovedTemporarily || + received.getStatus() == Response::SeeOther || received.getStatus() == Response::PermanentRedirect || received.getStatus() == Response::TemporaryRedirect ) && request.getFollowRedirect() ) { @@ -1094,9 +1095,38 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr newRequest.setProgressCallback( request.getProgressCallback() ); - if ( received.hasField( "set-cookie" ) ) { - newRequest.setField( - "Cookie", received.getField( "set-cookie" ) ); + if ( received.hasField( "set-cookie" ) && + ( uri.getHost().empty() || + uri.getHost() == getHostName() ) ) { + const std::string& setCookie = + received.getField( "set-cookie" ); + std::string_view cookiePair = String::trim( + std::string_view( setCookie ) + .substr( 0, setCookie.find( ';' ) ) ); + const size_t equals = cookiePair.find( '=' ); + if ( equals != std::string::npos && equals != 0 ) { + std::string cookieHeader( cookiePair ); + std::string_view cookieName = + cookiePair.substr( 0, equals ); + const std::string& previous = + request.getField( "cookie" ); + String::readBySeparator( + previous, + [&]( std::string_view part ) { + std::string_view pair = + String::trim( part ); + if ( !pair.empty() && + pair.substr( 0, + pair.find( '=' ) ) != + cookieName ) { + cookieHeader += "; "; + cookieHeader.append( pair.data(), + pair.size() ); + } + }, + ';' ); + newRequest.setField( "Cookie", cookieHeader ); + } } request.mRedirectionCount++; diff --git a/src/eepp/scene/eventdispatcher.cpp b/src/eepp/scene/eventdispatcher.cpp index bfe44057e..4c08f9426 100644 --- a/src/eepp/scene/eventdispatcher.cpp +++ b/src/eepp/scene/eventdispatcher.cpp @@ -186,7 +186,7 @@ void EventDispatcher::update( const Time& time ) { } if ( mInput->getReleaseTrigger() ) { - if ( NULL != mFocusNode ) { + if ( NULL != mFocusNode || ( mInput->getReleaseTrigger() & EE_BUTTON_MMASK ) ) { if ( !nodeWasDragging || mMousePos == mLastMousePos ) { // The focused node can change after the MouseUp ( since the node can call // "setFocus()" on other node And the MouseClick would be received by the new @@ -208,14 +208,21 @@ void EventDispatcher::update( const Time& time ) { } if ( mInput->getClickTrigger() && mDownNode == mOverNode ) { - mLastFocusNode->onMouseClick( mMousePosi, mInput->getClickTrigger() ); - sendMsg( mLastFocusNode, NodeMessage::MouseClick, mInput->getClickTrigger() ); + // Middle click should activate the hovered widget without changing keyboard + // focus. + Node* clickNode = ( mInput->getClickTrigger() & EE_BUTTON_MMASK ) + ? mOverNode + : mLastFocusNode; + if ( clickNode ) { + clickNode->onMouseClick( mMousePosi, mInput->getClickTrigger() ); + sendMsg( clickNode, NodeMessage::MouseClick, mInput->getClickTrigger() ); + } - if ( mInput->getDoubleClickTrigger() && + if ( clickNode && mInput->getDoubleClickTrigger() && mClickPos.distance( mMousePosi ) < 10 ) { - mLastFocusNode->onMouseDoubleClick( mMousePosi, - mInput->getDoubleClickTrigger() ); - sendMsg( mLastFocusNode, NodeMessage::MouseDoubleClick, + clickNode->onMouseDoubleClick( mMousePosi, + mInput->getDoubleClickTrigger() ); + sendMsg( clickNode, NodeMessage::MouseDoubleClick, mInput->getDoubleClickTrigger() ); } diff --git a/src/eepp/ui/tools/uiinspectorserver.cpp b/src/eepp/ui/tools/uiinspectorserver.cpp index 05009ea0c..caf00e3c3 100644 --- a/src/eepp/ui/tools/uiinspectorserver.cpp +++ b/src/eepp/ui/tools/uiinspectorserver.cpp @@ -352,7 +352,7 @@ struct UIInspectorServer::Impl { : "unauthenticated"; const char* message = request.is_discarded() ? "Malformed JSON" - : isConnect ? "Invalid inspector token" + : isConnect ? "Invalid inspector token" : "First request must be session.connect"; client.output += json( { { "id", id }, @@ -1068,8 +1068,14 @@ struct UIInspectorServer::Impl { const std::string windowHandle = inspector.windowHandle( window ); if ( method == "input.click" ) { const std::string targetHandle = inspector.widgetHandle( target ); - if ( params.contains( "button" ) && params["button"] != "left" ) - fail( "invalid-params", "Only left click is supported" ); + if ( params.contains( "button" ) && !params["button"].is_string() ) + fail( "invalid-params", "button must be a string" ); + const std::string button = params.value( "button", "left" ); + if ( button != "left" && button != "middle" && button != "right" ) + fail( "invalid-params", "button must be left, middle, or right" ); + const Uint8 inputButton = button == "right" ? EE_BUTTON_RIGHT + : button == "middle" ? EE_BUTTON_MIDDLE + : EE_BUTTON_LEFT; unsigned count = boundedUnsigned( params, "count", 1, 2 ); auto bounds = target->getWorldBounds(); Vector2i point( static_cast( ( bounds.Left + bounds.Right ) * 0.5f ), @@ -1102,7 +1108,7 @@ struct UIInspectorServer::Impl { InputEvent press( InputEvent::MouseButtonDown ); press.WinID = window->getWindowID(); press.button = {}; - press.button.button = EE_BUTTON_LEFT; + press.button.button = inputButton; press.button.x = point.x; press.button.y = point.y; input->pushEvent( press ); diff --git a/src/eepp/ui/tools/uitabwidgetsplitter.cpp b/src/eepp/ui/tools/uitabwidgetsplitter.cpp index 8015347a6..35f12d1a8 100644 --- a/src/eepp/ui/tools/uitabwidgetsplitter.cpp +++ b/src/eepp/ui/tools/uitabwidgetsplitter.cpp @@ -180,8 +180,11 @@ UITabWidget* UITabWidgetSplitter::createTabWidget( Node* parent ) { } ); if ( mOnTabWidgetCreateCb ) mOnTabWidgetCreateCb( tabWidget ); - Lock l( mTabWidgetMutex ); - mTabWidgets.push_back( tabWidget ); + { + Lock l( mTabWidgetMutex ); + mTabWidgets.push_back( tabWidget ); + } + updateTabBarVisibility(); return tabWidget; } @@ -691,6 +694,7 @@ void UITabWidgetSplitter::onTabClosed( const TabEvent* tabEvent ) { focusSomeWidget( nullptr ); } + updateTabBarVisibility(); eeASSERT( !mTabWidgets.empty() ); eeASSERT( !( mTabWidgets.size() == 1 && mTabWidgets[0]->getTabCount() == 0 ) ); return; @@ -771,12 +775,27 @@ bool UITabWidgetSplitter::getHideTabBarOnSingleTab() const { return mHideTabBarOnSingleTab; } +void UITabWidgetSplitter::updateTabBarVisibility() { + const bool hideSingleTabBar = mHideTabBarOnSingleTab && !( mShowTabBarWhenSplit && hasSplit() ); + for ( auto* widget : mTabWidgets ) + widget->setHideTabBarOnSingleTab( hideSingleTabBar ); +} + void UITabWidgetSplitter::setHideTabBarOnSingleTab( bool hideTabBarOnSingleTab ) { if ( hideTabBarOnSingleTab != mHideTabBarOnSingleTab ) { mHideTabBarOnSingleTab = hideTabBarOnSingleTab; + updateTabBarVisibility(); + } +} - for ( auto widget : mTabWidgets ) - widget->setHideTabBarOnSingleTab( hideTabBarOnSingleTab ); +bool UITabWidgetSplitter::getShowTabBarWhenSplit() const { + return mShowTabBarWhenSplit; +} + +void UITabWidgetSplitter::setShowTabBarWhenSplit( bool showTabBarWhenSplit ) { + if ( mShowTabBarWhenSplit != showTabBarWhenSplit ) { + mShowTabBarWhenSplit = showTabBarWhenSplit; + updateTabBarVisibility(); } } diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 21071aa0b..f8c227328 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -1195,7 +1195,7 @@ Uint32 UICodeEditor::onTextInput( const TextInputEvent& event ) { if ( plugin->onTextInput( this, event ) ) return 1; - return 0; + return 1; } void UICodeEditor::updateIMELocation() { @@ -1920,8 +1920,8 @@ Uint32 UICodeEditor::onMouseUp( const Vector2i& position, const Uint32& flags ) } Uint32 UICodeEditor::onMouseWheel( const Vector2f& offset, bool flipped ) { - const Vector2i position = getEventDispatcher() ? getEventDispatcher()->getMousePos() - : Vector2i::Zero; + const Vector2i position = + getEventDispatcher() ? getEventDispatcher()->getMousePos() : Vector2i::Zero; for ( auto& plugin : mPlugins ) if ( plugin->onMouseWheel( this, position, offset, flipped ) ) return 1; diff --git a/src/eepp/ui/uihtmlform.cpp b/src/eepp/ui/uihtmlform.cpp index b6af9bda9..5b6ca9912 100644 --- a/src/eepp/ui/uihtmlform.cpp +++ b/src/eepp/ui/uihtmlform.cpp @@ -83,7 +83,9 @@ void UIHTMLForm::submit() { UISceneNode* sceneNode = getUISceneNode(); NavigationRequest request; - request.uri = URI( mAction ); + // With no action, HTML submits to the current document URL, not its asset base URI. + request.uri = mAction.empty() ? sceneNode->getReferer() : URI( mAction ); + request.source = this; if ( mEnctype == "multipart/form-data" ) { request.method = "POST"; diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index cea5c3fa6..3d3274f65 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -919,7 +919,7 @@ void UISceneNode::requestWebResource( WebResourceRequest request, return; if ( !mReferer.empty() ) request.headers.emplace( "referer", mReferer.toString() ); - std::string cookie = mCookieManager.getCookieHeader( request.uri.getAuthority() ); + std::string cookie = getCookieManager().getCookieHeader( request.uri.getAuthority() ); if ( !cookie.empty() ) request.headers["Cookie"] = std::move( cookie ); auto resourceState = mAsyncResourceLoadState; @@ -933,7 +933,7 @@ void UISceneNode::requestWebResource( WebResourceRequest request, if ( !scene ) return; if ( !result.setCookie.empty() ) - scene->mCookieManager.storeCookiesFromHeader( authority, result.setCookie ); + scene->getCookieManager().storeCookiesFromHeader( authority, result.setCookie ); if ( callback ) callback( result ); }; @@ -947,7 +947,7 @@ TexturePtr UISceneNode::requestWebTexture( WebResourceRequest request, return {}; if ( !mReferer.empty() ) request.headers.emplace( "referer", mReferer.toString() ); - std::string cookie = mCookieManager.getCookieHeader( request.uri.getAuthority() ); + std::string cookie = getCookieManager().getCookieHeader( request.uri.getAuthority() ); if ( !cookie.empty() ) request.headers["Cookie"] = std::move( cookie ); auto resourceState = mAsyncResourceLoadState; @@ -967,7 +967,7 @@ TexturePtr UISceneNode::requestWebTexture( WebResourceRequest request, if ( !scene ) return; if ( !result.setCookie.empty() ) - scene->mCookieManager.storeCookiesFromHeader( authority, result.setCookie ); + scene->getCookieManager().storeCookiesFromHeader( authority, result.setCookie ); if ( callback ) callback( result ); }; diff --git a/src/eepp/ui/uiscrollview.cpp b/src/eepp/ui/uiscrollview.cpp index d15d34744..bdb4b8270 100644 --- a/src/eepp/ui/uiscrollview.cpp +++ b/src/eepp/ui/uiscrollview.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include #include +#include namespace EE { namespace UI { @@ -488,6 +490,10 @@ void UIScrollView::setScrollControllerPosition( const Vector2f& position ) { mVScroll->setValue( maxPosition.y > 0.f ? position.y / maxPosition.y : 0.f ); } +void UIScrollView::setEnableDefaultKeybindings( bool enable ) { + mDefaultKeybindings = enable; +} + bool UIScrollView::isAutoSetClipStep() const { return mAutoSetClipStep; } @@ -505,7 +511,16 @@ void UIScrollView::setAnchorScroll( bool anchor ) { } Uint32 UIScrollView::onKeyDown( const KeyEvent& event ) { - if ( !mDefaultKeybindings || event.getSanitizedMod() ) + const auto mod = event.getSanitizedMod(); + if ( mDefaultKeybindings && event.getKeyCode() == Window::KEY_SPACE && + ( mod & ~KEYMOD_SHIFT ) == 0 ) { + // Active text input gets priority through onTextInput's normal bubbling path. + if ( getUISceneNode()->getWindow()->isTextInputActive() ) + return UITouchDraggableWidget::onKeyDown( event ); + scrollByViewport( mod & KEYMOD_SHIFT ? -1.f : 1.f ); + return 1; + } + if ( !mDefaultKeybindings || mod ) return UITouchDraggableWidget::onKeyDown( event ); if ( event.getKeyCode() == Window::KEY_UP ) { @@ -515,10 +530,10 @@ Uint32 UIScrollView::onKeyDown( const KeyEvent& event ) { mVScroll->setValue( mVScroll->getValue() + mVScroll->getClickStep() ); return 1; } else if ( event.getKeyCode() == Window::KEY_PAGEDOWN ) { - mVScroll->setValue( mVScroll->getValue() + mVScroll->getPageStep() ); + scrollByViewport( 1.f ); return 1; } else if ( event.getKeyCode() == Window::KEY_PAGEUP ) { - mVScroll->setValue( mVScroll->getValue() - mVScroll->getPageStep() ); + scrollByViewport( -1.f ); return 1; } else if ( event.getKeyCode() == Window::KEY_HOME ) { mVScroll->setValue( mVScroll->getMinValue() ); @@ -531,4 +546,20 @@ Uint32 UIScrollView::onKeyDown( const KeyEvent& event ) { return UITouchDraggableWidget::onKeyDown( event ); } +Uint32 UIScrollView::onTextInput( const TextInputEvent& event ) { + if ( mDefaultKeybindings && event.getChar() == ' ' && event.isValid( getInput() ) ) { + scrollByViewport( getInput()->isShiftPressed() ? -1.f : 1.f ); + return 1; + } + return UITouchDraggableWidget::onTextInput( event ); +} + +void UIScrollView::scrollByViewport( Float direction ) { + const Float range = getScrollControllerMaxPosition().y; + if ( mVScroll->isEnabled() && range > 0.f ) { + mVScroll->setValue( mVScroll->getValue() + + direction * mContainer->getPixelsSize().getHeight() / range ); + } +} + }} // namespace EE::UI diff --git a/src/eepp/ui/uitextinput.cpp b/src/eepp/ui/uitextinput.cpp index 614bb9c41..fc10fca8a 100644 --- a/src/eepp/ui/uitextinput.cpp +++ b/src/eepp/ui/uitextinput.cpp @@ -406,6 +406,11 @@ void UITextInput::onFontStyleChanged() { invalidateDraw(); } +void UITextInput::onFontColorChanged() { + if ( mPassCache ) + mPassCache->setFillColor( mTextCache.getFillColor() ); +} + Text& UITextInput::getVisibleTextCache() { if ( mMode == TextInputMode::Password && mPassCache ) return *mPassCache; diff --git a/src/eepp/ui/uitextselectioncontroller.cpp b/src/eepp/ui/uitextselectioncontroller.cpp index 67550f9d6..4915ecbc6 100644 --- a/src/eepp/ui/uitextselectioncontroller.cpp +++ b/src/eepp/ui/uitextselectioncontroller.cpp @@ -810,7 +810,7 @@ bool UITextSelectionController::showContextMenu( const Vector2i& position, Uint3 } auto* selectAllItem = menu->add( mHost->i18n( "uicodeeditor_select_all", "Select All" ) ); selectAllItem->setId( "select-all" ); - ContextMenuEvent event( mHost, menu, Event::OnCreateContextMenu, position, flags ); + ContextMenuEvent event( mHost, menu, Event::OnCreateContextMenu, position, flags, target ); mHost->sendEvent( &event ); if ( menu->getCount() == 0 ) { menu->close(); diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index 6ddb715e4..d0a81bfdb 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #define PUGIXML_HEADER_ONLY #include @@ -765,8 +766,13 @@ Uint32 UIAnchorSpan::onMessage( const NodeMessage* Msg ) { if ( controller->consumeSuppressedClick() ) return 1; } - if ( !mHref.empty() && ( Msg->getFlags() & EE_BUTTON_LMASK ) ) - getUISceneNode()->openURL( mHref ); + if ( !mHref.empty() && ( Msg->getFlags() & ( EE_BUTTON_LMASK | EE_BUTTON_MMASK ) ) ) { + NavigationRequest request{ URI( mHref ) }; + request.source = this; + request.mouseButtons = Msg->getFlags(); + request.modifiers = getInput() ? getInput()->getModState() : 0; + getUISceneNode()->navigate( request ); + } return 1; } } diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index 408f67a55..d8adbb366 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -306,6 +306,7 @@ UITextView* UITextView::setFontColor( const Color& color ) { mFontStyleConfig.FontColor = color; Color newColor( color.r, color.g, color.b, color.a * mAlpha / 255.f ); mTextCache.setFillColor( newColor ); + onFontColorChanged(); invalidateDraw(); } @@ -527,6 +528,8 @@ void UITextView::onFontStyleChanged() { invalidateDraw(); } +void UITextView::onFontColorChanged() {} + void UITextView::onAlphaChange() { Color color( getFontColor() ); Color newColor( color.r, color.g, color.b, color.a * mAlpha / 255.f ); @@ -649,7 +652,7 @@ void UITextView::drawSelection( Text& textCache ) { mSelRectsCache.clear(); mLastSelCurInit = selCurInit(); mLastSelCurEnd = selCurEnd(); - mSelRectsCache = mTextCache.getSelectionRects( { selCurInit(), selCurEnd() } ); + mSelRectsCache = textCache.getSelectionRects( { selCurInit(), selCurEnd() } ); } if ( !mSelRectsCache.empty() ) { diff --git a/src/eepp/ui/uiwebview.cpp b/src/eepp/ui/uiwebview.cpp index 4e0443650..5401f8f35 100644 --- a/src/eepp/ui/uiwebview.cpp +++ b/src/eepp/ui/uiwebview.cpp @@ -10,6 +10,10 @@ #include #include #include +#include + +#define PUGIXML_HEADER_ONLY +#include namespace EE { namespace UI { @@ -168,6 +172,30 @@ UIWebView::UIWebView() : UIScrollView( "webview" ) { mDocumentLayout->setParent( this ); mDocumentScene = UISceneNode::New(); + std::weak_ptr loadState( mNavigationLoadState ); + mDocumentScene->setNavigationInterceptorCb( [loadState]( const NavigationRequest& request ) { + auto locked = loadState.lock(); + if ( !locked || !locked->alive || locked->owner == nullptr ) + return true; + UIWebView* self = locked->owner; + UISceneNode* docScene = self->getDocumentSceneNode(); + if ( !docScene ) + return true; + URI uri = docScene->solveRelativePath( request.uri ); + const bool requestNewTab = request.target == NavigationRequest::Target::NewTab || + ( request.mouseButtons & EE_BUTTON_MMASK ) || + ( ( request.mouseButtons & EE_BUTTON_LMASK ) && + ( request.modifiers & KeyMod::getDefaultModifier() ) ); + if ( requestNewTab ) { + LinkOpenEvent event( self, uri ); + self->sendEvent( &event ); + if ( event.handled ) + return true; + } + self->loadURI( uri, request.method != "GET", request.method, request.body, + request.extraHeaders ); + return true; + } ); mDocumentScene->setFollowParentSize( false ); mDocumentScene->setVisibleBoundsNode( mContainer ); mDocumentScene->setParent( mDocumentLayout ); @@ -290,6 +318,7 @@ void UIWebView::loadURI( URI uri, bool isHistoryNav, const std::string& method, const std::string& body, const Http::Request::FieldTable& headers ) { Uint64 generation = beginNavigationLoad(); mIsLoading = true; + mTitle.clear(); if ( !isHistoryNav ) pushHistory( uri ); @@ -415,32 +444,66 @@ void UIWebView::loadDocumentData( URI url, std::string data, Uint64 generation ) ui->getStyleSheet().removeAllWithoutMarker( self->mStyleSheetDefaultMarker ); ui->setURIFromURL( url ); + std::string documentXML = Tools::HTMLFormatter::HTMLtoXML( data ); + pugi::xml_document metadataDocument; + if ( metadataDocument.load_string( documentXML.c_str() ) ) { + self->mTitle = String::trim( std::string_view( + metadataDocument.child( "html" ).child( "head" ).child( "title" ).child_value() ) ); + } else { + self->mTitle.clear(); + } auto hash = String::hash( url.toString() ); - ui->loadLayoutFromString( Tools::HTMLFormatter::HTMLtoXML( data ), self->mDocContainer, - hash ); + ui->loadLayoutFromString( documentXML, self->mDocContainer, hash ); self->mTextSelectionController.onDocumentChanged(); - ui->setNavigationInterceptorCb( [loadState]( const NavigationRequest& request ) { - auto locked = loadState.lock(); - if ( !locked || !locked->alive || locked->owner == nullptr ) - return true; - UIWebView* self = locked->owner; - UISceneNode* docScene = self->getDocumentSceneNode(); - if ( !docScene ) - return true; - URI uri = docScene->solveRelativePath( request.uri ); - self->loadURI( uri, request.method != "GET", request.method, request.body, - request.extraHeaders ); - return true; - } ); - if ( !self->isNavigationLoadCurrent( generation ) ) return; self->mIsLoading = false; NavigationEvent ev( self, (Uint32)Event::OnNavigationCompleted, url, true ); self->sendEvent( &ev ); + TitleEvent titleEvent( self, self->mTitle ); + self->sendEvent( &titleEvent ); self->markDocumentExtentDirty( LayoutInvalidation::Document ); self->updateDocumentMetricsIfNeeded(); + + FaviconEvent cleared( self ); + self->sendEvent( &cleared ); + if ( !self->isNavigationLoadCurrent( generation ) ) + return; + for ( auto link : metadataDocument.child( "html" ).child( "head" ).children( "link" ) ) { + std::string_view tokens( link.attribute( "rel" ).value() ); + bool isIcon = false; + while ( !tokens.empty() ) { + const size_t start = tokens.find_first_not_of( " \t\r\n\f" ); + if ( start == std::string_view::npos ) + break; + tokens.remove_prefix( start ); + const size_t end = tokens.find_first_of( " \t\r\n\f" ); + if ( String::iequals( tokens.substr( 0, end ), "icon" ) ) { + isIcon = true; + break; + } + if ( end == std::string_view::npos ) + break; + tokens.remove_prefix( end ); + } + const char* href = link.attribute( "href" ).value(); + if ( !isIcon || !*href ) + continue; + WebResourceRequest request; + request.uri = ui->solveRelativePath( URI( href ) ); + request.kind = WebResourceKind::Image; + request.timeout = self->mDefaultTimeout; + ui->requestWebTexture( std::move( request ), + [loadState, generation]( const WebResourceResult& result ) { + auto* view = resolveNavigationLoad( loadState, generation ); + if ( !view || !result.success || !result.texture ) + return; + FaviconEvent iconEvent( view, result.texture ); + view->sendEvent( &iconEvent ); + } ); + break; + } } ); } @@ -532,6 +595,12 @@ UIWebView* UIWebView::setWebResourceCache( WebResourceCachePtr cache, CacheParti return this; } +UIWebView* UIWebView::setCookieManager( std::shared_ptr manager ) { + if ( mDocumentScene ) + mDocumentScene->setCookieManager( std::move( manager ) ); + return this; +} + bool UIWebView::updateDocumentViewportMetrics() { if ( !mDocumentScene || !mDocumentLayout || !mDocContainer || mUpdatingDocumentViewportMetrics ) return false; @@ -671,7 +740,7 @@ Uint32 UIWebView::onNavigationError( std::function cb ) { return on( Event::OnTitleChanged, - [cb]( const Event* e ) { cb( static_cast( e )->error ); } ); + [cb]( const Event* e ) { cb( static_cast( e )->title ); } ); } }} // namespace EE::UI diff --git a/src/eepp/ui/webresourcecache.cpp b/src/eepp/ui/webresourcecache.cpp index 32018ec31..8486aa8cf 100644 --- a/src/eepp/ui/webresourcecache.cpp +++ b/src/eepp/ui/webresourcecache.cpp @@ -33,10 +33,10 @@ std::string makeRequestKey( CachePartitionId partition, const WebResourceRequest canonicalURI( request.uri ).c_str() ); for ( const auto& header : request.headers ) { std::string name = String::toLower( header.first ); - // The partition identifies the cookie/authentication context. Cookie values evolve while a - // document is being used, and Referer changes with navigation history; including either one - // would duplicate an otherwise reusable resource on Back/Forward navigation. - if ( name == "cookie" || name == "referer" ) + // Documents can change with authentication, while static resources remain reusable as + // cookies evolve. Referer changes with navigation history but does not identify content. + if ( ( name == "cookie" && request.kind != WebResourceKind::Document ) || + name == "referer" ) continue; key += name; key += ':'; diff --git a/src/examples/ui_html/ui_html.cpp b/src/examples/ui_html/ui_html.cpp index 859e1937b..ad6b2dcea 100644 --- a/src/examples/ui_html/ui_html.cpp +++ b/src/examples/ui_html/ui_html.cpp @@ -1,9 +1,38 @@ #include +#include #include #include #include +using namespace EE::UI::Tools; + +struct BrowserTabs : UITabWidgetSplitter::Client { + UITabWidgetSplitter* splitter{ nullptr }; + UITextInput* urlBar{ nullptr }; + UIPushButton* backBtn{ nullptr }; + UIPushButton* fwdBtn{ nullptr }; + + UIWebView* current() const { + return splitter && splitter->getCurWidget() ? splitter->getCurWidget()->asType() + : nullptr; + } + + void updateNavigation() const { + UIWebView* view = current(); + backBtn->setEnabled( view && view->canGoBack() ); + fwdBtn->setEnabled( view && view->canGoForward() ); + } + + void onTabCreated( UITab*, UIWidget* ) override {} + + void onWidgetFocusChange( UIWidget* widget ) override { + auto* view = widget->asType(); + urlBar->setText( view->getCurrentURI().toString() ); + updateNavigation(); + } +}; + EE_MAIN_FUNC int main( int argc, char** argv ) { std::shared_ptr threadPool( ThreadPool::createShared( eemax( 4, Sys::getCPUCount() ) ) ); @@ -103,6 +132,8 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { auto vbox = ui->loadLayoutFromString( R"xml(