From 213c43dec67bf8814d0702158b8d9724e0a45ef7 Mon Sep 17 00:00:00 2001 From: FeudeyTF <72273449+FeudeyTF@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:36:21 +0300 Subject: [PATCH] NTOS Notepad update (#96892) ## About The Pull Request You can edit and save text files on your modular console through notepad app. The only thing that was useful in https://github.com/tgstation/tgstation/pull/96793 ## Why It's Good For The Game I think these are the basic features of any text editor ## Changelog :cl: qol: Notepad can edit and save text files /:cl: --- .../file_system/programs/notepad.dm | 86 ++++++++ tgui/packages/tgui/interfaces/NtosNotepad.tsx | 188 ++++++++++++++++-- 2 files changed, 257 insertions(+), 17 deletions(-) diff --git a/code/modules/modular_computers/file_system/programs/notepad.dm b/code/modules/modular_computers/file_system/programs/notepad.dm index af165952830..2a84ae20cd6 100644 --- a/code/modules/modular_computers/file_system/programs/notepad.dm +++ b/code/modules/modular_computers/file_system/programs/notepad.dm @@ -9,6 +9,10 @@ program_icon = "book" can_run_on_flags = PROGRAM_ALL circuit_comp_type = /obj/item/circuit_component/mod_program/notepad + + var/opened_file_uid + var/opened_file_on_disk = FALSE + var/opened_file_name = "Untitled" var/written_note = "Congratulations on your station upgrading to the new NtOS and Thinktronic based collaboration effort, \ bringing you the best in electronics and software since 2467!\n\ To help with navigation, we have provided the following definitions:\n\ @@ -19,17 +23,99 @@ Quarter - Either sides of Aft\n\ Bow - Either sides of Fore" +/datum/computer_file/program/notepad/proc/get_target_disk(opened_on_disk) + if(opened_on_disk && computer?.inserted_disk) + return computer.inserted_disk + return null + +/datum/computer_file/program/notepad/proc/find_text_file(uid, opened_on_disk, name) + var/obj/item/disk/computer/target_disk = get_target_disk(opened_on_disk) + var/datum/computer_file/data/text/file + if(uid) + file = computer.find_file_by_uid(uid, target_disk) + if(!file && length(name)) + file = computer.find_file_by_full_name("[name].TXT", target_disk) + return file + +/datum/computer_file/program/notepad/proc/load_text_file(datum/computer_file/data/text/file) + written_note = file.stored_text + opened_file_uid = file.uid + opened_file_on_disk = !!file.disk_host + opened_file_name = file.filename + +/datum/computer_file/program/notepad/proc/save_text_file(mob/user, name, extension, note, uid, opened_on_disk) + var/obj/item/disk/computer/target_disk = get_target_disk(opened_on_disk) + var/datum/computer_file/data/text/file = find_text_file(uid, opened_on_disk, name) + if(file) + file.stored_text = note + file.calculate_size() + load_text_file(file) + return + + var/datum/computer_file/data/text/new_file = create_text_file(extension) + new_file.filename = name + new_file.stored_text = note + new_file.calculate_size() + var/file_stored = target_disk ? target_disk.add_file(new_file) : computer.store_file(new_file, user) + if(!file_stored) + to_chat(user, span_warning("Error occured while saving the file!")) + qdel(new_file) + return + + load_text_file(new_file) + +/datum/computer_file/program/notepad/proc/create_text_file(extension) + if(extension == "LOG") + return new /datum/computer_file/data/text/logfile() + return new /datum/computer_file/data/text() + /datum/computer_file/program/notepad/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) . = ..() + var/mob/user = ui.user switch(action) if("UpdateNote") written_note = params["newnote"] return TRUE + if("SaveNote") + written_note = params["newnote"] || written_note + save_text_file(user, opened_file_name, null, written_note, opened_file_uid, opened_file_on_disk) + return TRUE + if("Save") + written_note = params["note"] || written_note + save_text_file(user, opened_file_name, null, written_note, opened_file_uid, opened_file_on_disk) + return TRUE + if("SaveAs") + var/new_name = trim(params["name"], MAX_MESSAGE_LEN) + var/extension = params["extension"] + if(!length(new_name) || !filter_filename_pda(new_name)) + return + written_note = params["note"] || written_note + save_text_file(user, new_name, extension, written_note, params["uid"], opened_file_on_disk) + return TRUE + if("Open") + var/datum/computer_file/data/text/file = find_text_file(params["uid"], params["onDisk"], params["name"]) + if(!file) + return + load_text_file(file) + return TRUE /datum/computer_file/program/notepad/ui_data(mob/user) var/list/data = list() data["note"] = written_note + data["documentName"] = opened_file_name + data["files"] = list() + if(computer) + for(var/datum/computer_file/file in computer.get_files(TRUE)) + if(!istype(file, /datum/computer_file/data/text)) + continue + data["files"] += list(list( + "uid" = file.uid, + "name" = file.filename, + "filetype" = file.filetype, + "onDisk" = !!file.disk_host, + "displayName" = "[file.filename].[file.filetype]", + )) return data diff --git a/tgui/packages/tgui/interfaces/NtosNotepad.tsx b/tgui/packages/tgui/interfaces/NtosNotepad.tsx index 969ea8131d5..adcd1b0daf8 100644 --- a/tgui/packages/tgui/interfaces/NtosNotepad.tsx +++ b/tgui/packages/tgui/interfaces/NtosNotepad.tsx @@ -4,8 +4,19 @@ * @license MIT */ -import { useRef, useState } from 'react'; -import { Box, Dialog, Divider, MenuBar, Section } from 'tgui-core/components'; +import { useEffect, useRef, useState } from 'react'; +import { + Box, + Button, + Dialog, + Divider, + Dropdown, + Flex, + Input, + MenuBar, + Section, + Stack, +} from 'tgui-core/components'; import { useBackend } from '../backend'; import { NtosWindow } from '../layouts'; @@ -16,6 +27,18 @@ const logger = createLogger('NtosNotepad'); const DEFAULT_DOCUMENT_NAME = 'Untitled'; +enum Extensions { + TXT = 'TXT', +} + +type NoteFileEntry = { + uid: number; + name: string; + filetype: string; + onDisk: boolean; + displayName: string; +}; + type PartiallyUnderlinedProps = { str: string; indexStart: number; @@ -40,16 +63,20 @@ enum Dialogs { UNSAVED_CHANGES = 1, OPEN = 2, ABOUT = 3, + SELECT_FILE = 4, + SAVE = 5, } type MenuBarProps = { onSave: () => void; + onSaveAs: () => void; onExit: () => void; onNewNote: () => void; onCutSelected: () => void; onCopySelected: () => void; onPasteSelected: () => void; onDeleteSelected: () => void; + onOpenSelected: () => void; showStatusBar: boolean; setShowStatusBar: (boolean) => void; wordWrap: boolean; @@ -60,12 +87,14 @@ type MenuBarProps = { const NtosNotepadMenuBar = (props: MenuBarProps) => { const { onSave, + onSaveAs, onExit, onNewNote, onCutSelected, onCopySelected, onPasteSelected, onDeleteSelected, + onOpenSelected, setShowStatusBar, showStatusBar, wordWrap, @@ -81,12 +110,18 @@ const NtosNotepadMenuBar = (props: MenuBarProps) => { case 'save': onSave(); break; + case 'save_as': + onSaveAs(); + break; case 'exit': onExit(); break; case 'new': onNewNote(); break; + case 'open': + onOpenSelected(); + break; case 'cut': onCutSelected(); break; @@ -135,7 +170,11 @@ const NtosNotepadMenuBar = (props: MenuBarProps) => { {...itemProps} > + + @@ -325,13 +364,19 @@ const AboutDialog = (props: AboutDialogProps) => { type NoteData = { note: string; + documentName: string; + files: NoteFileEntry[]; }; type RetryActionType = (retrying?: boolean) => void; export const NtosNotepad = (props) => { const { act, data } = useBackend(); - const { note } = data; - const [documentName, setDocumentName] = useState(DEFAULT_DOCUMENT_NAME); + const { + note, + documentName: backendDocumentName = DEFAULT_DOCUMENT_NAME, + files = [], + } = data; + const [documentName, setDocumentName] = useState(backendDocumentName); const [originalText, setOriginalText] = useState(note); const [text, setText] = useState(note); const [statuses, setStatuses] = useState({ @@ -342,23 +387,34 @@ export const NtosNotepad = (props) => { const [retryAction, setRetryAction] = useState(null); const [showStatusBar, setShowStatusBar] = useState(true); const [wordWrap, setWordWrap] = useState(true); + const [selectedFileUid, setSelectedFileUid] = useState(null); + const [saveName, setSaveName] = useState(DEFAULT_DOCUMENT_NAME); + + const [saveExtension, setSaveExtension] = useState(Extensions.TXT); + + useEffect(() => { + setText(note); + setOriginalText(note); + setDocumentName(backendDocumentName); + setStatuses(getStatusCounts(note, note.length)); + }, [note, backendDocumentName]); const handleCloseDialog = () => setActiveDialog(Dialogs.NONE); - const handleSave = (newDocumentName: string = documentName) => { - logger.log(`Saving the document as ${newDocumentName}`); - act('UpdateNote', { newnote: text }); - setOriginalText(text); - setDocumentName(newDocumentName); - logger.log('Attempting to retry previous action'); + const handleSave = () => { + logger.log(`Saving the document as ${documentName}`); + act('save', { note: text }); setActiveDialog(Dialogs.NONE); - - // Retry the previous action now that we've saved. The previous action could be to - // close the application, a new document being created or - // an existing document being opened if (retryAction) { - retryAction(true); + setRetryAction(null); } - setRetryAction(null); + }; + const handleSaveAs = () => setActiveDialog(Dialogs.SAVE); + const handleOpenNote = (retrying = false) => { + if (ensureUnsavedChangesAreHandled(handleOpenNote, retrying)) { + return; + } + setSelectedFileUid(null); + setActiveDialog(Dialogs.SELECT_FILE); }; const ensureUnsavedChangesAreHandled = ( action: () => void, @@ -391,6 +447,7 @@ export const NtosNotepad = (props) => { setDocumentName(DEFAULT_DOCUMENT_NAME); }; + const selectedFile = files.find((file) => file.uid === selectedFileUid); // MS Notepad displays an asterisk when there's unsaved changes const unsavedAsterisk = text !== originalText ? '*' : ''; return ( @@ -403,8 +460,10 @@ export const NtosNotepad = (props) => { document.execCommand('cut')} onCopySelected={() => document.execCommand('copy')} onPasteSelected={() => document.execCommand('paste')} @@ -426,13 +485,108 @@ export const NtosNotepad = (props) => { {showStatusBar && } + {activeDialog === Dialogs.SELECT_FILE && ( + +
+ + +
+ {files.length ? ( + file.displayName)} + onSelected={(filename) => { + setSelectedFileUid( + files.find((file) => file.displayName === filename) + ?.uid || files[0].uid, + ); + }} + /> + ) : ( + No text files available. + )} +
+
+
+
+
+ + Cancel +
+
+ )} + {activeDialog === Dialogs.SAVE && ( + +
+ + + setSaveName(value)} + /> + + + { + setSaveExtension(extension); + }} + /> + + +
+
+ { + act('SaveAs', { + name: saveName, + extension: saveExtension, + note: text, + }); + handleCloseDialog(); + }} + > + {'Save'} + + Cancel +
+
+ )} {activeDialog === Dialogs.UNSAVED_CHANGES && (
Do you want to save changes to {documentName}?
- Save + { + setRetryAction(null); + act('Save', { note: text }); + setActiveDialog(Dialogs.NONE); + }} + > + Save + Don't Save