Selaa lähdekoodia

LibGUI: Add DynamicWidgetContainter

Add a new widget "DynamicWidgetContainer" that is used to group it's
child widgets within an collapsable and detachable container. The
DynmnicWidgetContainer is able to persist it's view state if a config
domain has been provided. Having that set will allow the widget to
restore it's view state automatically.
Torstennator 1 vuosi sitten
vanhempi
commit
b65e711929

BIN
Base/res/icons/16x16/detach.png


+ 1 - 0
Base/usr/share/man/man5/GML.md

@@ -41,6 +41,7 @@ Or right clicking on a folder in the TreeView and using
         -   [CheckBox](help://man/5/GML/Widget/CheckBox)
         -   [CheckBox](help://man/5/GML/Widget/CheckBox)
         -   [ColorInput](help://man/5/GML/Widget/ColorInput)
         -   [ColorInput](help://man/5/GML/Widget/ColorInput)
         -   [ComboBox](help://man/5/GML/Widget/ComboBox)
         -   [ComboBox](help://man/5/GML/Widget/ComboBox)
+        -   [DynamicWidgetContainer](help://man/5/GML/Widget/DynamicWidgetContainer)
         -   [Frame](help://man/5/GML/Widget/Frame)
         -   [Frame](help://man/5/GML/Widget/Frame)
         -   [GroupBox](help://man/5/GML/Widget/GroupBox)
         -   [GroupBox](help://man/5/GML/Widget/GroupBox)
         -   [HorizontalProgressbar](help://man/5/GML/Widget/HorizontalProgressbar)
         -   [HorizontalProgressbar](help://man/5/GML/Widget/HorizontalProgressbar)

+ 67 - 0
Base/usr/share/man/man5/GML/Widget/DynamicWidgetContainer.md

@@ -0,0 +1,67 @@
+## Name
+
+GML DynamicWidgetContainer
+
+## Description
+
+Defines a container widget that will group its child widgets together so that they can be collapsed, expanded or detached to a new window as one unit. If DynamicWidgetContainers are nested within one DynamicWidgetContainer it is possible to move the positions of the child containers dynamically.
+
+| Property                    | Description                                                                                                                                                |
+| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| config_domain               | Defines if the changes to the widget's view state should be persisted. It is required that the domain has been already pleged by the application.          |
+| detached_size               | Defines a size that the detached widget window should initially have. If not defined, the window will have the current size of the widget.                 |
+| section_label               | The label that will be used for the section.                                                                                                               |
+| show_controls               | Defines if the buttons and label should be visible or not. This allows e.g. a parent container to hide its controls but provide rearrenage functionality.  |
+| with_individual_order       | Configured on a parent container to enable the persistence of rearranged child containers.                                                                 | 
+
+## Synopsis
+
+`@GUI::DynamicWidgetContainer`
+
+## Examples
+
+Simple container:
+```gml
+@GUI::DynamicWidgetContainer {
+    section_label: "Section 1"
+
+    @GUI::Widget {
+    }
+    
+    @GUI::Widget {
+    }
+}
+```
+
+Nested containers with persistence:
+
+```gml
+@GUI::DynamicWidgetContainer {
+    section_label: "Parent Section"
+    config_domain: "abc"
+    with_individual_order: true
+    detached_size: [200, 640]
+
+    @GUI::DynamicWidgetContainer {
+        section_label: "Section 1"
+        config_domain: "abc"
+
+        @GUI::Widget {
+        }
+        
+        @GUI::Widget {
+        }
+    }
+    
+    @GUI::DynamicWidgetContainer {
+        section_label: "Section 2"
+        config_domain: "abc"
+
+        @GUI::Widget {
+        }
+        
+        @GUI::Widget {
+        }
+    } 
+}
+```

+ 5 - 0
Userland/Libraries/LibGUI/CMakeLists.txt

@@ -1,3 +1,5 @@
+compile_gml(DynamicWidgetContainerControls.gml DynamicWidgetContainerControls.cpp)
+
 stringify_gml(AboutDialog.gml AboutDialogGML.h about_dialog_gml)
 stringify_gml(AboutDialog.gml AboutDialogGML.h about_dialog_gml)
 stringify_gml(EmojiInputDialog.gml EmojiInputDialogGML.h emoji_input_dialog_gml)
 stringify_gml(EmojiInputDialog.gml EmojiInputDialogGML.h emoji_input_dialog_gml)
 stringify_gml(FontPickerDialog.gml FontPickerDialogGML.h font_picker_dialog_gml)
 stringify_gml(FontPickerDialog.gml FontPickerDialogGML.h font_picker_dialog_gml)
@@ -38,6 +40,8 @@ set(SOURCES
     Dialog.cpp
     Dialog.cpp
     DisplayLink.cpp
     DisplayLink.cpp
     DragOperation.cpp
     DragOperation.cpp
+    DynamicWidgetContainer.cpp
+    DynamicWidgetContainerControls.cpp
     EditingEngine.cpp
     EditingEngine.cpp
     EmojiInputDialog.cpp
     EmojiInputDialog.cpp
     Event.cpp
     Event.cpp
@@ -65,6 +69,7 @@ set(SOURCES
     InputBox.cpp
     InputBox.cpp
     JsonArrayModel.cpp
     JsonArrayModel.cpp
     Label.cpp
     Label.cpp
+    LabelWithEventDispatcher.cpp
     Layout.cpp
     Layout.cpp
     LazyWidget.cpp
     LazyWidget.cpp
     LinkLabel.cpp
     LinkLabel.cpp

+ 460 - 0
Userland/Libraries/LibGUI/DynamicWidgetContainer.cpp

@@ -0,0 +1,460 @@
+/*
+ * Copyright (c) 2023, Torsten Engelmann <engelTorsten@gmx.de>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibConfig/Client.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/DynamicWidgetContainer.h>
+#include <LibGUI/DynamicWidgetContainerControls.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/LabelWithEventDispatcher.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Palette.h>
+
+REGISTER_WIDGET(GUI, DynamicWidgetContainer)
+
+namespace GUI {
+Vector<NonnullRefPtr<GUI::Window>> DynamicWidgetContainer::s_open_windows;
+
+DynamicWidgetContainer::DynamicWidgetContainer(Gfx::Orientation orientation)
+{
+    VERIFY(orientation == Gfx::Orientation::Vertical);
+    REGISTER_STRING_PROPERTY("section_label", section_label, set_section_label);
+    REGISTER_STRING_PROPERTY("config_domain", config_domain, set_config_domain);
+    REGISTER_SIZE_PROPERTY("detached_size", detached_size, set_detached_size);
+    REGISTER_BOOL_PROPERTY("with_individual_order", is_container_with_individual_order, set_container_with_individual_order);
+    REGISTER_BOOL_PROPERTY("show_controls", show_controls, set_show_controls);
+
+    set_layout<GUI::VerticalBoxLayout>(0, 0);
+    set_preferred_height(SpecialDimension::Shrink);
+
+    auto controls_widget = MUST(GUI::DynamicWidgetContainerControls::try_create());
+    m_controls_widget = controls_widget;
+    add_child(*m_controls_widget);
+
+    controls_widget->get_collapse_button()->on_click = [&](auto) {
+        set_view_state(ViewState::Collapsed);
+    };
+
+    controls_widget->get_expand_button()->on_click = [&](auto) {
+        set_view_state(ViewState::Expanded);
+    };
+
+    controls_widget->get_detach_button()->on_click = [&](auto) {
+        set_view_state(ViewState::Detached);
+    };
+
+    update_control_button_visibility();
+
+    m_label_widget = controls_widget->get_event_dispatcher();
+    m_label_widget->on_double_click = [&](MouseEvent& event) {
+        handle_doubleclick_event(event);
+    };
+    m_label_widget->on_mouseup_event = [&](MouseEvent& event) {
+        handle_mouseup_event(event);
+    };
+    m_label_widget->on_mousemove_event = [&](MouseEvent& event) {
+        handle_mousemove_event(event);
+    };
+
+    m_label_widget->set_grabbable_margins({ 0, 0, 0, m_label_widget->rect().width() });
+}
+
+DynamicWidgetContainer::~DynamicWidgetContainer()
+{
+    close_all_detached_windows();
+}
+
+template<typename Callback>
+void DynamicWidgetContainer::for_each_child_container(Callback callback)
+{
+    for_each_child([&](auto& child) {
+        if (is<DynamicWidgetContainer>(child))
+            return callback(static_cast<DynamicWidgetContainer&>(child));
+        return IterationDecision::Continue;
+    });
+}
+
+Vector<GUI::DynamicWidgetContainer&> DynamicWidgetContainer::child_containers() const
+{
+    Vector<GUI::DynamicWidgetContainer&> widgets;
+    widgets.ensure_capacity(children().size());
+    for (auto& child : children()) {
+        if (is<DynamicWidgetContainer>(*child))
+            widgets.append(static_cast<DynamicWidgetContainer&>(*child));
+    }
+    return widgets;
+}
+
+void DynamicWidgetContainer::set_view_state(ViewState state)
+{
+    if (view_state() == state)
+        return;
+
+    m_view_state = state;
+    set_visible(view_state() != ViewState::Detached);
+
+    for_each_child_widget([&](auto& widget) {
+        if (m_controls_widget != widget)
+            widget.set_visible(view_state() == ViewState::Expanded);
+        return IterationDecision::Continue;
+    });
+
+    if (m_dimensions_before_collapse.has_value()) {
+        set_min_size(m_dimensions_before_collapse->min_size);
+        set_preferred_size(m_dimensions_before_collapse->preferred_size);
+        m_dimensions_before_collapse = {};
+    }
+    if (view_state() == ViewState::Collapsed) {
+        // We still need to force a minimal height in case of a container is configured as "grow". Even then we would like to let it collapse.
+        m_dimensions_before_collapse = { { .preferred_size = preferred_size(), .min_size = min_size() } };
+
+        set_min_height(m_controls_widget->height() + content_margins().vertical_total());
+        set_preferred_size(preferred_width(), SpecialDimension::Shrink);
+    }
+
+    update_control_button_visibility();
+
+    if (view_state() == ViewState::Detached)
+        (void)detach_widgets();
+
+    if (persist_state())
+        Config::write_i32(config_domain(), "DynamicWidgetContainers"sv, section_label(), to_underlying(state));
+}
+
+void DynamicWidgetContainer::restore_view_state()
+{
+    if (!persist_state())
+        return;
+
+    deferred_invoke([&]() {
+        if (is_container_with_individual_order()) {
+            auto order_or_error = JsonValue::from_string(Config::read_string(config_domain(), "DynamicWidgetContainers"sv, section_label()));
+            if (order_or_error.is_error() || !order_or_error.value().is_array()) {
+                Config::remove_key(config_domain(), "DynamicWidgetContainers"sv, section_label());
+                return;
+            }
+
+            Vector<NonnullRefPtr<Widget>> new_child_order;
+            auto containers = child_containers();
+
+            order_or_error.value().as_array().for_each([&](auto& section_label) {
+                for (auto& container : containers) {
+                    if (container.section_label() == section_label.to_deprecated_string())
+                        new_child_order.append(container);
+                }
+            });
+
+            // Are there any children that are not known to our persisted order?
+            for (auto& container : containers) {
+                // FIXME: Optimize performance and get rid of contains_slow so that this does not become a issue when a lot of child containers are used.
+                if (!new_child_order.contains_slow(container))
+                    new_child_order.append(container);
+            }
+
+            // Rearrange child widgets to the defined order.
+            auto childs = child_widgets();
+            for (auto& child : childs) {
+                if (new_child_order.contains_slow(child))
+                    child.remove_from_parent();
+            }
+
+            for (auto const& child : new_child_order)
+                add_child(*child);
+        } else {
+            int persisted_state = Config::read_i32(config_domain(), "DynamicWidgetContainers"sv, section_label(), to_underlying(ViewState::Expanded));
+            set_view_state(static_cast<ViewState>(persisted_state));
+        }
+        update();
+    });
+}
+
+void DynamicWidgetContainer::set_section_label(String label)
+{
+    m_section_label = move(label);
+    m_label_widget->set_text(m_section_label);
+}
+
+void DynamicWidgetContainer::set_config_domain(String domain)
+{
+    m_config_domain = move(domain);
+    // FIXME: A much better solution would be to call the restore_view_state within a dedicated "initialization finished" method that is called by the gml runtime after that widget is ready.
+    //        We do not have such a method yet.
+    restore_view_state();
+}
+
+void DynamicWidgetContainer::set_detached_size(Gfx::IntSize const size)
+{
+    m_detached_size = { size };
+}
+
+void DynamicWidgetContainer::set_show_controls(bool value)
+{
+    m_show_controls = value;
+    m_controls_widget->set_visible(m_controls_widget->is_visible() && show_controls());
+    update();
+}
+
+void DynamicWidgetContainer::set_container_with_individual_order(bool value)
+{
+    m_is_container_with_individual_order = value;
+}
+
+void DynamicWidgetContainer::second_paint_event(PaintEvent&)
+{
+    GUI::Painter painter(*this);
+    painter.draw_line({ 0, height() - 1 }, { width(), height() - 1 }, palette().threed_shadow1());
+
+    if (!m_is_dragging && !m_render_as_move_target)
+        return;
+
+    if (m_is_dragging) {
+        // FIXME: Would be nice if we could paint outside our own boundaries.
+        auto move_widget_indicator = rect().translated(m_current_mouse_position).translated(-m_drag_start_location);
+        painter.fill_rect(move_widget_indicator, palette().rubber_band_fill());
+        painter.draw_rect_with_thickness(move_widget_indicator, palette().rubber_band_border(), 1);
+    } else if (m_render_as_move_target) {
+        painter.fill_rect(rect(), palette().rubber_band_fill());
+        painter.draw_rect_with_thickness({ rect().x(), rect().y(), rect().width() - 1, rect().height() - 1 }, palette().rubber_band_border(), 1);
+    }
+}
+
+ErrorOr<void> DynamicWidgetContainer::detach_widgets()
+{
+    if (!m_detached_widgets_window.has_value()) {
+        auto detached_window = TRY(GUI::Window::try_create());
+        detached_window->set_title(section_label().to_deprecated_string());
+        detached_window->set_window_type(WindowType::Normal);
+        if (has_detached_size())
+            detached_window->resize(detached_size());
+        else
+            detached_window->resize(size());
+
+        detached_window->center_on_screen();
+
+        auto root_container = detached_window->set_main_widget<GUI::Frame>();
+        root_container->set_fill_with_background_color(true);
+        root_container->set_layout<GUI::VerticalBoxLayout>();
+        root_container->set_frame_style(Gfx::FrameStyle::Window);
+        auto transfer_children = [this](auto reciever, auto children) {
+            for (NonnullRefPtr<GUI::Widget> widget : children) {
+                if (widget == m_controls_widget)
+                    continue;
+                widget->remove_from_parent();
+                widget->set_visible(true);
+                reciever->add_child(widget);
+            }
+        };
+
+        transfer_children(root_container, child_widgets());
+
+        detached_window->on_close = [this, root_container, transfer_children]() {
+            transfer_children(this, root_container->child_widgets());
+            set_view_state(ViewState::Expanded);
+            unregister_open_window(m_detached_widgets_window.value());
+            m_detached_widgets_window = {};
+        };
+
+        m_detached_widgets_window = detached_window;
+    }
+
+    register_open_window(m_detached_widgets_window.value());
+
+    if (m_is_dragging)
+        m_detached_widgets_window.value()->move_to(screen_relative_rect().location().translated(m_current_mouse_position).translated({ m_detached_widgets_window.value()->width() / -2, 0 }));
+    m_detached_widgets_window.value()->show();
+    return {};
+}
+
+void DynamicWidgetContainer::close_all_detached_windows()
+{
+    for (auto window : DynamicWidgetContainer::s_open_windows.in_reverse())
+        window->close();
+}
+
+void DynamicWidgetContainer::register_open_window(NonnullRefPtr<GUI::Window> window)
+{
+    s_open_windows.append(window);
+}
+
+void DynamicWidgetContainer::unregister_open_window(NonnullRefPtr<GUI::Window> window)
+{
+    Optional<size_t> match = s_open_windows.find_first_index(window);
+    if (match.has_value())
+        s_open_windows.remove(match.value());
+}
+
+void DynamicWidgetContainer::handle_mouseup_event(MouseEvent& event)
+{
+    if (event.button() != MouseButton::Primary)
+        return;
+
+    if (m_is_dragging) {
+        // If we dropped the widget outside of ourself, we would like to detach it.
+        if (m_parent_container == nullptr && !rect().contains(event.position()))
+            set_view_state(ViewState::Detached);
+
+        if (m_parent_container != nullptr) {
+            bool should_move_position = m_parent_container->check_has_move_target(relative_position().translated(m_current_mouse_position), MoveTargetOperation::ClearAllTargets);
+
+            if (should_move_position)
+                m_parent_container->swap_widget_positions(*this, relative_position().translated(m_current_mouse_position));
+            else
+                set_view_state(ViewState::Detached);
+        }
+
+        m_is_dragging = false;
+
+        // Change the cursor back to normal after dragging is finished. Otherwise the cursor will only change if the mouse moves.
+        m_label_widget->update_cursor(Gfx::StandardCursor::Arrow);
+
+        update();
+    }
+}
+
+void DynamicWidgetContainer::handle_mousemove_event(MouseEvent& event)
+{
+    auto changed_cursor = m_is_dragging ? Gfx::StandardCursor::Move : Gfx::StandardCursor::Arrow;
+    if (m_move_widget_knurl.contains(event.position()) && !m_is_dragging)
+        changed_cursor = Gfx::StandardCursor::Hand;
+
+    if (event.buttons() == MouseButton::Primary && !m_is_dragging) {
+        m_is_dragging = true;
+        m_drag_start_location = event.position();
+        changed_cursor = Gfx::StandardCursor::Move;
+    }
+
+    if (m_is_dragging) {
+        m_current_mouse_position = event.position();
+        if (m_parent_container != nullptr) {
+            m_parent_container->check_has_move_target(relative_position().translated(m_current_mouse_position), MoveTargetOperation::SetTarget);
+        }
+        update();
+    }
+
+    m_label_widget->update_cursor(changed_cursor);
+}
+
+void DynamicWidgetContainer::handle_doubleclick_event(MouseEvent& event)
+{
+    if (event.button() != MouseButton::Primary)
+        return;
+
+    if (view_state() == ViewState::Expanded)
+        set_view_state(ViewState::Collapsed);
+    else if (view_state() == ViewState::Collapsed)
+        set_view_state(ViewState::Expanded);
+}
+
+void DynamicWidgetContainer::resize_event(ResizeEvent&)
+{
+    // Check if there is any content to display, and hide ourself if there would be nothing to display.
+    // This allows us to make the whole section not taking up space if child-widget visibility is maintained outside.
+    if (m_previous_frame_style.has_value() && height() != 0) {
+        m_controls_widget->set_visible(show_controls());
+        set_frame_style(m_previous_frame_style.value());
+        m_previous_frame_style = {};
+
+        // FIXME: Get rid of this, without the deferred invoke the lower part of the containing widget might not be drawn correctly :-/
+        deferred_invoke([&]() {
+            invalidate_layout();
+        });
+    }
+
+    if (view_state() == ViewState::Expanded && !m_previous_frame_style.has_value() && height() == (content_margins().top() + content_margins().bottom() + m_controls_widget->height())) {
+        m_controls_widget->set_visible(false);
+        m_previous_frame_style = frame_style();
+        set_frame_style(Gfx::FrameStyle::NoFrame);
+        deferred_invoke([&]() {
+            invalidate_layout();
+        });
+    }
+}
+
+void DynamicWidgetContainer::child_event(Core::ChildEvent& event)
+{
+    if (event.type() == Event::ChildAdded && event.child() && is<GUI::DynamicWidgetContainer>(*event.child()))
+        static_cast<DynamicWidgetContainer&>(*event.child()).set_parent_container(*this);
+
+    GUI::Frame::child_event(event);
+}
+
+void DynamicWidgetContainer::set_parent_container(RefPtr<GUI::DynamicWidgetContainer> container)
+{
+    m_parent_container = container;
+}
+
+bool DynamicWidgetContainer::check_has_move_target(Gfx::IntPoint relative_mouse_position, MoveTargetOperation operation)
+{
+    bool matched = false;
+    for_each_child_container([&](auto& child) {
+        bool is_target = child.relative_rect().contains(relative_mouse_position);
+        matched |= is_target;
+        child.set_render_as_move_target(operation == MoveTargetOperation::SetTarget ? is_target : false);
+
+        return IterationDecision::Continue;
+    });
+    return matched;
+}
+
+void DynamicWidgetContainer::set_render_as_move_target(bool is_target)
+{
+    if (m_render_as_move_target == is_target)
+        return;
+    m_render_as_move_target = is_target;
+    update();
+}
+
+void DynamicWidgetContainer::swap_widget_positions(NonnullRefPtr<Core::EventReceiver> source_widget, Gfx::IntPoint destination_positon)
+{
+    Optional<NonnullRefPtr<Core::EventReceiver>> destination_widget;
+    for_each_child_container([&](auto& child) {
+        if (child.relative_rect().contains(destination_positon)) {
+            destination_widget = child;
+            return IterationDecision::Break;
+        }
+        return IterationDecision::Continue;
+    });
+
+    VERIFY(destination_widget.has_value());
+    if (source_widget == destination_widget.value())
+        return;
+
+    auto source_position = children().find_first_index(*source_widget);
+    auto destination_position = children().find_first_index(*destination_widget.value());
+    VERIFY(source_position.has_value());
+    VERIFY(destination_position.has_value());
+
+    swap(children()[source_position.value()], children()[destination_position.value()]);
+
+    // FIXME: Find a better solution to instantly display the new widget order.
+    //        invalidate_layout is not working :/
+    auto childs = child_widgets();
+    for (RefPtr<GUI::Widget> widget : childs) {
+        widget->remove_from_parent();
+        add_child(*widget);
+    }
+
+    if (!persist_state())
+        return;
+
+    JsonArray new_widget_order;
+    for (auto& child : child_containers())
+        new_widget_order.must_append(child.section_label());
+
+    Config::write_string(config_domain(), "DynamicWidgetContainers"sv, section_label(), new_widget_order.serialized<StringBuilder>());
+}
+
+void DynamicWidgetContainer::update_control_button_visibility()
+{
+    auto expand_button = m_controls_widget->find_descendant_of_type_named<GUI::Button>("expand_button");
+    expand_button->set_visible(view_state() == ViewState::Collapsed);
+
+    auto collapse_button = m_controls_widget->find_descendant_of_type_named<GUI::Button>("collapse_button");
+    collapse_button->set_visible(view_state() == ViewState::Expanded);
+}
+}

+ 99 - 0
Userland/Libraries/LibGUI/DynamicWidgetContainer.h

@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2023, Torsten Engelmann <engelTorsten@gmx.de>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/LabelWithEventDispatcher.h>
+#include <LibGUI/Window.h>
+
+namespace GUI {
+
+class DynamicWidgetContainer : public Frame {
+    C_OBJECT(DynamicWidgetContainer);
+
+public:
+    enum class ViewState {
+        Expanded,
+        Collapsed,
+        Detached
+    };
+
+    enum class MoveTargetOperation {
+        SetTarget,
+        ClearAllTargets
+    };
+
+    ViewState view_state() const { return m_view_state; }
+    void set_view_state(ViewState);
+    StringView section_label() const& { return m_section_label; }
+    void set_section_label(String);
+    StringView config_domain() const& { return m_config_domain; }
+    void set_config_domain(String);
+    bool persist_state() const { return !m_config_domain.is_empty(); }
+    void set_detached_size(Gfx::IntSize const);
+    Gfx::IntSize detached_size() const { return m_detached_size.value(); }
+    bool has_detached_size() { return m_detached_size.has_value(); }
+    void set_container_with_individual_order(bool);
+    bool is_container_with_individual_order() const { return m_is_container_with_individual_order; }
+    void set_show_controls(bool);
+    bool show_controls() const { return m_show_controls; }
+    void set_parent_container(RefPtr<GUI::DynamicWidgetContainer>);
+    bool check_has_move_target(Gfx::IntPoint, MoveTargetOperation);
+    // FIXME: this should not be a public api and static - but currently the destructor is not being called when the widget was created via a gml file.
+    static void close_all_detached_windows();
+    virtual ~DynamicWidgetContainer() override;
+
+protected:
+    explicit DynamicWidgetContainer(Gfx::Orientation = Gfx::Orientation::Vertical);
+    virtual void paint_event(PaintEvent&) override {};
+    virtual void second_paint_event(PaintEvent&) override;
+    virtual void resize_event(ResizeEvent&) override;
+    virtual void child_event(Core::ChildEvent&) override;
+
+    template<typename Callback>
+    void for_each_child_container(Callback callback);
+    Vector<GUI::DynamicWidgetContainer&> child_containers() const;
+
+private:
+    struct RelevantSizes {
+        UISize preferred_size;
+        UISize min_size;
+    };
+    ViewState m_view_state = ViewState::Expanded;
+    String m_section_label;
+    String m_config_domain;
+    bool m_is_container_with_individual_order { false };
+    bool m_persist_state { false };
+    bool m_is_dragging { false };
+    bool m_render_as_move_target { false };
+    bool m_show_controls { true };
+    Gfx::IntPoint m_drag_start_location;
+    Gfx::IntPoint m_current_mouse_position;
+    RefPtr<GUI::Widget> m_controls_widget;
+    RefPtr<GUI::LabelWithEventDispatcher> m_label_widget;
+    Gfx::IntRect m_move_widget_knurl = { 0, 0, 16, 16 };
+    Optional<NonnullRefPtr<GUI::Window>> m_detached_widgets_window;
+    Optional<Gfx::FrameStyle> m_previous_frame_style;
+    Optional<RelevantSizes> m_dimensions_before_collapse;
+    Optional<Gfx::IntSize> m_detached_size;
+    RefPtr<GUI::DynamicWidgetContainer> m_parent_container;
+    static Vector<NonnullRefPtr<GUI::Window>> s_open_windows;
+
+    ErrorOr<void> detach_widgets();
+    void restore_view_state();
+    void register_open_window(NonnullRefPtr<GUI::Window>);
+    void unregister_open_window(NonnullRefPtr<GUI::Window>);
+    void set_render_as_move_target(bool);
+    void swap_widget_positions(NonnullRefPtr<Core::EventReceiver> source, Gfx::IntPoint destination_positon);
+
+    void handle_mousemove_event(MouseEvent&);
+    void handle_mouseup_event(MouseEvent&);
+    void handle_doubleclick_event(MouseEvent&);
+    void update_control_button_visibility();
+};
+}

+ 33 - 0
Userland/Libraries/LibGUI/DynamicWidgetContainerControls.gml

@@ -0,0 +1,33 @@
+@GUI::DynamicWidgetContainerControls {
+    layout: @GUI::HorizontalBoxLayout {}
+    preferred_height: "shrink"
+
+    @GUI::LabelWithEventDispatcher {
+        name: "section_label"
+        text_alignment: "CenterLeft"
+    }
+
+    @GUI::Button {
+        name: "detach_button"
+        button_style: "Coolbar"
+        preferred_width: "shrink"
+        preferred_height: "shrink"
+        icon_from_path: "/res/icons/16x16/detach.png"
+    }
+
+    @GUI::Button {
+        name: "collapse_button"
+        button_style: "Coolbar"
+        preferred_width: "shrink"
+        preferred_height: "shrink"
+        icon_from_path: "/res/icons/16x16/upward-triangle.png"
+    }
+
+    @GUI::Button {
+        name: "expand_button"
+        button_style: "Coolbar"
+        preferred_width: "shrink"
+        preferred_height: "shrink"
+        icon_from_path: "/res/icons/16x16/downward-triangle.png"
+    }
+}

+ 45 - 0
Userland/Libraries/LibGUI/DynamicWidgetContainerControls.h

@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2023, Torsten Engelmann <engelTorsten@gmx.de>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibGUI/Button.h>
+#include <LibGUI/Frame.h>
+#include <LibGUI/LabelWithEventDispatcher.h>
+
+namespace GUI {
+
+class DynamicWidgetContainerControls : public GUI::Widget {
+    C_OBJECT_ABSTRACT(DynamicWidgetContainerControls)
+public:
+    static ErrorOr<NonnullRefPtr<DynamicWidgetContainerControls>> try_create();
+    virtual ~DynamicWidgetContainerControls() override = default;
+
+    RefPtr<GUI::Button> get_collapse_button()
+    {
+        return find_descendant_of_type_named<GUI::Button>("collapse_button");
+    }
+
+    RefPtr<GUI::Button> get_expand_button()
+    {
+        return find_descendant_of_type_named<GUI::Button>("expand_button");
+    }
+
+    RefPtr<GUI::Button> get_detach_button()
+    {
+        return find_descendant_of_type_named<GUI::Button>("detach_button");
+    }
+
+    RefPtr<GUI::LabelWithEventDispatcher> get_event_dispatcher()
+    {
+        return find_descendant_of_type_named<GUI::LabelWithEventDispatcher>("section_label");
+    }
+
+private:
+    DynamicWidgetContainerControls() = default;
+};
+
+}

+ 38 - 0
Userland/Libraries/LibGUI/LabelWithEventDispatcher.cpp

@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023, Torsten Engelmann <engelTorsten@gmx.de>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibGUI/LabelWithEventDispatcher.h>
+
+REGISTER_WIDGET(GUI, LabelWithEventDispatcher)
+
+namespace GUI {
+
+void LabelWithEventDispatcher::update_cursor(Gfx::StandardCursor cursor)
+{
+    if (override_cursor() == cursor)
+        return;
+    set_override_cursor(cursor);
+    update();
+}
+
+void LabelWithEventDispatcher::doubleclick_event(MouseEvent& event)
+{
+    if (on_double_click)
+        on_double_click(event);
+}
+
+void LabelWithEventDispatcher::mouseup_event(MouseEvent& event)
+{
+    if (on_mouseup_event)
+        on_mouseup_event(event);
+}
+
+void LabelWithEventDispatcher::mousemove_event(MouseEvent& event)
+{
+    if (on_mousemove_event)
+        on_mousemove_event(event);
+}
+}

+ 29 - 0
Userland/Libraries/LibGUI/LabelWithEventDispatcher.h

@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2023, Torsten Engelmann <engelTorsten@gmx.de>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Window.h>
+
+namespace GUI {
+class LabelWithEventDispatcher : public GUI::Label {
+    C_OBJECT(LabelWithEventDispatcher);
+
+public:
+    void update_cursor(Gfx::StandardCursor);
+    Function<void(MouseEvent&)> on_double_click;
+    Function<void(MouseEvent&)> on_mouseup_event;
+    Function<void(MouseEvent&)> on_mousemove_event;
+    virtual ~LabelWithEventDispatcher() override = default;
+
+protected:
+    void doubleclick_event(MouseEvent&) override;
+    virtual void mouseup_event(MouseEvent&) override;
+    virtual void mousemove_event(MouseEvent&) override;
+};
+}