def NewFile(self): """ Prompts the user for a type of file to create, and a location & name for the new file. Then creates that file, and loads the respective editor """ # Only allow this is there is an active project if not Settings.getInstance().user_project_name: self.ShowNoActiveProjectPrompt() else: new_file_prompt = NewFileMenu("Content/Icons/Engine_Logo.png", "Choose a File Type") # Did the user successfully choose something? if new_file_prompt.exec(): selected_type = new_file_prompt.GetSelection() prompt = FileSystemPrompt(self.main_window) result = prompt.SaveFile( Settings.getInstance().supported_content_types['Data'], Settings.getInstance().GetProjectContentDirectory(), "Save File As") # Did the user choose a place and a name for the new file? if result: with open(result, 'w') as new_file: pass Logger.getInstance().Log(f"File created - {result}", 2) # Create the editor, then export to initially populate the new file self.OpenEditor(result, selected_type) self.active_editor.Export() else: Logger.getInstance().Log( "File information was not provided - Cancelling 'New File' action", 3)
def OpenEditor(self, target_file_path, editor_type, import_file=False): """ Creates an editor tab based on the provided file information """ if not Settings.getInstance().editor_data["EditorSettings"][ "max_tabs"] <= self.e_ui.main_tab_editor.count(): editor_classes = { FileType.Scene_Dialogue: EditorDialogue, FileType.Scene_Point_And_Click: EditorPointAndClick, FileType.Project_Settings: EditorProjectSettings } # Let's check if we already have an editor open for this file result = self.CheckIfFileOpen(target_file_path) if result: #@TODO: Should there be a reimport if the user tries to open an opened file? Or maybe a refesh button? Logger.getInstance().Log( "An editor for the selected file is already open - Switching to the open editor ", 3) self.e_ui.main_tab_editor.setCurrentWidget(result) else: # Initialize the Editor self.active_editor = editor_classes[editor_type]( target_file_path) # Allow the caller to load the provided file instead of just marking it as the export target if import_file: self.active_editor.Import() self.e_ui.AddTab(self.active_editor.GetUI(), os.path.basename(target_file_path), self.e_ui.main_tab_editor) else: QtWidgets.QMessageBox.about( self.e_ui.central_widget, "Tab Limit Reached!", "You have reached the maximum number of open tabs. Please close " "a tab before attempting to open another")
def Import(self): super().Import() Logger.getInstance().Log( f"Importing Dialogue data for: {self.file_path}") file_data = Reader.ReadAll(self.file_path) # Skip importing if the file has no data to load if file_data: db_manager = DBManager() converted_data = db_manager.ConvertDialogueFileToEditorFormat( file_data["dialogue"]) # The main branch is treated specially since we don't need to create it for branch_name, branch_data in converted_data.items(): if not branch_name == "Main": self.editor_ui.branches.CreateBranch( branch_name, branch_data["description"]) for action in branch_data["entries"]: self.editor_ui.dialogue_sequence.AddEntry( action, None, True) # Select the main branch by default self.editor_ui.branches.ChangeBranch(0)
def MoveEntryDown(self): """ If an entry is selected, move it down one row """ if self.dialogue_table.rowCount(): selection = self.GetSelectedRow() # Only allow moving down if we're not already at the bottom of the sequence if selection + 1 >= self.dialogue_table.rowCount(): Logger.getInstance().Log( "Can't move entry down as we're at the bottom of the sequence", 3) else: # 'cellWidget' returns a pointer which becomes invalid once we override it's row. Given this, instead # of gently moving the row, we recreate it by transferring it's data to a newly created entry taken_entry = self.dialogue_table.cellWidget(selection, 0) # Delete the origin row self.dialogue_table.removeRow(selection) # Add a new entry two rows above the initial row new_row_num = selection + 1 new_entry = self.AddEntry(taken_entry.action_data, new_row_num) # Transfer the data from the original entry to the new one, before refreshing the details new_entry.action_data = taken_entry.action_data self.ed_core.UpdateActiveEntry()
def CreateFile(self, path: str, file_type) -> str: """ Create a file of the given file type, initially assigning it a temp name. Returns whether the action was successful """ file_name_exists = True # Keep trying to create the file using a simple iterator. At some point, don't allow creating if the user has # somehow created enough files to max this...I really hope they don't for num in range(0, 100): full_file_path = path + f"/New_{file_type}_{num}.yaml" if not os.path.exists(full_file_path): # Doesn't exist. Create it! Writer.WriteFile( "", full_file_path, Settings.getInstance().GetMetadataString( FileType[file_type])) return full_file_path # Somehow the user has all versions of the default name covered...Inform the user Logger.getInstance().Log( "Unable to create file as all default name iterations are taken", 4) return None
def GetAllDialogueData(self) -> dict: """ Collects all dialogue data in this file, including all branches, and returns them as a dict """ data_to_export = {} branch_count = self.editor_ui.branches.branches_list.count() for index in range(0, branch_count): # Get the actual branch entry widget instead of the containing item widget branch = self.editor_ui.branches.branches_list.itemWidget( self.editor_ui.branches.branches_list.item(index)) # Before we save, let's be double sure the current information in the details panel is cached properly self.editor_ui.details.UpdateCache() # If a branch is currently active, then it's likely to of not updated it's cached branch data (Only # happens when the active branch is switched). To account for this, make sure the active branch is checked # differently by scanning the current dialogue entries if branch is self.editor_ui.branches.active_branch: Logger.getInstance().Log("Scanning dialogue entries...") self.UpdateBranchData(branch) branch_name, branch_description = branch.Get() branch_data = branch.GetData() new_entry = { #"name": branch_name, "description": branch_description, "entries": branch_data } data_to_export[branch_name] = new_entry return data_to_export
def SaveAs(self): """ Prompts the user for a new location and file name to save the active editor's data """ # Only allow this is there is an active project if not Settings.getInstance().user_project_name: self.ShowNoActiveProjectPrompt() else: # The user is not allowed to rename the project settings file due to the number of dependencies on it if self.active_editor.file_type is FileType.Project_Settings: Logger.getInstance().Log( "Project Settings can not be renamed or saved to a new location", 3) else: prompt = FileSystemPrompt(self.main_window) new_file_path = prompt.SaveFile( Settings.getInstance().supported_content_types['Data'], self.active_editor.GetFilePath(), "Choose a Location to Save the File", True) if not new_file_path: Logger.getInstance().Log( "File path was not provided - Cancelling 'SaveAs' action", 3) else: can_save = self.ValidateNewFileLocation(new_file_path) if can_save: self.active_editor.file_path = new_file_path self.e_ui.main_tab_editor.setTabText( self.e_ui.main_tab_editor.currentIndex(), self.active_editor.GetFileName()) self.active_editor.Export()
def DeleteFile(self, path): """ Delete the provided file. Editor will remain open if the user wishes to resave """ try: os.remove(path) Logger.getInstance().Log(f"Successfully deleted file '{path}'", 2) except Exception as exc: Logger.getInstance().Log( f"Failed to delete file '{path}' - Please review the exception to understand more", 4)
def RemoveEditorTab(self, index): """ Remove the tab for the given index (Value is automatically provided by the tab system as an arg) """ #@TODO: Review if a memory leak is created here due to not going down the editor reference tree and deleting things Logger.getInstance().Log("Closing editor...") editor_widget = self.main_tab_editor.widget(index) del editor_widget self.main_tab_editor.removeTab(index) Logger.getInstance().Log("Editor closed")
def __init__(self, file_path): super().__init__(file_path) self.file_type = FileType.Scene_Dialogue self.editor_ui = EditorDialogueUI(self) self.editor_ui.branches.CreateBranch( "Main", "This is the default, main branch\nConsider this the root of your dialogue tree" ) Logger.getInstance().Log("Editor initialized")
def __init__(self, file_path): super().__init__(file_path) self.file_type = FileType.Project_Settings # Read this data in first as the U.I will need it to initialize properly self.project_settings = Reader.ReadAll(self.file_path) self.project_settings_schema = Reader.ReadAll( Settings.getInstance().ConvertPartialToAbsolutePath( "Config/ProjectSettingsSchema.yaml")) self.editor_ui = EditorProjectSettingsUI(self) Logger.getInstance().Log("Editor initialized")
def DeleteFolder(self, path): """ Delete the provided folder recursively. Editors will remain open if the user wishes to resave """ print("Deleting folder") try: print(path) shutil.rmtree(path) Logger.getInstance().Log( f"Successfully deleted folder '{path}' and all of it's contents", 2) except Exception as exc: Logger.getInstance().Log( f"Failed to delete folder '{path}' - Please review the exception to understand more", 4) print(exc)
def OpenProject(self): """ Prompts the user for a project directory, then loads that file in the respective editor """ Logger.getInstance().Log("Requesting path to project root...") prompt = FileSystemPrompt(self.main_window) existing_project_dir = prompt.GetDirectory( self.GetLastSearchPath(), "Choose a Project Directory", False) self.UpdateSearchHistory(existing_project_dir) if not existing_project_dir: Logger.getInstance().Log( "Project directory was not provided - Cancelling 'Open Project' action", 3) else: # Does the directory already have a project in it (Denoted by the admin folder's existence) if os.path.exists(existing_project_dir + "/" + Settings.getInstance().project_file): Logger.getInstance().Log( "Valid project selected - Setting as Active Project...") # Since we aren't asking for the project name, let's infer it from the path project_name = os.path.basename(existing_project_dir) self.SetActiveProject(project_name, existing_project_dir) else: Logger.getInstance().Log( "An invalid Heartbeat project was selected - Cancelling 'Open Project' action", 4) QtWidgets.QMessageBox.about( self.e_ui.central_widget, "Not a Valid Project Directory!", "The chosen directory is not a valid Heartbeat project.\n" "Please either choose a different project, or create a new project." )
def GetFile(self, starting_dir, type_filter, prompt_title="Choose a File", project_only=True) -> str: """ Opens up a prompt for choosing an existing file. If nothing is selected, return an empty string""" Logger.getInstance().Log("Requesting file path...") file_path = self.getOpenFileName(self.parent(), prompt_title, starting_dir, type_filter) # Did the user choose a value? if file_path[0]: selected_dir = file_path[0] if project_only: if Settings.getInstance().user_project_dir in selected_dir: Logger.getInstance().Log("Valid file chosen") return selected_dir else: self.ShowPathOutsideProjectMessage() return "" else: Logger.getInstance().Log("Valid file chosen") return selected_dir else: Logger.getInstance().Log("File name and path not provided", 3) return ""
def __init__(self, file_path): super().__init__(file_path) self.file_type = FileType.Scene_Point_And_Click # Since the Point & Click editor doesn't make use of all actions, but only those that can be rendered # (interactables, sprites, text, etc), we need to compile a custom action_data dict with only the options # we'll allow possible_actions = { "Renderables": ["Create Sprite", "Create Text", "Create Background"], } self.action_data = self.BuildActionDataDict(possible_actions) # Initialize the editor U.I self.editor_ui = EditorPointAndClickUI(self) Logger.getInstance().Log("Editor initialized")
def Clean(self): """ Cleans the active project's build folder """ # Only allow this is there is an active project if not Settings.getInstance().user_project_name: self.ShowNoActiveProjectPrompt() else: HBBuilder.Clean(Logger.getInstance(), Settings.getInstance().user_project_dir)
def CreateFolder(self, path: str) -> bool: """ Create a directory, initially assigning it a temp name. Returns whether the action was successful """ folder_name_exists = True # Keep trying to create the folder using a simple iterator. At some point, don't allow creating if the user has # somehow created enough folders to max this...I really hope they don't for num in range(0, 100): full_folder_path = path + f"/New_Folder_{num}" if not os.path.exists(full_folder_path): # Doesn't exist. Create it! os.mkdir(full_folder_path) return full_folder_path # Somehow the user has all versions of the default name covered...Inform the user Logger.getInstance().Log( "Unable to create folder as all default name iterations are taken", 4) return None
def Build(self): """ Launches the HBBuilder in order to generate an executable from the active project """ # Only allow this is there is an active project if not Settings.getInstance().user_project_name: self.ShowNoActiveProjectPrompt() else: HBBuilder.Build(Logger.getInstance(), Settings.getInstance().engine_root, Settings.getInstance().user_project_dir, Settings.getInstance().user_project_name)
def setupUi(self, MainWindow): # Configure the Window MainWindow.setObjectName("MainWindow") MainWindow.resize(1024, 720) MainWindow.setWindowIcon( QtGui.QIcon(Settings.getInstance().ConvertPartialToAbsolutePath( "Content/Icons/Engine_Logo.png"))) # Build the core window widget object self.central_widget = QtWidgets.QWidget(MainWindow) MainWindow.setCentralWidget(self.central_widget) # Build the core window layout object self.central_grid_layout = QtWidgets.QGridLayout(self.central_widget) self.central_grid_layout.setContentsMargins(0, 0, 0, 0) self.central_grid_layout.setSpacing(0) # Initialize the Menu Bar self.CreateMenuBar(MainWindow) # Allow the user to resize each row self.main_resize_container = QtWidgets.QSplitter(self.central_widget) self.main_resize_container.setOrientation(Qt.Vertical) # ****** Add everything to the interface ****** self.central_grid_layout.addWidget(self.main_resize_container, 0, 0) self.CreateGettingStartedDisplay() self.CreateBottomTabContainer() self.AddTab(Logger.getInstance().GetUI(), "Logger", self.bottom_tab_editor) # Adjust the main editor container so it takes up as much space as possible self.main_resize_container.setStretchFactor(0, 10) # Hook up buttons QtCore.QMetaObject.connectSlotsByName(MainWindow) # Update the text of all U.I elements using a translation function. That way, we centralize text updates # through a common point where localization can take place self.retranslateUi(MainWindow) Logger.getInstance().Log("Initialized Editor Interface")
def SaveFile(self, type_filter, starting_dir, prompt_title="Save File", project_only=True) -> str: """ Prompts the user with a filedialog which has them specify a file to create or write to. If nothing is selected, return an empty string """ file_path = self.getSaveFileName(self.parent(), prompt_title, starting_dir, type_filter) # Did the user choose a value? if file_path[0]: selected_dir = file_path[0] if project_only: if Settings.getInstance().user_project_dir in selected_dir: Logger.getInstance().Log("Valid file chosen") return selected_dir else: self.ShowPathOutsideProjectMessage() return "" else: Logger.getInstance().Log("Valid file chosen") return selected_dir else: Logger.getInstance().Log("File name and path not provided", 3) return ""
def Play(self, parent, project_path, engine_parent_root): Logger.getInstance().Log("Launching engine...") try: # Launch the engine, and wait until it shuts down before continuing result = subprocess.Popen( [ f"venv/Scripts/python", "HBEngine/hb_engine.py", "-p", project_path ], stdout=True, stderr=True ) Logger.getInstance().Log("Engine Launched - Editor temporarily unavailable") result.wait() Logger.getInstance().Log("Engine closed - Editor functionality resumed") except Exception as exc: QMessageBox.about( parent, "Unable to Launch Engine", "The HBEngine could not be launched.\n\n" "Please make sure the engine is available alongside the editor, or that you're\n" "PYTHONPATH is configured correctly to include the engine." ) print(exc)
def Export(self): super().Export() Logger.getInstance().Log(f"Exporting Project Settings") # Just in case the user has made any changes to the settings, save them self.editor_ui.UpdateProjectSettingsData() # Write the data out Logger.getInstance().Log("Writing data to file...") try: Writer.WriteFile( self.project_settings, self.file_path, f"# Type: {FileType.Project_Settings.name}\n" + f"# {Settings.getInstance().editor_data['EditorSettings']['version_string']}" ) Logger.getInstance().Log("File Exported!", 2) except: Logger.getInstance().Log("Failed to Export!", 4)
def ReadFromTemp(self, temp_file: str, key: str) -> str: """ Read from the provided temp file using the provided key, returning the results if found """ if not os.path.exists(temp_file): Logger.getInstance().Log( f"The temp file '{temp_file}' was not found") return "" try: req_data = Reader.ReadAll(temp_file)[key] return req_data except Exception as exc: Logger.getInstance().Log(f"Failed to read '{temp_file}'") Logger.getInstance().Log(str(exc), 4) return ""
def GetDirectory(self, starting_dir, prompt_title="Choose a Directory", project_only=True) -> str: """ Opens up a prompt for choosing an existing directory. If nothing is selected, return an empty string""" Logger.getInstance().Log("Requesting directory path...") dir_path = self.getExistingDirectory(self.parent(), prompt_title, starting_dir) if dir_path: if project_only: if Settings.getInstance().user_project_dir in dir_path: Logger.getInstance().Log("Valid directory chosen") return dir_path else: self.ShowPathOutsideProjectMessage() return "" else: Logger.getInstance().Log("Valid directory chosen") return dir_path else: Logger.getInstance().Log("No directory chosen", 3) return ""
def WriteToTemp(self, temp_file: str, data: dict) -> bool: """ Write to the provided temp file, creating it if it doesn't already exist """ if not os.path.exists(temp_file): try: if not os.path.exists(Settings.editor_temp_root): os.mkdir(Settings.editor_temp_root) with open(temp_file, "w"): pass except Exception as exc: Logger.getInstance().Log(f"Unable to create '{temp_file}'", 4) Logger.getInstance().Log(str(exc), 4) return False temp_data = Reader.ReadAll(temp_file) if not temp_data: temp_data = {} try: for key, val in data.items(): temp_data[key] = val except Exception as exc: Logger.getInstance().Log(f"Failed to update '{temp_file}'", 4) Logger.getInstance().Log(str(exc), 4) return False Writer.WriteFile(temp_data, temp_file) return True
def Export(self): super().Export() Logger.getInstance().Log( f"Exporting Dialogue data for: {self.file_path}") data_to_export = self.GetAllDialogueData() db_manager = DBManager() data_to_export = { "type": FileType.Scene_Dialogue.name, "dialogue": db_manager.ConvertDialogueFileToEngineFormat(data_to_export) } # Write the data out Logger.getInstance().Log("Writing data to file...") try: Writer.WriteFile( data_to_export, self.file_path, f"# Type: {FileType.Scene_Dialogue.name}\n" + f"# {Settings.getInstance().editor_data['EditorSettings']['version_string']}" ) Logger.getInstance().Log("File Exported!", 2) except: Logger.getInstance().Log("Failed to Export!", 4)
def OpenFile(self, target_file_path=None): """ Prompt the user to choose a file, then load the respective editor using the data found """ # Only allow this is there is an active project if not Settings.getInstance().user_project_name: self.ShowNoActiveProjectPrompt() else: existing_file = None # Validate whether the selected file is capable of being opened if ".yaml" not in target_file_path: Logger.getInstance().Log( "File type does not have any interact functionality", 3) return """ # *** Re-enable this code when the supported file types all have open functionality *** split_path = target_file_path.split(".") if len(split_path) > 1: extension = split_path[-1] is_supported = False for type_string in self.Settings.getInstance().supported_content_types.values(): if f".{extension}" in type_string: is_supported = True break if not is_supported: Logger.getInstance().Log("File type does not have any interact functionality", 3) pass """ # Is the user opening a file through the main "File->Open" mechanism? if not target_file_path: prompt = FileSystemPrompt(self.main_window) existing_file = prompt.GetFile( Settings.getInstance().GetProjectContentDirectory(), Settings.getInstance().supported_content_types['Data'], "Choose a File to Open") # Most likely the outliner requested a file be opened. Use the provided path else: existing_file = target_file_path # Does the file actually exist? if not existing_file: Logger.getInstance().Log( "File path was not provided - Cancelling 'Open File' action", 3) else: # Read the first line to determine the type of file with open(existing_file) as f: # Check the metadata at the top of the file to see which file type this is line = f.readline() search = re.search("# Type: (.*)", line) # Was the expected metadata found? if search: file_type = FileType[search.group(1)] self.OpenEditor(existing_file, file_type, True) else: Logger.getInstance().Log( "An invalid file was selected - Cancelling 'Open File' action", 4) QtWidgets.QMessageBox.about( self.e_ui.central_widget, "Not a Valid File!", "The chosen file was not created by the HBEditor.\n" "Please either choose a different file, or create a new one.\n\n" "If you authored this file by hand, please add the correct metadata to the top of the file" )
def __init__(self, file_path): self.file_path = file_path self.file_type = None self.editor_ui = None Logger.getInstance().Log("Initializing Editor...")
def NewProject(self): """ Prompts the user for a directory to create a new project, and for a project name. Then creates the chosen project """ Logger.getInstance().Log( "Requesting directory for the new project...'") prompt = FileSystemPrompt(self.main_window) new_project_dir = prompt.GetDirectory( self.GetLastSearchPath(), "Choose a Directory to Create a Project", False) self.UpdateSearchHistory(new_project_dir) if not new_project_dir: Logger.getInstance().Log( "Project directory was not provided - Cancelling 'New Project' action", 3) else: # [0] = user_input: str, [1] = value_provided: bool Logger.getInstance().Log( "Requesting a name for the new project...'") user_project_name = QtWidgets.QInputDialog.getText( self.e_ui.central_widget, "New Project", "Please Enter a Project Name:")[0] if not user_project_name: Logger.getInstance().Log( "Project name was not provided - Cancelling 'New Project' action", 3) else: # Check if the project folder exists. If so, inform the user that this is already a project dir if os.path.exists(new_project_dir + "/" + user_project_name): Logger.getInstance().Log( "Chosen project directory already exists - Cancelling 'New Project' action", 4) QtWidgets.QMessageBox.about( self.e_ui.central_widget, "Project Already Exists!", "The chosen directory already contains a project of the chosen name.\n" "Please either delete this project, or choose another directory" ) # Everything is good to go. Create a new project! else: Logger.getInstance().Log( "Valid project destination chosen! Creating project folder structure..." ) # Create the project directory project_path = new_project_dir + "/" + user_project_name os.mkdir(project_path) # Create the pre-requisite project folders for main_dir in Settings.getInstance( ).project_folder_structure: main_dir_path = project_path + "/" + main_dir os.mkdir(main_dir_path) # Create the project file project_file = project_path + "/" + Settings.getInstance( ).project_file with open(project_file, "w"): pass # Clone project default files for key, rel_path in Settings.getInstance( ).project_default_files.items(): shutil.copy( Settings.getInstance().engine_root + "/" + rel_path, project_path + "/" + rel_path) Logger.getInstance().Log( f"Project Created at: {project_path}", 2) # Set this as the active project self.SetActiveProject(user_project_name, project_path)