Jelajahi Sumber

More control of focus on document show, support for autofocus and focus previous, see #40.

Michael Ragazzon 6 tahun lalu
induk
melakukan
4f2e95b7ea

+ 3 - 0
Include/RmlUi/Core/Context.h

@@ -147,6 +147,9 @@ public:
 	/// Sends the document to the back of the document stack.
 	/// @param[in] document The document to push to the bottom of the stack.
 	void PushDocumentToBack(ElementDocument* document);
+	/// Remove the document from the focus history and focus the previous document.
+	/// @param[in] document The document to unfocus.
+	void UnfocusDocument(ElementDocument* document);
 
 	/// Adds an event listener to the context's root element.
 	/// @param[in] event The name of the event to attach to.

+ 17 - 16
Include/RmlUi/Core/ElementDocument.h

@@ -47,6 +47,19 @@ class DocumentHeader;
 class ElementText;
 class StyleSheet;
 
+/**
+	Flags used for displaying the document.
+	   None: No focus.
+	   Focus: Focus the first tab element with the 'autofocus' attribute or else the document.
+	   Modal: Focus the first tab element with the 'autofocus' attribute or else the document, other documents cannot receive focus.
+	   FocusPrevious: Focus the previously focused element in the document.
+	   ModalPrevious: Focus the previously focused element in the document, other documents cannot receive focus.
+	   FocusDocument: Focus the document.
+	   ModalDocument: Focus the document, other documents cannot receive focus.
+ */
+enum class FocusFlag { None, Focus, Modal, FocusPrevious, ModalPrevious, FocusDocument, ModalDocument };
+
+
 /**
 	Represents a document in the dom tree.
 
@@ -83,19 +96,9 @@ public:
 	/// Sends the document to the back of the document stack.
 	void PushToBack();
 
-	/**
-		Flags used for displaying the document.
-	 */
-	enum FocusFlags
-	{
-		NONE = 0,
-		FOCUS = (1 << 1),
-		MODAL = (1 << 2)
-	};
-
 	/// Show the document.
-	/// @param[in] focus_flags Flags controlling the changing of focus. Leave as FOCUS to switch focus to the document.
-	void Show(int focus_flags = FOCUS);
+	/// @param[in] focus_flag  Flags controlling the focus, see the 'FocusFlag' description for details.
+	void Show(FocusFlag focus_flag = FocusFlag::Focus);
 	/// Hide the document.
 	void Hide();
 	/// Close the document.
@@ -135,13 +138,12 @@ protected:
 
 private:
 	/// Find the next element to focus, starting at the current element
-	bool FocusNextTabElement(Element* current_element, bool forward);
+	Element* FindNextTabElement(Element* current_element, bool forward);
 	/// Searches forwards or backwards for a focusable element in the given substree
-	bool SearchFocusSubtree(Element* element, bool forward);
+	Element* SearchFocusSubtree(Element* element, bool forward);
 
 	/// Sets the dirty flag on the layout so the document will format its children before the next render.
 	void DirtyLayout() override;
-
 	/// Returns true if the document has been marked as needing a re-layout.
 	bool IsLayoutDirty() override;
 
@@ -153,7 +155,6 @@ private:
 
 	/// Updates the position of the document based on the style properties.
 	void UpdatePosition();
-
 	/// Sets the dirty flag for document positioning
 	void DirtyPosition();
 

+ 14 - 7
Samples/basic/demo/data/demo.rml

@@ -5,10 +5,14 @@
 <style>
 body.window
 {
-	max-width: 2000px;
-	max-height: 2000px;
-	width: 1600px;
-	height: 750px;
+	max-width: 1000px;
+	max-height: 8000px;
+	width: 1000px;
+	height: 800px;
+	opacity: 0.8;
+}
+body.window:focus {
+	opacity: 1.0;
 }
 div#title_bar div#icon
 {
@@ -70,6 +74,9 @@ panel
     height: 100%;
 }
 
