123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- /*
- * 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.as_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_byte_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);
- }
- }
|