├── .gitattributes ├── .gitignore ├── README.md ├── Releases ├── SplitNotes1.0.zip └── SplitNotes1.1.zip ├── build_instructions.txt ├── config.py ├── license.txt ├── ls_connection.py ├── main_window.py ├── note_reader.py ├── resources ├── green.png ├── red.png └── settings_icon.png ├── setting_handler.py └── setup_exe.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | TESTING.py 3 | .idea 4 | __pycache__ 5 | resources/config.cfg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SplitNotes 2 | Software for syncing notes with LiveSplit using the LiveSplit server component. 3 | 4 | Splitnotes automatically shows notes for the split you are currently on. 5 | 6 | ![Screenshot 1](http://i.imgur.com/CMlF2pj.png) 7 | ![Screenshot 2](http://i.imgur.com/4Ei2IiJ.png) 8 | 9 | ## Install 10 | 1. Download the latest version of LiveSplit Server Component from [this](https://github.com/LiveSplit/LiveSplit.Server/releases) site. 11 | 2. Unzip and move the files to the component folder in your LiveSplit install (...\LiveSplit\Components) 12 | 3. Download the latest version of SplitNotes from [this](https://github.com/joelnir/SplitNotes/releases) page. 13 | 4. Unzip SplitNotes anywhere. 14 | 15 | ## How To Use 16 | **Connect to LiveSplit** 17 | 1. Launch Splitnotes.exe 18 | 2. Launch LiveSplit 19 | 3. Go to "Edit Layout" -> "+" -> "Control" -> "LiveSplit Server". Hit ok. 20 | 4. In LiveSplit, select "Control" -> "Start Server". 21 | 5. SplitNotes should now be connected to LiveSplit. If connection is active the Icon for SplitNotes is green. 22 | 23 | **Format your notes** 24 | It is recommended to use a .txt file to store your notes. 25 | Note files should be formatted as following: 26 | 27 | * Normal text is treated as notes 28 | * Text in bracket ([some text]) is ignored from notes, this could be used to write down titles or other comments to keep the note file tidy. 29 | * A line only containing a split separator, such as a word (for example "\") or a simple newline, signals that notes for a specific split is over. You can set what you want to use as a split separator in the settings menu of SplitNotes. 30 | 31 | Example with newline as separator: 32 | 33 | >[Split1] 34 | >these are some notes for split 1. 35 | > 36 | >These are some notes for split2. 37 | >As you can see a title in brackets is not neccesary. 38 | >A simple new line is enough to separate notes for different splits. 39 | > 40 | >Also some notes for split 3. 41 | >You don't have to [ worry abut using brackets [ in the middle of a row]. 42 | 43 | Example with "-end-" as separator: 44 | 45 | >[Split1] 46 | >these are some notes for split 1. 47 | >-end- 48 | >These are some notes for split2. 49 | > 50 | >Now it is fine to use linebreaks in the middle of the notes. 51 | >This will still be on the notes for split 2 52 | >-end- 53 | >Also some notes for split 3. 54 | >You don't have to [ worry abut using brackets [ in the middle of a row]. 55 | 56 | **Load Notes** 57 | 1. Right-Click in SplitNotes and select "Load Notes". 58 | 2. Choose your text file. 59 | 3. Make sure that notes for the right amount of splits have been loaded. 60 | 61 | **Change Settings** 62 | 1. Right-Click in SplitNotes and select "Settings" 63 | 2. The settings menu will show up. Here you can set things like font, font size, text and background color, split separator and the server port. 64 | 65 | ## Features 66 | 67 | * Automatically displayed notes based on the active split in LiveSplit. 68 | * Ability to preview notes in the software when no run is going on by using the right and left arrow keys. 69 | * Two different font sizes to make sure that notes are easy to read. 70 | * Double layout to preview notes for both current and next split. 71 | * Multiple settings to change the look of the text. 72 | * Ability to set a custom port for interaction with the LiveSplit Server. 73 | 74 | #### Development 75 | Written in mainly procedural Python using tkinter GUI library. 76 | Made by Joelnir. 77 | -------------------------------------------------------------------------------- /Releases/SplitNotes1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeloskarsson/SplitNotes/199acd484727ec2ccc982d387d1f9a53ab1f6a39/Releases/SplitNotes1.0.zip -------------------------------------------------------------------------------- /Releases/SplitNotes1.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeloskarsson/SplitNotes/199acd484727ec2ccc982d387d1f9a53ab1f6a39/Releases/SplitNotes1.1.zip -------------------------------------------------------------------------------- /build_instructions.txt: -------------------------------------------------------------------------------- 1 | --- Build Instructions --- 2 | Using py2exe and python 3.4 3 | 4 | From the main directory run "python setup_exe.py py2exe". 5 | This creates a new "dist" folder with the SplitNotes .exe and needed data files. 6 | Zip up this folder for distribution. -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # CONFIG FILE WITH CONSTANTS 2 | 3 | # Livesplit connection 4 | HOST = "localhost" 5 | PORT = 16834 6 | 7 | # In network communication, time out after this time. (in seconds) 8 | COM_TIMEOUT = 0.5 9 | 10 | # Possible commands to send to LiveSplit 11 | LS_COMMANDS = { 12 | "best_possible": "getbestpossibletime\r\n", 13 | "cur_split_index": "getsplitindex\r\n", 14 | "cur_split_name": "getcurrentsplitname\r\n" 15 | } 16 | 17 | # Default Window Settings 18 | DEFAULT_WINDOW = {"TITLE": "SplitNotes"} 19 | 20 | # Default Welcome Message 21 | DEFAULT_MSG = "Right Click to Open Notes." 22 | 23 | # Update time for polling livesplit and other actions (in seconds) 24 | POLLING_TIME = 0.5 25 | 26 | # File names and path for resources 27 | RESOURCE_FOLDER = "resources" 28 | ICONS = {"GREEN": "green.png", "RED": "red.png", "SETTINGS": "settings_icon.png"} 29 | SETTINGS_FILE = "config.cfg" 30 | 31 | # Default Scrollbar Width 32 | SCROLLBAR_WIDTH = 16 33 | 34 | # Popup menu options 35 | MENU_OPTIONS = { 36 | "SINGLE": "Set Single Layout", 37 | "DOUBLE": "Set Double Layout", 38 | "LOAD": "Load Notes", 39 | "BIG": "Big Font", 40 | "SMALL": "Small Font", 41 | "SETTINGS": "Settings" 42 | } 43 | 44 | # Error messages 45 | ERRORS = {"NOTES_EMPTY": ("Error!", "Notes empty or can't be loaded!"), 46 | "FONT_SIZE": ("Error!", "Invalid Font Size!"), 47 | "SERVER_PORT": ("Error!", "Invalid server port!"), 48 | "SEPARATOR": ("Error!", "Invalid split separator!")} 49 | 50 | # Max file size for notes 51 | MAX_FILE_SIZE = 1000000000 # 1 Giga-Byte 52 | 53 | # To be added to title to alert user that timer is running 54 | RUNNING_ALERT = "RUNNING" 55 | 56 | # Font for gui widgets 57 | GUI_FONT = ("arial", 12) 58 | 59 | # Files tht should be displayed and opened a notes 60 | TEXT_FILES = [ 61 | ("Text Files", ("*.txt", "*.log", "*.asc", "*.conf", "*.cfg")), 62 | ('All', '*') 63 | ] 64 | 65 | # Default content of config.cfg file 66 | DEFAULT_CONFIG = "notes=\nfont_size=12\nfont=arial\ntext_color=#000000\nbackground_color=#FFFFFF\ndouble_layout=False\nserver_port=16834\nwidth=400\nheight=300\nseparator=new_line" 67 | 68 | NEWLINE_CONSTANT = "new_line" 69 | 70 | # Required settings 71 | REQUIRED_SETTINGS = ("notes", 72 | "font", 73 | "font_size", 74 | "text_color", 75 | "background_color", 76 | "server_port", 77 | "double_layout", 78 | "width", 79 | "height", 80 | "separator" 81 | ) 82 | 83 | # Settings window options 84 | SETTINGS_WINDOW = {"TITLE": "Settings", 85 | "WIDTH": 360, 86 | "HEIGHT": 410, 87 | "CANCEL": "Cancel", 88 | "SAVE": "Save"} 89 | 90 | # OPTIONS IN THE SETTINGS WINDOW 91 | SETTINGS_OPTIONS = {"FONT": "Font", 92 | "FONT_SIZE": "Font Size", 93 | "TEXT_COLOR": "Text Color", 94 | "BG_COLOR": "Background Color", 95 | "SERVER_PORT": "LiveSplit Server port", 96 | "DEFAULT_SERVER_PORT": "(Default is 16834)", 97 | "DOUBLE_LAYOUT": "Use double layout", 98 | "NEW_LINE_SEPARATOR": "Newline as split separator", 99 | "CUSTOM_SEPARATOR": "Custom split separator"} 100 | 101 | # Fonts that can be selected 102 | AVAILABLE_FONTS = ("arial", 103 | "courier new", 104 | "fixedsys", 105 | "ms sans serif", 106 | "ms serif", 107 | "system", 108 | "times new roman", 109 | "verdana") -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joel Oskarsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ls_connection.py: -------------------------------------------------------------------------------- 1 | """ 2 | ls refers to LiveSplit in the entire document. 3 | Conversation with livesplit is done through the server component. 4 | """ 5 | import socket 6 | from threading import Thread 7 | import config 8 | import select # used for checking if socket has data pending 9 | 10 | 11 | def ls_connect(ls_socket, call_func, window, server_port): 12 | """Connects given socket to the livesplit server.""" 13 | con_thread = Thread(target=try_connection, args=(ls_socket, call_func, window, server_port)) 14 | con_thread.start() 15 | 16 | 17 | def init_socket(): 18 | """Returns a fresh socket""" 19 | return socket.socket() 20 | 21 | 22 | def try_connection(ls_socket, call_func, window, server_port): 23 | """ 24 | Tries to connect given socket to ls. 25 | If connection is successful given "call_func" 26 | is called with window as argument. 27 | (made to be ran in a separate thread) 28 | """ 29 | try: 30 | ls_socket.connect((config.HOST, server_port)) 31 | except: 32 | return False 33 | 34 | call_func(window) 35 | 36 | 37 | def close_socket(com_socket): 38 | """Closes given socket.""" 39 | com_socket.close() 40 | 41 | 42 | def check_connection(ls_socket): 43 | """ 44 | Check so connection between socket and livesplit 45 | is still active and working. 46 | Returns boolean 47 | """ 48 | if send_to_ls(ls_socket, "best_possible"): 49 | return True 50 | else: 51 | return False 52 | 53 | 54 | def send_to_ls(ls_socket, command): 55 | """ 56 | Sends given command to ls using given socket. 57 | If connected is False, tries to send without socket being connected to ls. 58 | Returns the response, or False if an error occurs. 59 | Check config.LS_COMMANDS for avaiable commands. 60 | """ 61 | 62 | try: 63 | ls_socket.send(str.encode(config.LS_COMMANDS[command])) 64 | except: 65 | return False 66 | 67 | socket_ready = select.select([ls_socket], [], [], config.COM_TIMEOUT) 68 | if socket_ready[0]: 69 | try: 70 | return (ls_socket.recv(1000)).decode("utf-8") 71 | except: 72 | return False 73 | else: 74 | return False 75 | 76 | 77 | def get_split_index(ls_socket): 78 | """ 79 | Returns the index of the active split in livesplit. 80 | Returns -1 if timer is not yet started. 81 | (First split is 0) 82 | 83 | Returns False on Error 84 | """ 85 | ls_data = send_to_ls(ls_socket, "cur_split_index") 86 | 87 | if not isinstance(ls_data, bool): 88 | return int(ls_data) 89 | else: 90 | return False 91 | 92 | 93 | def get_split_name(ls_socket): 94 | """ 95 | Returns name of the active split in livesplit. 96 | Returns False if no split is active or Error occurs. 97 | """ 98 | ls_data = send_to_ls(ls_socket, "cur_split_name") 99 | 100 | if ls_data: 101 | return ls_data 102 | else: 103 | return False 104 | -------------------------------------------------------------------------------- /main_window.py: -------------------------------------------------------------------------------- 1 | import tkinter 2 | from tkinter import messagebox 3 | 4 | import os 5 | import sys 6 | 7 | import config 8 | import ls_connection as con 9 | import note_reader as noter 10 | import setting_handler 11 | 12 | runtime_info = { 13 | "ls_connected": False, 14 | "timer_running": False, 15 | "active_split": -1, 16 | "notes": [], 17 | "server_port": 0, 18 | "force_reset": False, 19 | "double_layout": False, 20 | "settings": {} 21 | } 22 | 23 | root = tkinter.Tk() 24 | 25 | red_path = os.path.join( 26 | str(os.path.dirname(os.path.realpath(sys.argv[0]))), 27 | config.RESOURCE_FOLDER, 28 | config.ICONS["RED"] 29 | ) 30 | green_path = os.path.join( 31 | str(os.path.dirname(os.path.realpath(sys.argv[0]))), 32 | config.RESOURCE_FOLDER, 33 | config.ICONS["GREEN"] 34 | ) 35 | red_icon = tkinter.Image("photo", file=red_path) 36 | green_icon = tkinter.Image("photo", file=green_path) 37 | 38 | 39 | def update(window, com_socket, text1, text2): 40 | """ 41 | Function to loop along tkinter mainloop. 42 | """ 43 | if runtime_info["force_reset"]: 44 | # Boolean flag to force a connection reset 45 | com_socket = reset_connection(com_socket, window, text1, text2) 46 | runtime_info["force_reset"] = False 47 | 48 | elif not runtime_info["ls_connected"]: 49 | # try connecting to ls 50 | con.ls_connect(com_socket, server_found, window, runtime_info["server_port"]) 51 | else: 52 | # is_connected 53 | if runtime_info["notes"]: 54 | # notes loaded 55 | 56 | # get index of current split 57 | new_index = con.get_split_index(com_socket) 58 | 59 | if isinstance(new_index, bool): 60 | # Connection error 61 | com_socket = test_connection(com_socket, window, text1, text2) 62 | else: 63 | # index retrieved succesfully 64 | if new_index == -1: 65 | # timer not running 66 | if runtime_info["timer_running"]: 67 | runtime_info["timer_running"] = False 68 | runtime_info["active_split"] = new_index 69 | update_GUI(window, com_socket, text1, text2) 70 | else: 71 | # timer is running 72 | if not runtime_info["timer_running"]: 73 | runtime_info["timer_running"] = True 74 | 75 | # special case to fix scrolling 76 | if runtime_info["active_split"] == 0: 77 | runtime_info["active_split"] = -1 78 | 79 | if not runtime_info["active_split"] == new_index: 80 | # new split, need to update 81 | runtime_info["active_split"] = new_index 82 | 83 | update_GUI(window, com_socket, text1, text2) 84 | else: 85 | # notes not yet loaded 86 | com_socket = test_connection(com_socket, window, text1, text2) 87 | 88 | # self looping 89 | window.after(int(config.POLLING_TIME * 1000), 90 | update, window, com_socket, text1, text2) 91 | 92 | 93 | def update_GUI(window, com_socket, text1, text2): 94 | """ 95 | Updates all graphics according to current runtime_info. 96 | Sets window title and Text-box content. 97 | Does NOT set window icon. 98 | """ 99 | index = runtime_info["active_split"] 100 | 101 | if index == -1: 102 | index = 0 103 | 104 | if runtime_info["timer_running"]: 105 | # Does not test connection if it fails 106 | split_name = con.get_split_name(com_socket) 107 | else: 108 | split_name = False 109 | 110 | if runtime_info["notes"]: 111 | set_title_notes(window, index, split_name) 112 | update_notes(text1, text2, index) 113 | else: 114 | update_title(config.DEFAULT_WINDOW["TITLE"], window) 115 | 116 | 117 | def test_connection(com_socket, window, text1, text2): 118 | """ 119 | Runs a connection test to ls using given socket. 120 | If test is unsuccessful, resets connection. 121 | Returns a socket that should be used for communication with ls. 122 | """ 123 | if con.check_connection(com_socket): 124 | return com_socket 125 | else: 126 | return reset_connection(com_socket, window, text1, text2) 127 | 128 | 129 | def reset_connection(com_socket, window, text1, text2): 130 | """ 131 | Resets all variables and closes given socket. 132 | Updates GUI to respond to connection loss. 133 | Returns a fresh socket that can be used to connect to ls. 134 | """ 135 | if runtime_info["timer_running"]: 136 | runtime_info["timer_running"] = False 137 | runtime_info["active_split"] = -1 138 | 139 | runtime_info["ls_connected"] = False 140 | 141 | update_icon(False, window) 142 | update_GUI(window, com_socket, text1, text2) 143 | 144 | # Close old and return a fresh socket 145 | con.close_socket(com_socket) 146 | 147 | return con.init_socket() 148 | 149 | 150 | def server_found(window): 151 | """ 152 | Executes correct settings for when 153 | ls connection has been established. 154 | """ 155 | runtime_info["ls_connected"] = True 156 | update_icon(True, window) 157 | 158 | 159 | def update_icon(active, window): 160 | """Updates icon of window depending on "active" variable""" 161 | if active: 162 | window.tk.call('wm', 'iconphoto', window._w, green_icon) 163 | else: 164 | window.tk.call('wm', 'iconphoto', window._w, red_icon) 165 | 166 | 167 | def update_title(name, window): 168 | """Sets the title of given window to name.""" 169 | window.wm_title(name) 170 | 171 | 172 | def adjust_content(window, box1, box2): 173 | """ 174 | Adjusts size of box1 and box2 according to 175 | layout and size of window. 176 | """ 177 | if runtime_info["double_layout"]: 178 | set_double_layout(window, box1, box2) 179 | else: 180 | set_single_layout(window, box1, box2) 181 | 182 | 183 | def set_double_layout(window, box1, box2): 184 | """ 185 | Configures boxes in the window to fit as in double layout. 186 | """ 187 | runtime_info["double_layout"] = True 188 | 189 | w_width = window.winfo_width() 190 | w_height = window.winfo_height() 191 | 192 | box1.place(height=(w_height // 2), width=w_width) 193 | box2.place(height=(w_height // 2), width=w_width, y=(w_height // 2)) 194 | 195 | 196 | def set_single_layout(window, box1, box2): 197 | """ 198 | Configures boxes in the window to fit as in single layout. 199 | """ 200 | runtime_info["double_layout"] = False 201 | 202 | box2.place_forget() 203 | box1.place(height=window.winfo_height(), width=window.winfo_width()) 204 | 205 | 206 | def show_popup(event, menu): 207 | """Displays given popup menu at cursor position.""" 208 | menu.post(event.x_root, event.y_root) 209 | 210 | 211 | def menu_load_notes(window, text1, text2, com_socket): 212 | """Menu selected load notes option.""" 213 | load_notes(window, text1, text2, com_socket) 214 | 215 | 216 | def load_notes(window, text1, text2, com_socket): 217 | """ 218 | Prompts user to select notes and then tries to load these into the UI. 219 | """ 220 | file = noter.select_file() 221 | 222 | if file: 223 | notes = noter.get_notes(file, runtime_info["settings"]["separator"]) 224 | if notes: 225 | # Notes loaded correctly 226 | runtime_info["notes"] = notes 227 | 228 | # Save notes to settings 229 | settings = setting_handler.load_settings() 230 | settings["notes"] = file 231 | setting_handler.save_settings(settings) 232 | 233 | split_c = len(notes) 234 | show_info(("Notes Loaded", 235 | ("Loaded notes with " + str(split_c) + " splits."))) 236 | 237 | if not runtime_info["timer_running"]: 238 | runtime_info["active_split"] = -1 239 | 240 | update_GUI(window, com_socket, text1, text2) 241 | 242 | else: 243 | show_info(config.ERRORS["NOTES_EMPTY"], True) 244 | 245 | 246 | def show_info(info, warning=False): 247 | """ 248 | Displays an info popup window. 249 | if warning is True window has a warning triangle. 250 | """ 251 | if warning: 252 | messagebox.showwarning(info[0], info[1]) 253 | else: 254 | messagebox.showinfo(info[0], info[1]) 255 | 256 | 257 | def update_notes(text1, text2, index): 258 | """ 259 | Displays notes with the given index in given text widgets. 260 | If index is lower than 0, displays notes for index 0. 261 | If index is higher than the highest index there are 262 | notes for the text widgets are left empty. 263 | 264 | text2 is always given the notes at index (index + 1) if existing 265 | """ 266 | max_index = (len(runtime_info["notes"]) - 1) 267 | 268 | if index < 0: 269 | index = 0 270 | 271 | text1.config(state=tkinter.NORMAL) 272 | text2.config(state=tkinter.NORMAL) 273 | 274 | text1.delete("1.0", tkinter.END) 275 | text2.delete("1.0", tkinter.END) 276 | 277 | if index <= max_index: 278 | text1.insert(tkinter.END, runtime_info["notes"][index]) 279 | 280 | # cant disply notes for index+1 281 | if index < max_index: 282 | text2.insert(tkinter.END, runtime_info["notes"][index + 1]) 283 | 284 | text1.config(state=tkinter.DISABLED) 285 | text2.config(state=tkinter.DISABLED) 286 | 287 | 288 | def right_arrow(window, com_socket, text1, text2): 289 | """Event handler for right arrow key.""" 290 | change_preview(window, com_socket, text1, text2, 1) 291 | 292 | 293 | def left_arrow(window, com_socket, text1, text2): 294 | """Event handler for left arrow key.""" 295 | change_preview(window, com_socket, text1, text2, -1) 296 | 297 | 298 | def change_preview(window, com_socket, text1, text2, move): 299 | """ 300 | Chnges notes that are currently displayed. 301 | Move is either 1 for next or -1 for previous. 302 | """ 303 | if runtime_info["notes"] and (not runtime_info["timer_running"]): 304 | max_index = (len(runtime_info["notes"]) - 1) 305 | index = runtime_info["active_split"] 306 | 307 | if index < 0: 308 | index = 0 309 | 310 | index += move 311 | 312 | if index > max_index: 313 | index = max_index 314 | 315 | runtime_info["active_split"] = index 316 | 317 | update_GUI(window, com_socket, text1, text2) 318 | 319 | 320 | def set_title_notes(window, index, split_name=False): 321 | """ 322 | Set window title to fit with displayed notes. 323 | """ 324 | title = config.DEFAULT_WINDOW["TITLE"] 325 | 326 | disp_index = str(index + 1) # start at 1 327 | title += " - " + disp_index 328 | 329 | if split_name: 330 | title += " - " + split_name 331 | 332 | if runtime_info["timer_running"]: 333 | title += " - " + config.RUNNING_ALERT 334 | 335 | update_title(title, window) 336 | 337 | 338 | def menu_open_settings(root_wnd, box1, box2, text1, text2, com_socket): 339 | """ 340 | Opens the settings menu. 341 | """ 342 | setting_handler.edit_settings(root_wnd, 343 | (lambda settings: apply_settings(settings, 344 | root_wnd, 345 | box1, box2, 346 | text1, text2, com_socket))) 347 | 348 | 349 | def apply_settings(settings, window, box1, box2, text1, text2, com_socket): 350 | """ 351 | Applies the given settings to the given components. 352 | Settings must be a correctly formatted dictionary. 353 | """ 354 | runtime_info["settings"] = settings 355 | 356 | # Server port change 357 | if not (runtime_info["server_port"] == int(settings["server_port"])): 358 | runtime_info["server_port"] = int(settings["server_port"]) 359 | runtime_info["force_reset"] = True 360 | 361 | text_font = (settings["font"], int(settings["font_size"])) 362 | 363 | if setting_handler.decode_boolean_setting(settings["double_layout"]): 364 | set_double_layout(window, box1, box2) 365 | else: 366 | set_single_layout(window, box1, box2) 367 | 368 | text1.config(font=text_font) 369 | text2.config(font=text_font) 370 | text1.config(fg=settings["text_color"], bg=settings["background_color"]) 371 | text2.config(fg=settings["text_color"], bg=settings["background_color"]) 372 | 373 | old_note_length = len(runtime_info["notes"]) 374 | 375 | if settings["notes"] and noter.file_exists(settings["notes"]): 376 | new_notes = noter.get_notes(settings["notes"], settings["separator"]) 377 | 378 | if new_notes: 379 | # Notes loaded correctly 380 | runtime_info["notes"] = new_notes 381 | 382 | new_note_length = len(new_notes) 383 | 384 | if not (new_note_length == old_note_length): 385 | show_info(("Notes Loaded", 386 | ("Loaded notes with " + str(new_note_length) + " splits."))) 387 | 388 | if not runtime_info["timer_running"]: 389 | runtime_info["active_split"] = -1 390 | 391 | update_GUI(window, com_socket, text1, text2) 392 | else: 393 | show_info(config.ERRORS["NOTES_EMPTY"], True) 394 | 395 | 396 | def save_geometry_settings(width, height): 397 | """ 398 | Saves given width and height to settigns file. 399 | """ 400 | settings = setting_handler.load_settings() 401 | settings["width"] = str(width) 402 | settings["height"] = str(height) 403 | setting_handler.save_settings(settings) 404 | 405 | 406 | def do_on_close(root_wnd): 407 | """ 408 | Function that is called when the main tk window is closed. 409 | Saves root_wnd's width and height to the settings file and 410 | then closes the window. 411 | """ 412 | save_geometry_settings(root_wnd.winfo_width(), root_wnd.winfo_height()) 413 | root_wnd.destroy() 414 | 415 | 416 | def init_UI(root): 417 | """Draws default UI and creates event bindings.""" 418 | 419 | # Create communication socket 420 | com_socket = con.init_socket() 421 | 422 | # Load Settings 423 | settings = setting_handler.load_settings() 424 | runtime_info["server_port"] = int(settings["server_port"]) 425 | runtime_info["settings"] = settings 426 | 427 | # Graphical components 428 | root.geometry(settings["width"] + "x" + settings["height"]) 429 | 430 | box1 = tkinter.Frame(root) 431 | box2 = tkinter.Frame(root) 432 | 433 | scroll1 = tkinter.Scrollbar(box1, width=config.SCROLLBAR_WIDTH) 434 | scroll2 = tkinter.Scrollbar(box2, width=config.SCROLLBAR_WIDTH) 435 | scroll1.pack(side=tkinter.RIGHT, fill=tkinter.Y) 436 | scroll2.pack(side=tkinter.RIGHT, fill=tkinter.Y) 437 | 438 | text1 = tkinter.Text( 439 | box1, 440 | yscrollcommand=scroll1.set, 441 | wrap=tkinter.WORD, 442 | cursor="arrow" 443 | ) 444 | text1.insert(tkinter.END, config.DEFAULT_MSG) 445 | text1.config(state=tkinter.DISABLED) 446 | text1.pack(fill=tkinter.BOTH, expand=True) 447 | 448 | text2 = tkinter.Text( 449 | box2, 450 | yscrollcommand=scroll2.set, 451 | wrap=tkinter.WORD, 452 | cursor="arrow" 453 | ) 454 | text2.insert(tkinter.END, config.DEFAULT_MSG) 455 | text2.config(state=tkinter.DISABLED) 456 | text2.pack(fill=tkinter.BOTH, expand=True) 457 | 458 | scroll1.config(command=text1.yview) 459 | scroll2.config(command=text2.yview) 460 | 461 | # Set font and color for text 462 | text_font = (settings["font"], int(settings["font_size"])) 463 | 464 | text1.config(font=text_font) 465 | text2.config(font=text_font) 466 | text1.config(fg=settings["text_color"], bg=settings["background_color"]) 467 | text2.config(fg=settings["text_color"], bg=settings["background_color"]) 468 | 469 | if setting_handler.decode_boolean_setting(settings["double_layout"]): 470 | set_double_layout(root, box1, box2) 471 | else: 472 | set_single_layout(root, box1, box2) 473 | 474 | # create popup menu 475 | popup = tkinter.Menu(root, tearoff=0) 476 | popup.add_command( 477 | label=config.MENU_OPTIONS["LOAD"], 478 | command=(lambda: menu_load_notes(root, text1, text2, com_socket)) 479 | ) 480 | popup.add_command( 481 | label=config.MENU_OPTIONS["SETTINGS"], 482 | command=(lambda: menu_open_settings(root, box1, box2, text1, text2, com_socket)) 483 | ) 484 | 485 | # Set default window icon and title 486 | root.tk.call('wm', 'iconphoto', root._w, red_icon) 487 | update_title(config.DEFAULT_WINDOW["TITLE"], root) 488 | 489 | # Check if notes can be loaded from settings 490 | settings = setting_handler.load_settings() 491 | 492 | if settings["notes"] and noter.file_exists(settings["notes"]): 493 | notes = noter.get_notes(settings["notes"], settings["separator"]) 494 | 495 | if notes: 496 | runtime_info["notes"] = notes 497 | update_GUI(root, com_socket, text1, text2) 498 | 499 | # Event binds 500 | root.bind("", (lambda e: adjust_content(root, box1, box2))) 501 | root.bind("", (lambda e: show_popup(e, popup))) 502 | root.bind("", (lambda e: right_arrow(root, com_socket, text1, text2))) 503 | root.bind("", (lambda e: left_arrow(root, com_socket, text1, text2))) 504 | 505 | # Window close bind 506 | root.protocol("WM_DELETE_WINDOW", (lambda: do_on_close(root))) 507 | 508 | # call update loop 509 | update(root, com_socket, text1, text2) 510 | 511 | root.geometry(settings["width"] + "x" + settings["height"]) 512 | 513 | init_UI(root) 514 | 515 | root.mainloop() 516 | -------------------------------------------------------------------------------- /note_reader.py: -------------------------------------------------------------------------------- 1 | import tkinter.filedialog as file_dia 2 | import os.path as path 3 | 4 | import config 5 | 6 | """ 7 | NOTE STANDARD FORMATTING 8 | empty newlines separate notes for different splits 9 | 10 | It is also possible to set your own split separator in the settings menu 11 | 12 | lines that start and end with [ ] are ignored for notes. 13 | these can be used for titles. 14 | (ex. [Split1] is not included in notes) 15 | 16 | all other lines of text are added as notes 17 | """ 18 | 19 | 20 | def get_note_lines(file_path): 21 | """ 22 | Reads file at given path and returns 23 | a list containing all rows of text in given file. 24 | Returns false if file can not be read. 25 | """ 26 | 27 | # check so file isn't too big 28 | if path.getsize(file_path) > config.MAX_FILE_SIZE: 29 | return False 30 | 31 | try: 32 | notes_file = open(file_path, "r") 33 | except: 34 | return False 35 | 36 | # read file line per line 37 | f_lines = [] 38 | keep_reading = True 39 | while keep_reading: 40 | 41 | try: 42 | cur_line = notes_file.readline() 43 | except: 44 | return False 45 | 46 | if cur_line: 47 | f_lines.append(cur_line) 48 | else: 49 | keep_reading = False 50 | 51 | return f_lines 52 | 53 | 54 | def decode_notes(note_lines, separator): 55 | """ 56 | Takes a list containing strings. 57 | Encodes given strings according to the note formatting. 58 | Returns the list containing the notes for every split. 59 | """ 60 | 61 | #Check if newline is being used as separator 62 | 63 | if separator == config.NEWLINE_CONSTANT: 64 | separator = "" # left after stripping newline 65 | 66 | def is_title(line): 67 | if not line: 68 | return False 69 | 70 | return (line[0] == "[") and (line[-1] == "]") 71 | 72 | def is_separator(line): 73 | return (line == separator) 74 | 75 | def is_newline(s): 76 | return (s == "\n") 77 | 78 | def remove_new_line(line): 79 | if (len(line) >= 1) and (is_newline(line[-1])): 80 | return line[:-1] 81 | else: 82 | return line 83 | 84 | note_list = [] 85 | cur_notes = "" 86 | 87 | for line in note_lines: 88 | line = remove_new_line(line) 89 | 90 | if is_separator(line): 91 | if cur_notes: 92 | note_list.append(cur_notes) 93 | cur_notes = "" 94 | else: 95 | if not is_title(line): 96 | cur_notes += line + "\n" # newline 97 | 98 | if cur_notes: 99 | note_list.append(cur_notes) 100 | 101 | return note_list 102 | 103 | 104 | def get_notes(file_path, separator): 105 | """ 106 | Takes a path to a file and returns a list with the notes 107 | in the file encoded according to the note fromatting. 108 | 109 | Returns False if file is empty. 110 | """ 111 | note_lines = get_note_lines(file_path) 112 | 113 | if not note_lines: 114 | return False 115 | 116 | note_list = decode_notes(note_lines, separator) 117 | 118 | return note_list 119 | 120 | 121 | def select_file(): 122 | """ 123 | Opens a file select window. 124 | Returns False upon no file selection. 125 | Otherwise returns absolute path to selected file. 126 | """ 127 | 128 | file = file_dia.askopenfilename(filetypes=config.TEXT_FILES) 129 | 130 | if file: 131 | return file 132 | else: 133 | return False 134 | 135 | 136 | def file_exists(file): 137 | """Checks if given path leads to an existing file.""" 138 | return path.isfile(file) 139 | -------------------------------------------------------------------------------- /resources/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeloskarsson/SplitNotes/199acd484727ec2ccc982d387d1f9a53ab1f6a39/resources/green.png -------------------------------------------------------------------------------- /resources/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeloskarsson/SplitNotes/199acd484727ec2ccc982d387d1f9a53ab1f6a39/resources/red.png -------------------------------------------------------------------------------- /resources/settings_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeloskarsson/SplitNotes/199acd484727ec2ccc982d387d1f9a53ab1f6a39/resources/settings_icon.png -------------------------------------------------------------------------------- /setting_handler.py: -------------------------------------------------------------------------------- 1 | import tkinter.colorchooser as colorchooser 2 | from tkinter import messagebox as msgbox 3 | import tkinter 4 | import os 5 | import sys 6 | 7 | import config 8 | 9 | settings_path = os.path.join( 10 | str(os.path.dirname(os.path.realpath(sys.argv[0]))), 11 | config.RESOURCE_FOLDER, 12 | config.SETTINGS_FILE 13 | ) 14 | 15 | settings_icon_path = os.path.join( 16 | str(os.path.dirname(os.path.realpath(sys.argv[0]))), 17 | config.RESOURCE_FOLDER, 18 | config.ICONS["SETTINGS"] 19 | ) 20 | 21 | 22 | def load_settings(): 23 | """ 24 | Tries to load settings from file. 25 | If no working settings-file exist one is created and the default settings are returned. 26 | 27 | returns a dictionary with all settings. 28 | """ 29 | 30 | # try to open default settings file 31 | try: 32 | settings_file = open(settings_path, "r+") 33 | settings_content = get_file_lines(settings_file) 34 | except: 35 | # File not found 36 | settings_content = set_default_settings() 37 | 38 | settings = format_settings(settings_content) 39 | 40 | # Check so settings file has all settings 41 | if not validate_settings(settings): 42 | settings = format_settings(set_default_settings()) 43 | 44 | return settings 45 | 46 | 47 | def set_default_settings(): 48 | """ 49 | Creates a config file with default settings. 50 | Returns the default config-file content. 51 | """ 52 | set_settings_file_content(config.DEFAULT_CONFIG) 53 | return config.DEFAULT_CONFIG.split("\n") 54 | 55 | 56 | def format_settings(file_rows): 57 | """ 58 | Takes a list of settings (as written in config files) and 59 | formats it to a dictionary. 60 | 61 | Returns a dictionary with all settings as keys. 62 | """ 63 | SETTING_PART_LENGTH = 2 64 | 65 | settings = {} 66 | 67 | for row in file_rows: 68 | row = row.strip("\n") 69 | parts = row.split("=", 1) 70 | 71 | if len(parts) == SETTING_PART_LENGTH: 72 | # Strip to remove whitespace at end and beginning 73 | settings[parts[0].strip()] = parts[1].strip() 74 | 75 | return settings 76 | 77 | 78 | def get_file_lines(file): 79 | """ 80 | Returns a list containing all the lines of the gicen file. 81 | """ 82 | # read file line per line 83 | f_lines = [] 84 | keep_reading = True 85 | while keep_reading: 86 | cur_line = file.readline() 87 | 88 | if cur_line: 89 | f_lines.append(cur_line) 90 | else: 91 | keep_reading = False 92 | 93 | return f_lines 94 | 95 | 96 | def validate_settings(settings): 97 | """ 98 | Checks a settings dictionary so that all the needed settings are present. 99 | """ 100 | 101 | for req_setting in config.REQUIRED_SETTINGS: 102 | if not (req_setting in settings): 103 | return False 104 | 105 | if not validate_font_size(settings["font_size"]): 106 | return False 107 | 108 | if not validate_server_port(settings["server_port"]): 109 | return False 110 | 111 | if not validate_color(settings["text_color"]): 112 | return False 113 | 114 | if not validate_color(settings["background_color"]): 115 | return False 116 | 117 | if not (settings["font"] in config.AVAILABLE_FONTS): 118 | return False 119 | 120 | if not ((settings["double_layout"] == "True") or 121 | (settings["double_layout"] == "False")): 122 | return False 123 | 124 | if not validate_pixels(settings["width"]): 125 | return False 126 | 127 | if not validate_pixels(settings["height"]): 128 | return False 129 | 130 | if not validate_separator(settings["separator"]): 131 | return False 132 | 133 | return True 134 | 135 | 136 | def set_settings_file_content(content): 137 | """ 138 | Saves given content to the config file, config.cfg, in the resources directory. 139 | """ 140 | settings_file = open(settings_path, "w") 141 | settings_file.write(content) 142 | settings_file.close() 143 | 144 | 145 | def edit_settings(root_wnd, apply_method): 146 | """ 147 | Sets up a window for editing settings. 148 | root_wnd is the main window that settings should be applied to. 149 | apply_method is the method to be called to apply validated settings. 150 | """ 151 | settings_wnd = tkinter.Toplevel(master=root_wnd, 152 | width=config.SETTINGS_WINDOW["WIDTH"], 153 | height=config.SETTINGS_WINDOW["HEIGHT"]) 154 | settings_wnd.title(config.SETTINGS_WINDOW["TITLE"]) 155 | settings_icon = tkinter.Image("photo", file=settings_icon_path) 156 | settings_wnd.tk.call('wm', 'iconphoto', settings_wnd._w, settings_icon) 157 | 158 | settings = load_settings() 159 | 160 | settings_wnd.resizable(0, 0) 161 | 162 | font_label = tkinter.Label(settings_wnd, 163 | text=config.SETTINGS_OPTIONS["FONT"], 164 | font=config.GUI_FONT) 165 | font_size_label = tkinter.Label(settings_wnd, 166 | text=config.SETTINGS_OPTIONS["FONT_SIZE"], 167 | font=config.GUI_FONT) 168 | text_color_label = tkinter.Label(settings_wnd, 169 | text=config.SETTINGS_OPTIONS["TEXT_COLOR"], 170 | font=config.GUI_FONT) 171 | bg_color_label = tkinter.Label(settings_wnd, 172 | text=config.SETTINGS_OPTIONS["BG_COLOR"], 173 | font=config.GUI_FONT) 174 | layout_label = tkinter.Label(settings_wnd, 175 | text=config.SETTINGS_OPTIONS["DOUBLE_LAYOUT"], 176 | font=config.GUI_FONT) 177 | newline_label = tkinter.Label(settings_wnd, 178 | text=config.SETTINGS_OPTIONS["NEW_LINE_SEPARATOR"], 179 | font=config.GUI_FONT) 180 | separator_label = tkinter.Label(settings_wnd, 181 | text=config.SETTINGS_OPTIONS["CUSTOM_SEPARATOR"], 182 | font=config.GUI_FONT) 183 | port_label = tkinter.Label(settings_wnd, 184 | text=config.SETTINGS_OPTIONS["SERVER_PORT"], 185 | font=config.GUI_FONT) 186 | default_port_label = tkinter.Label(settings_wnd, 187 | text=config.SETTINGS_OPTIONS["DEFAULT_SERVER_PORT"], 188 | font=config.GUI_FONT) 189 | 190 | # Font Selection 191 | selected_font = tkinter.StringVar(settings_wnd) 192 | selected_font.set(settings["font"]) 193 | 194 | font_dropdown = tkinter.OptionMenu(settings_wnd, 195 | selected_font, 196 | *config.AVAILABLE_FONTS) 197 | font_dropdown.configure(font=config.GUI_FONT) 198 | 199 | # Font Size Selection 200 | font_size_entry = tkinter.Entry(settings_wnd, width=2, font=config.GUI_FONT) 201 | font_size_entry.insert(0, settings["font_size"]) 202 | 203 | # Text Color Selection 204 | text_color = tkinter.Button(settings_wnd, 205 | width=3, 206 | height=1, 207 | ) 208 | 209 | if validate_color(settings["text_color"]): 210 | text_color.configure(background=settings["text_color"]) 211 | else: 212 | text_color.configure(background="#000000") 213 | 214 | def text_color_selection(): 215 | choosen_color = colorchooser.askcolor() 216 | if choosen_color[1]: 217 | settings["text_color"] = choosen_color[1] 218 | text_color.configure(background=settings["text_color"]) 219 | 220 | settings_wnd.focus_force() 221 | 222 | text_color.configure(command=text_color_selection) 223 | 224 | # Background color Selection 225 | bg_color = tkinter.Button(settings_wnd, 226 | width=3, 227 | height=1) 228 | 229 | if validate_color(settings["background_color"]): 230 | bg_color.configure(background=settings["background_color"]) 231 | else: 232 | bg_color.configure(background="#FFFFFF") 233 | 234 | def bg_color_selection(): 235 | choosen_color = colorchooser.askcolor() 236 | if choosen_color[1]: 237 | settings["background_color"] = choosen_color[1] 238 | bg_color.configure(background=settings["background_color"]) 239 | 240 | settings_wnd.focus_force() 241 | 242 | bg_color.configure(command=bg_color_selection) 243 | 244 | # Server port Selection 245 | port_entry = tkinter.Entry(settings_wnd, width=6, font=config.GUI_FONT) 246 | port_entry.insert(0, settings["server_port"]) 247 | 248 | # Double Layout Selection 249 | double_layout = tkinter.BooleanVar() 250 | double_layout_btn = tkinter.Checkbutton(settings_wnd, variable=double_layout) 251 | 252 | if decode_boolean_setting(settings["double_layout"]): 253 | double_layout_btn.select() 254 | 255 | # Separator selection 256 | separator_entry = tkinter.Entry(settings_wnd, width=14, font=config.GUI_FONT) 257 | 258 | def set_separator_active(active): 259 | if active: 260 | separator_entry.configure(state="normal") 261 | else: 262 | separator_entry.configure(state="disabled") 263 | 264 | use_newline = tkinter.BooleanVar() 265 | newline_btn = tkinter.Checkbutton(settings_wnd, 266 | variable=use_newline, 267 | command= 268 | (lambda: set_separator_active(not use_newline.get())) 269 | ) 270 | 271 | if settings["separator"] == config.NEWLINE_CONSTANT: 272 | newline_btn.select() 273 | set_separator_active(False); 274 | else: 275 | separator_entry.insert(0, settings["separator"]) 276 | 277 | # Save and cancel buttons 278 | def control_and_save(): 279 | errors_found = False 280 | 281 | settings["font"] = selected_font.get() 282 | 283 | chosen_font_size = font_size_entry.get() 284 | chosen_port = port_entry.get() 285 | 286 | if use_newline.get(): 287 | chosen_separator = config.NEWLINE_CONSTANT 288 | else: 289 | chosen_separator = separator_entry.get() 290 | 291 | settings["double_layout"] = encode_boolean_setting(double_layout.get()) 292 | 293 | if not validate_font_size(chosen_font_size): 294 | msgbox.showerror(config.ERRORS["FONT_SIZE"][0], config.ERRORS["FONT_SIZE"][1]) 295 | errors_found = True 296 | else: 297 | settings["font_size"] = chosen_font_size 298 | 299 | if not validate_server_port(chosen_port): 300 | msgbox.showerror(config.ERRORS["SERVER_PORT"][0], config.ERRORS["SERVER_PORT"][1]) 301 | errors_found = True 302 | else: 303 | settings["server_port"] = chosen_port 304 | 305 | if not validate_separator(chosen_separator): 306 | msgbox.showerror(config.ERRORS["SEPARATOR"][0], config.ERRORS["SEPARATOR"][1]) 307 | errors_found = True 308 | else: 309 | settings["separator"] = chosen_separator 310 | 311 | if not errors_found: 312 | save_settings(settings) 313 | apply_method(settings) 314 | settings_wnd.destroy() 315 | else: 316 | settings_wnd.focus_force() 317 | 318 | save_btn = tkinter.Button(settings_wnd, 319 | command=control_and_save, 320 | text=config.SETTINGS_WINDOW["SAVE"], 321 | font=config.GUI_FONT) 322 | cancel_btn = tkinter.Button(settings_wnd, 323 | command=settings_wnd.destroy, 324 | text=config.SETTINGS_WINDOW["CANCEL"], 325 | font=config.GUI_FONT) 326 | 327 | # Place all components 328 | font_label.place(x=15, y=15) 329 | font_size_label.place(x=15, y=55) 330 | text_color_label.place(x=15, y=95) 331 | bg_color_label.place(x=15, y=135) 332 | layout_label.place(x=15, y=175) 333 | newline_label.place(x=15, y=215) 334 | separator_label.place(x=15, y=240) 335 | port_label.place(x=15, y=280) 336 | default_port_label.place(x=15, y=305) 337 | 338 | font_dropdown.place(x=208, y=15) 339 | font_size_entry.place(x=210, y=55) 340 | text_color.place(x=210, y=95) 341 | bg_color.place(x=210, y=135) 342 | double_layout_btn.place(x=210, y=175) 343 | newline_btn.place(x=210, y=215) 344 | separator_entry.place(x=210, y=240) 345 | port_entry.place(x=210, y=280) 346 | 347 | save_btn.place(x=110, y=350) 348 | cancel_btn.place(x=190, y=350) 349 | 350 | 351 | def validate_color(color): 352 | """ 353 | Returns whether or not given color is valid in the hexadecimal format. 354 | """ 355 | return isinstance(color, str) and len(color) == 7 and color[0] == "#" 356 | 357 | 358 | def validate_font_size(size): 359 | """ 360 | Returns whether or not given size is an acceptable font size. 361 | """ 362 | try: 363 | size = int(size) 364 | except: 365 | return False 366 | 367 | return 0 < size < 70 368 | 369 | 370 | def validate_server_port(port): 371 | """ 372 | Returns Whether or not gicen port is a valid server port. 373 | """ 374 | try: 375 | int(port) 376 | return True 377 | except: 378 | return False 379 | 380 | 381 | def save_settings(settings): 382 | """ 383 | Saves given settings to the settings file. 384 | """ 385 | file_content = "" 386 | 387 | for key in settings.keys(): 388 | file_content += key + "=" + settings[key] + "\n" 389 | 390 | set_settings_file_content(file_content) 391 | 392 | 393 | def decode_boolean_setting(setting): 394 | """ 395 | Decodes a boolean string of "True" or "False" 396 | to the coorect boolean value. 397 | """ 398 | return setting == "True" 399 | 400 | 401 | def encode_boolean_setting(value): 402 | """ 403 | Encodes a boolean to the string "True" or "False". 404 | """ 405 | if value: 406 | return "True" 407 | else: 408 | return "False" 409 | 410 | 411 | def validate_pixels(pixels): 412 | """ 413 | Checks if given string can be used as a pixel value for height or width. 414 | Height or Width or assumed to never surpass 10000 415 | """ 416 | try: 417 | pixels = int(pixels) 418 | except: 419 | return False 420 | 421 | return 0 < pixels < 10000 422 | 423 | 424 | def validate_separator(separator): 425 | return separator.strip() 426 | -------------------------------------------------------------------------------- /setup_exe.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import py2exe 3 | 4 | setup( 5 | windows=[r'main_window.py'], 6 | options = {'py2exe': {'bundle_files': 2, 'compressed': True}}, 7 | zipfile = None, 8 | data_files = [('resources', ['resources/green.png']), 9 | ('resources', ['resources/red.png']), 10 | ('resources', ['resources/settings_icon.png'])] 11 | ) --------------------------------------------------------------------------------