Serenity Operating System
at hosted 672 lines 27 kB view raw
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 "CursorTool.h" 28#include "Editor.h" 29#include "EditorWrapper.h" 30#include "FindInFilesWidget.h" 31#include "FormEditorWidget.h" 32#include "FormWidget.h" 33#include "Locator.h" 34#include "Project.h" 35#include "TerminalWrapper.h" 36#include "WidgetTool.h" 37#include "WidgetTreeModel.h" 38#include <AK/StringBuilder.h> 39#include <LibCore/File.h> 40#include <LibGUI/AboutDialog.h> 41#include <LibGUI/Action.h> 42#include <LibGUI/ActionGroup.h> 43#include <LibGUI/Application.h> 44#include <LibGUI/BoxLayout.h> 45#include <LibGUI/Button.h> 46#include <LibGUI/CppSyntaxHighlighter.h> 47#include <LibGUI/FilePicker.h> 48#include <LibGUI/InputBox.h> 49#include <LibGUI/JSSyntaxHighlighter.h> 50#include <LibGUI/Label.h> 51#include <LibGUI/Menu.h> 52#include <LibGUI/MenuBar.h> 53#include <LibGUI/MessageBox.h> 54#include <LibGUI/Splitter.h> 55#include <LibGUI/StackWidget.h> 56#include <LibGUI/TabWidget.h> 57#include <LibGUI/TableView.h> 58#include <LibGUI/TextBox.h> 59#include <LibGUI/TextEditor.h> 60#include <LibGUI/ToolBar.h> 61#include <LibGUI/TreeView.h> 62#include <LibGUI/Widget.h> 63#include <LibGUI/Window.h> 64#include <LibVT/TerminalWidget.h> 65#include <stdio.h> 66#include <sys/wait.h> 67#include <unistd.h> 68 69NonnullRefPtrVector<EditorWrapper> g_all_editor_wrappers; 70RefPtr<EditorWrapper> g_current_editor_wrapper; 71Function<void(String)> g_open_file; 72 73String g_currently_open_file; 74OwnPtr<Project> g_project; 75RefPtr<GUI::Window> g_window; 76RefPtr<GUI::TreeView> g_project_tree_view; 77RefPtr<GUI::StackWidget> g_right_hand_stack; 78RefPtr<GUI::Splitter> g_text_inner_splitter; 79RefPtr<GUI::Widget> g_form_inner_container; 80RefPtr<FormEditorWidget> g_form_editor_widget; 81 82static RefPtr<GUI::TabWidget> s_action_tab_widget; 83 84void add_new_editor(GUI::Widget& parent) 85{ 86 auto wrapper = EditorWrapper::construct(); 87 if (s_action_tab_widget) { 88 parent.insert_child_before(wrapper, *s_action_tab_widget); 89 } else { 90 parent.add_child(wrapper); 91 } 92 g_current_editor_wrapper = wrapper; 93 g_all_editor_wrappers.append(wrapper); 94 wrapper->editor().set_focus(true); 95} 96 97enum class EditMode { 98 Text, 99 Form, 100}; 101 102void set_edit_mode(EditMode mode) 103{ 104 if (mode == EditMode::Text) { 105 g_right_hand_stack->set_active_widget(g_text_inner_splitter); 106 } else if (mode == EditMode::Form) { 107 g_right_hand_stack->set_active_widget(g_form_inner_container); 108 } 109} 110 111EditorWrapper& current_editor_wrapper() 112{ 113 ASSERT(g_current_editor_wrapper); 114 return *g_current_editor_wrapper; 115} 116 117Editor& current_editor() 118{ 119 return current_editor_wrapper().editor(); 120} 121 122static void build(TerminalWrapper&); 123static void run(TerminalWrapper&); 124void open_project(String); 125void open_file(const String&); 126bool make_is_available(); 127 128int main(int argc, char** argv) 129{ 130#ifdef __serenity__ 131 if (pledge("stdio tty accept rpath cpath wpath shared_buffer proc exec unix fattr thread", nullptr) < 0) { 132 perror("pledge"); 133 return 1; 134 } 135#endif 136 137 GUI::Application app(argc, argv); 138 139#ifdef __serenity__ 140 if (pledge("stdio tty accept rpath cpath wpath shared_buffer proc exec fattr thread", nullptr) < 0) { 141 perror("pledge"); 142 return 1; 143 } 144#endif 145 146 Function<void()> update_actions; 147 148 g_window = GUI::Window::construct(); 149 g_window->set_rect(90, 90, 840, 600); 150 g_window->set_title("HackStudio"); 151 152 auto& widget = g_window->set_main_widget<GUI::Widget>(); 153 154 widget.set_fill_with_background_color(true); 155 widget.set_layout<GUI::VerticalBoxLayout>(); 156 widget.layout()->set_spacing(0); 157 158 StringBuilder path; 159 path.append(getenv("PATH")); 160 if (path.length()) 161 path.append(":"); 162 path.append("/bin:/usr/bin:/usr/local/bin"); 163 setenv("PATH", path.to_string().characters(), true); 164 165 if (!make_is_available()) 166 GUI::MessageBox::show("The 'make' command is not available. You probably want to install the binutils, gcc, and make ports from the root of the Serenity repository.", "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, g_window); 167 168 open_project("/home/anon/js/javascript.files"); 169 170 auto& toolbar = widget.add<GUI::ToolBar>(); 171 172 auto selected_file_names = [&] { 173 Vector<String> files; 174 g_project_tree_view->selection().for_each_index([&](const GUI::ModelIndex& index) { 175 files.append(g_project->model().data(index).as_string()); 176 }); 177 return files; 178 }; 179 180 auto new_action = GUI::Action::create("Add new file to project...", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [&](const GUI::Action&) { 181 auto input_box = GUI::InputBox::construct("Enter name of new file:", "Add new file to project", g_window); 182 if (input_box->exec() == GUI::InputBox::ExecCancel) 183 return; 184 auto filename = input_box->text_value(); 185 auto file = Core::File::construct(filename); 186 if (!file->open((Core::IODevice::OpenMode)(Core::IODevice::WriteOnly | Core::IODevice::MustBeNew))) { 187 GUI::MessageBox::show(String::format("Failed to create '%s'", filename.characters()), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, g_window); 188 return; 189 } 190 if (!g_project->add_file(filename)) { 191 GUI::MessageBox::show(String::format("Failed to add '%s' to project", filename.characters()), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, g_window); 192 // FIXME: Should we unlink the file here maybe? 193 return; 194 } 195 g_project_tree_view->toggle_index(g_project_tree_view->model()->index(0, 0)); 196 open_file(filename); 197 }); 198 199 auto add_existing_file_action = GUI::Action::create("Add existing file to project...", Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), [&](auto&) { 200 auto result = GUI::FilePicker::get_open_filepath("Add existing file to project"); 201 if (!result.has_value()) 202 return; 203 auto& filename = result.value(); 204 if (!g_project->add_file(filename)) { 205 GUI::MessageBox::show(String::format("Failed to add '%s' to project", filename.characters()), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, g_window); 206 return; 207 } 208 g_project_tree_view->toggle_index(g_project_tree_view->model()->index(0, 0)); 209 open_file(filename); 210 }); 211 212 auto delete_action = GUI::CommonActions::make_delete_action([&](const GUI::Action& action) { 213 (void)action; 214 215 auto files = selected_file_names(); 216 if (files.is_empty()) 217 return; 218 219 String message; 220 if (files.size() == 1) { 221 message = String::format("Really remove %s from the project?", FileSystemPath(files[0]).basename().characters()); 222 } else { 223 message = String::format("Really remove %d files from the project?", files.size()); 224 } 225 226 auto result = GUI::MessageBox::show( 227 message, 228 "Confirm deletion", 229 GUI::MessageBox::Type::Warning, 230 GUI::MessageBox::InputType::OKCancel, 231 g_window); 232 if (result == GUI::MessageBox::ExecCancel) 233 return; 234 235 for (auto& file : files) { 236 if (!g_project->remove_file(file)) { 237 GUI::MessageBox::show( 238 String::format("Removing file %s from the project failed.", file.characters()), 239 "Removal failed", 240 GUI::MessageBox::Type::Error, 241 GUI::MessageBox::InputType::OK, 242 g_window); 243 break; 244 } 245 } 246 }); 247 delete_action->set_enabled(false); 248 249 auto project_tree_view_context_menu = GUI::Menu::construct("Project Files"); 250 project_tree_view_context_menu->add_action(new_action); 251 project_tree_view_context_menu->add_action(add_existing_file_action); 252 project_tree_view_context_menu->add_action(delete_action); 253 254 auto& outer_splitter = widget.add<GUI::HorizontalSplitter>(); 255 g_project_tree_view = outer_splitter.add<GUI::TreeView>(); 256 g_project_tree_view->set_model(g_project->model()); 257 g_project_tree_view->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 258 g_project_tree_view->set_preferred_size(140, 0); 259 g_project_tree_view->toggle_index(g_project_tree_view->model()->index(0, 0)); 260 261 g_project_tree_view->on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) { 262 if (index.is_valid()) { 263 project_tree_view_context_menu->popup(event.screen_position()); 264 } 265 }; 266 267 g_project_tree_view->on_selection_change = [&] { 268 delete_action->set_enabled(!g_project_tree_view->selection().is_empty()); 269 }; 270 271 g_right_hand_stack = outer_splitter.add<GUI::StackWidget>(); 272 273 g_form_inner_container = g_right_hand_stack->add<GUI::Widget>(); 274 g_form_inner_container->set_layout<GUI::HorizontalBoxLayout>(); 275 auto& form_widgets_toolbar = g_form_inner_container->add<GUI::ToolBar>(Orientation::Vertical, 26); 276 form_widgets_toolbar.set_preferred_size(38, 0); 277 278 GUI::ActionGroup tool_actions; 279 tool_actions.set_exclusive(true); 280 281 auto cursor_tool_action = GUI::Action::create("Cursor", Gfx::Bitmap::load_from_file("/res/icons/widgets/Cursor.png"), [&](auto&) { 282 g_form_editor_widget->set_tool(make<CursorTool>(*g_form_editor_widget)); 283 }); 284 cursor_tool_action->set_checkable(true); 285 cursor_tool_action->set_checked(true); 286 tool_actions.add_action(cursor_tool_action); 287 288 form_widgets_toolbar.add_action(cursor_tool_action); 289 290 GUI::WidgetClassRegistration::for_each([&](const GUI::WidgetClassRegistration& reg) { 291 auto icon_path = String::format("/res/icons/widgets/G%s.png", reg.class_name().characters()); 292 auto action = GUI::Action::create(reg.class_name(), Gfx::Bitmap::load_from_file(icon_path), [&reg](auto&) { 293 g_form_editor_widget->set_tool(make<WidgetTool>(*g_form_editor_widget, reg)); 294 auto widget = reg.construct(); 295 g_form_editor_widget->form_widget().add_child(widget); 296 widget->set_relative_rect(30, 30, 30, 30); 297 g_form_editor_widget->model().update(); 298 }); 299 action->set_checkable(true); 300 action->set_checked(false); 301 tool_actions.add_action(action); 302 form_widgets_toolbar.add_action(move(action)); 303 }); 304 305 auto& form_editor_inner_splitter = g_form_inner_container->add<GUI::HorizontalSplitter>(); 306 307 g_form_editor_widget = form_editor_inner_splitter.add<FormEditorWidget>(); 308 309 auto& form_editing_pane_container = form_editor_inner_splitter.add<GUI::VerticalSplitter>(); 310 form_editing_pane_container.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 311 form_editing_pane_container.set_preferred_size(190, 0); 312 form_editing_pane_container.set_layout<GUI::VerticalBoxLayout>(); 313 314 auto add_properties_pane = [&](auto& text, auto pane_widget) { 315 auto& wrapper = form_editing_pane_container.add<GUI::Widget>(); 316 wrapper.set_layout<GUI::VerticalBoxLayout>(); 317 auto& label = wrapper.add<GUI::Label>(text); 318 label.set_fill_with_background_color(true); 319 label.set_text_alignment(Gfx::TextAlignment::CenterLeft); 320 label.set_font(Gfx::Font::default_bold_font()); 321 label.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); 322 label.set_preferred_size(0, 16); 323 wrapper.add_child(pane_widget); 324 }; 325 326 auto form_widget_tree_view = GUI::TreeView::construct(); 327 form_widget_tree_view->set_model(g_form_editor_widget->model()); 328 form_widget_tree_view->on_selection_change = [&] { 329 g_form_editor_widget->selection().disable_hooks(); 330 g_form_editor_widget->selection().clear(); 331 form_widget_tree_view->selection().for_each_index([&](auto& index) { 332 // NOTE: Make sure we don't add the FormWidget itself to the selection, 333 // since that would allow you to drag-move the FormWidget. 334 if (index.internal_data() != &g_form_editor_widget->form_widget()) 335 g_form_editor_widget->selection().add(*(GUI::Widget*)index.internal_data()); 336 }); 337 g_form_editor_widget->update(); 338 g_form_editor_widget->selection().enable_hooks(); 339 }; 340 341 g_form_editor_widget->selection().on_add = [&](auto& widget) { 342 form_widget_tree_view->selection().add(g_form_editor_widget->model().index_for_widget(widget)); 343 }; 344 g_form_editor_widget->selection().on_remove = [&](auto& widget) { 345 form_widget_tree_view->selection().remove(g_form_editor_widget->model().index_for_widget(widget)); 346 }; 347 g_form_editor_widget->selection().on_clear = [&] { 348 form_widget_tree_view->selection().clear(); 349 }; 350 351 add_properties_pane("Form widget tree:", form_widget_tree_view); 352 add_properties_pane("Widget properties:", GUI::TableView::construct()); 353 354 g_text_inner_splitter = g_right_hand_stack->add<GUI::VerticalSplitter>(); 355 g_text_inner_splitter->layout()->set_margins({ 0, 3, 0, 0 }); 356 add_new_editor(*g_text_inner_splitter); 357 358 auto switch_to_next_editor = GUI::Action::create("Switch to next editor", { Mod_Ctrl, Key_E }, [&](auto&) { 359 if (g_all_editor_wrappers.size() <= 1) 360 return; 361 Vector<EditorWrapper*> wrappers; 362 g_text_inner_splitter->for_each_child_of_type<EditorWrapper>([&](auto& child) { 363 wrappers.append(&child); 364 return IterationDecision::Continue; 365 }); 366 for (size_t i = 0; i < wrappers.size(); ++i) { 367 if (g_current_editor_wrapper.ptr() == wrappers[i]) { 368 if (i == wrappers.size() - 1) 369 wrappers[0]->editor().set_focus(true); 370 else 371 wrappers[i + 1]->editor().set_focus(true); 372 } 373 } 374 }); 375 376 auto switch_to_previous_editor = GUI::Action::create("Switch to previous editor", { Mod_Ctrl | Mod_Shift, Key_E }, [&](auto&) { 377 if (g_all_editor_wrappers.size() <= 1) 378 return; 379 Vector<EditorWrapper*> wrappers; 380 g_text_inner_splitter->for_each_child_of_type<EditorWrapper>([&](auto& child) { 381 wrappers.append(&child); 382 return IterationDecision::Continue; 383 }); 384 for (int i = wrappers.size() - 1; i >= 0; --i) { 385 if (g_current_editor_wrapper.ptr() == wrappers[i]) { 386 if (i == 0) 387 wrappers.last()->editor().set_focus(true); 388 else 389 wrappers[i - 1]->editor().set_focus(true); 390 } 391 } 392 }); 393 394 auto remove_current_editor_action = GUI::Action::create("Remove current editor", { Mod_Alt | Mod_Shift, Key_E }, [&](auto&) { 395 if (g_all_editor_wrappers.size() <= 1) 396 return; 397 auto wrapper = g_current_editor_wrapper; 398 switch_to_next_editor->activate(); 399 g_text_inner_splitter->remove_child(*wrapper); 400 g_all_editor_wrappers.remove_first_matching([&](auto& entry) { return entry == wrapper.ptr(); }); 401 update_actions(); 402 }); 403 404 auto open_action = GUI::Action::create("Open project...", { Mod_Ctrl | Mod_Shift, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), [&](auto&) { 405 auto open_path = GUI::FilePicker::get_open_filepath("Open project"); 406 if (!open_path.has_value()) 407 return; 408 open_project(open_path.value()); 409 open_file(g_project->default_file()); 410 update_actions(); 411 }); 412 413 auto save_action = GUI::Action::create("Save", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), [&](auto&) { 414 if (g_currently_open_file.is_empty()) 415 return; 416 current_editor().write_to_file(g_currently_open_file); 417 }); 418 419 toolbar.add_action(new_action); 420 toolbar.add_action(add_existing_file_action); 421 toolbar.add_action(save_action); 422 toolbar.add_action(delete_action); 423 toolbar.add_separator(); 424 425 toolbar.add_action(GUI::CommonActions::make_cut_action([&](auto&) { current_editor().cut_action().activate(); })); 426 toolbar.add_action(GUI::CommonActions::make_copy_action([&](auto&) { current_editor().copy_action().activate(); })); 427 toolbar.add_action(GUI::CommonActions::make_paste_action([&](auto&) { current_editor().paste_action().activate(); })); 428 toolbar.add_separator(); 429 toolbar.add_action(GUI::CommonActions::make_undo_action([&](auto&) { current_editor().undo_action().activate(); })); 430 toolbar.add_action(GUI::CommonActions::make_redo_action([&](auto&) { current_editor().redo_action().activate(); })); 431 toolbar.add_separator(); 432 433 g_project_tree_view->on_activation = [&](auto& index) { 434 auto filename = g_project_tree_view->model()->data(index, GUI::Model::Role::Custom).to_string(); 435 open_file(filename); 436 }; 437 438 s_action_tab_widget = g_text_inner_splitter->add<GUI::TabWidget>(); 439 440 s_action_tab_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); 441 s_action_tab_widget->set_preferred_size(0, 24); 442 443 s_action_tab_widget->on_change = [&](auto&) { update_actions(); }; 444 445 auto reveal_action_tab = [&](auto& widget) { 446 if (s_action_tab_widget->preferred_size().height() < 200) 447 s_action_tab_widget->set_preferred_size(0, 200); 448 s_action_tab_widget->set_active_widget(&widget); 449 }; 450 451 auto hide_action_tabs = [&] { 452 s_action_tab_widget->set_preferred_size(0, 24); 453 }; 454 455 auto hide_action_tabs_action = GUI::Action::create("Hide action tabs", { Mod_Ctrl | Mod_Shift, Key_X }, [&](auto&) { 456 hide_action_tabs(); 457 }); 458 459 auto add_editor_action = GUI::Action::create("Add new editor", { Mod_Ctrl | Mod_Alt, Key_E }, 460 Gfx::Bitmap::load_from_file("/res/icons/TextEditor16.png"), 461 [&](auto&) { 462 add_new_editor(*g_text_inner_splitter); 463 update_actions(); 464 }); 465 466 auto add_terminal_action = GUI::Action::create("Add new Terminal", { Mod_Ctrl | Mod_Alt, Key_T }, 467 Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"), 468 [&](auto&) { 469 auto& terminal = s_action_tab_widget->add_tab<TerminalWrapper>("Terminal"); 470 reveal_action_tab(terminal); 471 update_actions(); 472 terminal.terminal()->set_focus(true); 473 }); 474 475 auto remove_current_terminal_action = GUI::Action::create("Remove current Terminal", { Mod_Alt | Mod_Shift, Key_T }, [&](auto&) { 476 auto widget = s_action_tab_widget->active_widget(); 477 if (!widget) 478 return; 479 if (strcmp(widget->class_name(), "TerminalWrapper") != 0) 480 return; 481 auto terminal = reinterpret_cast<TerminalWrapper*>(widget); 482 if (!terminal->user_spawned()) 483 return; 484 485 s_action_tab_widget->remove_tab(*terminal); 486 update_actions(); 487 }); 488 489 auto& find_in_files_widget = s_action_tab_widget->add_tab<FindInFilesWidget>("Find in files"); 490 auto& terminal_wrapper = s_action_tab_widget->add_tab<TerminalWrapper>("Build", false); 491 492 auto& locator = widget.add<Locator>(); 493 494 auto open_locator_action = GUI::Action::create("Open Locator...", { Mod_Ctrl, Key_K }, [&](auto&) { 495 locator.open(); 496 }); 497 498 auto menubar = make<GUI::MenuBar>(); 499 auto& app_menu = menubar->add_menu("HackStudio"); 500 app_menu.add_action(open_action); 501 app_menu.add_action(save_action); 502 app_menu.add_separator(); 503 app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { 504 app.quit(); 505 })); 506 507 auto& project_menu = menubar->add_menu("Project"); 508 project_menu.add_action(new_action); 509 project_menu.add_action(add_existing_file_action); 510 511 auto& edit_menu = menubar->add_menu("Edit"); 512 edit_menu.add_action(GUI::Action::create("Find in files...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [&](auto&) { 513 reveal_action_tab(find_in_files_widget); 514 find_in_files_widget.focus_textbox_and_select_all(); 515 })); 516 517 auto stop_action = GUI::Action::create("Stop", Gfx::Bitmap::load_from_file("/res/icons/16x16/stop.png"), [&](auto&) { 518 terminal_wrapper.kill_running_command(); 519 }); 520 521 stop_action->set_enabled(false); 522 terminal_wrapper.on_command_exit = [&] { 523 stop_action->set_enabled(false); 524 }; 525 526 auto build_action = GUI::Action::create("Build", { Mod_Ctrl, Key_B }, Gfx::Bitmap::load_from_file("/res/icons/16x16/build.png"), [&](auto&) { 527 reveal_action_tab(terminal_wrapper); 528 build(terminal_wrapper); 529 stop_action->set_enabled(true); 530 }); 531 toolbar.add_action(build_action); 532 533 auto run_action = GUI::Action::create("Run", { Mod_Ctrl, Key_R }, Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png"), [&](auto&) { 534 reveal_action_tab(terminal_wrapper); 535 run(terminal_wrapper); 536 stop_action->set_enabled(true); 537 }); 538 toolbar.add_action(run_action); 539 toolbar.add_action(stop_action); 540 541 auto& build_menu = menubar->add_menu("Build"); 542 build_menu.add_action(build_action); 543 build_menu.add_action(run_action); 544 build_menu.add_action(stop_action); 545 546 auto& view_menu = menubar->add_menu("View"); 547 view_menu.add_action(hide_action_tabs_action); 548 view_menu.add_action(open_locator_action); 549 view_menu.add_separator(); 550 view_menu.add_action(add_editor_action); 551 view_menu.add_action(remove_current_editor_action); 552 view_menu.add_action(add_terminal_action); 553 view_menu.add_action(remove_current_terminal_action); 554 555 auto& help_menu = menubar->add_menu("Help"); 556 help_menu.add_action(GUI::Action::create("About", [&](auto&) { 557 GUI::AboutDialog::show("HackStudio", Gfx::Bitmap::load_from_file("/res/icons/32x32/app-hack-studio.png"), g_window); 558 })); 559 560 app.set_menubar(move(menubar)); 561 562 g_window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-hack-studio.png")); 563 564 g_window->show(); 565 566 update_actions = [&]() { 567 auto is_remove_terminal_enabled = []() { 568 auto widget = s_action_tab_widget->active_widget(); 569 if (!widget) 570 return false; 571 if (strcmp(widget->class_name(), "TerminalWrapper") != 0) 572 return false; 573 if (!reinterpret_cast<TerminalWrapper*>(widget)->user_spawned()) 574 return false; 575 return true; 576 }; 577 578 remove_current_editor_action->set_enabled(g_all_editor_wrappers.size() > 1); 579 remove_current_terminal_action->set_enabled(is_remove_terminal_enabled()); 580 }; 581 582 g_open_file = open_file; 583 584 open_file(g_project->default_file()); 585 586 update_actions(); 587 return app.exec(); 588} 589 590void build(TerminalWrapper& wrapper) 591{ 592 if (g_project->type() == ProjectType::Javascript && g_currently_open_file.ends_with(".js")) 593 wrapper.run_command(String::format("js -A %s", g_currently_open_file.characters())); 594 else 595 wrapper.run_command("make"); 596} 597 598void run(TerminalWrapper& wrapper) 599{ 600 if (g_project->type() == ProjectType::Javascript && g_currently_open_file.ends_with(".js")) 601 wrapper.run_command(String::format("js %s", g_currently_open_file.characters())); 602 else 603 wrapper.run_command("make run"); 604} 605 606void open_project(String filename) 607{ 608 FileSystemPath path(filename); 609 if (chdir(path.dirname().characters()) < 0) { 610 perror("chdir"); 611 exit(1); 612 } 613 g_project = Project::load_from_file(filename); 614 ASSERT(g_project); 615 if (g_project_tree_view) { 616 g_project_tree_view->set_model(g_project->model()); 617 g_project_tree_view->toggle_index(g_project_tree_view->model()->index(0, 0)); 618 g_project_tree_view->update(); 619 } 620} 621 622void open_file(const String& filename) 623{ 624 auto project_file = g_project->get_file(filename); 625 if (project_file) { 626 current_editor().set_document(const_cast<GUI::TextDocument&>(project_file->document())); 627 current_editor().set_readonly(false); 628 } else { 629 auto external_file = ProjectFile::construct_with_name(filename); 630 current_editor().set_document(const_cast<GUI::TextDocument&>(external_file->document())); 631 current_editor().set_readonly(true); 632 } 633 634 if (filename.ends_with(".cpp") || filename.ends_with(".h")) 635 current_editor().set_syntax_highlighter(make<GUI::CppSyntaxHighlighter>()); 636 else if (filename.ends_with(".js")) 637 current_editor().set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>()); 638 else 639 current_editor().set_syntax_highlighter(nullptr); 640 641 if (filename.ends_with(".frm")) { 642 set_edit_mode(EditMode::Form); 643 } else { 644 set_edit_mode(EditMode::Text); 645 } 646 647 g_currently_open_file = filename; 648 g_window->set_title(String::format("%s - HackStudio", g_currently_open_file.characters())); 649 g_project_tree_view->update(); 650 651 current_editor_wrapper().filename_label().set_text(filename); 652 653 current_editor().set_focus(true); 654} 655 656bool make_is_available() 657{ 658 auto pid = fork(); 659 if (pid < 0) 660 return false; 661 662 if (!pid) { 663 int rc = execlp("make", "make", "--version", nullptr); 664 ASSERT(rc < 0); 665 perror("execl"); 666 exit(127); 667 } 668 669 int wstatus; 670 waitpid(pid, &wstatus, 0); 671 return WEXITSTATUS(wstatus) == 0; 672}