mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 18:42:43 +00:00 
			
		
		
		
	HackStudio: Project templates and New Project dialog
This commit adds a simple project template system to HackStudio, as well as a pretty New Project dialog, inspired by early VS.NET and MS Office.
This commit is contained in:
		
							parent
							
								
									a6fdc17f3f
								
							
						
					
					
						commit
						b671577223
					
				
					 24 changed files with 1178 additions and 1 deletions
				
			
		
							
								
								
									
										5
									
								
								Base/res/devel/templates/cpp-basic.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Base/res/devel/templates/cpp-basic.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| [HackStudioTemplate] | ||||
| Name=Command-line Application (C++) | ||||
| Description=Template for creating a basic C++ command-line application. | ||||
| Priority=95 | ||||
| IconName32x=cpp-basic | ||||
							
								
								
									
										19
									
								
								Base/res/devel/templates/cpp-basic.postcreate
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Base/res/devel/templates/cpp-basic.postcreate
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| #!/bin/sh | ||||
| 
 | ||||
| echo "PROGRAM = $1" >> $2/Makefile | ||||
| echo "OBJS = main.o" >> $2/Makefile | ||||
| echo "CXXFLAGS = -g -std=c++2a" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "all: \$(PROGRAM)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "\$(PROGRAM): \$(OBJS)" >> $2/Makefile | ||||
| echo "	\$(CXX) -o \$@ \$(OBJS)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "%.o: %.cpp" >> $2/Makefile | ||||
| echo "	\$(CXX) \$(CXXFLAGS) -o \$@ -c \$< " >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "clean:" >> $2/Makefile | ||||
| echo "	rm \$(OBJS) \$(PROGRAM)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "run:" >> $2/Makefile | ||||
| echo "	./\$(PROGRAM)" >> $2/Makefile | ||||
							
								
								
									
										7
									
								
								Base/res/devel/templates/cpp-basic/main.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Base/res/devel/templates/cpp-basic/main.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| #include <stdio.h> | ||||
| 
 | ||||
| int main(int argc, char** argv) | ||||
| { | ||||
|     printf("Hello friends!\n"); | ||||
|     return 0; | ||||
| } | ||||
							
								
								
									
										5
									
								
								Base/res/devel/templates/cpp-gui.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Base/res/devel/templates/cpp-gui.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| [HackStudioTemplate] | ||||
| Name=Graphical Application (C++) | ||||
| Description=Template for creating a basic C++ graphical application. | ||||
| Priority=90 | ||||
| IconName32x=cpp-gui | ||||
							
								
								
									
										19
									
								
								Base/res/devel/templates/cpp-gui.postcreate
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Base/res/devel/templates/cpp-gui.postcreate
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| #!/bin/sh | ||||
| 
 | ||||
| echo "PROGRAM = $1" >> $2/Makefile | ||||
| echo "OBJS = main.o" >> $2/Makefile | ||||
| echo "CXXFLAGS = -lgui -g -std=c++2a" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "all: \$(PROGRAM)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "\$(PROGRAM): \$(OBJS)" >> $2/Makefile | ||||
| echo "	\$(CXX) \$(CXXFLAGS) -o \$@ \$(OBJS)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "%.o: %.cpp" >> $2/Makefile | ||||
| echo "	\$(CXX) \$(CXXFLAGS) -o \$@ -c \$< " >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "clean:" >> $2/Makefile | ||||
| echo "	rm \$(OBJS) \$(PROGRAM)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "run:" >> $2/Makefile | ||||
| echo "	./\$(PROGRAM)" >> $2/Makefile | ||||
							
								
								
									
										26
									
								
								Base/res/devel/templates/cpp-gui/main.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Base/res/devel/templates/cpp-gui/main.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| #include <stdio.h> | ||||
| #include <LibGUI/Application.h> | ||||
| #include <LibGUI/Window.h> | ||||
| #include <LibGUI/Button.h> | ||||
| #include <LibGUI/MessageBox.h> | ||||
| 
 | ||||
| int main(int argc, char** argv) | ||||
| { | ||||
|     auto app = GUI::Application::construct(argc, argv); | ||||
| 
 | ||||
|     auto window = GUI::Window::construct(); | ||||
|     window->set_title("Hello friends!"); | ||||
|     window->resize(200, 100); | ||||
| 
 | ||||
|     auto button = GUI::Button::construct(); | ||||
|     button->set_text("Click me!"); | ||||
|     button->on_click = [&](auto) { | ||||
|         GUI::MessageBox::show(window, "Hello friends!", ":^)"); | ||||
|     }; | ||||
| 
 | ||||
|     window->set_main_widget(button); | ||||
| 
 | ||||
|     window->show(); | ||||
| 
 | ||||
|     return app->exec(); | ||||
| } | ||||
							
								
								
									
										4
									
								
								Base/res/devel/templates/cpp-library.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								Base/res/devel/templates/cpp-library.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| [HackStudioTemplate] | ||||
| Name=Shared Library (C++) | ||||
| Description=Template for creating a C++ shared library. | ||||
| IconName32x=cpp-library | ||||
							
								
								
									
										49
									
								
								Base/res/devel/templates/cpp-library.postcreate
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								Base/res/devel/templates/cpp-library.postcreate
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| #!/bin/sh | ||||
| 
 | ||||
| # $1: Project name, filesystem safe | ||||
| # $2: Project full path | ||||
| # $3: Project name, namespace safe | ||||
| 
 | ||||
| # Generate Makefile | ||||
| echo "LIBRARY = $1.so" >> $2/Makefile | ||||
| echo "OBJS = Class1.o" >> $2/Makefile | ||||
| echo "CXXFLAGS = -g -std=c++2a" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "all: \$(LIBRARY)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "\$(LIBRARY): \$(OBJS)" >> $2/Makefile | ||||
| echo "	\$(CXX) -shared -o \$@ \$(OBJS)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "%.o: %.cpp" >> $2/Makefile | ||||
| echo "	\$(CXX) \$(CXXFLAGS) -fPIC -o \$@ -c \$< " >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| echo "clean:" >> $2/Makefile | ||||
| echo "	rm \$(OBJS) \$(LIBRARY)" >> $2/Makefile | ||||
| echo "" >> $2/Makefile | ||||
| 
 | ||||
| # Generate 'Class1' header file | ||||
| echo "#pragma once" >> $2/Class1.h | ||||
| echo "" >> $2/Class1.h | ||||
| echo "namespace $3 {" >> $2/Class1.h | ||||
| echo "" >> $2/Class1.h | ||||
| echo "class Class1 {" >> $2/Class1.h | ||||
| echo "public:" >> $2/Class1.h | ||||
| echo "    void hello();" >> $2/Class1.h | ||||
| echo "};" >> $2/Class1.h | ||||
| echo "" >> $2/Class1.h | ||||
| echo "}" >> $2/Class1.h | ||||
| echo "" >> $2/Class1.h | ||||
| 
 | ||||