+button:focus {
+	image-color: #0ff;
+}
 #decorators button.big
 {
 	width: 250px;
@@ -124,7 +131,7 @@ panel
 	</div>
 	<div>	
 		<button class="small">Image</button>
-		<button class="small ninepatch">Ninepatch</button>
+		<button class="small ninepatch" autofocus>Ninepatch</button>
 	</div>
 	<div>	
 		<button class="tiny"></button>
@@ -134,14 +141,14 @@ panel
 <tab>Buttons</tab>
 <panel>
 	<button id="start_game">Start Game</button><br />
-	<button id="high_scores" onkeydown="hello">High Scores</button><br />
+	<button id="high_scores" onkeydown="hello" autofocus>High Scores</button><br />
 	<button id="options">Options</button><br />
 	<button id="help">Help</button><br />
 	<div><button id="exit" onclick="exit">Exit</button></div>
 </panel>
 <tab>Controls</tab>
 <panel>
-	<div>Type something here: <input style="vertical-align: -7px;" type="text" value="Sample text"/></div>
+	<div>Type something here: <input style="vertical-align: -7px;" type="text" value="Sample text" autofocus/></div>
 </panel>
 </tabset>
 

+ 50 - 7
Samples/basic/demo/src/main.cpp

@@ -39,6 +39,9 @@
 bool run_loop = true;
 bool single_loop = false;
 
+class DemoWindow;
+DemoWindow* window = nullptr;
+DemoWindow* window2 = nullptr;
 
 class DemoWindow : public Rml::Core::EventListener
 {
@@ -54,20 +57,19 @@ public:
 				document->SetProperty(PropertyId::Left, Property(position.x, Property::PX));
 				document->SetProperty(PropertyId::Top, Property(position.y, Property::PX));
 			}
-
+			
 			document->Show();
 		}
 	}
 
-	~DemoWindow()
-	{
+	void Shutdown() {
 		if (document)
 		{
 			document->Close();
+			document = nullptr;
 		}
 	}
 
-
 	void ProcessEvent(Rml::Core::Event& event) override
 	{
 		using namespace Rml::Core;
@@ -95,6 +97,42 @@ public:
 			{
 				Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
 			}
+			else if (key_identifier == Rml::Core::Input::KI_H)
+			{
+				window2->GetDocument()->Hide();
+			}
+			else if (key_identifier == Rml::Core::Input::KI_Q)
+			{
+				window2->GetDocument()->Show(Rml::Core::FocusFlag::None);
+			}
+			else if (key_identifier == Rml::Core::Input::KI_W)
+			{
+				window2->GetDocument()->Show(Rml::Core::FocusFlag::Focus);
+			}
+			else if (key_identifier == Rml::Core::Input::KI_E)
+			{
+				window2->GetDocument()->Show(Rml::Core::FocusFlag::Modal);
+			}
+			else if (key_identifier == Rml::Core::Input::KI_R)
+			{
+				window2->GetDocument()->Show(Rml::Core::FocusFlag::FocusPrevious);
+			}
+			else if (key_identifier == Rml::Core::Input::KI_T)
+			{
+				window2->GetDocument()->Show(Rml::Core::FocusFlag::ModalPrevious);
+			}
+			else if (key_identifier == Rml::Core::Input::KI_Y)
+			{
+				window2->GetDocument()->Show(Rml::Core::FocusFlag::FocusDocument);
+			}
+			else if (key_identifier == Rml::Core::Input::KI_U)
+			{
+				window2->GetDocument()->Show(Rml::Core::FocusFlag::ModalDocument);
+			}
+			else if (key_identifier == Rml::Core::Input::KI_G)
+			{
+				window2->GetDocument()->GetContext()->UnfocusDocument(window2->GetDocument());
+			}
 		}
 		break;
 
@@ -114,7 +152,6 @@ private:
 
 Rml::Core::Context* context = nullptr;
 ShellRenderInterfaceExtensions *shell_renderer;
-DemoWindow* window = nullptr;
 
 
 void GameLoop()
@@ -205,7 +242,7 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 	RMLUI_UNUSED(argv);
 #endif
 
-	const int width = 1800;
+	const int width = 2200;
 	const int height = 1000;
 
 
@@ -255,10 +292,13 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 	window->GetDocument()->AddEventListener(Rml::Core::EventId::Keyup, window);
 	window->GetDocument()->AddEventListener(Rml::Core::EventId::Animationend, window);
 
+	window2 = new DemoWindow("Demo sample 2", Rml::Core::Vector2f(1150, 100), context);
+	window2->GetDocument()->AddEventListener(Rml::Core::EventId::Keydown, window2);
 
 	Shell::EventLoop(GameLoop);
 
-	delete window;
+	window->Shutdown();
+	window2->Shutdown();
 
 	// Shutdown RmlUi.
 	Rml::Core::Shutdown();
@@ -266,5 +306,8 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 	Shell::CloseWindow();
 	Shell::Shutdown();
 
+	delete window;
+	delete window2;
+
 	return 0;
 }

