import tkinter as tk from tkinter import messagebox from tkinter import filedialog from tkinter import simpledialog import tkinter.scrolledtext as tkst import subprocess color1 = ["var", "print", "set", "debug", "plot"] color2 = ["string", "eval", "times", "action", "attribute", "bool"] color3 = ["=", "<", "<=", ">", ">=", "if", "for"] color4 = ["@"] color5 = ["make", "see", "add", "class", "func", "call"] ###### needed for line numbers ###### class TextLineNumbers(tk.Canvas): def __init__(self, *args, **kwargs): tk.Canvas.__init__(self, *args, **kwargs) self.textwidget = None def attach(self, text_widget): self.textwidget = text_widget def redraw(self, *args): self.delete("all") i = self.textwidget.index("@0,0") while True: dline = self.textwidget.dlineinfo(i) if dline is None: break y = dline[1] linenum = str(i).split(".")[0] self.create_text(2, y, anchor="nw", text=linenum) i = self.textwidget.index("%s+1line" % i) class CustomText(tk.Text): def __init__(self, *args, **kwargs): tkst.ScrolledText.__init__(self, *args, **kwargs) self._orig = self._w + "_orig" self.tk.call("rename", self._w, self._orig) self.tk.createcommand(self._w, self._proxy) def _proxy(self, *args): try: cmd = (self._orig,) + args result = self.tk.call(cmd) if ( args[0] in ("insert", "replace", "delete") or args[0:3] == ("mark", "set", "insert") or args[0:2] == ("xview", "moveto") or args[0:2] == ("xview", "scroll") or args[0:2] == ("yview", "moveto") or args[0:2] == ("yview", "scroll") ): self.event_generate("<<Change>>", when="tail") return result except: # this prevents error '_tkinter.TclError: text doesn't contain any characters tagged with "sel"' pass ###### needed for line numbers ###### class MessageBox(tk.simpledialog.Dialog): """Similar to tk.messagebox but updates parent if destroyed""" def __init__(self, parent, title, message): self.messageText = message tk.simpledialog.Dialog.__init__(self, parent, title) def body(self, master): self.frame = tk.Frame(master) self.message = tk.Message(self.frame, text=self.messageText) self.btn_cancel = tk.Button(self.frame, text="Cancel", command=self.cancel) self.bind("<Return>", self.cancel) self.frame.grid(column=0, row=0, sticky="NSEW") self.message.grid(column=0, row=1) self.btn_cancel.grid(column=0, row=2) return self.btn_cancel def destroy(self): """Update parent when destroyed""" self.parent.messageOpen = False super(MessageBox, self).destroy() def buttonbox(self): """Override default simpledialog.Dialog buttons""" pass class SearchDialog(tk.simpledialog.Dialog): """Dialog for text find and replace""" def __init__(self, parent, txt, old_text, title="Find and replace"): self.txt = txt self.messageOpen = False self.messageRef = None tk.simpledialog.Dialog.__init__(self, parent, title) def body(self, master): """Create dialog body, return widget with initial focus""" # Vars self.search_text = tk.StringVar() self.replace_text = tk.StringVar() self.isCaseSensitive = tk.IntVar() self.isCaseSensitive.set(1) self.isBackward = tk.IntVar() self.isRegExp = tk.IntVar() self.matchLength = tk.IntVar() # Widgets self.frame = tk.Frame(master) self.frame_btn = tk.Frame(self.frame) self.frame_check = tk.Frame(self.frame) self.frame_entry = tk.Frame(self.frame) self.search_entry = tk.Entry( self.frame_entry, width=20, textvariable=self.search_text ) self.replace_entry = tk.Entry( self.frame_entry, width=20, textvariable=self.replace_text ) self.check_case = tk.Checkbutton( self.frame_check, text="Case sensitive", var=self.isCaseSensitive ) self.check_search_backward = tk.Checkbutton( self.frame_check, text="Search backward", var=self.isBackward ) self.check_regexp = tk.Checkbutton( self.frame_check, text="Use regular expression", var=self.isRegExp ) self.btn_search = tk.Button(self.frame_btn, text="Find", command=self.search) self.btn_replace = tk.Button( self.frame_btn, text="Replace", command=self.replace ) self.btn_search_and_replace = tk.Button( self.frame_btn, text="Find and Replace", command=self.search_and_replace ) self.btn_cancel = tk.Button(self.frame, text="Cancel", command=self.cancel) # Frame placements self.frame.grid(column=0, row=0, sticky="NSEW") self.btn_cancel.grid(column=1, row=1, sticky="E", padx=(4, 8), pady=(4, 8)) self.frame_entry.grid(column=0, row=0) tk.Label(self.frame_entry, text="Find:").grid(column=0, row=0, sticky="W") self.search_entry.grid(column=1, row=0) tk.Label(self.frame_entry, text="Replace:").grid( column=0, row=1, sticky="W", pady=(6, 12) ) self.replace_entry.grid(column=1, row=1, pady=(6, 12)) self.frame_btn.grid(column=0, row=1, padx=(8, 4), pady=(4, 8)) self.btn_search.grid(column=0, row=0, sticky="W") self.btn_replace.grid(column=1, row=0, sticky="W", padx=(2, 10)) self.btn_search_and_replace.grid(column=2, row=0, sticky="E") self.frame_check.grid(column=1, row=0, pady=(6, 12)) self.check_case.grid(column=0, row=0, sticky="W") self.check_search_backward.grid(column=0, row=1, sticky="W") self.check_regexp.grid(column=0, row=2, sticky="W") return self.search_entry def _createMessage(self, text): """Create MessageBox, update state; recreate if already open""" if self.messageOpen: self._destroyMessage() self.messageRef = MessageBox(self, title="", message=text) self.messageOpen = True def _destroyMessage(self): """Destroy MessageBox and update message state""" if self.messageOpen: self.messageRef.destroy() self.messageRef = None self.messageOpen = False def _searchData(self): """Return snapshot of dialog vars relevant to _search""" return { "caseSensitive": self.isCaseSensitive.get(), "backwards": self.isBackward.get(), "regexp": self.isRegExp.get(), "search_text": self.search_text.get(), "replace_text": self.replace_text.get(), } def _search(self, doSearch, doReplace): """Internal method to search and/or replace""" if not doSearch and not doReplace: return self.txt.tag_configure("found", background="#aaaaaa") self.txt.tag_configure("replaced", background="#aaaaaa") data = self._searchData() n_search = len(data["search_text"]) if doSearch and not n_search > 0: return if doSearch: if data["backwards"]: self.txt.mark_set("search_start", "insert") self.txt.mark_set("search_end", "1.0" + "-1c") else: self.txt.mark_set("search_start", "insert") self.txt.mark_set("search_end", "end") if data["caseSensitive"]: nocase = 0 else: nocase = 1 if data["regexp"]: start = self.txt.search( r"{}".format(data["search_text"]), self.txt.index("search_start"), stopindex=self.txt.index("search_end"), backwards=data["backwards"], count=self.matchLength, nocase=nocase, regexp=True, ) else: start = self.txt.search( data["search_text"], self.txt.index("search_start"), stopindex=self.txt.index("search_end"), backwards=data["backwards"], count=self.matchLength, nocase=nocase, ) if start: end = start + "+{0}c".format(self.matchLength.get()) self.txt.tag_add("found", start, end) if data["backwards"]: self.txt.mark_set("insert", start) else: self.txt.mark_set("insert", end) else: # if no results found self._createMessage("No matches found.") return if doReplace: foundRanges = self.txt.tag_ranges("found") if not foundRanges: # If no 'found' tags, then do a search instead self._search(doSearch=True, doReplace=False) return foundStarts = [idx for i, idx in enumerate(foundRanges) if i % 2 == 0] foundEnds = [idx for i, idx in enumerate(foundRanges) if i % 2 == 1] for foundStart, foundEnd in zip(foundStarts, foundEnds): self.txt.delete(foundStart, foundEnd) self.txt.insert(foundStart, data["replace_text"], ("replaced",)) def search(self, event=0): """Command for Search button""" self._search(doSearch=True, doReplace=False) def replace(self, event=0): """Command for Replace button""" self._search(doSearch=False, doReplace=True) def search_and_replace(self, event=0): """Command for Search and Replace button""" self._search(doSearch=True, doReplace=True) def destroy(self): """Add text tag cleanup to simpledialog.Dialog destroy""" self.txt.tag_remove("found", "1.0", "end") self.txt.tag_remove("replaced", "1.0", "end") super(SearchDialog, self).destroy() def buttonbox(self): """Override default simpledialog.Dialog buttons""" pass class Files(tk.Frame): def __init__(self, parent): tk.Frame.__init__(self, parent) self.parent = parent parent.protocol("WM_DELETE_WINDOW", self.wclose) self.parent.title("greenBerry IDE - Untitled") self.pack(fill="both", expand=True) menubar = tk.Menu(self.parent) self.parent.config(menu=menubar) fileMenu = tk.Menu(menubar) runMenu = tk.Menu(menubar) searchMenu = tk.Menu(menubar) fileMenu.add_command(label="Save", command=self.save_file, accelerator="Ctrl+S") fileMenu.add_command( label="Save As", command=self.save_as_command, accelerator="Ctrl+Shift+S" ) fileMenu.add_command(label="Open", command=self.open_file, accelerator="Ctrl+O") menubar.add_cascade(label="File", menu=fileMenu) runMenu.add_command(label="Run", command=self.run_command, accelerator="F5") menubar.add_cascade(label="Run", menu=runMenu, command=self.open_file) searchMenu.add_command( label="Find and replace", command=self.search_command, accelerator="Ctrl+F" ) menubar.add_cascade(label="Search", menu=searchMenu) self.bind_all("<F5>", self.run_command) self.bind_all("<Control-o>", self.open_file) self.bind_all("<Control-s>", self.save_file) self.bind_all("<Control-S>", self.save_as_command) self.bind_all("<Control-f>", self.search_command) self.bind_all("<Key>", self.key_pressed) self.run_button = tk.Button(root, command=self.run_command) self.run_photo = tk.PhotoImage(file="../docs/run_button.png") self.run_button.config(image=self.run_photo, height=20, width=20) self.run_button.pack() self.txt = CustomText(self) self.linenumbers = TextLineNumbers(self, width=30) self.linenumbers.attach(self.txt) self.linenumbers.pack(side="left", fill="y") self.txt.pack(side="right", fill="both", expand=True) self.txt.bind("<<Change>>", self._on_change) self.txt.bind("<Configure>", self._on_change) self.old_text = self.txt.get("1.0", "end" + "-1c") self.file_dir = "" self.first = True def _on_change(self, event): self.linenumbers.redraw() def _on_change2(self, event): self.linenumbers2.redraw() def key_pressed(self, event=0): self.color_text() # run syntax highlighting a = self.parent.title() if self.txt.get("1.0", "end" + "-1c") != self.old_text and a[0] != "*": self.parent.title("*" + self.parent.title()) elif self.txt.get("1.0", "end" + "-1c") == self.old_text and a[0] == "*": self.parent.title(self.parent.title()[1:]) self.txt.yview_pickplace("insert") def open_file(self, event=0): self.txt.delete("insert") # Ctrl+o causes a new line so we need to delete it ftypes = [("greenBerry files", "*.gb"), ("All files", "*")] file = filedialog.askopenfile(filetypes=ftypes) if file != None: self.file_dir = file.name self.parent.title("greenBerry IDE" + " - " + file.name.replace("/", "\\")) self.txt.delete("1.0", "end" + "-1c") text = self.read_file(file.name) self.txt.insert("end", text) self.old_text = self.txt.get("1.0", "end" + "-1c") self.key_pressed() def read_file(self, filename): f = open(filename, "r") text = f.read() return text def save_file(self, event=0): try: with open(self.file_dir, "w") as file: file.write(self.txt.get("1.0", "end" + "-1c")) file.close() self.old_text = self.txt.get("1.0", "end" + "-1c") self.key_pressed() except: self.save_as_command() def search_command(self, event=0): d = SearchDialog( self.parent, txt=self.txt, old_text=self.old_text, title="Find and replace" ) def save_as_command(self, event=0): file = filedialog.asksaveasfile( mode="w", defaultextension=".gb", filetypes=(("greenBerry files", "*.gb"), ("All files", "*")), ) if file != None: self.parent.title("greenBerry IDE" + " - " + file.name.replace("/", "\\")) self.file_dir = file.name data = self.txt.get("1.0", "end" + "-1c") file.write(data) file.close() self.old_text = self.txt.get("1.0", "end" + "-1c") def run_command(self, event=0): x = self.txt.get("1.0", "end" + "-1c") if x == self.old_text and x != "": if self.first: self.outwin = tk.Toplevel(root) self.outwin.title("greenBerry IDE - output") self.outwin.geometry("600x640") self.txtout = CustomText(self.outwin) self.linenumbers2 = TextLineNumbers(self.outwin, width=30) self.linenumbers2.attach(self.txtout) self.linenumbers2.pack(side="left", fill="y") self.txtout.pack(fill="both", expand=True) self.txtout.bind("<<Change>>", self._on_change2) self.txtout.bind("<Configure>", self._on_change2) proc = subprocess.Popen( [ "python", "-c", 'import greenBerry; greenBerry.greenBerry_eval("""{0}""")'.format( self.read_file(self.file_dir) ), ], stdout=subprocess.PIPE, ) out = proc.communicate()[0][:-2] self.txtout.config(state="normal") if not self.first: self.txtout.insert("end", "\n" + "=" * 25 + "NEW RUN" + "=" * 25 + "\n") else: self.first = False self.txtout.insert("end", out) self.txtout.config(state="disabled") self.txtout.tag_add("colorout", "1.0", "end") self.txtout.tag_config("colorout", foreground="blue") self.txtout.yview_pickplace("end") elif messagebox.askokcancel( "Save before run", "Your file must be saved before running.\nPress OK to save.", ): self.save_file() try: if self.first: self.outwin = tk.Toplevel(root) self.outwin.title("greenBerry IDE - output") self.outwin.geometry("600x640") self.txtout = CustomText(self.outwin) self.linenumbers2 = TextLineNumbers(self.outwin, width=30) self.linenumbers2.attach(self.txtout) self.linenumbers2.pack(side="left", fill="y") self.txtout.pack(fill="both", expand=True) self.txtout.bind("<<Change>>", self._on_change2) self.txtout.bind("<Configure>", self._on_change2) proc = subprocess.Popen( [ "python", "-c", 'import greenBerry; greenBerry.greenBerry_eval("""{0}""")'.format( self.read_file(self.file_dir) ), ], stdout=subprocess.PIPE, ) out = proc.communicate()[0][:-2] self.txtout.config(state="normal") if not self.first: self.txtout.insert( "end", "\n" + "=" * 25 + "NEW RUN" + "=" * 25 + "\n" ) else: self.first = False self.txtout.insert("end", out) self.txtout.config(state="disabled") self.txtout.tag_add("colorout", "1.0", "end") self.txtout.tag_config("colorout", foreground="blue") self.txtout.yview_pickplace("end") except: self.run_command() def wclose(self, event=0): if self.parent.title()[0] == "*": save = messagebox.askyesnocancel( "Save file", "You have unsaved changes.\nDo you want to save before closing?", ) if save: self.save_file() if self.parent.title()[0] == "*": self.wclose() else: root.destroy() elif not save: root.destroy() else: root.destroy() def color_text(self, event=0): file_text = self.txt.get("1.0", "end" + "-1c") + " " words = [] line = 1 column = -1 word = "" for char in file_text: word += char column += 1 if char == "\n": words.append(word[:-1] + " : " + str(line) + "." + str(column)) word = "" line += 1 column = -1 if char == " ": words.append(word[:-1] + " : " + str(line) + "." + str(column)) word = "" for ( tag ) in self.txt.tag_names(): # deletes all tags so it can refresh them later self.txt.tag_delete(tag) for i in words: i = i.split() if len(i) < 3: i.insert(0, " ") if i[0] in color1: self.txt.tag_add( "color1", str(i[2].split(".")[0]) + "." + str(int(i[2].split(".")[1]) - len(i[0])), i[2], ) self.txt.tag_config("color1", foreground="#9a1777") elif i[0] in color2: self.txt.tag_add( "color2", str(i[2].split(".")[0]) + "." + str(int(i[2].split(".")[1]) - len(i[0])), i[2], ) self.txt.tag_config("color2", foreground="orange") elif i[0] in color3: self.txt.tag_add( "color3", str(i[2].split(".")[0]) + "." + str(int(i[2].split(".")[1]) - len(i[0])), i[2], ) self.txt.tag_config("color3", foreground="#e60000") elif i[0][0] in color4: self.txt.tag_add( "color4", str(i[2].split(".")[0]) + "." + str(int(i[2].split(".")[1]) - len(i[0])), str(i[2].split(".")[0]) + "." + str(int(i[2].split(".")[1]) - len(i[0]) + 1), ) self.txt.tag_config("color4", foreground="orange") elif i[0] in color5: self.txt.tag_add( "color5", str(i[2].split(".")[0]) + "." + str(int(i[2].split(".")[1]) - len(i[0])), i[2], ) self.txt.tag_config("color5", foreground="#00cc00") root = tk.Tk() ex = Files(root) ex.pack(side="bottom") root.geometry("600x640") root.iconbitmap(default="../docs/favicon.ico") root.mainloop()