| # Generate 'Class1' source file | ||||
| echo "#include \"Class1.h\"" >> $2/Class1.cpp | ||||
| echo "#include <stdio.h>" >> $2/Class1.cpp | ||||
| echo "" >> $2/Class1.cpp | ||||
| echo "namespace $3 {" >> $2/Class1.cpp | ||||
| echo "" >> $2/Class1.cpp | ||||
| echo "void Class1::hello()" >> $2/Class1.cpp | ||||
| echo "{" >> $2/Class1.cpp | ||||
| echo "    printf(\"Hello friends! :^)\\n\");" >> $2/Class1.cpp | ||||
| echo "}" >> $2/Class1.cpp | ||||
| echo "" >> $2/Class1.cpp | ||||
| echo "}" >> $2/Class1.cpp | ||||
| echo "" >> $2/Class1.cpp | ||||
							
								
								
									
										5
									
								
								Base/res/devel/templates/empty.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Base/res/devel/templates/empty.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| [HackStudioTemplate] | ||||
| Name=Empty Project | ||||
| Description=Template for creating an empty project with no files. | ||||
| Priority=100 | ||||
| IconName32x=empty | ||||
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-basic.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-basic.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 511 B | 
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-gui.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-gui.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-library.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-library.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/empty.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/empty.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.9 KiB | 
|  | @ -62,6 +62,7 @@ chmod 4750 mnt/bin/keymap | |||
| chown 0:$utmp_gid mnt/bin/utmpupdate | ||||
| chmod 2755 mnt/bin/utmpupdate | ||||
| chmod 600 mnt/etc/shadow | ||||
| chmod 755 mnt/res/devel/templates/*.postcreate | ||||
| echo "done" | ||||
| 
 | ||||
| printf "creating initial filesystem structure... " | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| add_subdirectory(LanguageServers) | ||||
| add_subdirectory(LanguageClients) | ||||
| 
 | ||||
| compile_gml(Dialogs/NewProjectDialog.gml Dialogs/NewProjectDialogGML.h new_project_dialog_gml) | ||||
| 
 | ||||
| set(SOURCES | ||||
|     CodeDocument.cpp | ||||
|     CursorTool.cpp | ||||
|  | @ -11,6 +13,9 @@ set(SOURCES | |||
|     Debugger/DisassemblyWidget.cpp | ||||
|     Debugger/RegistersModel.cpp | ||||
|     Debugger/VariablesModel.cpp | ||||
|     Dialogs/NewProjectDialog.cpp | ||||
|     Dialogs/NewProjectDialogGML.h | ||||
|     Dialogs/ProjectTemplatesModel.cpp | ||||
|     Editor.cpp | ||||
|     EditorWrapper.cpp | ||||
|     FindInFilesWidget.cpp | ||||
|  | @ -26,6 +31,7 @@ set(SOURCES | |||
|     Locator.cpp | ||||
|     Project.cpp | ||||
|     ProjectFile.cpp | ||||
|     ProjectTemplate.cpp | ||||
|     TerminalWrapper.cpp | ||||
|     WidgetTool.cpp | ||||
|     WidgetTreeModel.cpp | ||||
|  | @ -33,5 +39,5 @@ set(SOURCES | |||
| ) | ||||
| 
 | ||||
| serenity_app(HackStudio ICON app-hack-studio) | ||||
| target_link_libraries(HackStudio LibWeb LibMarkdown LibGUI LibCpp LibGfx LibCore LibVT LibDebug LibX86 LibDiff LibShell) | ||||
| target_link_libraries(HackStudio LibWeb LibMarkdown LibGUI LibCpp LibGfx LibCore LibVT LibDebug LibX86 LibDiff LibShell LibRegex) | ||||
| add_dependencies(HackStudio CppLanguageServer) | ||||
|  |  | |||
							
								
								
									
										244
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,244 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2021, Nick Vella <nick@nxk.io> | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  * modification, are permitted provided that the following conditions are met: | ||||
|  * | ||||
|  * 1. Redistributions of source code must retain the above copyright notice, this | ||||
|  *    list of conditions and the following disclaimer. | ||||
|  * | ||||
|  * 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|  *    this list of conditions and the following disclaimer in the documentation | ||||
|  *    and/or other materials provided with the distribution. | ||||
|  * | ||||
|  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
|  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
|  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
|  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
|  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
|  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
|  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
|  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #include "NewProjectDialog.h" | ||||
| #include "ProjectTemplatesModel.h" | ||||
| #include <DevTools/HackStudio/Dialogs/NewProjectDialogGML.h> | ||||
| #include <DevTools/HackStudio/ProjectTemplate.h> | ||||
| 
 | ||||
| #include <AK/LexicalPath.h> | ||||
| #include <AK/String.h> | ||||
| #include <LibCore/File.h> | ||||
| #include <LibGUI/BoxLayout.h> | ||||
| #include <LibGUI/Button.h> | ||||
| #include <LibGUI/FilePicker.h> | ||||
| #include <LibGUI/IconView.h> | ||||
| #include <LibGUI/Label.h> | ||||
| #include <LibGUI/MessageBox.h> | ||||
| #include <LibGUI/RadioButton.h> | ||||
| #include <LibGUI/TextBox.h> | ||||
| #include <LibGUI/Widget.h> | ||||
| #include <LibGfx/Font.h> | ||||
| #include <LibGfx/FontDatabase.h> | ||||
| #include <LibRegex/Regex.h> | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| 
 | ||||
| static const Regex<PosixExtended> s_project_name_validity_regex("^([A-Za-z0-9_-])*$"); | ||||
| 
 | ||||
| int NewProjectDialog::show(GUI::Window* parent_window) | ||||
| { | ||||
|     auto dialog = NewProjectDialog::construct(parent_window); | ||||
| 
 | ||||
|     if (parent_window) | ||||
|         dialog->set_icon(parent_window->icon()); | ||||
| 
 | ||||
|     auto result = dialog->exec(); | ||||
| 
 | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| NewProjectDialog::NewProjectDialog(GUI::Window* parent) | ||||
|     : Dialog(parent) | ||||
|     , m_model(ProjectTemplatesModel::create()) | ||||
| { | ||||
|     resize(500, 385); | ||||
|     center_on_screen(); | ||||
|     set_resizable(false); | ||||
|     set_modal(true); | ||||
|     set_title("New project"); | ||||
| 
 | ||||
|     auto& main_widget = set_main_widget<GUI::Widget>(); | ||||
|     main_widget.load_from_gml(new_project_dialog_gml); | ||||
| 
 | ||||
|     m_icon_view_container = *main_widget.find_descendant_of_type_named<GUI::Widget>("icon_view_container"); | ||||
|     m_icon_view = m_icon_view_container->add<GUI::IconView>(); | ||||
|     m_icon_view->set_always_wrap_item_labels(true); | ||||
|     m_icon_view->set_model(m_model); | ||||
|     m_icon_view->set_model_column(ProjectTemplatesModel::Column::Name); | ||||
|     m_icon_view->on_selection_change = [&]() { | ||||
|         update_dialog(); | ||||
|     }; | ||||
|     m_icon_view->on_activation = [&]() { | ||||
|         if (m_input_valid) | ||||
|             do_create_project(); | ||||
|     }; | ||||
| 
 | ||||
|     m_description_label = *main_widget.find_descendant_of_type_named<GUI::Label>("description_label"); | ||||
|     m_name_input = *main_widget.find_descendant_of_type_named<GUI::TextBox>("name_input"); | ||||
|     m_name_input->on_change = [&]() { | ||||
|         update_dialog(); | ||||
|     }; | ||||
|     m_name_input->on_return_pressed = [&]() { | ||||
|         if (m_input_valid) | ||||
|             do_create_project(); | ||||
|     }; | ||||
|     m_create_in_input = *main_widget.find_descendant_of_type_named<GUI::TextBox>("create_in_input"); | ||||
|     m_create_in_input->on_change = [&]() { | ||||
|         update_dialog(); | ||||
|     }; | ||||
|     m_create_in_input->on_return_pressed = [&]() { | ||||
|         if (m_input_valid) | ||||
|             do_create_project(); | ||||
|     }; | ||||
|     m_full_path_label = *main_widget.find_descendant_of_type_named<GUI::Label>("full_path_label"); | ||||
| 
 | ||||
|     m_ok_button = *main_widget.find_descendant_of_type_named<GUI::Button>("ok_button"); | ||||
|     m_ok_button->on_click = [this](auto) { | ||||
|         do_create_project(); | ||||
|     }; | ||||
| 
 | ||||
|     m_cancel_button = *main_widget.find_descendant_of_type_named<GUI::Button>("cancel_button"); | ||||
|     m_cancel_button->on_click = [this](auto) { | ||||
|         done(ExecResult::ExecCancel); | ||||
|     }; | ||||
| 
 | ||||
|     m_browse_button = *find_descendant_of_type_named<GUI::Button>("browse_button"); | ||||
|     m_browse_button->on_click = [this](auto) { | ||||
|         Optional<String> path = GUI::FilePicker::get_open_filepath(this); | ||||
|         if (path.has_value()) | ||||
|             m_create_in_input->set_text(path.value().view()); | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| NewProjectDialog::~NewProjectDialog() | ||||
| { | ||||
| } | ||||
| 
 | ||||
| RefPtr<ProjectTemplate> NewProjectDialog::selected_template() | ||||
| { | ||||
|     if (m_icon_view->selection().is_empty()) { | ||||
|         return {}; | ||||
|     } | ||||
| 
 | ||||
|     auto project_template = m_model->template_for_index(m_icon_view->selection().first()); | ||||
|     ASSERT(!project_template.is_null()); | ||||
| 
 | ||||
|     return project_template; | ||||
| } | ||||
| 
 | ||||
| void NewProjectDialog::update_dialog() | ||||
| { | ||||
|     auto project_template = selected_template(); | ||||
|     m_input_valid = true; | ||||
| 
 | ||||
|     if (project_template) { | ||||
|         m_description_label->set_text(project_template->description()); | ||||
|     } else { | ||||
|         m_description_label->set_text("Select a project template to continue."); | ||||
|         m_input_valid = false; | ||||
|     } | ||||
| 
 | ||||
|     auto maybe_project_path = get_project_full_path(); | ||||
| 
 | ||||
|     if (maybe_project_path.has_value()) { | ||||
|         m_full_path_label->set_text(maybe_project_path.value()); | ||||
|     } else { | ||||
|         m_full_path_label->set_text("Invalid name or creation directory."); | ||||
|         m_input_valid = false; | ||||
|     } | ||||
| 
 | ||||
|     m_ok_button->set_enabled(m_input_valid); | ||||
| } | ||||
| 
 | ||||
| Optional<String> NewProjectDialog::get_available_project_name() | ||||
| { | ||||
|     auto create_in = m_create_in_input->text(); | ||||
|     auto chosen_name = m_name_input->text(); | ||||
| 
 | ||||
|     // Ensure project name isn't empty or entirely whitespace
 | ||||
|     if (chosen_name.is_empty() || chosen_name.is_whitespace()) | ||||
|         return {}; | ||||
| 
 | ||||
|     // Validate project name with validity regex
 | ||||
|     if (!s_project_name_validity_regex.has_match(chosen_name)) | ||||
|         return {}; | ||||
| 
 | ||||
|     if (!Core::File::exists(create_in) || !Core::File::is_directory(create_in)) | ||||
|         return {}; | ||||
| 
 | ||||
|     // Check for up-to 999 variations of the project name, in case it's already taken
 | ||||
|     for (int i = 0; i < 1000; i++) { | ||||
|         auto candidate = (i == 0) | ||||
|             ? chosen_name | ||||
|             : String::formatted("{}-{}", chosen_name, i); | ||||
| 
 | ||||
|         if (!Core::File::exists(String::formatted("{}/{}", create_in, candidate))) | ||||
|             return candidate; | ||||
|     } | ||||
| 
 | ||||
|     return {}; | ||||
| } | ||||
| 
 | ||||
| Optional<String> NewProjectDialog::get_project_full_path() | ||||
| { | ||||
|     // Do not permit forward-slashes in project names
 | ||||
|     if (m_name_input->text().contains("/")) | ||||
|         return {}; | ||||
| 
 | ||||
|     auto create_in = m_create_in_input->text(); | ||||
|     auto maybe_project_name = get_available_project_name(); | ||||
| 
 | ||||
|     if (!maybe_project_name.has_value()) { | ||||
|         return {}; | ||||
|     } | ||||
| 
 | ||||
|     auto project_name = maybe_project_name.value(); | ||||
|     auto full_path = LexicalPath(String::formatted("{}/{}", create_in, project_name)); | ||||
| 
 | ||||
|     // Do not permit otherwise invalid paths.
 | ||||
|     if (!full_path.is_valid()) | ||||
|         return {}; | ||||
| 
 | ||||
|     return full_path.string(); | ||||
| } | ||||
| 
 | ||||
| void NewProjectDialog::do_create_project() | ||||
| { | ||||
|     auto project_template = selected_template(); | ||||
|     if (!project_template) { | ||||
|         GUI::MessageBox::show_error(this, "Could not create project: no template selected."); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     auto maybe_project_name = get_available_project_name(); | ||||
|     auto maybe_project_full_path = get_project_full_path(); | ||||
|     if (!maybe_project_name.has_value() || !maybe_project_full_path.has_value()) { | ||||
|         GUI::MessageBox::show_error(this, "Could not create project: invalid project name or path."); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     auto creation_result = project_template->create_project(maybe_project_name.value(), maybe_project_full_path.value()); | ||||
|     if (!creation_result.is_error()) { | ||||
|         // Succesfully created, attempt to open the new project
 | ||||
|         m_created_project_path = maybe_project_full_path.value(); | ||||
|         done(ExecResult::ExecOK); | ||||
|     } else { | ||||
|         GUI::MessageBox::show_error(this, String::formatted("Could not create project: {}", creation_result.error())); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										115
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | |||
| @GUI::Widget { | ||||
|     fill_with_background_color: true | ||||
| 
 | ||||
|     layout: @GUI::VerticalBoxLayout { | ||||
|         margins: [4, 4, 4, 4] | ||||
|     } | ||||
| 
 | ||||
|     @GUI::Label { | ||||
|         text: "Templates:" | ||||
|         text_alignment: "CenterLeft" | ||||
|         max_height: 20 | ||||
|     } | ||||
| 
 | ||||
|     @GUI::Widget { | ||||
|         layout: @GUI::VerticalBoxLayout { | ||||
|         } | ||||
| 
 | ||||
|         name: "icon_view_container" | ||||
|     } | ||||
| 
 | ||||
|     @GUI::Label { | ||||
|         name: "description_label" | ||||
|         text_alignment: "CenterLeft" | ||||
|         thickness: 2 | ||||
|         shadow: "Sunken" | ||||
|         shape: "Container" | ||||
|         max_height: 24 | ||||
|     } | ||||
| 
 | ||||
|     @GUI::Widget { | ||||
|         layout: @GUI::HorizontalBoxLayout { | ||||
|         } | ||||
|          | ||||
|         max_height: 24 | ||||
| 
 | ||||
|         @GUI::Label { | ||||
|             text: "Name:" | ||||
|             text_alignment: "CenterLeft" | ||||
|             max_width: 75 | ||||
|         } | ||||
| 
 | ||||
|         @GUI::TextBox { | ||||
|             name: "name_input" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     @GUI::Widget { | ||||
|         layout: @GUI::HorizontalBoxLayout { | ||||
|         } | ||||
|          | ||||
|         max_height: 24 | ||||
| 
 | ||||
|         @GUI::Label { | ||||
|             text: "Create in:" | ||||
|             text_alignment: "CenterLeft" | ||||
|             max_width: 75 | ||||
|         } | ||||
| 
 | ||||
|         @GUI::TextBox { | ||||
|             name: "create_in_input" | ||||
|             text: "/home/anon/Source" | ||||
|         } | ||||
|          | ||||
|         @GUI::Button { | ||||
|             name: "browse_button" | ||||
|             text: "Browse" | ||||
|             max_width: 75 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @GUI::Widget { | ||||
|         layout: @GUI::HorizontalBoxLayout { | ||||
|         } | ||||
|          | ||||
|         max_height: 24 | ||||
| 
 | ||||
|         @GUI::Label { | ||||
|             text: "Full path:" | ||||
|             text_alignment: "CenterLeft" | ||||
|             max_width: 75 | ||||
|         } | ||||
| 
 | ||||
|         @GUI::Label { | ||||
|             name: "full_path_label" | ||||
|             text_alignment: "CenterLeft" | ||||
|             text: "" | ||||
|             thickness: 2 | ||||
|             shadow: "Sunken" | ||||
|             shape: "Container" | ||||
|             max_height: 22 | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     @GUI::Widget { | ||||
|         layout: @GUI::HorizontalBoxLayout { | ||||
|         } | ||||
|          | ||||
|         max_height: 24 | ||||
| 
 | ||||
|         @GUI::Widget { | ||||
|         } | ||||
|          | ||||
|         @GUI::Button { | ||||
|             name: "ok_button" | ||||
|             text: "OK" | ||||
|             max_width: 75 | ||||
|         } | ||||
|          | ||||
|         @GUI::Button { | ||||
|             name: "cancel_button" | ||||
|             text: "Cancel" | ||||
|             max_width: 75 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										79
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2021, Nick Vella <nick@nxk.io> | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  * modification, are permitted provided that the following conditions are met: | ||||
|  * | ||||
|  * 1. Redistributions of source code must retain the above copyright notice, this | ||||
|  *    list of conditions and the following disclaimer. | ||||
|  * | ||||
|  * 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|  *    this list of conditions and the following disclaimer in the documentation | ||||
|  *    and/or other materials provided with the distribution. | ||||
|  * | ||||
|  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
|  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
|  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
|  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
|  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
|  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
|  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
|  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "ProjectTemplatesModel.h" | ||||
| #include <DevTools/HackStudio/ProjectTemplate.h> | ||||
| 
 | ||||
| #include <AK/Result.h> | ||||
| #include <AK/Vector.h> | ||||
| #include <LibGUI/Button.h> | ||||
| #include <LibGUI/Dialog.h> | ||||
| #include <LibGUI/Label.h> | ||||
| #include <LibGUI/TextBox.h> | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| 
 | ||||
| class NewProjectDialog : public GUI::Dialog { | ||||
|     C_OBJECT(NewProjectDialog); | ||||
| 
 | ||||
| public: | ||||
|     static int show(GUI::Window* parent_window); | ||||
| 
 | ||||
|     Optional<String> created_project_path() const { return m_created_project_path; } | ||||
| 
 | ||||
| private: | ||||
|     NewProjectDialog(GUI::Window* parent); | ||||
|     virtual ~NewProjectDialog() override; | ||||
| 
 | ||||
|     void update_dialog(); | ||||
|     Optional<String> get_available_project_name(); | ||||
|     Optional<String> get_project_full_path(); | ||||
| 
 | ||||
|     void do_create_project(); | ||||
| 
 | ||||
|     RefPtr<ProjectTemplate> selected_template(); | ||||
| 
 | ||||
|     NonnullRefPtr<ProjectTemplatesModel> m_model; | ||||
|     bool m_input_valid { false }; | ||||
| 
 | ||||
|     RefPtr<GUI::Widget> m_icon_view_container; | ||||
|     RefPtr<GUI::IconView> m_icon_view; | ||||
| 
 | ||||
|     RefPtr<GUI::Label> m_description_label; | ||||
|     RefPtr<GUI::TextBox> m_name_input; | ||||
|     RefPtr<GUI::TextBox> m_create_in_input; | ||||
|     RefPtr<GUI::Label> m_full_path_label; | ||||
| 
 | ||||
|     RefPtr<GUI::Button> m_ok_button; | ||||
|     RefPtr<GUI::Button> m_cancel_button; | ||||
|     RefPtr<GUI::Button> m_browse_button; | ||||
| 
 | ||||
|     Optional<String> m_created_project_path; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										156
									
								
								Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,156 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2021, Nick Vella <nick@nxk.io> | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  * modification, are permitted provided that the following conditions are met: | ||||
|  * | ||||
|  * 1. Redistributions of source code must retain the above copyright notice, this | ||||
|  *    list of conditions and the following disclaimer. | ||||
|  * | ||||
|  * 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|  *    this list of conditions and the following disclaimer in the documentation | ||||
|  *    and/or other materials provided with the distribution. | ||||
|  * | ||||
|  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
|  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
|  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
|  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
|  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
|  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
|  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
|  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #include "ProjectTemplatesModel.h" | ||||
| 
 | ||||
| #include <AK/LexicalPath.h> | ||||
| #include <AK/QuickSort.h> | ||||
| #include <LibCore/DirIterator.h> | ||||
| #include <LibGUI/Icon.h> | ||||
| #include <LibGUI/Variant.h> | ||||
| #include <LibGfx/TextAlignment.h> | ||||
| #include <ctype.h> | ||||
| #include <stdio.h> | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| 
 | ||||
| ProjectTemplatesModel::ProjectTemplatesModel() | ||||
|     : m_templates() | ||||
|     , m_mapping() | ||||
| { | ||||
|     auto watcher_or_error = Core::FileWatcher::watch(ProjectTemplate::templates_path()); | ||||
|     if (!watcher_or_error.is_error()) { | ||||
|         m_file_watcher = watcher_or_error.release_value(); | ||||
|         m_file_watcher->on_change = [&](auto) { | ||||
|             update(); | ||||
|         }; | ||||
|     } else { | ||||
|         warnln("Unable to watch templates directory, templates will not automatically refresh. Error: {}", watcher_or_error.error()); | ||||
|     } | ||||
| 
 | ||||
|     rescan_templates(); | ||||
| } | ||||
| 
 | ||||
| ProjectTemplatesModel::~ProjectTemplatesModel() | ||||
| { | ||||
| } | ||||
| 
 | ||||
| int ProjectTemplatesModel::row_count(const GUI::ModelIndex&) const | ||||
| { | ||||
|     return m_mapping.size(); | ||||
| } | ||||
| 
 | ||||
| int ProjectTemplatesModel::column_count(const GUI::ModelIndex&) const | ||||
| { | ||||
|     return Column::__Count; | ||||
| } | ||||
| 
 | ||||
| String ProjectTemplatesModel::column_name(int column) const | ||||
| { | ||||
|     switch (column) { | ||||
|     case Column::Icon: | ||||
|         return "Icon"; | ||||
|     case Column::Id: | ||||
|         return "ID"; | ||||
|     case Column::Name: | ||||
|         return "Name"; | ||||
|     } | ||||
|     ASSERT_NOT_REACHED(); | ||||
| } | ||||
| 
 | ||||
| GUI::Variant ProjectTemplatesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const | ||||
| { | ||||
|     if (static_cast<size_t>(index.row()) >= m_mapping.size()) | ||||
|         return {}; | ||||
| 
 | ||||
|     if (role == GUI::ModelRole::TextAlignment) | ||||
|         return Gfx::TextAlignment::CenterLeft; | ||||
| 
 | ||||
|     if (role == GUI::ModelRole::Display) { | ||||
|         switch (index.column()) { | ||||
|         case Column::Name: | ||||
|             return m_mapping[index.row()]->name(); | ||||
|         case Column::Id: | ||||
|             return m_mapping[index.row()]->id(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (role == GUI::ModelRole::Icon) { | ||||
|         return m_mapping[index.row()]->icon(); | ||||
|     } | ||||
| 
 | ||||
|     return {}; | ||||
| } | ||||
| 
 | ||||
| RefPtr<ProjectTemplate> ProjectTemplatesModel::template_for_index(const GUI::ModelIndex& index) | ||||
| { | ||||
|     if (static_cast<size_t>(index.row()) >= m_mapping.size()) | ||||
|         return {}; | ||||
| 
 | ||||
|     return m_mapping[index.row()]; | ||||
| } | ||||
| 
 | ||||
| void ProjectTemplatesModel::update() | ||||
| { | ||||
|     rescan_templates(); | ||||
|     did_update(); | ||||
| } | ||||
| 
 | ||||
| void ProjectTemplatesModel::rescan_templates() | ||||
| { | ||||
|     m_templates.clear(); | ||||
| 
 | ||||
|     // Iterate over template manifest INI files in the templates path
 | ||||
|     Core::DirIterator di(ProjectTemplate::templates_path(), Core::DirIterator::SkipDots); | ||||
|     if (di.has_error()) { | ||||
|         warnln("DirIterator: {}", di.error_string()); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     while (di.has_next()) { | ||||
|         auto full_path = LexicalPath(di.next_full_path()); | ||||
|         if (!full_path.has_extension(".ini")) | ||||
|             continue; | ||||
| 
 | ||||
|         auto project_template = ProjectTemplate::load_from_manifest(full_path.string()); | ||||
|         if (!project_template) { | ||||
|             warnln("Template manifest {} is invalid.", full_path.string()); | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         m_templates.append(project_template.release_nonnull()); | ||||
|     } | ||||
| 
 | ||||
|     // Enumerate the loaded projects into a sorted mapping, by priority value descending.
 | ||||
|     m_mapping.clear(); | ||||
|     for (auto& project_template : m_templates) | ||||
|         m_mapping.append(&project_template); | ||||
|     quick_sort(m_mapping, [](auto a, auto b) { | ||||
|         return a->priority() > b->priority(); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										73
									
								
								Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2021, Nick Vella <nick@nxk.io> | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  * modification, are permitted provided that the following conditions are met: | ||||
|  * | ||||
|  * 1. Redistributions of source code must retain the above copyright notice, this | ||||
|  *    list of conditions and the following disclaimer. | ||||
|  * | ||||
|  * 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|  *    this list of conditions and the following disclaimer in the documentation | ||||
|  *    and/or other materials provided with the distribution. | ||||
|  * | ||||
|  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
|  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
|  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
|  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
|  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
|  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
|  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
|  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <AK/NonnullPtrVector.h> | ||||
| #include <AK/RefPtr.h> | ||||
| #include <AK/WeakPtr.h> | ||||
| #include <DevTools/HackStudio/ProjectTemplate.h> | ||||
| #include <LibCore/FileWatcher.h> | ||||
| #include <LibGUI/Model.h> | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| 
 | ||||
| class ProjectTemplatesModel final : public GUI::Model { | ||||
| public: | ||||
|     static NonnullRefPtr<ProjectTemplatesModel> create() | ||||
|     { | ||||
|         return adopt(*new ProjectTemplatesModel()); | ||||
|     } | ||||
| 
 | ||||
|     enum Column { | ||||
|         Icon = 0, | ||||
|         Id, | ||||
|         Name, | ||||
|         __Count | ||||
|     }; | ||||
| 
 | ||||
|     virtual ~ProjectTemplatesModel() override; | ||||
| 
 | ||||
|     RefPtr<ProjectTemplate> template_for_index(const GUI::ModelIndex& index); | ||||
| 
 | ||||
|     virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; | ||||
|     virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; | ||||
|     virtual String column_name(int) const override; | ||||
|     virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; | ||||
|     virtual void update() override; | ||||
| 
 | ||||
|     void rescan_templates(); | ||||
| 
 | ||||
| private: | ||||
|     explicit ProjectTemplatesModel(); | ||||
| 
 | ||||
|     NonnullRefPtrVector<ProjectTemplate> m_templates; | ||||
|     Vector<ProjectTemplate*> m_mapping; | ||||
| 
 | ||||
|     RefPtr<Core::FileWatcher> m_file_watcher; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
|  | @ -31,6 +31,7 @@ | |||
| #include "Debugger/DebugInfoWidget.h" | ||||
| #include "Debugger/Debugger.h" | ||||
| #include "Debugger/DisassemblyWidget.h" | ||||
| #include "Dialogs/NewProjectDialog.h" | ||||
| #include "Editor.h" | ||||
| #include "EditorWrapper.h" | ||||
| #include "FindInFilesWidget.h" | ||||
|  | @ -57,6 +58,7 @@ | |||
| #include <LibGUI/Application.h> | ||||
| #include <LibGUI/BoxLayout.h> | ||||
| #include <LibGUI/Button.h> | ||||
| #include <LibGUI/Dialog.h> | ||||
| #include <LibGUI/EditingEngine.h> | ||||
| #include <LibGUI/FilePicker.h> | ||||
| #include <LibGUI/InputBox.h> | ||||
|  | @ -130,6 +132,7 @@ HackStudioWidget::HackStudioWidget(const String& path_to_project) | |||
|     m_remove_current_editor_action = create_remove_current_editor_action(); | ||||
|     m_open_action = create_open_action(); | ||||
|     m_save_action = create_save_action(); | ||||
|     m_new_project_action = create_new_project_action(); | ||||
| 
 | ||||
|     create_action_tab(*m_right_hand_splitter); | ||||
| 
 | ||||
|  | @ -383,6 +386,18 @@ NonnullRefPtr<GUI::Action> HackStudioWidget::create_delete_action() | |||
|     return delete_action; | ||||
| } | ||||
| 
 | ||||
| NonnullRefPtr<GUI::Action> HackStudioWidget::create_new_project_action() | ||||
| { | ||||
|     return GUI::Action::create("Create new project...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [this](const GUI::Action&) { | ||||
|         auto dialog = NewProjectDialog::construct(window()); | ||||
|         dialog->set_icon(window()->icon()); | ||||
|         auto result = dialog->exec(); | ||||
| 
 | ||||
|         if (result == GUI::Dialog::ExecResult::ExecOK && dialog->created_project_path().has_value()) | ||||
|             open_project(dialog->created_project_path().value()); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| void HackStudioWidget::add_new_editor(GUI::Widget& parent) | ||||
| { | ||||
|     auto wrapper = EditorWrapper::construct(); | ||||
|  | @ -849,6 +864,7 @@ void HackStudioWidget::create_action_tab(GUI::Widget& parent) | |||
| void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar) | ||||
| { | ||||
|     auto& app_menu = menubar.add_menu("Hack Studio"); | ||||
|     app_menu.add_action(*m_new_project_action); | ||||
|     app_menu.add_action(*m_open_action); | ||||
|     app_menu.add_action(*m_save_action); | ||||
|     app_menu.add_separator(); | ||||
|  |  | |||
|  | @ -84,6 +84,7 @@ private: | |||
|     NonnullRefPtr<GUI::Action> create_new_directory_action(); | ||||
|     NonnullRefPtr<GUI::Action> create_open_selected_action(); | ||||
|     NonnullRefPtr<GUI::Action> create_delete_action(); | ||||
|     NonnullRefPtr<GUI::Action> create_new_project_action(); | ||||
|     NonnullRefPtr<GUI::Action> create_switch_to_next_editor_action(); | ||||
|     NonnullRefPtr<GUI::Action> create_switch_to_previous_editor_action(); | ||||
|     NonnullRefPtr<GUI::Action> create_remove_current_editor_action(); | ||||
|  | @ -158,6 +159,7 @@ private: | |||
|     RefPtr<GUI::Action> m_new_directory_action; | ||||
|     RefPtr<GUI::Action> m_open_selected_action; | ||||
|     RefPtr<GUI::Action> m_delete_action; | ||||
|     RefPtr<GUI::Action> m_new_project_action; | ||||
|     RefPtr<GUI::Action> m_switch_to_next_editor; | ||||
|     RefPtr<GUI::Action> m_switch_to_previous_editor; | ||||
|     RefPtr<GUI::Action> m_remove_current_editor_action; | ||||
|  |  | |||
							
								
								
									
										279
									
								
								Userland/DevTools/HackStudio/ProjectTemplate.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								Userland/DevTools/HackStudio/ProjectTemplate.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,279 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2021, Nick Vella <nick@nxk.io> | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  * modification, are permitted provided that the following conditions are met: | ||||
|  * | ||||
|  * 1. Redistributions of source code must retain the above copyright notice, this | ||||
|  *    list of conditions and the following disclaimer. | ||||
|  * | ||||
|  * 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|  *    this list of conditions and the following disclaimer in the documentation | ||||
|  *    and/or other materials provided with the distribution. | ||||
|  * | ||||
|  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
|  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
|  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
|  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
|  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
|  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
|  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
|  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #include "ProjectTemplate.h" | ||||
| #include <AK/LexicalPath.h> | ||||
| #include <AK/String.h> | ||||
| #include <AK/StringBuilder.h> | ||||
| #include <LibCore/ArgsParser.h> | ||||
| #include <LibCore/ConfigFile.h> | ||||
| #include <LibCore/DirIterator.h> | ||||
| #include <LibCore/File.h> | ||||
| #include <assert.h> | ||||
| #include <fcntl.h> | ||||
| #include <spawn.h> | ||||
| #include <sys/stat.h> | ||||
| #include <sys/wait.h> | ||||
| #include <unistd.h> | ||||
| 
 | ||||
| // FIXME: shameless copy+paste from Userland/cp. We should have system-wide file management functions.
 | ||||
| // Issue #5209
 | ||||
| bool copy_file_or_directory(String, String, bool, bool); | ||||
| bool copy_file(String, String, const struct stat&, int); | ||||
| bool copy_directory(String, String, bool); | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| 
 | ||||
| ProjectTemplate::ProjectTemplate(const String& id, const String& name, const String& description, const GUI::Icon& icon, int priority) | ||||
|     : m_id(id) | ||||
|     , m_name(name) | ||||
|     , m_description(description) | ||||
|     , m_icon(icon) | ||||
|     , m_priority(priority) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| RefPtr<ProjectTemplate> ProjectTemplate::load_from_manifest(const String& manifest_path) | ||||
| { | ||||
|     auto config = Core::ConfigFile::open(manifest_path); | ||||
| 
 | ||||
|     if (!config->has_group("HackStudioTemplate") | ||||
|         || !config->has_key("HackStudioTemplate", "Name") | ||||
|         || !config->has_key("HackStudioTemplate", "Description") | ||||
|         || !config->has_key("HackStudioTemplate", "IconName32x")) | ||||
|         return {}; | ||||
| 
 | ||||
|     auto id = LexicalPath(manifest_path).title(); | ||||
|     auto name = config->read_entry("HackStudioTemplate", "Name"); | ||||
|     auto description = config->read_entry("HackStudioTemplate", "Description"); | ||||
|     int priority = config->read_num_entry("HackStudioTemplate", "Priority", 0); | ||||
| 
 | ||||
|     // Attempt to read in the template icons
 | ||||
|     // Fallback to a generic executable icon if one isn't found
 | ||||
|     auto icon = GUI::Icon::default_icon("filetype-executable"); | ||||
| 
 | ||||
|     auto bitmap_path_32 = String::formatted("/res/icons/hackstudio/templates-32x32/{}.png", config->read_entry("HackStudioTemplate", "IconName32x")); | ||||
| 
 | ||||
|     if (Core::File::exists(bitmap_path_32)) { | ||||
|         auto bitmap32 = Gfx::Bitmap::load_from_file(bitmap_path_32); | ||||
|         icon = GUI::Icon(move(bitmap32)); | ||||
|     } | ||||
| 
 | ||||
|     return adopt(*new ProjectTemplate(id, name, description, icon, priority)); | ||||
| } | ||||
| 
 | ||||
| Result<void, String> ProjectTemplate::create_project(const String& name, const String& path) | ||||
| { | ||||
|     // Check if a file or directory already exists at the project path
 | ||||
|     if (Core::File::exists(path)) | ||||
|         return String("File or directory already exists at specified location."); | ||||
| 
 | ||||
|     dbgln("Creating project at path '{}' with name '{}'", path, name); | ||||
| 
 | ||||
|     // Verify that the template content directory exists. If it does, copy it's contents.
 | ||||
|     // Otherwise, create an empty directory at the project path.
 | ||||
|     if (Core::File::is_directory(content_path())) { | ||||
|         if (!copy_directory(content_path(), path, false)) | ||||
|             return String("Failed to copy template contents."); | ||||
|     } else { | ||||
|         dbgln("No template content directory found for '{}', creating an empty directory for the project.", m_id); | ||||
|         int rc; | ||||
|         if ((rc = mkdir(path.characters(), 0755)) < 0) { | ||||
|             return String::formatted("Failed to mkdir empty project directory, error: {}, rc: {}.", strerror(errno), rc); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Check for an executable post-create script in $TEMPLATES_DIR/$ID.postcreate,
 | ||||
|     // and run it with the path and name
 | ||||
| 
 | ||||
|     auto postcreate_script_path = LexicalPath::canonicalized_path(String::formatted("{}/{}.postcreate", templates_path(), m_id)); | ||||
|     struct stat postcreate_st; | ||||
|     int result = stat(postcreate_script_path.characters(), &postcreate_st); | ||||
|     if (result == 0 && (postcreate_st.st_mode & S_IXOTH) == S_IXOTH) { | ||||
|         dbgln("Running post-create script '{}'", postcreate_script_path); | ||||
| 
 | ||||
|         // Generate a namespace-safe project name (replace hyphens with underscores)
 | ||||
|         String namespace_safe(name.characters()); | ||||
|         namespace_safe.replace("-", "_", true); | ||||
| 
 | ||||
|         pid_t child_pid; | ||||
|         const char* argv[] = { postcreate_script_path.characters(), name.characters(), path.characters(), namespace_safe.characters(), nullptr }; | ||||
| 
 | ||||
|         if ((errno = posix_spawn(&child_pid, postcreate_script_path.characters(), nullptr, nullptr, const_cast<char**>(argv), environ))) { | ||||
|             perror("posix_spawn"); | ||||
|             return String("Failed to spawn project post-create script."); | ||||
|         } | ||||
| 
 | ||||
|         // Command spawned, wait for exit.
 | ||||
|         int status; | ||||
|         if (waitpid(child_pid, &status, 0) < 0) | ||||
|             return String("Failed to spawn project post-create script."); | ||||
| 
 | ||||
|         int child_error = WEXITSTATUS(status); | ||||
|         dbgln("Post-create script exited with code {}", child_error); | ||||
| 
 | ||||
|         if (child_error != 0) | ||||
|             return String("Project post-creation script exited with non-zero error code."); | ||||
|     } | ||||
| 
 | ||||
|     return {}; | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // FIXME: shameless copy+paste from Userland/cp. We should have system-wide file management functions.
 | ||||
| // Issue #5209
 | ||||
| bool copy_file_or_directory(String src_path, String dst_path, bool recursion_allowed, bool link) | ||||
| { | ||||
|     int src_fd = open(src_path.characters(), O_RDONLY); | ||||
|     if (src_fd < 0) { | ||||
|         perror("open src"); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     struct stat src_stat; | ||||
|     int rc = fstat(src_fd, &src_stat); | ||||
|     if (rc < 0) { | ||||
|         perror("stat src"); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     if (S_ISDIR(src_stat.st_mode)) { | ||||
|         if (!recursion_allowed) { | ||||
|             fprintf(stderr, "cp: -R not specified; omitting directory '%s'\n", src_path.characters()); | ||||
|             return false; | ||||
|         } | ||||
|         return copy_directory(src_path, dst_path, link); | ||||
|     } | ||||
|     if (link) { | ||||
|         if (::link(src_path.characters(), dst_path.characters()) < 0) { | ||||
|             perror("link"); | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     return copy_file(src_path, dst_path, src_stat, src_fd); | ||||
| } | ||||
| 
 | ||||
| bool copy_file(String src_path, String dst_path, const struct stat& src_stat, int src_fd) | ||||
| { | ||||
|     // Get umask
 | ||||
|     auto my_umask = umask(0); | ||||
|     umask(my_umask); | ||||
| 
 | ||||
|     // NOTE: We don't copy the set-uid and set-gid bits.
 | ||||
|     mode_t mode = (src_stat.st_mode & ~my_umask) & ~06000; | ||||
| 
 | ||||
|     int dst_fd = creat(dst_path.characters(), mode); | ||||
|     if (dst_fd < 0) { | ||||
|         if (errno != EISDIR) { | ||||
|             perror("open dst"); | ||||
|             return false; | ||||
|         } | ||||
|         StringBuilder builder; | ||||
|         builder.append(dst_path); | ||||
|         builder.append('/'); | ||||
|         builder.append(LexicalPath(src_path).basename()); | ||||
|         dst_path = builder.to_string(); | ||||
|         dst_fd = creat(dst_path.characters(), 0666); | ||||
|         if (dst_fd < 0) { | ||||
|             perror("open dst"); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (src_stat.st_size > 0) { | ||||
|         if (ftruncate(dst_fd, src_stat.st_size) < 0) { | ||||
|             perror("cp: ftruncate"); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     for (;;) { | ||||
|         char buffer[32768]; | ||||
|         ssize_t nread = read(src_fd, buffer, sizeof(buffer)); | ||||
|         if (nread < 0) { | ||||
|             perror("read src"); | ||||
|             return false; | ||||
|         } | ||||
|         if (nread == 0) | ||||
|             break; | ||||
|         ssize_t remaining_to_write = nread; | ||||
|         char* bufptr = buffer; | ||||
|         while (remaining_to_write) { | ||||
|             ssize_t nwritten = write(dst_fd, bufptr, remaining_to_write); | ||||
|             if (nwritten < 0) { | ||||
|                 perror("write dst"); | ||||
|                 return false; | ||||
|             } | ||||
|             assert(nwritten > 0); | ||||
|             remaining_to_write -= nwritten; | ||||
|             bufptr += nwritten; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     close(src_fd); | ||||
|     close(dst_fd); | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| bool copy_directory(String src_path, String dst_path, bool link) | ||||
| { | ||||
|     int rc = mkdir(dst_path.characters(), 0755); | ||||
|     if (rc < 0) { | ||||
|         perror("cp: mkdir"); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     String src_rp = Core::File::real_path_for(src_path); | ||||
|     src_rp = String::format("%s/", src_rp.characters()); | ||||
|     String dst_rp = Core::File::real_path_for(dst_path); | ||||
|     dst_rp = String::format("%s/", dst_rp.characters()); | ||||
| 
 | ||||
|     if (!dst_rp.is_empty() && dst_rp.starts_with(src_rp)) { | ||||
|         fprintf(stderr, "cp: Cannot copy %s into itself (%s)\n", | ||||
|             src_path.characters(), dst_path.characters()); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     Core::DirIterator di(src_path, Core::DirIterator::SkipDots); | ||||
|     if (di.has_error()) { | ||||
|         fprintf(stderr, "cp: DirIterator: %s\n", di.error_string()); | ||||
|         return false; | ||||
|     } | ||||
|     while (di.has_next()) { | ||||
|         String filename = di.next_path(); | ||||
|         bool is_copied = copy_file_or_directory( | ||||
|             String::format("%s/%s", src_path.characters(), filename.characters()), | ||||
|             String::format("%s/%s", dst_path.characters(), filename.characters()), | ||||
|             true, link); | ||||
|         if (!is_copied) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
							
								
								
									
										67
									
								
								Userland/DevTools/HackStudio/ProjectTemplate.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Userland/DevTools/HackStudio/ProjectTemplate.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,67 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2021, Nick Vella <nick@nxk.io> | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  * modification, are permitted provided that the following conditions are met: | ||||
|  * | ||||
|  * 1. Redistributions of source code must retain the above copyright notice, this | ||||
|  *    list of conditions and the following disclaimer. | ||||
|  * | ||||
|  * 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|  *    this list of conditions and the following disclaimer in the documentation | ||||
|  *    and/or other materials provided with the distribution. | ||||
|  * | ||||
|  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
|  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
|  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
|  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
|  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
|  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
|  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
|  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <AK/ByteBuffer.h> | ||||
| #include <AK/LexicalPath.h> | ||||
| #include <AK/RefCounted.h> | ||||
| #include <AK/Result.h> | ||||
| #include <AK/String.h> | ||||
| #include <AK/Weakable.h> | ||||
| #include <LibGUI/Icon.h> | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| 
 | ||||
| class ProjectTemplate : public RefCounted<ProjectTemplate> { | ||||
| public: | ||||
|     static String templates_path() { return "/res/devel/templates"; } | ||||
| 
 | ||||
|     static RefPtr<ProjectTemplate> load_from_manifest(const String& manifest_path); | ||||
| 
 | ||||
|     explicit ProjectTemplate(const String& id, const String& name, const String& description, const GUI::Icon& icon, int priority); | ||||
| 
 | ||||
|     Result<void, String> create_project(const String& name, const String& path); | ||||
| 
 | ||||
|     const String& id() const { return m_id; } | ||||
|     const String& name() const { return m_name; } | ||||
|     const String& description() const { return m_description; } | ||||
|     const GUI::Icon& icon() const { return m_icon; } | ||||
|     const String content_path() const | ||||
|     { | ||||
|         return LexicalPath::canonicalized_path(String::formatted("{}/{}", templates_path(), m_id)); | ||||
|     } | ||||
|     int priority() const { return m_priority; } | ||||
| 
 | ||||
| private: | ||||
|     String m_id; | ||||
|     String m_name; | ||||
|     String m_description; | ||||
|     GUI::Icon m_icon; | ||||
|     int m_priority { 0 }; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Nick Vella
						Nick Vella