Browse Source

Merged boolean component flags into a combined state system with shared update notifications.

David Piuva 2 years ago
parent
commit
65bfdb267f

+ 62 - 29
Source/DFPSR/gui/VisualComponent.cpp

@@ -39,6 +39,40 @@ VisualComponent::~VisualComponent() {
 	}
 }
 
+void VisualComponent::declareAttributes(StructureDefinition &target) const {
+	target.declareAttribute(U"Name");
+	target.declareAttribute(U"Index");
+	target.declareAttribute(U"Visible");
+	target.declareAttribute(U"Left");
+	target.declareAttribute(U"Top");
+	target.declareAttribute(U"Right");
+	target.declareAttribute(U"Bottom");
+}
+
+Persistent* VisualComponent::findAttribute(const ReadableString &name) {
+	if (string_caseInsensitiveMatch(name, U"Name")) {
+		return &(this->name);
+	} else if (string_caseInsensitiveMatch(name, U"Index")) {
+		return &(this->index);
+	} else if (string_caseInsensitiveMatch(name, U"Visible")) {
+		return &(this->visible);
+	} else if (string_caseInsensitiveMatch(name, U"Left")) {
+		this->regionAccessed = true;
+		return &(this->region.left);
+	} else if (string_caseInsensitiveMatch(name, U"Top")) {
+		this->regionAccessed = true;
+		return &(this->region.top);
+	} else if (string_caseInsensitiveMatch(name, U"Right")) {
+		this->regionAccessed = true;
+		return &(this->region.right);
+	} else if (string_caseInsensitiveMatch(name, U"Bottom")) {
+		this->regionAccessed = true;
+		return &(this->region.bottom);
+	} else {
+		return nullptr;
+	}
+}
+
 IVector2D VisualComponent::getDesiredDimensions() {
 	// Unless this virtual method is overridden, toolbars and such will try to give these dimensions to the component.
 	return IVector2D(32, 32);
@@ -126,9 +160,14 @@ void VisualComponent::updateChildLocations() {
 
 // Overlays are only cropped by the entire canvas, so the offset is the upper left corner of component relative to the upper left corner of the canvas.
 static void drawOverlays(ImageRgbaU8& targetImage, VisualComponent &component, const IVector2D& offset) {
+	// Invisible components are not allowed to display overlays, because the component system is
+	//   responsible for visibility settings that specific components are likely to forget about.
 	if (component.getVisible()) {
-		// Draw the component's own overlays at the bottom. 
-		component.drawOverlay(targetImage, offset - component.location.upperLeft());
+		// Check if the component has the overlay shown.
+		if (component.showingOverlay()) {
+			// Draw the component's own overlay below child overlays. 
+			component.drawOverlay(targetImage, offset - component.location.upperLeft());
+		}
 		// Draw overlays in each child component on top.
 		for (int i = 0; i < component.getChildCount(); i++) {
 			drawOverlays(targetImage, *(component.children[i]), offset + component.children[i]->location.upperLeft());
@@ -341,7 +380,7 @@ void VisualComponent::defocusChildren() {
 			return; // Reached the end.
 		} else {
 			parent->focusComponent = std::shared_ptr<VisualComponent>(); // The parent removes the focus pointer from the child.
-			child->focused = false; // Remember that it is not focused, for quick access.
+			child->currentState &= ~componentState_focusTail; // Remember that it is not focused, for quick access.
 			parent = child; // Prepare for the next iteration.
 		}
 	}
@@ -378,20 +417,21 @@ void VisualComponent::makeFocused() {
 					parent->defocusChildren();
 				}
 				parent->focusComponent = current->getShared();
-				current->focused = true;
+				this->currentState |= componentState_focusTail;
 				current = parent;
 			}
 		}
 	}
 }
 
