DynamicWidgetContainer.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. /*
  2. * Copyright (c) 2023, Torsten Engelmann <engelTorsten@gmx.de>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <LibConfig/Client.h>
  7. #include <LibGUI/BoxLayout.h>
  8. #include <LibGUI/Button.h>
  9. #include <LibGUI/DynamicWidgetContainer.h>
  10. #include <LibGUI/DynamicWidgetContainerControls.h>
  11. #include <LibGUI/Label.h>
  12. #include <LibGUI/LabelWithEventDispatcher.h>
  13. #include <LibGUI/Painter.h>
  14. #include <LibGUI/Window.h>
  15. #include <LibGfx/Palette.h>
  16. REGISTER_WIDGET(GUI, DynamicWidgetContainer)
  17. namespace GUI {
  18. Vector<NonnullRefPtr<GUI::Window>> DynamicWidgetContainer::s_open_windows;
  19. DynamicWidgetContainer::DynamicWidgetContainer(Gfx::Orientation orientation)
  20. {
  21. VERIFY(orientation == Gfx::Orientation::Vertical);
  22. REGISTER_STRING_PROPERTY("section_label", section_label, set_section_label);
  23. REGISTER_STRING_PROPERTY("config_domain", config_domain, set_config_domain);
  24. REGISTER_SIZE_PROPERTY("detached_size", detached_size, set_detached_size);
  25. REGISTER_BOOL_PROPERTY("with_individual_order", is_container_with_individual_order, set_container_with_individual_order);
  26. REGISTER_BOOL_PROPERTY("show_controls", show_controls, set_show_controls);
  27. set_layout<GUI::VerticalBoxLayout>(0, 0);
  28. set_preferred_height(SpecialDimension::Shrink);
  29. auto controls_widget = MUST(GUI::DynamicWidgetContainerControls::try_create());
  30. m_controls_widget = controls_widget;
  31. add_child(*m_controls_widget);
  32. controls_widget->get_collapse_button()->on_click = [&](auto) {
  33. set_view_state(ViewState::Collapsed);
  34. };
  35. controls_widget->get_expand_button()->on_click = [&](auto) {
  36. set_view_state(ViewState::Expanded);
  37. };
  38. controls_widget->get_detach_button()->on_click = [&](auto) {
  39. set_view_state(ViewState::Detached);
  40. };
  41. update_control_button_visibility();
  42. m_label_widget = controls_widget->get_event_dispatcher();
  43. m_label_widget->on_double_click = [&](MouseEvent& event) {
  44. handle_doubleclick_event(event);
  45. };
  46. m_label_widget->on_mouseup_event = [&](MouseEvent& event) {
  47. handle_mouseup_event(event);
  48. };
  49. m_label_widget->on_mousemove_event = [&](MouseEvent& event) {
  50. handle_mousemove_event(event);
  51. };
  52. m_label_widget->set_grabbable_margins({ 0, 0, 0, m_label_widget->rect().width() });
  53. }
  54. DynamicWidgetContainer::~DynamicWidgetContainer()
  55. {
  56. close_all_detached_windows();
  57. }
  58. template<typename Callback>
  59. void DynamicWidgetContainer::for_each_child_container(Callback callback)
  60. {
  61. for_each_child([&](auto& child) {
  62. if (is<DynamicWidgetContainer>(child))
  63. return callback(static_cast<DynamicWidgetContainer&>(child));
  64. return IterationDecision::Continue;
  65. });
  66. }
  67. Vector<GUI::DynamicWidgetContainer&> DynamicWidgetContainer::child_containers() const
  68. {
  69. Vector<GUI::DynamicWidgetContainer&> widgets;
  70. widgets.ensure_capacity(children().size());
  71. for (auto& child : children()) {
  72. if (is<DynamicWidgetContainer>(*child))
  73. widgets.append(static_cast<DynamicWidgetContainer&>(*child));
  74. }
  75. return widgets;
  76. }
  77. void DynamicWidgetContainer::set_view_state(ViewState state)
  78. {
  79. if (view_state() == state)
  80. return;
  81. m_view_state = state;
  82. set_visible(view_state() != ViewState::Detached);
  83. for_each_child_widget([&](auto& widget) {
  84. if (m_controls_widget != widget)
  85. widget.set_visible(view_state() == ViewState::Expanded);
  86. return IterationDecision::Continue;
  87. });
  88. if (m_dimensions_before_collapse.has_value()) {
  89. set_min_size(m_dimensions_before_collapse->min_size);
  90. set_preferred_size(m_dimensions_before_collapse->preferred_size);
  91. m_dimensions_before_collapse = {};
  92. }
  93. if (view_state() == ViewState::Collapsed) {
  94. // 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.
  95. m_dimensions_before_collapse = { { .preferred_size = preferred_size(), .min_size = min_size() } };
  96. set_min_height(m_controls_widget->height() + content_margins().vertical_total());
  97. set_preferred_size(preferred_width(), SpecialDimension::Shrink);
  98. }
  99. update_control_button_visibility();
  100. if (view_state() == ViewState::Detached)
  101. (void)detach_widgets();
  102. if (persist_state())
  103. Config::write_i32(config_domain(), "DynamicWidgetContainers"sv, section_label(), to_underlying(state));
  104. }
  105. void DynamicWidgetContainer::restore_view_state()
  106. {
  107. if (!persist_state())
  108. return;
  109. deferred_invoke([&]() {
  110. if (is_container_with_individual_order()) {
  111. auto order_or_error = JsonValue::from_string(Config::read_string(config_domain(), "DynamicWidgetContainers"sv, section_label()));
  112. if (order_or_error.is_error() || !order_or_error.value().is_array()) {
  113. Config::remove_key(config_domain(), "DynamicWidgetContainers"sv, section_label());
  114. return;
  115. }
  116. Vector<NonnullRefPtr<Widget>> new_child_order;
  117. auto containers = child_containers();
  118. order_or_error.value().as_array().for_each([&](auto& section_label) {
  119. for (auto& container : containers) {
  120. if (container.section_label() == section_label.as_string())
  121. new_child_order.append(container);
  122. }
  123. });
  124. // Are there any children that are not known to our persisted order?
  125. for (auto& container : containers) {
  126. // 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.
  127. if (!new_child_order.contains_slow(container))
  128. new_child_order.append(container);
  129. }
  130. // Rearrange child widgets to the defined order.
  131. auto childs = child_widgets();
  132. for (auto& child : childs) {
  133. if (new_child_order.contains_slow(child))
  134. child.remove_from_parent();
  135. }
  136. for (auto const& child : new_child_order)
  137. add_child(*child);
  138. } else {
  139. int persisted_state = Config::read_i32(config_domain(), "DynamicWidgetContainers"sv, section_label(), to_underlying(ViewState::Expanded));
  140. set_view_state(static_cast<ViewState>(persisted_state));
  141. }
  142. update();
  143. });
  144. }
  145. void DynamicWidgetContainer::set_section_label(String label)
  146. {
  147. m_section_label = move(label);
  148. m_label_widget->set_text(m_section_label);
  149. }
  150. void DynamicWidgetContainer::set_config_domain(String domain)
  151. {
  152. m_config_domain = move(domain);
  153. // 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.
  154. // We do not have such a method yet.
  155. restore_view_state();
  156. }
  157. void DynamicWidgetContainer::set_detached_size(Gfx::IntSize const size)
  158. {
  159. m_detached_size = { size };
  160. }
  161. void DynamicWidgetContainer::set_show_controls(bool value)
  162. {
  163. m_show_controls = value;
  164. m_controls_widget->set_visible(m_controls_widget->is_visible() && show_controls());
  165. update();
  166. }
  167. void DynamicWidgetContainer::set_container_with_individual_order(bool value)
  168. {
  169. m_is_container_with_individual_order = value;
  170. }
  171. void DynamicWidgetContainer::second_paint_event(PaintEvent&)
  172. {
  173. GUI::Painter painter(*this);
  174. painter.draw_line({ 0, height() - 1 }, { width(), height() - 1 }, palette().threed_shadow1());
  175. if (!m_is_dragging && !m_render_as_move_target)
  176. return;
  177. if (m_is_dragging) {
  178. // FIXME: Would be nice if we could paint outside our own boundaries.
  179. auto move_widget_indicator = rect().translated(m_current_mouse_position).translated(-m_drag_start_location);
  180. painter.fill_rect(move_widget_indicator, palette().rubber_band_fill());
  181. painter.draw_rect_with_thickness(move_widget_indicator, palette().rubber_band_border(), 1);
  182. } else if (m_render_as_move_target) {
  183. painter.fill_rect(rect(), palette().rubber_band_fill());
  184. painter.draw_rect_with_thickness({ rect().x(), rect().y(), rect().width() - 1, rect().height() - 1 }, palette().rubber_band_border(), 1);
  185. }
  186. }
  187. ErrorOr<void> DynamicWidgetContainer::detach_widgets()
  188. {
  189. if (!m_detached_widgets_window.has_value()) {
  190. auto detached_window = TRY(GUI::Window::try_create());
  191. detached_window->set_title(section_label().to_byte_string());
  192. detached_window->set_window_type(WindowType::Normal);
  193. if (has_detached_size())
  194. detached_window->resize(detached_size());
  195. else
  196. detached_window->resize(size());
  197. detached_window->center_on_screen();
  198. auto root_container = detached_window->set_main_widget<GUI::Frame>();
  199. root_container->set_fill_with_background_color(true);
  200. root_container->set_layout<GUI::VerticalBoxLayout>();
  201. root_container->set_frame_style(Gfx::FrameStyle::Window);
  202. auto transfer_children = [this](auto reciever, auto children) {
  203. for (NonnullRefPtr<GUI::Widget> widget : children) {
  204. if (widget == m_controls_widget)
  205. continue;
  206. widget->remove_from_parent();
  207. widget->set_visible(true);
  208. reciever->add_child(widget);
  209. }
  210. };
  211. transfer_children(root_container, child_widgets());
  212. detached_window->on_close = [this, root_container, transfer_children]() {
  213. transfer_children(this, root_container->child_widgets());
  214. set_view_state(ViewState::Expanded);
  215. unregister_open_window(m_detached_widgets_window.value());
  216. m_detached_widgets_window = {};
  217. };
  218. m_detached_widgets_window = detached_window;
  219. }
  220. register_open_window(m_detached_widgets_window.value());
  221. if (m_is_dragging)
  222. 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 }));
  223. m_detached_widgets_window.value()->show();
  224. return {};
  225. }
  226. void DynamicWidgetContainer::close_all_detached_windows()
  227. {
  228. for (auto window : DynamicWidgetContainer::s_open_windows.in_reverse())
  229. window->close();
  230. }
  231. void DynamicWidgetContainer::register_open_window(NonnullRefPtr<GUI::Window> window)
  232. {
  233. s_open_windows.append(window);
  234. }
  235. void DynamicWidgetContainer::unregister_open_window(NonnullRefPtr<GUI::Window> window)
  236. {
  237. Optional<size_t> match = s_open_windows.find_first_index(window);
  238. if (match.has_value())
  239. s_open_windows.remove(match.value());
  240. }
  241. void DynamicWidgetContainer::handle_mouseup_event(MouseEvent& event)
  242. {
  243. if (event.button() != MouseButton::Primary)
  244. return;
  245. if (m_is_dragging) {
  246. // If we dropped the widget outside of ourself, we would like to detach it.
  247. if (m_parent_container == nullptr && !rect().contains(event.position()))
  248. set_view_state(ViewState::Detached);
  249. if (m_parent_container != nullptr) {
  250. bool should_move_position = m_parent_container->check_has_move_target(relative_position().translated(m_current_mouse_position), MoveTargetOperation::ClearAllTargets);
  251. if (should_move_position)
  252. m_parent_container->swap_widget_positions(*this, relative_position().translated(m_current_mouse_position));
  253. else
  254. set_view_state(ViewState::Detached);
  255. }
  256. m_is_dragging = false;
  257. // Change the cursor back to normal after dragging is finished. Otherwise the cursor will only change if the mouse moves.
  258. m_label_widget->update_cursor(Gfx::StandardCursor::Arrow);
  259. update();
  260. }
  261. }
  262. void DynamicWidgetContainer::handle_mousemove_event(MouseEvent& event)
  263. {
  264. auto changed_cursor = m_is_dragging ? Gfx::StandardCursor::Move : Gfx::StandardCursor::Arrow;
  265. if (m_move_widget_knurl.contains(event.position()) && !m_is_dragging)
  266. changed_cursor = Gfx::StandardCursor::Hand;
  267. if (event.buttons() == MouseButton::Primary && !m_is_dragging) {
  268. m_is_dragging = true;
  269. m_drag_start_location = event.position();
  270. changed_cursor = Gfx::StandardCursor::Move;
  271. }
  272. if (m_is_dragging) {
  273. m_current_mouse_position = event.position();
  274. if (m_parent_container != nullptr) {
  275. m_parent_container->check_has_move_target(relative_position().translated(m_current_mouse_position), MoveTargetOperation::SetTarget);
  276. }
  277. update();
  278. }
  279. m_label_widget->update_cursor(changed_cursor);
  280. }
  281. void DynamicWidgetContainer::handle_doubleclick_event(MouseEvent& event)
  282. {
  283. if (event.button() != MouseButton::Primary)
  284. return;
  285. if (view_state() == ViewState::Expanded)
  286. set_view_state(ViewState::Collapsed);
  287. else if (view_state() == ViewState::Collapsed)
  288. set_view_state(ViewState::Expanded);
  289. }
  290. void DynamicWidgetContainer::resize_event(ResizeEvent&)
  291. {
  292. // Check if there is any content to display, and hide ourself if there would be nothing to display.
  293. // This allows us to make the whole section not taking up space if child-widget visibility is maintained outside.
  294. if (m_previous_frame_style.has_value() && height() != 0) {
  295. m_controls_widget->set_visible(show_controls());
  296. set_frame_style(m_previous_frame_style.value());
  297. m_previous_frame_style = {};
  298. // FIXME: Get rid of this, without the deferred invoke the lower part of the containing widget might not be drawn correctly :-/
  299. deferred_invoke([&]() {
  300. invalidate_layout();
  301. });
  302. }
  303. if (view_state() == ViewState::Expanded && !m_previous_frame_style.has_value() && height() == (content_margins().top() + content_margins().bottom() + m_controls_widget->height())) {
  304. m_controls_widget->set_visible(false);
  305. m_previous_frame_style = frame_style();
  306. set_frame_style(Gfx::FrameStyle::NoFrame);
  307. deferred_invoke([&]() {
  308. invalidate_layout();
  309. });
  310. }
  311. }
  312. void DynamicWidgetContainer::child_event(Core::ChildEvent& event)
  313. {
  314. if (event.type() == Event::ChildAdded && event.child() && is<GUI::DynamicWidgetContainer>(*event.child()))
  315. static_cast<DynamicWidgetContainer&>(*event.child()).set_parent_container(*this);
  316. GUI::Frame::child_event(event);
  317. }
  318. void DynamicWidgetContainer::set_parent_container(RefPtr<GUI::DynamicWidgetContainer> container)
  319. {
  320. m_parent_container = container;
  321. }
  322. bool DynamicWidgetContainer::check_has_move_target(Gfx::IntPoint relative_mouse_position, MoveTargetOperation operation)
  323. {
  324. bool matched = false;
  325. for_each_child_container([&](auto& child) {
  326. bool is_target = child.relative_rect().contains(relative_mouse_position);
  327. matched |= is_target;
  328. child.set_render_as_move_target(operation == MoveTargetOperation::SetTarget ? is_target : false);
  329. return IterationDecision::Continue;
  330. });
  331. return matched;
  332. }
  333. void DynamicWidgetContainer::set_render_as_move_target(bool is_target)
  334. {
  335. if (m_render_as_move_target == is_target)
  336. return;
  337. m_render_as_move_target = is_target;
  338. update();
  339. }
  340. void DynamicWidgetContainer::swap_widget_positions(NonnullRefPtr<Core::EventReceiver> source_widget, Gfx::IntPoint destination_positon)
  341. {
  342. Optional<NonnullRefPtr<Core::EventReceiver>> destination_widget;
  343. for_each_child_container([&](auto& child) {
  344. if (child.relative_rect().contains(destination_positon)) {
  345. destination_widget = child;
  346. return IterationDecision::Break;
  347. }
  348. return IterationDecision::Continue;
  349. });
  350. VERIFY(destination_widget.has_value());
  351. if (source_widget == destination_widget.value())
  352. return;
  353. auto source_position = children().find_first_index(*source_widget);
  354. auto destination_position = children().find_first_index(*destination_widget.value());
  355. VERIFY(source_position.has_value());
  356. VERIFY(destination_position.has_value());
  357. swap(children()[source_position.value()], children()[destination_position.value()]);
  358. // FIXME: Find a better solution to instantly display the new widget order.
  359. // invalidate_layout is not working :/
  360. auto childs = child_widgets();
  361. for (RefPtr<GUI::Widget> widget : childs) {
  362. widget->remove_from_parent();
  363. add_child(*widget);
  364. }
  365. if (!persist_state())
  366. return;
  367. JsonArray new_widget_order;
  368. for (auto& child : child_containers())
  369. new_widget_order.must_append(child.section_label());
  370. Config::write_string(config_domain(), "DynamicWidgetContainers"sv, section_label(), new_widget_order.serialized<StringBuilder>());
  371. }
  372. void DynamicWidgetContainer::update_control_button_visibility()
  373. {
  374. auto expand_button = m_controls_widget->find_descendant_of_type_named<GUI::Button>("expand_button");
  375. expand_button->set_visible(view_state() == ViewState::Collapsed);
  376. auto collapse_button = m_controls_widget->find_descendant_of_type_named<GUI::Button>("collapse_button");
  377. collapse_button->set_visible(view_state() == ViewState::Expanded);
  378. }
  379. }