t#!/usr/bin/env python3 # FutureLearn course: Programming with GUIs # URL: https://www.futurelearn.com/courses/programming-with-guis # # Tested with # Python 3.8.0 on Windows 10 (mainly) # Python 3.7.3 on Raspbian 10.2 # # Known bug on Raspbian with Python 3.7.3 # (works under Windows 10 with Python 3.8.0): # Start the app, click Help/About and than exit the app. # After closing the info box the app thinks, there is unsaved text # in the editor. # I have the same effect also in some other situations on Raspbian 10.2 # and not on Windows 10 # IMHO: this seems to be a problem in the underlying TK implementation. from guizero import App, Box, Combo, MenuBar, Slider, Text, TextBox from pathlib import Path import os import sys app_name = 'Simple Text Editor' app_version = '5' app_verdate = '13-DEC-2019' app_version_string = app_name + ' v' + app_version + ' (' + app_verdate + ')' app = App(title=app_name, bg='slategray', width=800, height=600) # remember the last folder in use, default: . - the current folder folder = '.' # True: no unsaved text in the editor text_saved = True # True: dark mode is enabled dark_mode = False # get the current folder ins use from the complete file path, stored in file_name_text_box.value def get_folder(): global file_name_text_box name = file_name_text_box.value if name is None: return '.' elif len(name) == 0: return '.' else: return Path(name).parent # show some version information def about_editor(): global app_version_string app.info('About', app_version_string + "\nA simple Python guizero based text editor") return # open a file at startup of the editor # store the file name in file_name_text_box.value def open_file_at_startup(file_name): global text_saved global editor global file_name_text_box try: with open(file_name, "r") as f: editor.value = f.read() except Exception as e: print('Error in reading file:', e) return file_name_text_box.value = file_name text_saved = True # open a file, selected in a file selector box, in the editor and # store the file name in file_name_text_box.value def open_file(): global text_saved global app global editor global file_name_text_box if not text_saved: resp = app.yesno('Warning', 'You have unsaved text in the editor - proceed?') if not resp: return f = get_folder() name = app.select_file('Open file', folder=f, filetypes=[["Text files", "*.txt"], ["Markdown files", "*.md"]], save=False) if name is None: text_saved = True return if len(name) == 0: text_saved = True return try: with open(name, "r") as f: editor.value = f.read() except Exception as e: app.error('Read Error', 'Error in reading file: ' + str(e)) return file_name_text_box.value = name text_saved = True # save the text in the editor the the file name, stored in file_name_text_box.value def save_file(): global text_saved global file_name_text_box name = file_name_text_box.value if name is None: save_as_file() return if len(name) == 0: save_as_file() return try: with open(name, "w") as f: f.write(editor.value) except Exception as e: app.error('Write Error', 'Error in reading file: ' + str(e)) return text_saved = True # save the text in the editor into a file, selected in a file selector box and # store the name in file_name_text_box.value def save_as_file(): global text_saved global app global file_name_text_box f = get_folder() name = app.select_file('Save file', folder=f, filetypes=[["Text files", "*.txt"], ["Markdown files", "*.md"]], save=True) if name is None: return if len(name) == 0: return try: with open(name, "w") as f: f.write(editor.value) except Exception as e: app.error('Write Error', 'Error in reading file: ' + str(e)) return file_name_text_box.value = name text_saved = True # remove all text from the editor and set the file name to '' in file_name_text_box.value def new_file(): global text_saved global app global file_name_text_box global editor global char_count_text_box if not text_saved: resp = app.yesno('Warning', 'You have unsaved text in the editor - proceed?') if not resp: return file_name_text_box.value = '' editor.value = '' char_count_text_box.value = '0' text_saved = True # close the editor app def exit_app(): global text_saved global app if not text_saved: resp = app.yesno('Warning', 'You have unsaved text in the editor - proceed?') if not resp: return app.destroy() def change_font(): global editor global font editor.font = font.value def change_text_color(): global editor global text_color editor.text_color = text_color.value def change_text_size(): global editor global size editor.text_size = size.value editor.resize(1, 1) editor.resize("fill", "fill") # get number of characters in the editor # observation: even if editor.value is '' the len(editor.value) is 1 def get_number_of_text_chars(): global editor chars = len(editor.value) if chars > 0: return chars -1 else: return 0 # wil be called by any change in the editor's text area def count_text_chars(): global text_saved global char_count_text_box char_count_text_box.value = get_number_of_text_chars() text_saved = False def toggle_status_line(): global status_box if status_box.visible: status_box.visible = False else: status_box.visible = True def toggle_char_count(): global char_count_box if char_count_box.visible: char_count_box.visible = False else: char_count_box.visible = True def toggle_preferences_controls(): global preferences_controls if preferences_controls.visible: preferences_controls.visible = False else: preferences_controls.visible = True def toggle_dark_mode(): global dark_mode global editor global text_color if dark_mode: dark_mode = False editor.bg = 'white' editor.text_color = text_color.value text_color.visible = True else: dark_mode = True editor.bg = '#0f0f0f' editor.text_color = 'white' text_color.visible = False # same handling as if clicked o 'File/Exit' app.when_closed = exit_app # create the app's menu bar and the related menu items menubar = MenuBar(app, # menu bar toplevel=["File", "Options", "Help"], # menu items options=[ # File [ # items in File ["New", new_file], ["Open...", open_file], ["Save", save_file], ["Save as...", save_as_file], ["Exit", exit_app] ], # Options [ # items in Options ["Toggle status line", toggle_status_line], ["Toggle char count", toggle_char_count], ["Toggle preferences", toggle_preferences_controls], ["Toggle dark mode", toggle_dark_mode] ], # Help [ # items in Help ["About", about_editor] ] ]) # create the status line, copntains: file name and character count status_box = Box(app, width='fill', border=True) file_name_label = Text(status_box, text='File: ', align="left") file_name_text_box = TextBox(status_box, text='', width='fill', align="left", enabled=False) file_name_text_box.bg = 'white' char_count_box = Box(status_box, width='fill', border=True) char_count_text_box = TextBox(char_count_box, text='', width='20', align="right", enabled=False) char_count_label = Text(char_count_box, text='Chars: ', align="right") char_count_text_box.value = '0' text_saved = True # create the preferences line preferences_controls = Box(app, align="top", width="fill", border=True) font = Combo(preferences_controls, options=["Courier", "Times New Roman", "Verdana"], align="left", selected='Courier', command=change_font) size = Slider(preferences_controls, align="left", command=change_text_size, start=10, end=18) size.value = 10 text_color = Combo(preferences_controls, options=['black', 'blue', 'red', 'magenta'], align="left", selected='black', command=change_text_color) # create the editor editor = TextBox(app, multiline=True, height="fill", width="fill", text='', scrollbar=True) editor.bg = 'white' editor.font = font.value editor.text_size = size.value editor.text_color = text_color.value editor.update_command(count_text_chars) editor.focus() if len(sys.argv) > 1: # argument available, assumption: this is the name of an existing file # read file into editor before displaying the GUI open_file_at_startup(os.path.realpath(sys.argv[1])) app.display()