+void VisualComponent::stateChanged(ComponentState oldState, ComponentState newState) {}
+
 void VisualComponent::sendNotifications() {
-	if (this->focused && !this->previouslyFocused) {
-		this->gotFocus();
-	} else if (!this->focused && this->previouslyFocused) {
-		this->lostFocus();
+	// Detect differences for all flags at once using bits in the integers.
+	if (this->currentState != this->previousState) {
+		stateChanged(this->previousState, this->currentState);
+		this->previousState = this->currentState;
 	}
-	this->previouslyFocused = this->focused;
 	for (int i = this->getChildCount() - 1; i >= 0; i--) {
 		this->children[i]->sendNotifications();
 	}
@@ -400,14 +440,19 @@ void VisualComponent::sendNotifications() {
 // Find the topmost overlay by searching backwards with the parent last and returning a pointer to the component.
 // The point is relative to the upper left corner of component.
 static VisualComponent *getTopmostOverlay(VisualComponent *component, const IVector2D &point) {
-	// Go through child components in reverse draw order to stop when reaching the one that is visible.
-	for (int i = component->getChildCount() - 1; i >= 0; i--) {
-		VisualComponent *result = getTopmostOverlay(component->children[i].get(), point - component->children[i]->location.upperLeft());
-		if (result != nullptr) return result;
-	}
-	// Check itself behind child overlays.
-	if (component->showingOverlay && component->pointIsInsideOfOverlay(point + component->location.upperLeft())) {
-		return component;
+	// Only visible component may show its overlay or child components.
+	if (component->getVisible()) {
+		// Go through child components in reverse draw order to stop when reaching the one that is visible.
+		for (int i = component->getChildCount() - 1; i >= 0; i--) {
+			VisualComponent *result = getTopmostOverlay(component->children[i].get(), point - component->children[i]->location.upperLeft());
+			if (result != nullptr) return result;
+		}
+		// Check itself behind child overlays.
+		if (component->showingOverlay() && component->pointIsInsideOfOverlay(point + component->location.upperLeft())) {
+			return component;
+		} else {
+			return nullptr;
+		}
 	} else {
 		return nullptr;
 	}
@@ -541,18 +586,6 @@ String VisualComponent::call(const ReadableString &methodName, const ReadableStr
 	return U"";
 }
 
-bool VisualComponent::isFocused() {
-	return this->focused && this->focusComponent.get() == nullptr;
-}
-
-bool VisualComponent::ownsFocus() {
-	return this->focused;
-}
-
-// Override these in components to handle changes of focus without having to remember the previous focus.
-void VisualComponent::gotFocus() {}
-void VisualComponent::lostFocus() {}
-
 bool VisualComponent::managesChildren() {
 	return false;
 }

+ 48 - 60
Source/DFPSR/gui/VisualComponent.h

@@ -37,15 +37,23 @@ namespace dsr {
 // A reusable method for calling the media machine that allow providing additional variables as style flags.
 MediaResult component_generateImage(VisualTheme theme, MediaMethod &method, int width, int height, int red, int green, int blue, int pressed = 0, int focused = 0, int hover = 0);
 
+class VisualComponent;
+
+// Bit flags for component states.
+//  The size of ComponentState may change if running out of bits for new flags.
+using ComponentState = uint32_t;
+static const ComponentState componentState_focusTail = 1 << 0;
+static const ComponentState componentState_hoverTail = 1 << 1;
+static const ComponentState componentState_showingOverlay = 1 << 2;
+
 class VisualComponent : public Persistent {
 PERSISTENT_DECLARATION(VisualComponent)
-public:
+public: // Relations
 	// TODO: Make as much as possible private using methods for access, so that things won't break when changes are made.
 	// Parent component
 	VisualComponent *parent = nullptr;
 	IRect givenSpace; // Remembering the local region that was reserved inside of the parent component.
 	bool regionAccessed = false; // If someone requested access to the region, remember to update layout in case of new settings.
-	bool showingOverlay = false; // Should be true when the component is currently projecting an overlay, which requires it to be own focus (be along the focusComponent pointer path from root).
 	// Child components
 	List<std::shared_ptr<VisualComponent>> children;
 	// Remember the component used for a drag event.
@@ -53,56 +61,55 @@ public:
 	int holdCount = 0;
 	// Remember the pressed component for sending mouse move events outside of its region.
 	std::shared_ptr<VisualComponent> dragComponent;
+private: // States
+	// Use methods to set the current state, then have it copied to previousState after calling stateChanged in sendNotifications.
+	ComponentState currentState, previousState;
+public: // State updates
+	// Looking for recent state changes and sending notifications through stateChanged for each components that had a state change.
+	//   Deferring update notifications using this makes sure that events that trigger updates get to finish before the next one starts.
+	//   This reduces the risk of dead-locks, race-conditions, pointer errors...
+	void sendNotifications();
+	// Called after a component's state changed, when it is deemed safe to do so.
+	//   All state changes will be sent at the same time, because state changes are often used to trigger other changes.
+	//   Changes to the state made within the notification will not trigger new notifications, because the old state is saved after the call is finished.
+	virtual void stateChanged(ComponentState oldState, ComponentState newState);
+public: // Focus
 	// Remember the focused component for keyboard input and showing overlays.
 	// Focus is a trail of pointers from the root to the component that was clicked on.
 	// When makeFocused is called, the old trail will be removed until reaching a common parent with the new focus trail.
 	// When a component's focus changes, changedFocus will be called on it with the new value. 
 	std::shared_ptr<VisualComponent> focusComponent;
-private:
-	// Temporary boolean flags
-	//   Should be private, so that it can later be implemented as bit flags without breaking compatibility.
-	// Keeping track on if the component is focused
-	bool previouslyFocused = false;
-	bool focused = false;
+	// All components from root to the clicked component will have the focus flag set, isFocused only care about the focused component holding no more focused child components.
+	inline bool isFocused() { return (this->currentState & componentState_focusTail) && (this->focusComponent.get() == nullptr); }
+	// ownsFOcus is for nested focus, such as automatically closing menu overlays that no longer have focus within them.
+	inline bool ownsFocus() { return (this->currentState & componentState_focusTail) != 0; }
+	// Remove focus from all of the component's children.
+	void defocusChildren();
+	// Give focus to a trail from root to the component.
+	void makeFocused();
+public: // Showing overlay
+	inline bool showingOverlay() { return (this->currentState & componentState_showingOverlay) != 0; }
+	inline void showOverlay() { this->currentState |= componentState_showingOverlay; }
+	inline void hideOverlay() { this->currentState &= ~componentState_showingOverlay; }
+public: // Hover
+	// TODO: Implement hover in the same way as grabbing and focus, by always checking which components are hovered.
+	//       One component will be the visibly hovered component and others will keep track of when the mouse enters and exits.
+	//       Come up with a good naming convention for this.
+	// TODO: How can the state be noted if only the tail has a bit.
+	//       Should the directly hovered and focused components have their own head bits?
+	//       One can have combined bit masks for checking if something is a head or tail, for focus and hover flags.
+	// TODO: Make a permanent debug mode where selected states in a mask are drawn on top of components.
+	// bool isHovered() { return (this->currentState & componentState_hoverTail) && (this->hoverComponent.get() == nullptr); }
+	// bool ownsHover() { return (this->currentState & componentState_hoverTail) != 0; }
 public:
 	// Saved properties
 	FlexRegion region;
 	PersistentString name;
 	PersistentInteger index;
 	PersistentBoolean visible = PersistentBoolean(true);
-	void declareAttributes(StructureDefinition &target) const override {
-		target.declareAttribute(U"Name");
-		target.declareAttribute(U"Index");
-		target.declareAttribute(U"Visible");
-		target.declareAttribute(U"Left");
-		target.declareAttribute(U"Top");
-		target.declareAttribute(U"Right");
-		target.declareAttribute(U"Bottom");
-	}
+	void declareAttributes(StructureDefinition &target) const override;
 public:
-	Persistent* findAttribute(const ReadableString &name) override {
-		if (string_caseInsensitiveMatch(name, U"Name")) {
-			return &(this->name);
-		} else if (string_caseInsensitiveMatch(name, U"Index")) {
-			return &(this->index);
-		} else if (string_caseInsensitiveMatch(name, U"Visible")) {
-			return &(this->visible);
-		} else if (string_caseInsensitiveMatch(name, U"Left")) {
-			this->regionAccessed = true;
-			return &(this->region.left);
-		} else if (string_caseInsensitiveMatch(name, U"Top")) {
-			this->regionAccessed = true;
-			return &(this->region.top);
-		} else if (string_caseInsensitiveMatch(name, U"Right")) {
-			this->regionAccessed = true;
-			return &(this->region.right);
-		} else if (string_caseInsensitiveMatch(name, U"Bottom")) {
-			this->regionAccessed = true;
-			return &(this->region.bottom);
-		} else {
-			return nullptr;
-		}
-	}
+	Persistent* findAttribute(const ReadableString &name) override;
 public:
 	// Generated automatically from region in applyLayout
 	IRect location;
@@ -212,6 +219,7 @@ public:
 	virtual void applyLayout(const IRect& givenSpace);
 	// Update layout when the component moved but the parent has the same dimensions
 	void updateLayout();
+	// TODO: Remake desiredDimensions into a private variable, so that one can update it when attributes are changed and notify parents that they need to change size and redraw images.
 	// Parent components that place child components automatically can ask them what their minimum useful dimensions are in pixels, so that their text will be visible.
 	// The component can still be resized to less than these dimensions, because the outer components can't give more space than what is given by the window.
 	virtual IVector2D getDesiredDimensions();
@@ -251,26 +259,6 @@ public:
 	virtual void changedLocation(const IRect &oldLocation, const IRect &newLocation) {};
 	// Custom call handler to manipulate components across a generic API
 	virtual String call(const ReadableString &methodName, const ReadableString &arguments);
-	// TODO: Rename to something better.
-	// Returns true iff the component is at the end of the focus trail, not referring to any of its children for focus.
-	bool isFocused();
-	// Returns true iff itself, a direct child or an indirect child has focus.
-	//   Used for menus to keep the whole path of sub-menus alive all the way down to the focused component.
-	bool ownsFocus();
-	// Remove focus from all of the component's children and call lostFocus() on them.
-	void defocusChildren();
-	// Give focus to a trail from root to the component and call gotFocus() on them.
-	void makeFocused();
-	// Looking for recent state changes and sending notifications.
-	//   Deferring update notifications using this makes sure that events that trigger updates get to finish before the next one starts.
-	//   This reduces the risk of dead-locks, race-conditions, pointer errors...
-	void sendNotifications();
-	// Called after the component's focus went from false to true.
-	//   Currently not safe to make arbitrary event callbacks that might detach components, due to raw pointer traversal.
-	virtual void gotFocus();
-	// Called after the component's focus went from true to false.
-	//   Currently not safe to make arbitrary event callbacks that might detach components, due to raw pointer traversal.
-	virtual void lostFocus();
 };
 
 }

+ 40 - 31
Source/DFPSR/gui/components/Menu.cpp

@@ -114,7 +114,7 @@ void Menu::generateGraphics() {
 // Fill the listBackgroundImageMethod with a solid color
 void Menu::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
 	this->generateGraphics();
-	draw_alphaFilter(targetImage, this->showingOverlay ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
+	draw_alphaFilter(targetImage, this->showingOverlay() ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
 }
 
 void Menu::generateBackground() {
@@ -127,15 +127,17 @@ void Menu::generateBackground() {
 	}
 }
 
-void Menu::showOverlay() {
-	// Both showingOverlay and makeFocused are currently required for the overlay to be visible.
-	this->showingOverlay = true;
-	this->makeFocused();
-	IRect memberBound = this->children[0]->location;
-	for (int i = 1; i < this->getChildCount(); i++) {
-		memberBound = IRect::merge(memberBound, this->children[i]->location);
+void Menu::createOverlay() {
+	if (!this->showingOverlay()) {
+		this->showOverlay();
+		this->makeFocused();
+		IRect memberBound = this->children[0]->location;
+		for (int i = 1; i < this->getChildCount(); i++) {
+			memberBound = IRect::merge(memberBound, this->children[i]->location);
+		}
+		// Calculate the new list bound.
+		this->overlayLocation = memberBound.expanded(this->padding.value) + this->location.upperLeft();
 	}
-	this->overlayLocation = memberBound.expanded(this->padding.value) + this->location.upperLeft();
 }
 
 bool Menu::managesChildren() {
@@ -146,16 +148,13 @@ bool Menu::pointIsInsideOfOverlay(const IVector2D& pixelPosition) {
 	return pixelPosition.x > this->overlayLocation.left() && pixelPosition.x < this->overlayLocation.right() && pixelPosition.y > this->overlayLocation.top() && pixelPosition.y < this->overlayLocation.bottom();
 }
 
-// TODO: Draw decorations using headImage.
 void Menu::drawOverlay(ImageRgbaU8& targetImage, const IVector2D &absoluteOffset) {
-	if (this->showingOverlay) {
-		this->generateBackground();
-		// TODO: Let the theme select between solid and alpha filtered drawing.
-		IVector2D overlayOffset = absoluteOffset + this->overlayLocation.upperLeft();
-		draw_copy(targetImage, this->listBackgroundImage, overlayOffset.x, overlayOffset.y);
-		for (int i = 0; i < this->getChildCount(); i++) {
-			this->children[i]->draw(targetImage, absoluteOffset + this->location.upperLeft());
-		}
+	this->generateBackground();
+	// TODO: Let the theme select between solid and alpha filtered drawing.
+	IVector2D overlayOffset = absoluteOffset + this->overlayLocation.upperLeft();
+	draw_copy(targetImage, this->listBackgroundImage, overlayOffset.x, overlayOffset.y);
+	for (int i = 0; i < this->getChildCount(); i++) {
+		this->children[i]->draw(targetImage, absoluteOffset + this->location.upperLeft());
 	}
 }
 
@@ -190,11 +189,19 @@ void Menu::changedAttribute(const ReadableString &name) {
 	}
 }
 
-void Menu::lostFocus() {
-	// Hide the menu when losing focus.
-	this->showingOverlay = false;
-	// Erase the list image to save memory.
-	this->listBackgroundImage = OrderedImageRgbaU8();
+void Menu::stateChanged(ComponentState oldState, ComponentState newState) {
+	// The states lost what they don't have now but had before.
+	ComponentState lostStates = ~newState & oldState;
+	if (lostStates & componentState_focusTail) {
+		// Hide the menu when losing focus.
+		this->hideOverlay();
+		// State notifications are not triggered from within the same notification, so that one can handle all the updates safely in the desired order.
+		this->listBackgroundImage = OrderedImageRgbaU8();
+	}
+	if (lostStates & componentState_showingOverlay) {		
+		// Clean up the background image to save memory and allow it to be regenerated in another size later.
+		this->listBackgroundImage = OrderedImageRgbaU8();
+	}
 }
 
 void Menu::updateLocationEvent(const IRect& oldLocation, const IRect& newLocation) {	
@@ -222,7 +229,9 @@ void Menu::updateLocationEvent(const IRect& oldLocation, const IRect& newLocatio
 
 static void closeEntireMenu(VisualComponent* menu) {
 	while (menu->parent != nullptr) {
-		menu->showingOverlay = false;
+		// Hide the menu when closing the menu. Notifications to stateChanged will do the proper cleanup for each component's type.
+		menu->hideOverlay();
+		// Move on to the parent component.
 		menu = menu->parent;
 	}
 }
@@ -232,7 +241,7 @@ void Menu::receiveMouseEvent(const MouseEvent& event) {
 	IVector2D positionFromParent = event.position;
 	MouseEvent localEvent = event;
 	localEvent.position -= this->location.upperLeft();
-	if (this->showingOverlay && this->pointIsInsideOfOverlay(event.position)) {
+	if (this->showingOverlay() && this->pointIsInsideOfOverlay(event.position)) {
 		for (int i = childCount - 1; i >= 0; i--) {
 			if (this->children[i]->pointIsInside(localEvent.position)) {
 				this->children[i]->makeFocused();
@@ -246,28 +255,28 @@ void Menu::receiveMouseEvent(const MouseEvent& event) {
 		if (childCount > 0) { // Has a list of members to open, toggle expansion when clicked.
 			if (this->subMenu) { // Menu within another menu.
 				// Hover to expand sub-menu's list.
-				if (event.mouseEventType == MouseEventType::MouseMove && !this->showingOverlay) {
-					this->showOverlay();
+				if (event.mouseEventType == MouseEventType::MouseMove && !this->showingOverlay()) {
+					this->createOverlay();
 				}
 			} else { // Top menu, which is usually placed in a toolbar.
 				bool toggleExpansion = false;
 				if (event.mouseEventType == MouseEventType::MouseDown) {
 					// Toggle expansion when headImageMethod is clicked.
 					toggleExpansion = true;
-				} else if (event.mouseEventType == MouseEventType::MouseMove && !this->showingOverlay) {
+				} else if (event.mouseEventType == MouseEventType::MouseMove && !this->showingOverlay()) {
 					// Automatically expand hovered top-menus neighboring an opened top menu.
 					if (this->parent != nullptr) {
-						if (this->parent->focusComponent.get() != nullptr && this->parent->focusComponent->showingOverlay) {
+						if (this->parent->focusComponent.get() != nullptr && this->parent->focusComponent->showingOverlay()) {
 							toggleExpansion = true;
 						}
 					}
 				}
 				if (toggleExpansion) {
 					// Menu components with child members will toggle visibility for its list when pressed.
-					if (this->showingOverlay) {
+					if (this->showingOverlay()) {
 						closeEntireMenu(this);
 					} else {
-						this->showOverlay();
+						this->createOverlay();
 					}
 				}
 			}

+ 2 - 2
Source/DFPSR/gui/components/Menu.h

@@ -70,8 +70,8 @@ public:
 	// Helper functions for decorations.
 	bool hasArrow();
 	// Helper functions for overlay projecting components.
-	void showOverlay();
-	void lostFocus() override;
+	void createOverlay();
+	void stateChanged(ComponentState oldState, ComponentState newState) override;
 	bool pointIsInsideOfOverlay(const IVector2D& pixelPosition) override;
 };