diff --git a/.gitignore b/.gitignore index 88b6a2a..e3cb549 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.pyc -TESTING.py \ No newline at end of file +TESTING.py +.idea +__pycache__ \ No newline at end of file diff --git a/__pycache__/config.cpython-35.pyc b/__pycache__/config.cpython-35.pyc index 693fc9b..f437d40 100644 Binary files a/__pycache__/config.cpython-35.pyc and b/__pycache__/config.cpython-35.pyc differ diff --git a/__pycache__/ls_connection.cpython-35.pyc b/__pycache__/ls_connection.cpython-35.pyc index 953c755..ad423dc 100644 Binary files a/__pycache__/ls_connection.cpython-35.pyc and b/__pycache__/ls_connection.cpython-35.pyc differ diff --git a/__pycache__/note_reader.cpython-35.pyc b/__pycache__/note_reader.cpython-35.pyc index 051c11c..2ae472a 100644 Binary files a/__pycache__/note_reader.cpython-35.pyc and b/__pycache__/note_reader.cpython-35.pyc differ diff --git a/config.py b/config.py index b61916d..bfb55de 100644 --- a/config.py +++ b/config.py @@ -1,103 +1,102 @@ -#CONFIG FILE WITH CONSTANTS +# CONFIG FILE WITH CONSTANTS -DEBUG = True - -#Livesplit connection +# Livesplit connection HOST = "localhost" PORT = 16834 -#In network communication, time out after this time. (in seconds) +# In network communication, time out after this time. (in seconds) COM_TIMEOUT = 0.5 -#Possible commands to send to LiveSplit +# Possible commands to send to LiveSplit LS_COMMANDS = { "best_possible": "getbestpossibletime\r\n", "cur_split_index": "getsplitindex\r\n", "cur_split_name": "getcurrentsplitname\r\n" } -#Default Window Size +# Default Window Size DEFAULT_WINDOW = {"WIDTH": 400, "HEIGHT": 300, "TITLE": "SplitNotes"} -#Color Scheme -COLOR_SCHEME = {} - -#Default Welcome Message +# Default Welcome Message DEFAULT_MSG = "Right Click to Open Notes." -#Update time for polling livesplit and other actions (in seconds) +# Update time for polling livesplit and other actions (in seconds) POLLING_TIME = 0.5 -#file names and path for resources +# file names and path for resources RESOURCE_FOLDER = "resources" -ICONS= {"GREEN": "green.png", "RED": "red.png"} -SETTINGS_FILE ="config.cfg" +ICONS = {"GREEN": "green.png", "RED": "red.png", "SETTINGS": "settings_icon.png"} +SETTINGS_FILE = "config.cfg" -#Popup menu options +# Popup menu options MENU_OPTIONS = { - "SINGLE": "Set Single Layout", - "DOUBLE": "Set Double Layout", - "LOAD": "Load Notes", - "BIG": "Big Font", - "SMALL": "Small Font", - "SETTINGS": "Settings" - } + "SINGLE": "Set Single Layout", + "DOUBLE": "Set Double Layout", + "LOAD": "Load Notes", + "BIG": "Big Font", + "SMALL": "Small Font", + "SETTINGS": "Settings" +} -#Error messages -ERRORS = {"NOTES_EMPTY": ("Error", "Notes empty or can't be loaded!")} +# Error messages +ERRORS = {"NOTES_EMPTY": ("Error!", "Notes empty or can't be loaded!"), + "FONT_SIZE": ("Error!", "Invalid Font Size!"), + "SERVER_PORT": ("Error!", "Invalid server port!")} -#Max file size for notes -MAX_FILE_SIZE = 1000000000 #1 Giga-Byte +# Max file size for notes +MAX_FILE_SIZE = 1000000000 # 1 Giga-Byte -#TO be added to title to alert user that timer is running +# To be added to title to alert user that timer is running RUNNING_ALERT = "RUNNING" -#Font for notes/ -#TODO FIX THIS +# Font for notes/ +# TODO FIX THIS FONT = {"NAME": "arial", "SMALL": 12, "BIG": 16} GUI_FONT = ("arial", 12) -#Color scheme +# Color scheme COLOR = {"TEXT": "black", "TEXT_BG": "ivory"} -#Files tht should be displayed and opened a notes +# Files tht should be displayed and opened a notes TEXT_FILES = [ - ("Text Files", ("*.txt", "*.log", "*.asc", "*.conf", "*.cfg")), - ('All','*') - ] - -#Default content of config.cfg file -DEFAULT_CONFIG = "notes=\nfont_size=12\nfont=arial\ntext_color=FFFFFF\nbackground_color=000000" + ("Text Files", ("*.txt", "*.log", "*.asc", "*.conf", "*.cfg")), + ('All', '*') +] -#Required settings -REQUIRED_SETTINGS =["notes", - "font", - "font_size", - "text_color", - "background_color"] +# Default content of config.cfg file +DEFAULT_CONFIG = "notes=\nfont_size=12\nfont=arial\ntext_color=#FFFFFF\nbackground_color=#000000\nserver_port=16834" -#Settings window options -SETTINGS_WINDOW = {"TITLE": "Settings", - "WIDTH": 300, - "HEIGHT": 400, - "CANCEL": "Cancel", - "SAVE": "Save"} +# Required settings +REQUIRED_SETTINGS = ("notes", + "font", + "font_size", + "text_color", + "background_color", + "server_port" + ) -#OPTIONS IN THE SETTINGS WINDOW -SETTINGS_OPTIONS = {"FONT": "Font", - "FONT_SIZE": "Font Size", - "TEXT_COLOR": "Text Color", - "BG_COLOR": "Background Color"} - -#Fonts that can be selected -AVAILABLE_FONTS = ("arial", - "courier new", - "comic sans", - "fixedsys", - "ms sans serif", - "ms serif", - "system", - "times new roman", - "verdana") +# Settings window options +SETTINGS_WINDOW = {"TITLE": "Settings", + "WIDTH": 360, + "HEIGHT": 280, + "CANCEL": "Cancel", + "SAVE": "Save"} +# OPTIONS IN THE SETTINGS WINDOW +SETTINGS_OPTIONS = {"FONT": "Font", + "FONT_SIZE": "Font Size", + "TEXT_COLOR": "Text Color", + "BG_COLOR": "Background Color", + "SERVER_PORT": "LiveSplit Server port", + "DEFAULT_SERVER_PORT": "(Default is 16834)"} +# Fonts that can be selected +AVAILABLE_FONTS = ("arial", + "courier new", + "comic sans", + "fixedsys", + "ms sans serif", + "ms serif", + "system", + "times new roman", + "verdana") diff --git a/ls_connection.py b/ls_connection.py index 5c99d51..705dc3d 100644 --- a/ls_connection.py +++ b/ls_connection.py @@ -5,18 +5,20 @@ Conversation with livesplit is done through the server component. import socket from threading import Thread import config -import select #used for checking if socket has data pending +import select # used for checking if socket has data pending + def ls_connect(ls_socket, call_func, window): """Connects given socket to the livesplit server.""" con_thread = Thread(target=try_connection, args=(ls_socket, call_func, window)) con_thread.start() - - + + def init_socket(): """Returns a fresh socket""" return socket.socket() - + + def try_connection(ls_socket, call_func, window): """ Tries to connect given socket to ls. @@ -28,13 +30,15 @@ def try_connection(ls_socket, call_func, window): ls_socket.connect((config.HOST, config.PORT)) except: return False - + call_func(window) - + + def close_socket(com_socket): """Closes given socket.""" com_socket.close() + def check_connection(ls_socket): """ Check so connection between socket and livesplit @@ -45,8 +49,8 @@ def check_connection(ls_socket): return True else: return False - - + + def send_to_ls(ls_socket, command): """ Sends given command to ls using given socket. @@ -59,7 +63,7 @@ def send_to_ls(ls_socket, command): ls_socket.send(str.encode(config.LS_COMMANDS[command])) except: return False - + socket_ready = select.select([ls_socket], [], [], config.COM_TIMEOUT) if socket_ready[0]: try: @@ -68,7 +72,7 @@ def send_to_ls(ls_socket, command): return False else: return False - + def get_split_index(ls_socket): """ @@ -79,12 +83,12 @@ def get_split_index(ls_socket): Returns False on Error """ ls_data = send_to_ls(ls_socket, "cur_split_index") - + if not isinstance(ls_data, bool): return int(ls_data) else: return False - + def get_split_name(ls_socket): """ @@ -92,8 +96,8 @@ def get_split_name(ls_socket): Returns False if no split is active or Error occurs. """ ls_data = send_to_ls(ls_socket, "cur_split_name") - + if ls_data: return ls_data else: - return False \ No newline at end of file + return False diff --git a/main_window.py b/main_window.py index e9ca14b..e1f027f 100644 --- a/main_window.py +++ b/main_window.py @@ -12,105 +12,104 @@ import note_reader as noter import setting_handler as settings runtime_info = { - "ls_connected": False, - "timer_running": False, - "icon_active": False, - "active_split": -1, - "notes": [], - "double_layout": False, - "big_font": False - } + "ls_connected": False, + "timer_running": False, + "icon_active": False, + "active_split": -1, + "notes": [], + "double_layout": False, + "big_font": False +} root = tkinter.Tk() -red_path= os.path.join( - str(os.path.dirname(os.path.realpath(sys.argv[0]))), - config.RESOURCE_FOLDER, - config.ICONS["RED"] - ) -green_path= os.path.join( - str(os.path.dirname(os.path.realpath(sys.argv[0]))), - config.RESOURCE_FOLDER, - config.ICONS["GREEN"] - ) +red_path = os.path.join( + str(os.path.dirname(os.path.realpath(sys.argv[0]))), + config.RESOURCE_FOLDER, + config.ICONS["RED"] +) +green_path = os.path.join( + str(os.path.dirname(os.path.realpath(sys.argv[0]))), + config.RESOURCE_FOLDER, + config.ICONS["GREEN"] +) red_icon = tkinter.Image("photo", file=red_path) green_icon = tkinter.Image("photo", file=green_path) + def update(window, com_socket, text1, text2): """ Function to loop along tkinter mainloop. """ if not runtime_info["ls_connected"]: - #try connecting to ls + # try connecting to ls con.ls_connect(com_socket, server_found, window) else: - #is_connected + # is_connected if runtime_info["notes"]: - #notes loaded - - #get index of current split + # notes loaded + + # get index of current split new_index = con.get_split_index(com_socket) - + if isinstance(new_index, bool): - #Connection error + # Connection error com_socket = test_connection(com_socket, window, text1, text2) else: - #index retrieved succesfully + # index retrieved succesfully if new_index == -1: - #timer not running + # timer not running if runtime_info["timer_running"]: runtime_info["timer_running"] = False runtime_info["active_split"] = new_index update_GUI(window, com_socket, text1, text2) else: - #timer is running + # timer is running if not runtime_info["timer_running"]: runtime_info["timer_running"] = True - - #special case to fix scrolling + + # special case to fix scrolling if runtime_info["active_split"] == 0: runtime_info["active_split"] = -1 - + if not runtime_info["active_split"] == new_index: - #new split, need to update + # new split, need to update runtime_info["active_split"] = new_index - - update_GUI(window, com_socket, text1, text2) + + update_GUI(window, com_socket, text1, text2) else: - #notes not yet loaded + # notes not yet loaded com_socket = test_connection(com_socket, window, text1, text2) - - - - #self looping - window.after(int(config.POLLING_TIME * 1000), - update, window, com_socket, text1, text2) + + # self looping + window.after(int(config.POLLING_TIME * 1000), + update, window, com_socket, text1, text2) -def update_GUI(window, com_socket, text1, text2): +def update_GUI(window, com_socket, text1, text2): """ Updates all graphics according to current runtime_info. Sets window title and Text-box content. Does NOT set window icon. """ index = runtime_info["active_split"] - + if index == -1: index = 0 - + if runtime_info["timer_running"]: - #Does not test connection if it fails + # Does not test connection if it fails split_name = con.get_split_name(com_socket) else: split_name = False - + if runtime_info["notes"]: set_title_notes(window, index, split_name) update_notes(window, text1, text2, index) else: update_title(config.DEFAULT_WINDOW["TITLE"], window) - - + + def test_connection(com_socket, window, text1, text2): """ Runs a connection test to ls using given socket. @@ -132,16 +131,16 @@ def reset_connection(com_socket, window, text1, text2): if runtime_info["timer_running"]: runtime_info["timer_running"] = False runtime_info["active_split"] = -1 - + runtime_info["ls_connected"] = False - + update_icon(False, window) update_GUI(window, com_socket, text1, text2) - - #Close old and return a fresh socket + + # Close old and return a fresh socket con.close_socket(com_socket) return con.init_socket() - + def server_found(window): """ @@ -151,7 +150,7 @@ def server_found(window): runtime_info["ls_connected"] = True update_icon(True, window) - + def update_icon(active, window): """Updates icon of window depending on "active" variable""" if active and not runtime_info["icon_active"]: @@ -160,13 +159,13 @@ def update_icon(active, window): elif runtime_info["icon_active"]: window.tk.call('wm', 'iconphoto', window._w, red_icon) runtime_info["icon_active"] = False - - + + def update_title(name, window): """Sets the title of given window to name.""" window.wm_title(name) - - + + def adjust_content(window, box1, box2): """ Adjusts size of box1 and box2 according to @@ -176,36 +175,36 @@ def adjust_content(window, box1, box2): set_double_layout(window, box1, box2) else: set_single_layout(window, box1, box2) - - + + def set_double_layout(window, box1, box2): """ Configures boxes in the window to fit as in double layout. """ runtime_info["double_layout"] = True - + w_width = window.winfo_width() w_height = window.winfo_height() - + box1.place(height=(w_height // 2), width=w_width) box2.place(height=(w_height // 2), width=w_width, y=(w_height // 2)) - + def set_single_layout(window, box1, box2): """ Configures boxes in the window to fit as in single layout. """ runtime_info["double_layout"] = False - + box2.place_forget() box1.place(height=window.winfo_height(), width=window.winfo_width()) - + def show_popup(event, menu): """Displays given popup menu at cursor position.""" menu.post(event.x_root, event.y_root) - - + + def menu_change_layout(window, box1, box2, popup): """Menu option for changing layout selected.""" if runtime_info["double_layout"]: @@ -215,38 +214,38 @@ def menu_change_layout(window, box1, box2, popup): set_double_layout(window, box1, box2) popup.entryconfig(0, label=config.MENU_OPTIONS["SINGLE"]) - + def menu_load_notes(window, text1, text2, com_socket): """Menu selected load notes option.""" load_notes(window, text1, text2, com_socket) - - + + def load_notes(window, text1, text2, com_socket): """ Prompts user to select notes and then tries to load these into the UI. """ file = noter.select_file() - - if file: + + if file: notes = noter.get_notes(file) if notes: - #Notes loaded correctly + # Notes loaded correctly runtime_info["notes"] = notes - + split_c = len(notes) - show_info(("Notes Loaded", - ("Loaded notes with " + str(split_c) + " splits."))) - + show_info(("Notes Loaded", + ("Loaded notes with " + str(split_c) + " splits."))) + if not runtime_info["timer_running"]: runtime_info["active_split"] = -1 - + update_GUI(window, com_socket, text1, text2) - + else: show_info(config.ERRORS["NOTES_EMPTY"], True) - -def show_info(info, warning = False): + +def show_info(info, warning=False): """ Displays an infor popup window. if warning is True window has a warning triangle. @@ -255,8 +254,8 @@ def show_info(info, warning = False): messagebox.showwarning(info[0], info[1]) else: messagebox.showinfo(info[0], info[1]) - - + + def update_notes(window, text1, text2, index): """ Displays notes with the given index in given text widgets. @@ -267,32 +266,32 @@ def update_notes(window, text1, text2, index): text2 is always given the notes at index (index + 1) if existing """ max_index = (len(runtime_info["notes"]) - 1) - + if index < 0: index = 0 - + text1.config(state=tkinter.NORMAL) text2.config(state=tkinter.NORMAL) - + text1.delete("1.0", tkinter.END) text2.delete("1.0", tkinter.END) - + if index <= max_index: text1.insert(tkinter.END, runtime_info["notes"][index]) - - #cand disply notes for index+1 + + # cand disply notes for index+1 if index < max_index: text2.insert(tkinter.END, runtime_info["notes"][index + 1]) - + text1.config(state=tkinter.DISABLED) text2.config(state=tkinter.DISABLED) - + def right_arrow(window, com_socket, text1, text2): """Event handler for right arrow key.""" change_preview(window, com_socket, text1, text2, 1) - - + + def left_arrow(window, com_socket, text1, text2): """Event handler for left arrow key.""" change_preview(window, com_socket, text1, text2, -1) @@ -303,71 +302,77 @@ def change_preview(window, com_socket, text1, text2, move): if runtime_info["notes"] and (not runtime_info["timer_running"]): max_index = (len(runtime_info["notes"]) - 1) index = runtime_info["active_split"] - + if index < 0: index = 0 - + index += move - + if index > max_index: index = max_index - + runtime_info["active_split"] = index - + update_GUI(window, com_socket, text1, text2) - -def set_title_notes(window, index, split_name = False): + + +def set_title_notes(window, index, split_name=False): """ Set window title to fit with displayed notes. """ title = config.DEFAULT_WINDOW["TITLE"] - - disp_index = str(index + 1) #start at 1 + + disp_index = str(index + 1) # start at 1 title += " - " + disp_index - + if split_name: title += " - " + split_name - + if runtime_info["timer_running"]: title += " - " + config.RUNNING_ALERT - + update_title(title, window) - - -def menu_font_size(text_font, menu): + + +def menu_font_size(text_font, menu): if runtime_info["big_font"]: runtime_info["big_font"] = False menu.entryconfig(1, label=config.MENU_OPTIONS["BIG"]) else: runtime_info["big_font"] = True menu.entryconfig(1, label=config.MENU_OPTIONS["SMALL"]) - + update_font_size(text_font) - + + def menu_open_settings(root_wnd, apply_method, text1, text2): - settings.edit_settings(root_wnd, apply_method, text1, text2) - + settings.edit_settings(root_wnd, (lambda settings: apply_settings(settings, text1, text2))) + + def apply_settings(config, text1, text2): - #TODO apply all settings to the root window + # TODO apply all settings to the root window print("Applying settings") - + print(config) + + def update_font_size(text_font): if runtime_info["big_font"]: font_size = config.FONT["BIG"] else: font_size = config.FONT["SMALL"] - + text_font.config(size=font_size) + def init_UI(root): """Draws default UI and creates event bindings.""" - - #Create communication socket + + # Create communication socket com_socket = con.init_socket() - - #Graphical components + + # Graphical components root.geometry(str(config.DEFAULT_WINDOW["WIDTH"]) + "x" + str(config.DEFAULT_WINDOW["HEIGHT"])) - + box1 = tkinter.Frame(root) box2 = tkinter.Frame(root) @@ -377,80 +382,78 @@ def init_UI(root): scroll2 = tkinter.Scrollbar(box2) scroll2.pack(side=tkinter.RIGHT, fill=tkinter.Y) - text1 = tkinter.Text( - box1, - yscrollcommand=scroll1.set, - wrap=tkinter.WORD, - cursor="arrow" - ) + box1, + yscrollcommand=scroll1.set, + wrap=tkinter.WORD, + cursor="arrow" + ) text1.insert(tkinter.END, config.DEFAULT_MSG) text1.config(state=tkinter.DISABLED) text1.pack(fill=tkinter.BOTH, expand=True) text2 = tkinter.Text( - box2, - yscrollcommand=scroll2.set, - wrap=tkinter.WORD, - cursor="arrow" - ) + box2, + yscrollcommand=scroll2.set, + wrap=tkinter.WORD, + cursor="arrow" + ) text2.insert(tkinter.END, config.DEFAULT_MSG) text2.config(state=tkinter.DISABLED) text2.pack(fill=tkinter.BOTH, expand=True) scroll1.config(command=text1.yview) scroll2.config(command=text2.yview) - - #Set font and color for text + + # Set font and color for text text_font = font.Font( - family=config.FONT["NAME"], - size=config.FONT["SMALL"] - ) + family=config.FONT["NAME"], + size=config.FONT["SMALL"] + ) text1.config(font=text_font) text2.config(font=text_font) text1.config(fg=config.COLOR["TEXT"], bg=config.COLOR["TEXT_BG"]) text2.config(fg=config.COLOR["TEXT"], bg=config.COLOR["TEXT_BG"]) - + set_single_layout(root, box1, box2) - - #create popup menu + + # create popup menu popup = tkinter.Menu(root, tearoff=0) popup.add_command( - label=config.MENU_OPTIONS["DOUBLE"], - command=( - lambda: menu_change_layout(root, box1, box2, popup) - ) - ) #Needs to be at index 0 + label=config.MENU_OPTIONS["DOUBLE"], + command=( + lambda: menu_change_layout(root, box1, box2, popup) + ) + ) # Needs to be at index 0 popup.add_command( - label=config.MENU_OPTIONS["BIG"], - command=( - lambda: menu_font_size(text_font, popup) - ) - ) #Needs to be at index 1 + label=config.MENU_OPTIONS["BIG"], + command=( + lambda: menu_font_size(text_font, popup) + ) + ) # Needs to be at index 1 popup.add_command( - label=config.MENU_OPTIONS["LOAD"], - command=(lambda: menu_load_notes(root, text1, text2, com_socket)) - ) + label=config.MENU_OPTIONS["LOAD"], + command=(lambda: menu_load_notes(root, text1, text2, com_socket)) + ) popup.add_command( - label=config.MENU_OPTIONS["SETTINGS"], - command=(lambda: menu_open_settings(root, apply_settings, text1, text2)) - ) - - - #Set default window icon and title + label=config.MENU_OPTIONS["SETTINGS"], + command=(lambda: menu_open_settings(root, apply_settings, text1, text2)) + ) + + # Set default window icon and title root.tk.call('wm', 'iconphoto', root._w, red_icon) update_title(config.DEFAULT_WINDOW["TITLE"], root) - - #Event binds + + # Event binds root.bind("", (lambda e: adjust_content(root, box1, box2))) root.bind("", (lambda e: show_popup(e, popup))) root.bind("", (lambda e: right_arrow(root, com_socket, text1, text2))) root.bind("", (lambda e: left_arrow(root, com_socket, text1, text2))) - - #call update loop + + # call update loop update(root, com_socket, text1, text2) - + init_UI(root) -root.mainloop() \ No newline at end of file +root.mainloop() diff --git a/note_reader.py b/note_reader.py index 3db90a1..a0b431f 100644 --- a/note_reader.py +++ b/note_reader.py @@ -15,14 +15,15 @@ these can be used for titles. all other lines of text are added as notes """ + def get_note_lines(file_path): """ Reads file at given path and returns a list containing all rows of text in given file. Returns false if file can not be read. """ - - #check so file isn't too big + + # check so file isn't too big if path.getsize(file_path) > config.MAX_FILE_SIZE: return False @@ -30,37 +31,37 @@ def get_note_lines(file_path): notes_file = open(file_path, "r") except: return False - - #read file line per line + + # read file line per line f_lines = [] keep_reading = True while keep_reading: cur_line = notes_file.readline() - + if cur_line: f_lines.append(cur_line) else: keep_reading = False return f_lines - - + + def encode_notes(note_lines): """ Takes a list containing strings. Encodes given strings according to the note formatting. Returns the list containing the notes for every split. """ - + def is_title(line): if not line: return False - + return (line[0] == "[") and (line[-1] == "]") - + def is_newline(line): return (line == "\n") or (line == "\r") - + def remove_new_line(line): if (len(line) >= 1) and (is_newline(line[-1])): return line[:-1] @@ -69,9 +70,9 @@ def encode_notes(note_lines): note_list = [] cur_notes = "" - + for line in note_lines: - #remove whitespace at beginning and end + # remove whitespace at beginning and end line = line.strip(" ") if is_newline(line): @@ -81,11 +82,11 @@ def encode_notes(note_lines): else: line = remove_new_line(line) if not is_title(line): - cur_notes += line + "\n" #newline - + cur_notes += line + "\n" # newline + if cur_notes: note_list.append(cur_notes) - + return note_list @@ -97,12 +98,12 @@ def get_notes(file_path): Returns False if file is empty. """ note_lines = get_note_lines(file_path) - + if not note_lines: return False - + note_list = encode_notes(note_lines) - + return note_list @@ -112,10 +113,10 @@ def select_file(): Returns False upon no file selection. Otherwise returns absolute path to selected file. """ - + file = file_dia.askopenfilename(filetypes=config.TEXT_FILES) - + if file: return file else: - return False \ No newline at end of file + return False diff --git a/resources/config.cfg b/resources/config.cfg new file mode 100644 index 0000000..05e2cfa --- /dev/null +++ b/resources/config.cfg @@ -0,0 +1,6 @@ +notes= +font=times new roman +font_size=12 +server_port=34 +background_color=#4d73f4 +text_color=#ff8080 diff --git a/resources/settings_icon.png b/resources/settings_icon.png new file mode 100644 index 0000000..11374a6 Binary files /dev/null and b/resources/settings_icon.png differ diff --git a/setting_handler.py b/setting_handler.py index f209b97..e84ba07 100644 --- a/setting_handler.py +++ b/setting_handler.py @@ -1,3 +1,5 @@ +import tkinter.colorchooser as colorchooser +from tkinter import messagebox as msgbox import tkinter import os import sys @@ -5,10 +7,16 @@ import sys import config settings_path = os.path.join( - str(os.path.dirname(os.path.realpath(sys.argv[0]))), - config.RESOURCE_FOLDER, - config.SETTINGS_FILE - ) + str(os.path.dirname(os.path.realpath(sys.argv[0]))), + config.RESOURCE_FOLDER, + config.SETTINGS_FILE +) + +settings_icon_path = os.path.join( + str(os.path.dirname(os.path.realpath(sys.argv[0]))), + config.RESOURCE_FOLDER, + config.ICONS["SETTINGS"] +) def load_settings(): """ @@ -18,34 +26,32 @@ def load_settings(): returns a dictionary with all settings. """ - #try to open default settings file + # try to open default settings file try: - settings_file = open(settings_path,"r+") + settings_file = open(settings_path, "r+") settings_content = get_file_lines(settings_file) except: - #File not found + # File not found settings_content = set_default_settings(); - + settings = format_settings(settings_content) - - #Check so settings file has all settings + + # Check so settings file has all settings if not validate_settings(settings): settings = format_settings(set_default_settings()) - + return settings - + + def set_default_settings(): """ Creates a config file with default settings. Returns the default config-file content. """ - #TODO create file - settings_file = open(settings_path, "w") - settings_file.write(config.DEFAULT_CONFIG) - settings_file.close() - + set_settings_file_content(config.DEFAULT_CONFIG) return config.DEFAULT_CONFIG.split("\n") - + + def format_settings(file_rows): """ Takes a list of settings (as written in config files) and @@ -60,23 +66,23 @@ def format_settings(file_rows): for row in file_rows: row = row.strip("\n") parts = row.split("=") - - if(len(parts) == SETTING_PART_LENGTH): + + if len(parts) == SETTING_PART_LENGTH: settings[parts[0]] = parts[1] - - + return settings - + + def get_file_lines(file): """ Returns a list containing all the lines of the gicen file. """ - #read file line per line + # read file line per line f_lines = [] keep_reading = True while keep_reading: cur_line = file.readline() - + if cur_line: f_lines.append(cur_line) else: @@ -84,70 +90,228 @@ def get_file_lines(file): return f_lines - + def validate_settings(settings): """ Checks a settings dictionary so that all the needed settings are present. """ + for req_setting in config.REQUIRED_SETTINGS: - if not req_setting in settings: + if not (req_setting in settings): return False - + + if not validate_font_size(settings["font_size"]): + return False + + if not validate_server_port(settings["server_port"]): + return False + + if not validate_color(settings["text_color"]): + return False + + if not validate_color(settings["background_color"]): + return False + + if not (settings["font"] in config.AVAILABLE_FONTS): + return False + return True - - -def edit_settings(root_wnd, apply_method, text1, text2): - settings_wnd = tkinter.Toplevel(master=root_wnd, - width=config.SETTINGS_WINDOW["WIDTH"], + + +def set_settings_file_content(content): + """ + Saves given content to the config file, config.cfg, in the resources directory. + """ + settings_file = open(settings_path, "w") + settings_file.write(content) + settings_file.close() + + +def edit_settings(root_wnd, apply_method): + """ + Sets up a window for editing settings. + root_wnd is the main window that settings should be applied to. + apply_method is the method to be called to apply validated settings. + """ + settings_wnd = tkinter.Toplevel(master=root_wnd, + width=config.SETTINGS_WINDOW["WIDTH"], height=config.SETTINGS_WINDOW["HEIGHT"]) settings_wnd.title(config.SETTINGS_WINDOW["TITLE"]) - - settings = load_settings() - - settings_wnd.resizable(0,0) - - font_label = tkinter.Label(settings_wnd, - text=config.SETTINGS_OPTIONS["FONT"], - font=config.GUI_FONT) - font_size_label = tkinter.Label(settings_wnd, - text=config.SETTINGS_OPTIONS["FONT_SIZE"], - font=config.GUI_FONT) - text_color_label = tkinter.Label(settings_wnd, - text=config.SETTINGS_OPTIONS["TEXT_COLOR"], - font=config.GUI_FONT) - bg_color_label = tkinter.Label(settings_wnd, - text=config.SETTINGS_OPTIONS["BG_COLOR"], - font=config.GUI_FONT) - - font_label.place(x=15, y=15) - font_size_label.place(x=15, y=45) - text_color_label.place(x=15, y=75) - bg_color_label.place(x=15, y=105) - - #Font Selection - selected_font = tkinter.StringVar(settings_wnd) - - font_dropdown = tkinter.OptionMenu(settings_wnd, selected_font, "saker") - font_dropdown.place(x=100, y=15) - - #TODO finish font selection and rest of gui - - def save_settings(): - #TODO collect settings - #font = selected_font.get() - settings = [] - - apply_method(settings, text1, text2) - settings_wnd.destroy() - - save_btn = tkinter.Button(settings_wnd, - command=save_settings, - text=config.SETTINGS_WINDOW["SAVE"]) - cancel_btn = tkinter.Button(settings_wnd, - command=settings_wnd.destroy, - text=config.SETTINGS_WINDOW["CANCEL"]) - - save_btn.place(x=10, y=10) - cancel_btn.place(x=10, y=50) + settings_icon = tkinter.Image("photo", file=settings_icon_path) + settings_wnd.tk.call('wm', 'iconphoto', settings_wnd._w, settings_icon) - + settings = load_settings() + + settings_wnd.resizable(0, 0) + + font_label = tkinter.Label(settings_wnd, + text=config.SETTINGS_OPTIONS["FONT"], + font=config.GUI_FONT) + font_size_label = tkinter.Label(settings_wnd, + text=config.SETTINGS_OPTIONS["FONT_SIZE"], + font=config.GUI_FONT) + text_color_label = tkinter.Label(settings_wnd, + text=config.SETTINGS_OPTIONS["TEXT_COLOR"], + font=config.GUI_FONT) + bg_color_label = tkinter.Label(settings_wnd, + text=config.SETTINGS_OPTIONS["BG_COLOR"], + font=config.GUI_FONT) + port_label = tkinter.Label(settings_wnd, + text=config.SETTINGS_OPTIONS["SERVER_PORT"], + font=config.GUI_FONT) + default_port_label = tkinter.Label(settings_wnd, + text=config.SETTINGS_OPTIONS["DEFAULT_SERVER_PORT"], + font=config.GUI_FONT) + + # Font Selection + selected_font = tkinter.StringVar(settings_wnd) + selected_font.set(settings["font"]) + + font_dropdown = tkinter.OptionMenu(settings_wnd, + selected_font, + *config.AVAILABLE_FONTS) + font_dropdown.configure(font=config.GUI_FONT) + + # Font Size Selection + font_size_entry = tkinter.Entry(settings_wnd, width=2, font=config.GUI_FONT) + font_size_entry.insert(0, settings["font_size"]) + + # Text Color Selection + text_color = tkinter.Button(settings_wnd, + width=3, + height=1, + ) + + if validate_color(settings["text_color"]): + text_color.configure(background=settings["text_color"]) + else: + text_color.configure(background="#000000") + + def text_color_selection(): + choosen_color = colorchooser.askcolor() + if choosen_color[1]: + settings["text_color"] = choosen_color[1] + text_color.configure(background=settings["text_color"]) + + settings_wnd.focus_force() + + text_color.configure(command=text_color_selection) + + # Background color Selection + bg_color = tkinter.Button(settings_wnd, + width=3, + height=1, + ) + + if validate_color(settings["background_color"]): + bg_color.configure(background=settings["background_color"]) + else: + bg_color.configure(background="#FFFFFF") + + def bg_color_selection(): + choosen_color = colorchooser.askcolor() + if choosen_color[1]: + print(choosen_color) + settings["background_color"] = choosen_color[1] + bg_color.configure(background=settings["background_color"]) + + settings_wnd.focus_force() + + bg_color.configure(command=bg_color_selection) + + # Server port Selection + port_entry = tkinter.Entry(settings_wnd, width=6, font=config.GUI_FONT) + port_entry.insert(0, settings["server_port"]) + + # Save and cancel buttons + def control_and_save(): + errors_found = False + + settings["font"] = selected_font.get() + + chosen_font_size = font_size_entry.get() + chosen_port = port_entry.get() + + if not validate_font_size(chosen_font_size): + msgbox.showerror(config.ERRORS["FONT_SIZE"][0], config.ERRORS["FONT_SIZE"][1]) + errors_found = True + else: + settings["font_size"] = chosen_font_size + + if not validate_server_port(chosen_port): + msgbox.showerror(config.ERRORS["SERVER_PORT"][0], config.ERRORS["SERVER_PORT"][1]) + errors_found = True + else: + settings["server_port"] = chosen_port + + if not errors_found: + save_settings(settings) + apply_method(settings) + settings_wnd.destroy() + else: + settings_wnd.focus_force() + + save_btn = tkinter.Button(settings_wnd, + command=control_and_save, + text=config.SETTINGS_WINDOW["SAVE"], + font=config.GUI_FONT) + cancel_btn = tkinter.Button(settings_wnd, + command=settings_wnd.destroy, + text=config.SETTINGS_WINDOW["CANCEL"], + font=config.GUI_FONT) + + # Place all components + font_label.place(x=15, y=15) + font_size_label.place(x=15, y=55) + text_color_label.place(x=15, y=95) + bg_color_label.place(x=15, y=135) + port_label.place(x=15, y=175) + default_port_label.place(x=15, y=200) + + font_dropdown.place(x=178, y=15) + font_size_entry.place(x=180, y=55) + text_color.place(x=180, y=95) + bg_color.place(x=180, y=135) + port_entry.place(x=180, y=175) + + save_btn.place(x=110, y=230) + cancel_btn.place(x=190, y=230) + + +def validate_color(color): + """ + Returns whether or not given color is valid in the hexadecimal format. + """ + return isinstance(color, str) and len(color) == 7 and color[0] == "#" + + +def validate_font_size(size): + """ + Returns whether or not given size is an acceptable font size. + """ + try: + size = int(size) + except: + return False + + return 0 < size < 70 + + +def validate_server_port(port): + """ + Returns Whether or not gicen port is a valid server port. + """ + try: + port = int(port) + return True + except: + return False + + +def save_settings(settings): + file_content = "" + + for key in settings.keys(): + file_content += key + "=" + settings[key] + "\n" + + set_settings_file_content(file_content) \ No newline at end of file