Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "VBForm.h"
28#include "VBProperty.h"
29#include "VBWidget.h"
30#include "VBWidgetRegistry.h"
31#include <AK/JsonArray.h>
32#include <AK/JsonObject.h>
33#include <AK/StringBuilder.h>
34#include <LibCore/File.h>
35#include <LibGUI/Action.h>
36#include <LibGUI/BoxLayout.h>
37#include <LibGUI/Menu.h>
38#include <LibGUI/MessageBox.h>
39#include <LibGUI/Painter.h>
40
41static VBForm* s_current;
42VBForm* VBForm::current()
43{
44 return s_current;
45}
46
47VBForm::VBForm(const String& name)
48 : m_name(name)
49{
50 s_current = this;
51 set_fill_with_background_color(true);
52 set_greedy_for_hits(true);
53
54 m_context_menu = GUI::Menu::construct();
55 m_context_menu->add_action(GUI::CommonActions::make_move_to_front_action([this](auto&) {
56 if (auto* widget = single_selected_widget())
57 widget->gwidget()->move_to_front();
58 }));
59 m_context_menu->add_action(GUI::CommonActions::make_move_to_back_action([this](auto&) {
60 if (auto* widget = single_selected_widget())
61 widget->gwidget()->move_to_back();
62 }));
63 m_context_menu->add_separator();
64 m_context_menu->add_action(GUI::Action::create("Lay out horizontally", Gfx::Bitmap::load_from_file("/res/icons/16x16/layout-horizontally.png"), [this](auto&) {
65 if (auto* widget = single_selected_widget()) {
66 dbg() << "Giving " << *widget->gwidget() << " a horizontal box layout";
67 widget->gwidget()->set_layout<GUI::HorizontalBoxLayout>();
68 }
69 }));
70 m_context_menu->add_action(GUI::Action::create("Lay out vertically", Gfx::Bitmap::load_from_file("/res/icons/16x16/layout-vertically.png"), [this](auto&) {
71 if (auto* widget = single_selected_widget()) {
72 dbg() << "Giving " << *widget->gwidget() << " a vertical box layout";
73 widget->gwidget()->set_layout<GUI::VerticalBoxLayout>();
74 }
75 }));
76 m_context_menu->add_separator();
77 m_context_menu->add_action(GUI::CommonActions::make_delete_action([this](auto&) {
78 delete_selected_widgets();
79 }));
80}
81
82void VBForm::context_menu_event(GUI::ContextMenuEvent& event)
83{
84 m_context_menu->popup(event.screen_position());
85}
86
87void VBForm::insert_widget(VBWidgetType type)
88{
89 auto* insertion_parent = single_selected_widget();
90 auto widget = VBWidget::create(type, *this, insertion_parent);
91 Gfx::Point insertion_position = m_next_insertion_position;
92 if (insertion_parent)
93 insertion_position.move_by(insertion_parent->gwidget()->window_relative_rect().location());
94 widget->set_rect({ insertion_position, { m_grid_size * 10 + 1, m_grid_size * 5 + 1 } });
95 m_next_insertion_position.move_by(m_grid_size, m_grid_size);
96 m_widgets.append(move(widget));
97}
98
99VBForm::~VBForm()
100{
101}
102
103void VBForm::paint_event(GUI::PaintEvent& event)
104{
105 GUI::Painter painter(*this);
106 painter.add_clip_rect(event.rect());
107
108 for (int y = 0; y < height(); y += m_grid_size) {
109 for (int x = 0; x < width(); x += m_grid_size) {
110 painter.set_pixel({ x, y }, Color::from_rgb(0x404040));
111 }
112 }
113}
114
115void VBForm::second_paint_event(GUI::PaintEvent& event)
116{
117 GUI::Painter painter(*this);
118 painter.add_clip_rect(event.rect());
119
120 for (auto& widget : m_widgets) {
121 if (widget.is_selected()) {
122 for_each_direction([&](auto direction) {
123 bool in_layout = widget.is_in_layout();
124 auto grabber_rect = widget.grabber_rect(direction);
125 painter.fill_rect(grabber_rect, in_layout ? Color::White : Color::Black);
126 if (in_layout)
127 painter.draw_rect(grabber_rect, Color::Black);
128 });
129 }
130 }
131}
132
133bool VBForm::is_selected(const VBWidget& widget) const
134{
135 // FIXME: Fix HashTable and remove this const_cast.
136 return m_selected_widgets.contains(const_cast<VBWidget*>(&widget));
137}
138
139VBWidget* VBForm::widget_at(const Gfx::Point& position)
140{
141 auto result = hit_test(position, GUI::Widget::ShouldRespectGreediness::No);
142 if (!result.widget)
143 return nullptr;
144 auto* gwidget = result.widget;
145 while (gwidget) {
146 if (auto* widget = m_gwidget_map.get(gwidget).value_or(nullptr))
147 return widget;
148 gwidget = gwidget->parent_widget();
149 }
150 return nullptr;
151}
152
153void VBForm::grabber_mousedown_event(GUI::MouseEvent& event, Direction grabber)
154{
155 m_transform_event_origin = event.position();
156 for_each_selected_widget([](auto& widget) { widget.capture_transform_origin_rect(); });
157 m_resize_direction = grabber;
158}
159
160void VBForm::keydown_event(GUI::KeyEvent& event)
161{
162 if (event.key() == KeyCode::Key_Delete) {
163 delete_selected_widgets();
164 return;
165 }
166 if (event.key() == KeyCode::Key_Tab) {
167 if (m_widgets.is_empty())
168 return;
169 if (m_selected_widgets.is_empty()) {
170 set_single_selected_widget(&m_widgets.first());
171 update();
172 return;
173 }
174 size_t selected_widget_index = 0;
175 for (; selected_widget_index < m_widgets.size(); ++selected_widget_index) {
176 if (&m_widgets[selected_widget_index] == *m_selected_widgets.begin())
177 break;
178 }
179 ++selected_widget_index;
180 if (selected_widget_index == m_widgets.size())
181 selected_widget_index = 0;
182 set_single_selected_widget(&m_widgets[selected_widget_index]);
183 update();
184 return;
185 }
186 if (!m_selected_widgets.is_empty()) {
187 switch (event.key()) {
188 case KeyCode::Key_Up:
189 update();
190 for_each_selected_widget([this](auto& widget) {
191 if (widget.is_in_layout())
192 return;
193 widget.gwidget()->move_by(0, -m_grid_size);
194 });
195 break;
196 case KeyCode::Key_Down:
197 update();
198 for_each_selected_widget([this](auto& widget) {
199 if (widget.is_in_layout())
200 return;
201 widget.gwidget()->move_by(0, m_grid_size);
202 });
203 break;
204 case KeyCode::Key_Left:
205 update();
206 for_each_selected_widget([this](auto& widget) {
207 if (widget.is_in_layout())
208 return;
209 widget.gwidget()->move_by(-m_grid_size, 0);
210 });
211 break;
212 case KeyCode::Key_Right:
213 update();
214 for_each_selected_widget([this](auto& widget) {
215 if (widget.is_in_layout())
216 return;
217 widget.gwidget()->move_by(m_grid_size, 0);
218 });
219 break;
220 }
221 return;
222 }
223}
224
225void VBForm::set_single_selected_widget(VBWidget* widget)
226{
227 if (!widget) {
228 if (!m_selected_widgets.is_empty()) {
229 m_selected_widgets.clear();
230 on_widget_selected(nullptr);
231 update();
232 }
233 return;
234 }
235 m_selected_widgets.clear();
236 m_selected_widgets.set(widget);
237 on_widget_selected(m_selected_widgets.size() == 1 ? widget : nullptr);
238 update();
239}
240
241void VBForm::add_to_selection(VBWidget& widget)
242{
243 m_selected_widgets.set(&widget);
244 update();
245}
246
247void VBForm::remove_from_selection(VBWidget& widget)
248{
249 m_selected_widgets.remove(&widget);
250 update();
251}
252
253void VBForm::mousedown_event(GUI::MouseEvent& event)
254{
255 if (m_resize_direction == Direction::None) {
256 bool hit_grabber = false;
257 for_each_selected_widget([&](auto& widget) {
258 if (widget.is_in_layout())
259 return;
260 auto grabber = widget.grabber_at(event.position());
261 if (grabber != Direction::None) {
262 hit_grabber = true;
263 return grabber_mousedown_event(event, grabber);
264 }
265 });
266 if (hit_grabber)
267 return;
268 }
269 auto* widget = widget_at(event.position());
270 if (!widget) {
271 set_single_selected_widget(nullptr);
272 return;
273 }
274 if (event.button() == GUI::MouseButton::Left || event.button() == GUI::MouseButton::Right) {
275 m_transform_event_origin = event.position();
276 if (event.modifiers() == Mod_Ctrl)
277 remove_from_selection(*widget);
278 else if (event.modifiers() == Mod_Shift)
279 add_to_selection(*widget);
280 else if (!m_selected_widgets.contains(widget))
281 set_single_selected_widget(widget);
282 for_each_selected_widget([](auto& widget) { widget.capture_transform_origin_rect(); });
283 on_widget_selected(single_selected_widget());
284 }
285}
286
287void VBForm::mousemove_event(GUI::MouseEvent& event)
288{
289 if (event.buttons() & GUI::MouseButton::Left) {
290 if (m_resize_direction == Direction::None) {
291 update();
292 auto delta = event.position() - m_transform_event_origin;
293 for_each_selected_widget([&](auto& widget) {
294 if (widget.is_in_layout())
295 return;
296 auto new_rect = widget.transform_origin_rect().translated(delta);
297 new_rect.set_x(new_rect.x() - (new_rect.x() % m_grid_size));
298 new_rect.set_y(new_rect.y() - (new_rect.y() % m_grid_size));
299 widget.set_rect(new_rect);
300 });
301 return;
302 }
303 int diff_x = event.x() - m_transform_event_origin.x();
304 int diff_y = event.y() - m_transform_event_origin.y();
305
306 int change_x = 0;
307 int change_y = 0;
308 int change_w = 0;
309 int change_h = 0;
310
311 switch (m_resize_direction) {
312 case Direction::DownRight:
313 change_w = diff_x;
314 change_h = diff_y;
315 break;
316 case Direction::Right:
317 change_w = diff_x;
318 break;
319 case Direction::UpRight:
320 change_w = diff_x;
321 change_y = diff_y;
322 change_h = -diff_y;
323 break;
324 case Direction::Up:
325 change_y = diff_y;
326 change_h = -diff_y;
327 break;
328 case Direction::UpLeft:
329 change_x = diff_x;
330 change_w = -diff_x;
331 change_y = diff_y;
332 change_h = -diff_y;
333 break;
334 case Direction::Left:
335 change_x = diff_x;
336 change_w = -diff_x;
337 break;
338 case Direction::DownLeft:
339 change_x = diff_x;
340 change_w = -diff_x;
341 change_h = diff_y;
342 break;
343 case Direction::Down:
344 change_h = diff_y;
345 break;
346 default:
347 ASSERT_NOT_REACHED();
348 }
349
350 update();
351 for_each_selected_widget([&](auto& widget) {
352 if (widget.is_in_layout())
353 return;
354 auto new_rect = widget.transform_origin_rect();
355 Gfx::Size minimum_size { 5, 5 };
356 new_rect.set_x(new_rect.x() + change_x);
357 new_rect.set_y(new_rect.y() + change_y);
358 new_rect.set_width(max(minimum_size.width(), new_rect.width() + change_w));
359 new_rect.set_height(max(minimum_size.height(), new_rect.height() + change_h));
360 new_rect.set_x(new_rect.x() - (new_rect.x() % m_grid_size));
361 new_rect.set_y(new_rect.y() - (new_rect.y() % m_grid_size));
362 new_rect.set_width(new_rect.width() - (new_rect.width() % m_grid_size) + 1);
363 new_rect.set_height(new_rect.height() - (new_rect.height() % m_grid_size) + 1);
364 widget.set_rect(new_rect);
365 });
366
367 set_cursor_type_from_grabber(m_resize_direction);
368 } else {
369 for (auto& widget : m_selected_widgets) {
370 if (widget->is_in_layout())
371 continue;
372 auto grabber_at = widget->grabber_at(event.position());
373 set_cursor_type_from_grabber(grabber_at);
374 if (grabber_at != Direction::None)
375 break;
376 }
377 }
378}
379
380void VBForm::load_from_file(const String& path)
381{
382 auto file = Core::File::construct(path);
383 if (!file->open(Core::IODevice::ReadOnly)) {
384 GUI::MessageBox::show(String::format("Could not open '%s' for reading", path.characters()), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window());
385 return;
386 }
387
388 auto file_contents = file->read_all();
389 auto form_json = JsonValue::from_string(file_contents);
390
391 if (!form_json.is_object()) {
392 GUI::MessageBox::show(String::format("Could not parse '%s'", path.characters()), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window());
393 return;
394 }
395
396 m_name = form_json.as_object().get("name").to_string();
397 auto widgets = form_json.as_object().get("widgets").as_array();
398
399 widgets.for_each([&](const JsonValue& widget_value) {
400 auto& widget_object = widget_value.as_object();
401 auto widget_class = widget_object.get("class").as_string();
402 auto widget_type = widget_type_from_class_name(widget_class);
403 // FIXME: Construct VBWidget within the right parent..
404 auto vbwidget = VBWidget::create(widget_type, *this, nullptr);
405 widget_object.for_each_member([&](auto& property_name, const JsonValue& property_value) {
406 (void)property_name;
407 (void)property_value;
408 VBProperty& property = vbwidget->property(property_name);
409 dbgprintf("Set property %s.%s to '%s'\n", widget_class.characters(), property_name.characters(), property_value.to_string().characters());
410 property.set_value(property_value);
411 });
412 m_widgets.append(vbwidget);
413 });
414}
415
416void VBForm::write_to_file(const String& path)
417{
418 auto file = Core::File::construct(path);
419 if (!file->open(Core::IODevice::WriteOnly)) {
420 GUI::MessageBox::show(String::format("Could not open '%s' for writing", path.characters()), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window());
421 return;
422 }
423
424 JsonObject form_object;
425 form_object.set("name", m_name);
426 JsonArray widget_array;
427 for (auto& widget : m_widgets) {
428 JsonObject widget_object;
429 widget.for_each_property([&](auto& property) {
430 if (property.value().is_bool())
431 widget_object.set(property.name(), property.value().to_bool());
432 else if (property.value().is_i32())
433 widget_object.set(property.name(), property.value().to_i32());
434 else if (property.value().is_i64())
435 widget_object.set(property.name(), property.value().to_i64());
436 else
437 widget_object.set(property.name(), property.value().to_string());
438 });
439 widget_array.append(widget_object);
440 }
441 form_object.set("widgets", widget_array);
442 file->write(form_object.to_string());
443}
444
445void VBForm::dump()
446{
447 dbgprintf("[Form]\n");
448 dbgprintf("Name=%s\n", m_name.characters());
449 dbgprintf("\n");
450 int i = 0;
451 for (auto& widget : m_widgets) {
452 dbgprintf("[Widget %d]\n", i++);
453 widget.for_each_property([](auto& property) {
454 dbgprintf("%s=%s\n", property.name().characters(), property.value().to_string().characters());
455 });
456 dbgprintf("\n");
457 }
458}
459
460void VBForm::mouseup_event(GUI::MouseEvent& event)
461{
462 if (event.button() == GUI::MouseButton::Left) {
463 m_transform_event_origin = {};
464 m_resize_direction = Direction::None;
465 }
466}
467
468void VBForm::delete_selected_widgets()
469{
470 Vector<VBWidget*> to_delete;
471 for_each_selected_widget([&](auto& widget) {
472 to_delete.append(&widget);
473 });
474 if (to_delete.is_empty())
475 return;
476 for (auto& widget : to_delete)
477 m_widgets.remove_first_matching([&widget](auto& entry) { return entry == widget; });
478 on_widget_selected(single_selected_widget());
479 update();
480}
481
482template<typename Callback>
483void VBForm::for_each_selected_widget(Callback callback)
484{
485 for (auto& widget : m_selected_widgets)
486 callback(*widget);
487}
488
489void VBForm::set_cursor_type_from_grabber(Direction grabber)
490{
491 if (grabber == m_mouse_direction_type)
492 return;
493
494 switch (grabber) {
495 case Direction::Up:
496 case Direction::Down:
497 window()->set_override_cursor(GUI::StandardCursor::ResizeVertical);
498 break;
499 case Direction::Left:
500 case Direction::Right:
501 window()->set_override_cursor(GUI::StandardCursor::ResizeHorizontal);
502 break;
503 case Direction::UpLeft:
504 case Direction::DownRight:
505 window()->set_override_cursor(GUI::StandardCursor::ResizeDiagonalTLBR);
506 break;
507 case Direction::UpRight:
508 case Direction::DownLeft:
509 window()->set_override_cursor(GUI::StandardCursor::ResizeDiagonalBLTR);
510 break;
511 case Direction::None:
512 window()->set_override_cursor(GUI::StandardCursor::None);
513 break;
514 }
515
516 m_mouse_direction_type = grabber;
517}
518
519VBWidget* VBForm::single_selected_widget()
520{
521 if (m_selected_widgets.size() != 1)
522 return nullptr;
523 return *m_selected_widgets.begin();
524}