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(