+ 10 - 0
Source/Core/Context.cpp

@@ -448,6 +448,16 @@ void Context::PushDocumentToBack(ElementDocument* document)
 	}
 }
 
+void Context::UnfocusDocument(ElementDocument* document)
+{
+	auto it = std::find(document_focus_history.begin(), document_focus_history.end(), document);
+	if (it != document_focus_history.end())
+		document_focus_history.erase(it);
+
+	if (!document_focus_history.empty())
+		document_focus_history.back()->GetFocusLeafNode()->Focus();
+}
+
 // Adds an event listener to the root element.
 void Context::AddEventListener(const String& event, EventListener* listener, bool in_capture_phase)
 {

+ 89 - 26
Source/Core/ElementDocument.cpp

@@ -204,10 +204,43 @@ void ElementDocument::PushToBack()
 		context->PushDocumentToBack(this);
 }
 
-void ElementDocument::Show(int focus_flags)
+void ElementDocument::Show(FocusFlag focus_flag)
 {
-	// Store the modal attribute
-	modal = (focus_flags & MODAL) > 0;
+	modal = false;
+	bool autofocus = false;
+	bool focus = false;
+	bool focus_previous = false;
+
+	switch (focus_flag)
+	{
+	case FocusFlag::None:
+		break;
+	case FocusFlag::Focus:
+		focus = true;
+		autofocus = true;
+		break;
+	case FocusFlag::Modal:
+		focus = true;
+		autofocus = true;
+		modal = true;
+		break;
+	case FocusFlag::FocusPrevious:
+		focus = true;
+		focus_previous = true;
+		break;
+	case FocusFlag::ModalPrevious:
+		focus = true;
+		focus_previous = true;
+		modal = true;
+		break;
+	case FocusFlag::FocusDocument:
+		focus = true;
+		break;
+	case FocusFlag::ModalDocument:
+		focus = true;
+		modal = true;
+		break;
+	}
 
 	// Set to visible and switch focus if necessary
 	SetProperty(PropertyId::Visibility, Property(Style::Visibility::Visible));
@@ -216,10 +249,38 @@ void ElementDocument::Show(int focus_flags)
 	// If this turns out to be slow, the more performant approach is just to compute the new visibility property
 	UpdateDocument();
 
-	if (focus_flags & FOCUS || focus_flags & MODAL)
+	if (focus)
 	{
-		// Focus the window when shown
-		Focus();
+		Element* focus_element = this;
+
+		if (autofocus)
+		{
+			Element* first_element = nullptr;
+			Element* element = FindNextTabElement(this, true);
+
+			while (element && element != first_element)
+			{
+				if (!first_element)
+					first_element = element;
+
+				if (element->HasAttribute("autofocus"))
+				{
+					focus_element = element;
+					break;
+				}
+
+				element = FindNextTabElement(element, true);
+			}
+		}
+		else if (focus_previous)
+		{
+			focus_element = GetFocusLeafNode();
+		}
+
+		// Focus the window or element
+		bool focused = focus_element->Focus();
+		if (focused && focus_element != this)
+			focus_element->ScrollIntoView(false);
 	}
 
 	DispatchEvent(EventId::Show, Dictionary());
@@ -229,16 +290,14 @@ void ElementDocument::Hide()
 {
 	SetProperty(PropertyId::Visibility, Property(Style::Visibility::Hidden));
 
-	// We should update the document now, so that the focusing below will get the correct visibility
+	// We should update the document now, so that the (un)focusing will get the correct visibility
 	UpdateDocument();
 
 	DispatchEvent(EventId::Hide, Dictionary());
 	
 	if (context)
 	{
-		auto focus = context->GetFocusElement();
-		if (focus && focus != this && focus->GetOwnerDocument() == this)
-			Focus();
+		context->UnfocusDocument(this);
 	}
 }
 
@@ -410,7 +469,11 @@ void ElementDocument::ProcessDefaultAction(Event& event)
 		// Process TAB
 		if (key_identifier == Input::KI_TAB)
 		{
-			FocusNextTabElement(event.GetTargetElement(), !event.GetParameter<bool>("shift_key", false));
+			if (Element* element = FindNextTabElement(event.GetTargetElement(), !event.GetParameter<bool>("shift_key", false)))
+			{
+				element->Focus();
+				element->ScrollIntoView(false);
+			}
 		}
 		// Process ENTER being pressed on a focusable object (emulate click)
 		else if (key_identifier == Input::KI_RETURN ||
@@ -431,20 +494,21 @@ void ElementDocument::OnResize()
 	DirtyPosition();
 }
 
+
 // Find the next element to focus, starting at the current element
 //
 // This algorithm is quite sneaky, I originally thought a depth first search would
 // work, but it appears not. What is required is to cut the tree in half along the nodes
 // from current_element up the root and then either traverse the tree in a clockwise or
 // anticlock wise direction depending if you're searching forward or backward respectively
-bool ElementDocument::FocusNextTabElement(Element* current_element, bool forward)
+Element* ElementDocument::FindNextTabElement(Element* current_element, bool forward)
 {
 	// If we're searching forward, check the immediate children of this node first off
 	if (forward)
 	{
 		for (int i = 0; i < current_element->GetNumChildren(); i++)
-			if (SearchFocusSubtree(current_element->GetChild(i), forward))
-				return true;
+			if (Element* result = SearchFocusSubtree(current_element->GetChild(i), forward))
+				return result;
 	}
 
 	// Now walk up the tree, testing either the bottom or top
@@ -468,8 +532,9 @@ bool ElementDocument::FocusNextTabElement(Element* current_element, bool forward
 			Element* search_child = parent->GetChild(child_index);
 
 			// Do a search if its enabled
-			if (search_enabled && SearchFocusSubtree(search_child, forward))
-				return true;
+			if (search_enabled)
+				if(Element* result = SearchFocusSubtree(search_child, forward))
+					return result;
 
 			// If we find the child, enable searching
 			if (search_child == child)
@@ -487,27 +552,25 @@ bool ElementDocument::FocusNextTabElement(Element* current_element, bool forward
 			search_enabled = false;
 	}
 
-	return false;
+	return nullptr;
 }
 
-bool ElementDocument::SearchFocusSubtree(Element* element, bool forward)
+Element* ElementDocument::SearchFocusSubtree(Element* element, bool forward)
 {
 	// Skip disabled elements
 	if (element->IsPseudoClassSet("disabled"))
 	{
-		return false;
+		return nullptr;
 	}
 	if (!element->IsVisible())
 	{
-		return false;
+		return nullptr;
 	}
 
 	// Check if this is the node we're looking for
 	if (element->GetComputedValues().tab_index == Style::TabIndex::Auto)
 	{
-		element->Focus();
-		element->ScrollIntoView(false);
-		return true;
+		return element;
 	}
 
 	// Check all children
@@ -516,11 +579,11 @@ bool ElementDocument::SearchFocusSubtree(Element* element, bool forward)
 		int child_index = i;
 		if (!forward)
 			child_index = element->GetNumChildren() - i - 1;
-		if (SearchFocusSubtree(element->GetChild(child_index), forward))
-			return true;
+		if (Element * result = SearchFocusSubtree(element->GetChild(child_index), forward))
+			return result;
 	}
 
-	return false;
+	return nullptr;
 }
 
 }

