' main.bas #inclib "gtk-3" #define __USE_GTK3__ #include once "gtk/gtk.bi" #include once "doc.bi" ' ---------- Globals ---------- dim shared gWin as GtkWidget ptr dim shared gView as GtkWidget ptr dim shared gBuf as GtkTextBuffer ptr dim shared gStatus as GtkWidget ptr dim shared gStatusId as guint dim shared gDoc as DocState const APP_NAME = "FREEnote" const APP_VERSION = "0.1" ' ---------- Helpers ---------- function AppTitle() as string dim appBase as string = APP_NAME dim docLabel as string if gDoc.path <> "" then docLabel = gDoc.path else docLabel = "Untitled" end if if gDoc.modified <> 0 then return appBase & " - " & docLabel & " *" else return appBase & " - " & docLabel end if end function declare function OnDeleteEvent cdecl (byval widget as GtkWidget ptr, byval event as GdkEvent ptr, byval userData as gpointer) as gboolean declare function SaveCurrent() as integer sub UpdateTitle() gtk_window_set_title(GTK_WINDOW(gWin), AppTitle()) end sub sub UpdateStatus() ' Line/Col from insert mark dim insMark as GtkTextMark ptr = gtk_text_buffer_get_insert(gBuf) dim it as GtkTextIter gtk_text_buffer_get_iter_at_mark(gBuf, @it, insMark) dim ln as integer = gtk_text_iter_get_line(@it) + 1 dim col as integer = gtk_text_iter_get_line_offset(@it) + 1 dim msg as string = "Ln " & str(ln) & ", Col " & str(col) & _ iif(gDoc.modified<>0, " (Modified)", " (Saved)") gtk_statusbar_pop(GTK_STATUSBAR(gStatus), gStatusId) gtk_statusbar_push(GTK_STATUSBAR(gStatus), gStatusId, msg) end sub sub SetBufferText(byref s as string) gtk_text_buffer_set_text(gBuf, s, len(s)) ' Clear modified flag after programmatic set gtk_text_buffer_set_modified(gBuf, FALSE) Doc_SetModified(gDoc, 0) UpdateTitle() UpdateStatus() end sub function GetBufferText() as string dim a as GtkTextIter, b as GtkTextIter gtk_text_buffer_get_bounds(gBuf, @a, @b) dim p as zstring ptr = gtk_text_buffer_get_text(gBuf, @a, @b, TRUE) dim textOut as string = *p g_free(p) return textOut end function ' returns: 0 cancel, 1 discard, 2 save function ConfirmDiscardChanges() as integer if gDoc.modified = 0 then return 1 dim dlg as GtkWidget ptr dlg = gtk_message_dialog_new( _ GTK_WINDOW(gWin), _ GTK_DIALOG_MODAL, _ GTK_MESSAGE_WARNING, _ GTK_BUTTONS_NONE, _ "This document has unsaved changes." _ ) gtk_dialog_add_button(GTK_DIALOG(dlg), "Cancel", GTK_RESPONSE_CANCEL) gtk_dialog_add_button(GTK_DIALOG(dlg), "Discard", GTK_RESPONSE_REJECT) gtk_dialog_add_button(GTK_DIALOG(dlg), "Save", GTK_RESPONSE_ACCEPT) gtk_message_dialog_format_secondary_text( _ GTK_MESSAGE_DIALOG(dlg), _ "Do you want to save your changes before continuing?" _ ) dim resp as integer = gtk_dialog_run(GTK_DIALOG(dlg)) gtk_widget_destroy(dlg) select case resp case GTK_RESPONSE_ACCEPT return 2 case GTK_RESPONSE_REJECT return 1 case else return 0 end select end function function WriteFile(byref path as string, byref contents as string) as integer dim f as integer = freefile() if open(path for output as #f) <> 0 then return 0 print #f, contents; close #f return 1 end function function ReadFile(byref path as string, byref outText as string) as integer dim f as integer = freefile() if open(path for input as #f) <> 0 then return 0 outText = "" while not eof(f) dim oneLine as string line input #f, oneLine if outText <> "" then outText &= chr(10) outText &= oneLine wend close #f return 1 end function function OnDeleteEvent cdecl (byval widget as GtkWidget ptr, byval event as GdkEvent ptr, byval userData as gpointer) as gboolean dim r as integer = ConfirmDiscardChanges() if r = 0 then return TRUE if r = 2 then if SaveCurrent() = 0 then return TRUE end if return FALSE end function function ChooseOpenPath() as string dim dlg as GtkWidget ptr dlg = gtk_file_chooser_dialog_new( _ "Open File", _ GTK_WINDOW(gWin), _ GTK_FILE_CHOOSER_ACTION_OPEN, _ "_Cancel", GTK_RESPONSE_CANCEL, _ "_Open", GTK_RESPONSE_ACCEPT, _ NULL _ ) dim result as string = "" if gtk_dialog_run(GTK_DIALOG(dlg)) = GTK_RESPONSE_ACCEPT then dim p as zstring ptr = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)) if p <> 0 then result = *p g_free(p) end if end if gtk_widget_destroy(dlg) return result end function function ChooseSavePath(byref suggested as string) as string dim dlg as GtkWidget ptr dlg = gtk_file_chooser_dialog_new( _ "Save File", _ GTK_WINDOW(gWin), _ GTK_FILE_CHOOSER_ACTION_SAVE, _ "_Cancel", GTK_RESPONSE_CANCEL, _ "_Save", GTK_RESPONSE_ACCEPT, _ NULL _ ) gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE) if suggested <> "" then gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dlg), suggested) end if dim result as string = "" if gtk_dialog_run(GTK_DIALOG(dlg)) = GTK_RESPONSE_ACCEPT then dim p as zstring ptr = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)) if p <> 0 then result = *p g_free(p) end if end if gtk_widget_destroy(dlg) return result end function function SaveToPath(byref path as string) as integer dim txt as string = GetBufferText() if WriteFile(path, txt) = 0 then dim errDlg as GtkWidget ptr errDlg = gtk_message_dialog_new( _ GTK_WINDOW(gWin), GTK_DIALOG_MODAL, _ GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _ "Could not save file." _ ) gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(errDlg), path) gtk_dialog_run(GTK_DIALOG(errDlg)) gtk_widget_destroy(errDlg) return 0 end if Doc_SetPath(gDoc, path) gtk_text_buffer_set_modified(gBuf, FALSE) Doc_SetModified(gDoc, 0) UpdateTitle() UpdateStatus() return 1 end function function SaveCurrent() as integer if gDoc.path = "" then dim chosen as string = ChooseSavePath("") if chosen = "" then return 0 return SaveToPath(chosen) else return SaveToPath(gDoc.path) end if end function ' ---------- Callbacks ---------- sub OnBufferModifiedChanged cdecl (byval buf as GtkTextBuffer ptr, byval userData as gpointer) dim m as gboolean = gtk_text_buffer_get_modified(buf) Doc_SetModified(gDoc, iif(m, 1, 0)) UpdateTitle() UpdateStatus() end sub sub OnMarkSet cdecl (byval buf as GtkTextBuffer ptr, byval iter as GtkTextIter ptr, byval mark as GtkTextMark ptr, byval userData as gpointer) UpdateStatus() end sub sub OnNew cdecl (byval widget as GtkWidget ptr, byval userData as gpointer) dim r as integer = ConfirmDiscardChanges() if r = 0 then exit sub if r = 2 then if SaveCurrent() = 0 then exit sub end if Doc_SetPath(gDoc, "") SetBufferText("") end sub sub OnOpen cdecl (byval widget as GtkWidget ptr, byval userData as gpointer) dim r as integer = ConfirmDiscardChanges() if r = 0 then exit sub if r = 2 then if SaveCurrent() = 0 then exit sub end if dim path as string = ChooseOpenPath() if path = "" then exit sub dim txt as string if ReadFile(path, txt) = 0 then dim errDlg as GtkWidget ptr errDlg = gtk_message_dialog_new( _ GTK_WINDOW(gWin), GTK_DIALOG_MODAL, _ GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _ "Could not open file." _ ) gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(errDlg), path) gtk_dialog_run(GTK_DIALOG(errDlg)) gtk_widget_destroy(errDlg) exit sub end if Doc_SetPath(gDoc, path) SetBufferText(txt) end sub sub OnSave cdecl (byval widget as GtkWidget ptr, byval userData as gpointer) SaveCurrent() end sub sub OnSaveAs cdecl (byval widget as GtkWidget ptr, byval userData as gpointer) dim suggested as string = gDoc.path dim chosen as string = ChooseSavePath(suggested) if chosen = "" then exit sub SaveToPath(chosen) end sub sub OnExit cdecl (byval widget as GtkWidget ptr, byval userData as gpointer) gtk_widget_destroy(gWin) end sub sub OnAbout cdecl (byval widget as GtkWidget ptr, byval userData as gpointer) dim dlg as GtkWidget ptr dlg = gtk_message_dialog_new( _ GTK_WINDOW(gWin), _ GTK_DIALOG_MODAL, _ GTK_MESSAGE_INFO, _ GTK_BUTTONS_OK, _ APP_NAME & " " & APP_VERSION _ ) gtk_message_dialog_format_secondary_text( _ GTK_MESSAGE_DIALOG(dlg), _ "A lightweight text editor written in FreeBASIC + GTK3." & chr(10) & _ "Created by John Paul Wohlscheid and ChatGPT." _ ) gtk_dialog_run(GTK_DIALOG(dlg)) gtk_widget_destroy(dlg) end sub ' ---------- UI Construction ---------- function MakeMenuBar() as GtkWidget ptr dim menubar as GtkWidget ptr = gtk_menu_bar_new() ' File menu dim fileItem as GtkWidget ptr = gtk_menu_item_new_with_label("File") dim fileMenu as GtkWidget ptr = gtk_menu_new() dim miNew as GtkWidget ptr = gtk_menu_item_new_with_label("New") dim miOpen as GtkWidget ptr = gtk_menu_item_new_with_label("Open") dim miSave as GtkWidget ptr = gtk_menu_item_new_with_label("Save") dim miSaveA as GtkWidget ptr = gtk_menu_item_new_with_label("Save As") dim sep1 as GtkWidget ptr = gtk_separator_menu_item_new() dim miExit as GtkWidget ptr = gtk_menu_item_new_with_label("Exit") gtk_menu_shell_append(GTK_MENU_SHELL(fileMenu), miNew) gtk_menu_shell_append(GTK_MENU_SHELL(fileMenu), miOpen) gtk_menu_shell_append(GTK_MENU_SHELL(fileMenu), miSave) gtk_menu_shell_append(GTK_MENU_SHELL(fileMenu), miSaveA) gtk_menu_shell_append(GTK_MENU_SHELL(fileMenu), sep1) gtk_menu_shell_append(GTK_MENU_SHELL(fileMenu), miExit) gtk_menu_item_set_submenu(GTK_MENU_ITEM(fileItem), fileMenu) gtk_menu_shell_append(GTK_MENU_SHELL(menubar), fileItem) g_signal_connect(miNew, "activate", G_CALLBACK(@OnNew), NULL) g_signal_connect(miOpen, "activate", G_CALLBACK(@OnOpen), NULL) g_signal_connect(miSave, "activate", G_CALLBACK(@OnSave), NULL) g_signal_connect(miSaveA,"activate", G_CALLBACK(@OnSaveAs),NULL) g_signal_connect(miExit, "activate", G_CALLBACK(@OnExit), NULL) ' Help menu dim helpItem as GtkWidget ptr = gtk_menu_item_new_with_label("Help") dim helpMenu as GtkWidget ptr = gtk_menu_new() dim miAbout as GtkWidget ptr = gtk_menu_item_new_with_label("About") gtk_menu_shell_append(GTK_MENU_SHELL(helpMenu), miAbout) gtk_menu_item_set_submenu(GTK_MENU_ITEM(helpItem), helpMenu) gtk_menu_shell_append(GTK_MENU_SHELL(menubar), helpItem) g_signal_connect(miAbout, "activate", G_CALLBACK(@OnAbout), NULL) return menubar end function ' ---------- Main ---------- gtk_init(NULL, NULL) Doc_Init(gDoc) gWin = gtk_window_new(GTK_WINDOW_TOPLEVEL) gtk_window_set_default_size(GTK_WINDOW(gWin), 900, 600) UpdateTitle() g_signal_connect(gWin, "delete-event", G_CALLBACK(@OnDeleteEvent), NULL) dim vbox as GtkWidget ptr = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0) gtk_container_add(GTK_CONTAINER(gWin), vbox) dim menubar as GtkWidget ptr = MakeMenuBar() gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 0) ' Scrolled editor dim scroller as GtkWidget ptr = gtk_scrolled_window_new(NULL, NULL) gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC) gtk_box_pack_start(GTK_BOX(vbox), scroller, TRUE, TRUE, 0) gView = gtk_text_view_new() gtk_container_add(GTK_CONTAINER(scroller), gView) gBuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gView)) ' Statusbar gStatus = gtk_statusbar_new() gStatusId = gtk_statusbar_get_context_id(GTK_STATUSBAR(gStatus), "cursor") gtk_box_pack_end(GTK_BOX(vbox), gStatus, FALSE, FALSE, 0) ' Buffer signals for modified + cursor status g_signal_connect(gBuf, "modified-changed", G_CALLBACK(@OnBufferModifiedChanged), NULL) g_signal_connect(gBuf, "mark-set", G_CALLBACK(@OnMarkSet), NULL) UpdateStatus() gtk_widget_show_all(gWin) gtk_main()