Serenity Operating System
1/*
2 * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/NeverDestroyed.h>
8#include <LibConfig/Client.h>
9#include <LibCore/EventLoop.h>
10#include <LibGUI/Action.h>
11#include <LibGUI/Application.h>
12#include <LibGUI/Clipboard.h>
13#include <LibGUI/ConnectionToWindowServer.h>
14#include <LibGUI/Desktop.h>
15#include <LibGUI/Label.h>
16#include <LibGUI/Menubar.h>
17#include <LibGUI/Painter.h>
18#include <LibGUI/Window.h>
19#include <LibGfx/Font/Font.h>
20#include <LibGfx/Palette.h>
21
22namespace GUI {
23
24class Application::TooltipWindow final : public Window {
25 C_OBJECT(TooltipWindow);
26
27public:
28 void set_tooltip(DeprecatedString const& tooltip)
29 {
30 m_label->set_text(Gfx::parse_ampersand_string(tooltip));
31 int tooltip_width = m_label->effective_min_size().width().as_int() + 10;
32 int line_count = m_label->text().count("\n"sv);
33 int font_size = m_label->font().pixel_size_rounded_up();
34 int tooltip_height = font_size * (1 + line_count) + ((font_size + 1) / 2) * line_count + 8;
35
36 Gfx::IntRect desktop_rect = Desktop::the().rect();
37 if (tooltip_width > desktop_rect.width())
38 tooltip_width = desktop_rect.width();
39
40 set_rect(rect().x(), rect().y(), tooltip_width, tooltip_height);
41 }
42
43private:
44 TooltipWindow()
45 {
46 set_window_type(WindowType::Tooltip);
47 set_obey_widget_min_size(false);
48 m_label = set_main_widget<Label>().release_value_but_fixme_should_propagate_errors();
49 m_label->set_background_role(Gfx::ColorRole::Tooltip);
50 m_label->set_foreground_role(Gfx::ColorRole::TooltipText);
51 m_label->set_fill_with_background_color(true);
52 m_label->set_frame_thickness(1);
53 m_label->set_frame_shape(Gfx::FrameShape::Container);
54 m_label->set_frame_shadow(Gfx::FrameShadow::Plain);
55 m_label->set_autosize(true);
56 }
57
58 RefPtr<Label> m_label;
59};
60
61static NeverDestroyed<WeakPtr<Application>> s_the;
62
63Application* Application::the()
64{
65 // NOTE: If we don't explicitly call revoke_weak_ptrs() in the
66 // ~Application destructor, we would have to change this to
67 // return s_the->strong_ref().ptr();
68 // This is because this is using the unsafe operator*/operator->
69 // that do not have the ability to check the ref count!
70 return *s_the;
71}
72
73Application::Application(int argc, char** argv, Core::EventLoop::MakeInspectable make_inspectable)
74{
75 VERIFY(!*s_the);
76 *s_the = *this;
77 m_event_loop = make<Core::EventLoop>(make_inspectable);
78 ConnectionToWindowServer::the();
79 Clipboard::initialize({});
80 if (argc > 0)
81 m_invoked_as = argv[0];
82
83 if (getenv("GUI_FOCUS_DEBUG"))
84 m_focus_debugging_enabled = true;
85
86 if (getenv("GUI_HOVER_DEBUG"))
87 m_hover_debugging_enabled = true;
88
89 if (getenv("GUI_DND_DEBUG"))
90 m_dnd_debugging_enabled = true;
91
92 for (int i = 1; i < argc; i++) {
93 DeprecatedString arg(argv[i]);
94 m_args.append(move(arg));
95 }
96
97 m_tooltip_show_timer = Core::Timer::create_single_shot(700, [this] {
98 request_tooltip_show();
99 }).release_value_but_fixme_should_propagate_errors();
100
101 m_tooltip_hide_timer = Core::Timer::create_single_shot(50, [this] {
102 tooltip_hide_timer_did_fire();
103 }).release_value_but_fixme_should_propagate_errors();
104}
105
106static bool s_in_teardown;
107
108bool Application::in_teardown()
109{
110 return s_in_teardown;
111}
112
113Application::~Application()
114{
115 s_in_teardown = true;
116 revoke_weak_ptrs();
117}
118
119int Application::exec()
120{
121 return m_event_loop->exec();
122}
123
124void Application::quit(int exit_code)
125{
126 m_event_loop->quit(exit_code);
127}
128
129void Application::register_global_shortcut_action(Badge<Action>, Action& action)
130{
131 m_global_shortcut_actions.set(action.shortcut(), &action);
132 m_global_shortcut_actions.set(action.alternate_shortcut(), &action);
133}
134
135void Application::unregister_global_shortcut_action(Badge<Action>, Action& action)
136{
137 m_global_shortcut_actions.remove(action.shortcut());
138 m_global_shortcut_actions.remove(action.alternate_shortcut());
139}
140
141Action* Application::action_for_shortcut(Shortcut const& shortcut) const
142{
143 auto it = m_global_shortcut_actions.find(shortcut);
144 if (it == m_global_shortcut_actions.end())
145 return nullptr;
146 return (*it).value;
147}
148
149void Application::show_tooltip(DeprecatedString tooltip, Widget const* tooltip_source_widget)
150{
151 if (!Desktop::the().system_effects().tooltips())
152 return;
153 m_tooltip_source_widget = tooltip_source_widget;
154 if (!m_tooltip_window) {
155 m_tooltip_window = TooltipWindow::construct();
156 m_tooltip_window->set_double_buffering_enabled(false);
157 }
158 m_tooltip_window->set_tooltip(move(tooltip));
159
160 if (m_tooltip_window->is_visible()) {
161 request_tooltip_show();
162 m_tooltip_show_timer->stop();
163 m_tooltip_hide_timer->stop();
164 } else {
165 m_tooltip_show_timer->restart();
166 m_tooltip_hide_timer->stop();
167 }
168}
169
170void Application::show_tooltip_immediately(DeprecatedString tooltip, Widget const* tooltip_source_widget)
171{
172 if (!Desktop::the().system_effects().tooltips())
173 return;
174 m_tooltip_source_widget = tooltip_source_widget;
175 if (!m_tooltip_window) {
176 m_tooltip_window = TooltipWindow::construct();
177 m_tooltip_window->set_double_buffering_enabled(false);
178 }
179 m_tooltip_window->set_tooltip(move(tooltip));
180
181 request_tooltip_show();
182 m_tooltip_show_timer->stop();
183 m_tooltip_hide_timer->stop();
184}
185
186void Application::hide_tooltip()
187{
188 m_tooltip_show_timer->stop();
189 m_tooltip_hide_timer->start();
190}
191
192void Application::did_create_window(Badge<Window>)
193{
194 if (m_event_loop->was_exit_requested())
195 m_event_loop->unquit();
196}
197
198void Application::did_delete_last_window(Badge<Window>)
199{
200 if (m_quit_when_last_window_deleted)
201 m_event_loop->quit(0);
202}
203
204void Application::set_system_palette(Core::AnonymousBuffer& buffer)
205{
206 if (!m_system_palette)
207 m_system_palette = Gfx::PaletteImpl::create_with_anonymous_buffer(buffer);
208 else
209 m_system_palette->replace_internal_buffer({}, buffer);
210
211 if (!m_palette)
212 m_palette = m_system_palette;
213}
214
215void Application::set_palette(Palette& palette)
216{
217 m_palette = palette.impl();
218}
219
220Gfx::Palette Application::palette() const
221{
222 return Palette(*m_palette);
223}
224
225void Application::request_tooltip_show()
226{
227 VERIFY(m_tooltip_window);
228 Gfx::IntRect desktop_rect = Desktop::the().rect();
229
230 int const margin = 30;
231 Gfx::IntPoint adjusted_pos = ConnectionToWindowServer::the().get_global_cursor_position();
232
233 adjusted_pos.translate_by(0, 14);
234
235 if (adjusted_pos.x() + m_tooltip_window->width() >= desktop_rect.width() - margin) {
236 adjusted_pos = adjusted_pos.translated(-m_tooltip_window->width(), 0);
237 }
238 if (adjusted_pos.y() + m_tooltip_window->height() >= desktop_rect.height() - margin) {
239 adjusted_pos = adjusted_pos.translated(0, -(m_tooltip_window->height() * 2));
240 }
241 if (adjusted_pos.x() < 0)
242 adjusted_pos.set_x(0);
243
244 m_tooltip_window->move_to(adjusted_pos);
245 m_tooltip_window->show();
246}
247
248void Application::tooltip_hide_timer_did_fire()
249{
250 m_tooltip_source_widget = nullptr;
251 if (m_tooltip_window)
252 m_tooltip_window->hide();
253}
254
255void Application::window_did_become_active(Badge<Window>, Window& window)
256{
257 m_active_window = window.make_weak_ptr<Window>();
258 window.update();
259}
260
261void Application::window_did_become_inactive(Badge<Window>, Window& window)
262{
263 if (m_active_window.ptr() != &window)
264 return;
265 window.update();
266 m_active_window = nullptr;
267}
268
269void Application::set_pending_drop_widget(Widget* widget)
270{
271 if (m_pending_drop_widget == widget)
272 return;
273 if (m_pending_drop_widget)
274 m_pending_drop_widget->update();
275 m_pending_drop_widget = widget;
276 if (m_pending_drop_widget)
277 m_pending_drop_widget->update();
278}
279
280void Application::set_drag_hovered_widget_impl(Widget* widget, Gfx::IntPoint position, Vector<DeprecatedString> mime_types)
281{
282 if (widget == m_drag_hovered_widget)
283 return;
284
285 if (m_drag_hovered_widget) {
286 Event leave_event(Event::DragLeave);
287 m_drag_hovered_widget->dispatch_event(leave_event, m_drag_hovered_widget->window());
288 }
289
290 set_pending_drop_widget(nullptr);
291 m_drag_hovered_widget = widget;
292
293 if (m_drag_hovered_widget) {
294 DragEvent enter_event(Event::DragEnter, position, move(mime_types));
295 enter_event.ignore();
296 m_drag_hovered_widget->dispatch_event(enter_event, m_drag_hovered_widget->window());
297 if (enter_event.is_accepted())
298 set_pending_drop_widget(m_drag_hovered_widget);
299 ConnectionToWindowServer::the().async_set_accepts_drag(enter_event.is_accepted());
300 }
301}
302
303void Application::notify_drag_cancelled(Badge<ConnectionToWindowServer>)
304{
305 set_drag_hovered_widget_impl(nullptr);
306}
307
308void Application::event(Core::Event& event)
309{
310 if (event.type() == GUI::Event::ActionEnter || event.type() == GUI::Event::ActionLeave) {
311 auto& action_event = static_cast<ActionEvent&>(event);
312 auto& action = action_event.action();
313 if (action_event.type() == GUI::Event::ActionEnter) {
314 if (on_action_enter)
315 on_action_enter(action);
316 } else {
317 if (on_action_leave)
318 on_action_leave(action);
319 }
320 }
321 if (event.type() == GUI::Event::ThemeChange) {
322 if (on_theme_change)
323 on_theme_change();
324 }
325 Object::event(event);
326}
327
328void Application::set_config_domain(String config_domain)
329{
330 m_config_domain = move(config_domain);
331}
332
333void Application::register_recent_file_actions(Badge<GUI::Menu>, Vector<NonnullRefPtr<GUI::Action>> actions)
334{
335 m_recent_file_actions = move(actions);
336 update_recent_file_actions();
337}
338
339void Application::update_recent_file_actions()
340{
341 VERIFY(!m_config_domain.is_empty());
342
343 size_t number_of_recently_open_files = 0;
344 auto update_action = [&](size_t index) {
345 auto& action = m_recent_file_actions[index];
346 char buffer = static_cast<char>('0' + index);
347 auto key = StringView(&buffer, 1);
348 auto path = Config::read_string(m_config_domain, "RecentFiles"sv, key);
349
350 if (path.is_empty()) {
351 action->set_visible(false);
352 action->set_enabled(false);
353 } else {
354 action->set_visible(true);
355 action->set_enabled(true);
356 action->set_text(path);
357 ++number_of_recently_open_files;
358 }
359 };
360 for (size_t i = 0; i < max_recently_open_files(); ++i)
361 update_action(i);
362
363 // Hide or show the "(No recently open files)" placeholder.
364 m_recent_file_actions.last()->set_visible(number_of_recently_open_files == 0);
365}
366
367void Application::set_most_recently_open_file(String new_path)
368{
369 Vector<DeprecatedString> new_recent_files_list;
370
371 for (size_t i = 0; i < max_recently_open_files(); ++i) {
372 static_assert(max_recently_open_files() < 10);
373 char buffer = static_cast<char>('0' + i);
374 auto key = StringView(&buffer, 1);
375 new_recent_files_list.append(Config::read_string(m_config_domain, "RecentFiles"sv, key));
376 }
377
378 new_recent_files_list.remove_all_matching([&](auto& existing_path) {
379 return existing_path.view() == new_path;
380 });
381
382 new_recent_files_list.prepend(new_path.to_deprecated_string());
383
384 for (size_t i = 0; i < max_recently_open_files(); ++i) {
385 auto& path = new_recent_files_list[i];
386 char buffer = static_cast<char>('0' + i);
387 auto key = StringView(&buffer, 1);
388 Config::write_string(
389 m_config_domain,
390 "RecentFiles"sv,
391 key,
392 path);
393 }
394
395 update_recent_file_actions();
396}
397
398}