+ 19 - 0
readme.md

@@ -272,6 +272,24 @@ Some relevant changes for users:
 - Chaining transforms and perspectives now provides more expected results. However, as opposed to CSS we don't flatten transforms.
 - Have a look at the updated transforms sample for some fun with 3d boxes.
 
+
+### Focus flags, autofocus
+
+Elements with property `tab-index: auto;` and the `autofocus` attribute set on them can receive focus when showing a document, `ElementDocument::Show(FocusFlag focus_flag)`. This behavior can be controlled with the new document focus flags:
+
+```
+enum class FocusFlag { None, Focus, Modal, FocusPrevious, ModalPrevious, FocusDocument, ModalDocument };
+
+None: No focus.
+Focus: Focus the first tab element with the 'autofocus' attribute or else the document.
+Modal: Focus the first tab element with the 'autofocus' attribute or else the document, other documents cannot receive focus.
+FocusPrevious: Focus the previously focused element in the document.
+ModalPrevious: Focus the previously focused element in the document, other documents cannot receive focus.
+FocusDocument: Focus the document.
+ModalDocument: Focus the document, other documents cannot receive focus.
+```
+
+
 ### CMake options
 
 Two new CMake options added.
@@ -299,6 +317,7 @@ Breaking changes since RmlUi v2.0.
 - Removed 'top' and 'bottom' from z-index property.
 - See changes to the declaration of decorators and font-effects above.
 - See changes to the render interface regarding transforms above.
+- The focus flag in `ElementDocument::Show` has been changed, with a new enum name and new options.
 - Also, see removal of manual reference counting above.