Serenity Operating System
at master 213 lines 7.5 kB view raw
1/* 2 * Copyright (c) 2021, Nick Vella <nick@nxk.io> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "NewProjectDialog.h" 9#include "ProjectTemplatesModel.h" 10#include <DevTools/HackStudio/Dialogs/NewProjectDialogGML.h> 11#include <DevTools/HackStudio/ProjectTemplate.h> 12 13#include <AK/DeprecatedString.h> 14#include <AK/LexicalPath.h> 15#include <LibCore/DeprecatedFile.h> 16#include <LibCore/Directory.h> 17#include <LibGUI/BoxLayout.h> 18#include <LibGUI/Button.h> 19#include <LibGUI/FilePicker.h> 20#include <LibGUI/IconView.h> 21#include <LibGUI/Label.h> 22#include <LibGUI/MessageBox.h> 23#include <LibGUI/TextBox.h> 24#include <LibGUI/Widget.h> 25#include <LibRegex/Regex.h> 26 27namespace HackStudio { 28 29static Regex<PosixExtended> const s_project_name_validity_regex("^([A-Za-z0-9_-])*$"); 30 31GUI::Dialog::ExecResult NewProjectDialog::show(GUI::Window* parent_window) 32{ 33 auto dialog = NewProjectDialog::construct(parent_window); 34 35 if (parent_window) 36 dialog->set_icon(parent_window->icon()); 37 38 auto result = dialog->exec(); 39 40 return result; 41} 42 43NewProjectDialog::NewProjectDialog(GUI::Window* parent) 44 : Dialog(parent) 45 , m_model(ProjectTemplatesModel::create()) 46{ 47 resize(500, 385); 48 center_on_screen(); 49 set_resizable(false); 50 set_title("New project"); 51 52 auto main_widget = set_main_widget<GUI::Widget>().release_value_but_fixme_should_propagate_errors(); 53 main_widget->load_from_gml(new_project_dialog_gml).release_value_but_fixme_should_propagate_errors(); 54 55 m_icon_view_container = *main_widget->find_descendant_of_type_named<GUI::Widget>("icon_view_container"); 56 m_icon_view = m_icon_view_container->add<GUI::IconView>(); 57 m_icon_view->set_always_wrap_item_labels(true); 58 m_icon_view->set_model(m_model); 59 m_icon_view->set_model_column(ProjectTemplatesModel::Column::Name); 60 m_icon_view->on_selection_change = [&]() { 61 update_dialog(); 62 }; 63 m_icon_view->on_activation = [&](auto&) { 64 if (m_input_valid) 65 do_create_project(); 66 }; 67 68 m_description_label = *main_widget->find_descendant_of_type_named<GUI::Label>("description_label"); 69 m_name_input = *main_widget->find_descendant_of_type_named<GUI::TextBox>("name_input"); 70 m_name_input->on_change = [&]() { 71 update_dialog(); 72 }; 73 m_create_in_input = *main_widget->find_descendant_of_type_named<GUI::TextBox>("create_in_input"); 74 m_create_in_input->on_change = [&]() { 75 update_dialog(); 76 }; 77 m_full_path_label = *main_widget->find_descendant_of_type_named<GUI::Label>("full_path_label"); 78 79 m_ok_button = *main_widget->find_descendant_of_type_named<GUI::Button>("ok_button"); 80 m_ok_button->set_default(true); 81 m_ok_button->on_click = [this](auto) { 82 do_create_project(); 83 }; 84 85 m_cancel_button = *main_widget->find_descendant_of_type_named<GUI::Button>("cancel_button"); 86 m_cancel_button->on_click = [this](auto) { 87 done(ExecResult::Cancel); 88 }; 89 90 m_browse_button = *find_descendant_of_type_named<GUI::Button>("browse_button"); 91 m_browse_button->on_click = [this](auto) { 92 Optional<DeprecatedString> path = GUI::FilePicker::get_open_filepath(this, {}, Core::StandardPaths::home_directory(), true); 93 if (path.has_value()) 94 m_create_in_input->set_text(path.value().view()); 95 }; 96} 97 98RefPtr<ProjectTemplate> NewProjectDialog::selected_template() 99{ 100 if (m_icon_view->selection().is_empty()) { 101 return {}; 102 } 103 104 auto project_template = m_model->template_for_index(m_icon_view->selection().first()); 105 VERIFY(!project_template.is_null()); 106 107 return project_template; 108} 109 110void NewProjectDialog::update_dialog() 111{ 112 auto project_template = selected_template(); 113 m_input_valid = true; 114 115 if (project_template) { 116 m_description_label->set_text(project_template->description()); 117 } else { 118 m_description_label->set_text("Select a project template to continue."); 119 m_input_valid = false; 120 } 121 122 auto maybe_project_path = get_project_full_path(); 123 124 if (maybe_project_path.has_value()) { 125 m_full_path_label->set_text(maybe_project_path.value()); 126 } else { 127 m_full_path_label->set_text("Invalid name or creation directory."); 128 m_input_valid = false; 129 } 130 131 m_ok_button->set_enabled(m_input_valid); 132} 133 134Optional<DeprecatedString> NewProjectDialog::get_available_project_name() 135{ 136 auto create_in = m_create_in_input->text(); 137 auto chosen_name = m_name_input->text(); 138 139 // Ensure project name isn't empty or entirely whitespace 140 if (chosen_name.is_empty() || chosen_name.is_whitespace()) 141 return {}; 142 143 // Validate project name with validity regex 144 if (!s_project_name_validity_regex.has_match(chosen_name)) 145 return {}; 146 147 // Check for up-to 999 variations of the project name, in case it's already taken 148 for (int i = 0; i < 1000; i++) { 149 auto candidate = (i == 0) 150 ? chosen_name 151 : DeprecatedString::formatted("{}-{}", chosen_name, i); 152 153 if (!Core::DeprecatedFile::exists(DeprecatedString::formatted("{}/{}", create_in, candidate))) 154 return candidate; 155 } 156 157 return {}; 158} 159 160Optional<DeprecatedString> NewProjectDialog::get_project_full_path() 161{ 162 // Do not permit forward-slashes in project names 163 if (m_name_input->text().contains('/')) 164 return {}; 165 166 auto create_in = m_create_in_input->text(); 167 auto maybe_project_name = get_available_project_name(); 168 169 if (!maybe_project_name.has_value()) 170 return {}; 171 172 return LexicalPath::join(create_in, *maybe_project_name).string(); 173} 174 175void NewProjectDialog::do_create_project() 176{ 177 auto project_template = selected_template(); 178 if (!project_template) { 179 GUI::MessageBox::show_error(this, "Could not create project: no template selected."sv); 180 return; 181 } 182 183 auto maybe_project_name = get_available_project_name(); 184 auto maybe_project_full_path = get_project_full_path(); 185 if (!maybe_project_name.has_value() || !maybe_project_full_path.has_value()) { 186 GUI::MessageBox::show_error(this, "Could not create project: invalid project name or path."sv); 187 return; 188 } 189 190 auto create_in = m_create_in_input->text(); 191 if (!Core::DeprecatedFile::exists(create_in) || !Core::DeprecatedFile::is_directory(create_in)) { 192 auto result = GUI::MessageBox::show(this, DeprecatedString::formatted("The directory {} does not exist yet, would you like to create it?", create_in), "New project"sv, GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo); 193 if (result != GUI::MessageBox::ExecResult::Yes) 194 return; 195 196 auto created = Core::Directory::create(maybe_project_full_path.value(), Core::Directory::CreateDirectories::Yes); 197 if (created.is_error()) { 198 GUI::MessageBox::show_error(this, DeprecatedString::formatted("Could not create directory {}", create_in)); 199 return; 200 } 201 } 202 203 auto creation_result = project_template->create_project(maybe_project_name.value(), maybe_project_full_path.value()); 204 if (!creation_result.is_error()) { 205 // Successfully created, attempt to open the new project 206 m_created_project_path = maybe_project_full_path.value(); 207 done(ExecResult::OK); 208 } else { 209 GUI::MessageBox::show_error(this, DeprecatedString::formatted("Could not create project: {}", creation_result.error())); 210 } 211} 212 213}