diff --git a/doc.bas b/doc.bas new file mode 100644 index 0000000..d91d835 --- /dev/null +++ b/doc.bas @@ -0,0 +1,15 @@ +' doc.bas +#include once "doc.bi" + +sub Doc_Init( byref d as DocState ) + d.path = "" + d.modified = 0 +end sub + +sub Doc_SetPath( byref d as DocState, byref p as string ) + d.path = p +end sub + +sub Doc_SetModified( byref d as DocState, byval m as integer ) + d.modified = iif(m <> 0, 1, 0) +end sub diff --git a/doc.bi b/doc.bi new file mode 100644 index 0000000..0aaa568 --- /dev/null +++ b/doc.bi @@ -0,0 +1,11 @@ +' doc.bi +#pragma once + +type DocState + path as string + modified as integer +end type + +declare sub Doc_Init( byref d as DocState ) +declare sub Doc_SetPath( byref d as DocState, byref p as string ) +declare sub Doc_SetModified( byref d as DocState, byval m as integer ) diff --git a/main b/main new file mode 100755 index 0000000..e4b6adf Binary files /dev/null and b/main differ diff --git a/main.bas b/main.bas new file mode 100644 index 0000000..be31bba --- /dev/null +++ b/main.bas @@ -0,0 +1,406 @@ +' 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()