Serenity Operating System
at master 258 lines 8.8 kB view raw
1/* 2 * Copyright (c) 2021, Fabian Blatz <fabianblatz@gmail.com> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "QuickLaunchWidget.h" 8#include <AK/LexicalPath.h> 9#include <AK/OwnPtr.h> 10#include <Kernel/API/InodeWatcherFlags.h> 11#include <LibConfig/Client.h> 12#include <LibCore/FileWatcher.h> 13#include <LibCore/MimeData.h> 14#include <LibCore/Process.h> 15#include <LibCore/System.h> 16#include <LibDesktop/Launcher.h> 17#include <LibGUI/BoxLayout.h> 18#include <LibGUI/FileIconProvider.h> 19#include <LibGUI/Menu.h> 20#include <LibGUI/MessageBox.h> 21#include <serenity.h> 22#include <sys/stat.h> 23 24namespace Taskbar { 25 26constexpr auto quick_launch = "QuickLaunch"sv; 27constexpr int quick_launch_button_size = 24; 28 29ErrorOr<void> QuickLaunchEntryAppFile::launch() const 30{ 31 auto executable = m_app_file->executable(); 32 33 pid_t pid = TRY(Core::System::fork()); 34 if (pid == 0) { 35 if (chdir(Core::StandardPaths::home_directory().characters()) < 0) { 36 perror("chdir"); 37 exit(1); 38 } 39 if (m_app_file->run_in_terminal()) 40 execl("/bin/Terminal", "Terminal", "-e", executable.characters(), nullptr); 41 else 42 execl(executable.characters(), executable.characters(), nullptr); 43 perror("execl"); 44 VERIFY_NOT_REACHED(); 45 } else 46 TRY(Core::System::disown(pid)); 47 return {}; 48} 49 50ErrorOr<void> QuickLaunchEntryExecutable::launch() const 51{ 52 TRY(Core::Process::spawn(m_path)); 53 return {}; 54} 55 56GUI::Icon QuickLaunchEntryExecutable::icon() const 57{ 58 return GUI::FileIconProvider::icon_for_executable(m_path); 59} 60 61DeprecatedString QuickLaunchEntryExecutable::name() const 62{ 63 return LexicalPath { m_path }.basename(); 64} 65 66ErrorOr<void> QuickLaunchEntryFile::launch() const 67{ 68 if (!Desktop::Launcher::open(URL::create_with_url_or_path(m_path))) { 69 // FIXME: LaunchServer doesn't inform us about errors 70 return Error::from_string_literal("Failed to open file"); 71 } 72 return {}; 73} 74 75GUI::Icon QuickLaunchEntryFile::icon() const 76{ 77 return GUI::FileIconProvider::icon_for_path(m_path); 78} 79 80DeprecatedString QuickLaunchEntryFile::name() const 81{ 82 // '=' is a special character in config files 83 return m_path; 84} 85 86ErrorOr<NonnullRefPtr<QuickLaunchWidget>> QuickLaunchWidget::create() 87{ 88 Vector<NonnullOwnPtr<QuickLaunchEntry>> entries; 89 auto keys = Config::list_keys("Taskbar"sv, quick_launch); 90 for (auto& name : keys) { 91 auto value = Config::read_string("Taskbar"sv, quick_launch, name); 92 auto entry = QuickLaunchEntry::create_from_config_value(value); 93 if (!entry) 94 continue; 95 96 entries.append(entry.release_nonnull()); 97 } 98 99 auto widget = TRY(AK::adopt_nonnull_ref_or_enomem(new (nothrow) QuickLaunchWidget())); 100 TRY(widget->create_context_menu()); 101 TRY(widget->add_quick_launch_buttons(move(entries))); 102 return widget; 103} 104 105QuickLaunchWidget::QuickLaunchWidget() 106{ 107 set_shrink_to_fit(true); 108 set_layout<GUI::HorizontalBoxLayout>(GUI::Margins {}, 0); 109 set_frame_thickness(0); 110 set_fixed_height(24); 111} 112 113ErrorOr<void> QuickLaunchWidget::create_context_menu() 114{ 115 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/delete.png"sv)); 116 m_context_menu = GUI::Menu::construct(); 117 m_context_menu_default_action = GUI::Action::create("&Remove", icon, [this](auto&) { 118 Config::remove_key("Taskbar"sv, quick_launch, m_context_menu_app_name); 119 auto button = find_child_of_type_named<GUI::Button>(m_context_menu_app_name); 120 if (button) { 121 remove_child(*button); 122 } 123 }); 124 m_context_menu->add_action(*m_context_menu_default_action); 125 126 return {}; 127} 128 129ErrorOr<void> QuickLaunchWidget::add_quick_launch_buttons(Vector<NonnullOwnPtr<QuickLaunchEntry>> entries) 130{ 131 for (auto& entry : entries) { 132 auto name = entry->name(); 133 TRY(add_or_adjust_button(name, move(entry))); 134 } 135 136 return {}; 137} 138 139OwnPtr<QuickLaunchEntry> QuickLaunchEntry::create_from_config_value(StringView value) 140{ 141 if (!value.starts_with('/') && value.ends_with(".af"sv)) { 142 auto af_path = DeprecatedString::formatted("{}/{}", Desktop::AppFile::APP_FILES_DIRECTORY, value); 143 return make<QuickLaunchEntryAppFile>(Desktop::AppFile::open(af_path)); 144 } 145 return create_from_path(value); 146} 147 148OwnPtr<QuickLaunchEntry> QuickLaunchEntry::create_from_path(StringView path) 149{ 150 if (path.ends_with(".af"sv)) 151 return make<QuickLaunchEntryAppFile>(Desktop::AppFile::open(path)); 152 auto stat_or_error = Core::System::stat(path); 153 if (stat_or_error.is_error()) { 154 dbgln("Failed to stat quick launch entry file: {}", stat_or_error.release_error()); 155 return {}; 156 } 157 158 auto stat = stat_or_error.release_value(); 159 if (S_ISREG(stat.st_mode) && (stat.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) 160 return make<QuickLaunchEntryExecutable>(path); 161 return make<QuickLaunchEntryFile>(path); 162} 163 164static DeprecatedString sanitize_entry_name(DeprecatedString const& name) 165{ 166 return name.replace(" "sv, ""sv, ReplaceMode::All).replace("="sv, ""sv, ReplaceMode::All); 167} 168 169ErrorOr<void> QuickLaunchWidget::add_or_adjust_button(DeprecatedString const& button_name, NonnullOwnPtr<QuickLaunchEntry>&& entry) 170{ 171 auto file_name_to_watch = entry->file_name_to_watch(); 172 if (!file_name_to_watch.is_null()) { 173 if (!m_watcher) { 174 m_watcher = TRY(Core::FileWatcher::create()); 175 m_watcher->on_change = [this](Core::FileWatcherEvent const& event) { 176 auto name = sanitize_entry_name(event.event_path); 177 dbgln("Removing QuickLaunch entry {}", name); 178 auto button = find_child_of_type_named<GUI::Button>(name); 179 if (button) 180 remove_child(*button); 181 }; 182 } 183 TRY(m_watcher->add_watch(file_name_to_watch, Core::FileWatcherEvent::Type::Deleted)); 184 } 185 186 auto button = find_child_of_type_named<GUI::Button>(button_name); 187 if (!button) 188 button = &add<GUI::Button>(); 189 190 button->set_fixed_size(quick_launch_button_size, quick_launch_button_size); 191 button->set_button_style(Gfx::ButtonStyle::Coolbar); 192 auto icon = entry->icon(); 193 button->set_icon(icon.bitmap_for_size(16)); 194 button->set_tooltip(entry->name()); 195 button->set_name(button_name); 196 button->on_click = [entry = move(entry), this](auto) { 197 auto result = entry->launch(); 198 if (result.is_error()) { 199 // FIXME: This message box is displayed in a weird position 200 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to open quick launch entry: {}", result.release_error())); 201 } 202 }; 203 button->on_context_menu_request = [this, button_name](auto& context_menu_event) { 204 m_context_menu_app_name = button_name; 205 m_context_menu->popup(context_menu_event.screen_position(), m_context_menu_default_action); 206 }; 207 208 return {}; 209} 210 211void QuickLaunchWidget::config_key_was_removed(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key) 212{ 213 if (domain == "Taskbar" && group == quick_launch) { 214 auto button = find_child_of_type_named<GUI::Button>(key); 215 if (button) 216 remove_child(*button); 217 } 218} 219 220void QuickLaunchWidget::config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value) 221{ 222 if (domain == "Taskbar" && group == quick_launch) { 223 auto entry = QuickLaunchEntry::create_from_config_value(value); 224 if (!entry) 225 return; 226 auto result = add_or_adjust_button(key, entry.release_nonnull()); 227 if (result.is_error()) 228 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to change quick launch entry: {}", result.release_error())); 229 } 230} 231 232void QuickLaunchWidget::drag_enter_event(GUI::DragEvent& event) 233{ 234 auto const& mime_types = event.mime_types(); 235 if (mime_types.contains_slow("text/uri-list")) 236 event.accept(); 237} 238 239void QuickLaunchWidget::drop_event(GUI::DropEvent& event) 240{ 241 event.accept(); 242 243 if (event.mime_data().has_urls()) { 244 auto urls = event.mime_data().urls(); 245 for (auto& url : urls) { 246 auto entry = QuickLaunchEntry::create_from_path(url.path()); 247 if (entry) { 248 auto item_name = sanitize_entry_name(entry->name()); 249 auto result = add_or_adjust_button(item_name, entry.release_nonnull()); 250 if (result.is_error()) 251 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to add quick launch entry: {}", result.release_error())); 252 Config::write_string("Taskbar"sv, quick_launch, item_name, url.path()); 253 } 254 } 255 } 256} 257 258}