15 Commits
Author SHA1 Message Date
Joelnir 91d71308df Added formatting options
Added the option to use another split separator than newline. Also
removed stripping of spaces at the beginning and end of lines of notes.
2016-04-15 21:16:34 +02:00
Joelnir 6e30be4b4a Merge branch 'master' of https://github.com/joelnir/SplitNotes 2016-04-15 16:18:59 +02:00
Joel a49e0934af Updated readme to reflect version 1.1
Changed a screenshot to show customization options and added text to "How to use" and "Features" about the changes in 1.1.
2016-02-20 10:43:58 +01:00
Joelnir fe83570cec Fixed Folder Name in Release 1.1
Fixed the folder name int eh compressed 1.1 folder to SplitNotes.
2016-02-20 00:59:58 +01:00
joelnir 4037122692 Added release 1.1 to release folder
Compiled version 1.1 to an exe. Still requires testing
2016-02-20 00:43:52 +01:00
Joelnir b74903daf4 Finished settings implementation
Implemented settings for window width and height. Cleaned up and fixed
bugs.
2016-02-19 23:51:22 +01:00
Joelnir 2d56e617c0 Deleted unwanted files
deleted pycache, testing file and test config-file
2016-02-18 21:38:16 +01:00
Joelnir d314860608 Finished settings menu and automatic note loads
Implemented the settings menu to the main window so the settings are
applied. Moved the double layout option to the settings instead of the
right click menu. Also implemented a system to automatically load the
notes that were active when SplitNotes was closed last time.
2016-02-18 21:34:02 +01:00
Joelnir af125173c4 Settings Menu
Finished the settings menu and systems for saving and loading settings
from a config file.
2016-02-18 16:28:54 +01:00
Joelnir c3e7c275f7 Start of setting implementation
Created methods for loading settings and started working on the gui of
the settings menu.
2016-02-17 22:43:09 +01:00
Joel c09cb1603c Update README.md 2016-01-07 19:27:46 +01:00
Joel f513cef7af Update README.md 2016-01-06 00:02:20 +01:00
Joel 917e65f7c1 Update README.md 2016-01-06 00:01:58 +01:00
Joel 99e8f606b9 Update README.md 2016-01-05 23:58:59 +01:00
Joel 07c336782a Update README.md 2016-01-05 23:49:13 +01:00
13 changed files with 868 additions and 295 deletions
+5 -1
View File
@@ -1 +1,5 @@
*.pyc
*.pyc
TESTING.py
.idea
__pycache__
resources/config.cfg
+15 -6
View File
@@ -3,6 +3,9 @@ Software for syncing notes with LiveSplit using the LiveSplit server component.
Splitnotes automatically shows notes for the split you are currently on.
![Screenshot 1](http://i.imgur.com/CMlF2pj.png)
![Screenshot 2](http://i.imgur.com/4Ei2IiJ.png)
## Install
1. Download the latest version of LiveSplit Server Component from [this](https://github.com/LiveSplit/LiveSplit.Server/releases) site.
2. Unzip and move the files to the component folder in your LiveSplit install (...\LiveSplit\Components)
@@ -11,9 +14,9 @@ Splitnotes automatically shows notes for the split you are currently on.
## How To Use
**Connect to LiveSplit**
1. Launch Splitnotes
1. Launch Splitnotes.exe
2. Launch LiveSplit
3. Go to "Edit Layout" -> "+" -> "Control" -> "LiveSplit Server". Hit ok.
3. Go to "Edit Layout" -> "+" -> "Control" -> "LiveSplit Server". Hit ok. (Let the server use the default port: 16834)
4. In LiveSplit, select "Control" -> "Start Server".
5. SplitNotes should now be connected to LiveSplit. If connection is active the Icon for SplitNotes is green.
@@ -35,20 +38,26 @@ Example:
>A simple new line is enough to separate notes for different splits.
>
>Also som notes for split 3.
>You don't have to [ be afraid to use brackets [ in the middle of notes].
>You don't have to [ worry abut using brackets [ in the middle of a row].
**Load Notes**
1. Right-Click in SplitNotes and select "Load Notes".
2. Choose your text file.
3. Make sure that notes for the right number of splits has been loaded.
3. Make sure that notes for the right amount of splits have been loaded.
**Change Settings**
1. Right-Click in SplitNotes and select "Settings"
2. The settings menu will show up. Here you can set things like font, font size, text and background color and the server port.
## Features
* Automatically displayed notes based on the active split in LiveSplit.
* Ability to preview notes in the software before when no run is going on by using the right and left arrow keys.
* Ability to preview notes in the software when no run is going on by using the right and left arrow keys.
* Two different font sizes to make sure that notes are easy to read.
* Double layout to preview notes for both current and next split.
* Multiple settings to change the look of the text.
* Ability to set a custom port for interaction with the LiveSplit Server.
#### Development
Written in Python using tkinter GUI library.
Written in mainly procedural Python using tkinter GUI library.
Made by Joelnir.
Binary file not shown.
-3
View File
@@ -1,3 +0,0 @@
import tkinter
tkinter.messagebox.showinfo("hi!")
Binary file not shown.
Binary file not shown.
Binary file not shown.
+82 -37
View File
@@ -1,64 +1,109 @@
#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 = {"WIDTH": 400, "HEIGHT": 300, "TITLE": "SplitNotes"}
# Default Window Settings
DEFAULT_WINDOW = {"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 of icons
ICON_FOLDER = "resources"
ICONS= {"GREEN": "green.png", "RED": "red.png"}
# File names and path for resources
RESOURCE_FOLDER = "resources"
ICONS = {"GREEN": "green.png", "RED": "red.png", "SETTINGS": "settings_icon.png"}
SETTINGS_FILE = "config.cfg"
#Popup menu options
# Default Scrollbar Width
SCROLLBAR_WIDTH = 16
# Popup menu options
MENU_OPTIONS = {
"SINGLE": "Set Single Layout",
"DOUBLE": "Set Double Layout",
"LOAD": "Load Notes",
"BIG": "Big Font",
"SMALL": "Small Font"
}
"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!"),
"SEPARATOR": ("Error!", "Invalid split separator!")}
#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
FONT = {"NAME": "Arial", "SMALL": 12, "BIG": 16}
# Font for gui widgets
GUI_FONT = ("arial", 12)
#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','*')
]
("Text Files", ("*.txt", "*.log", "*.asc", "*.conf", "*.cfg")),
('All', '*')
]
# Default content of config.cfg file
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"
NEWLINE_CONSTANT = "new_line"
# Required settings
REQUIRED_SETTINGS = ("notes",
"font",
"font_size",
"text_color",
"background_color",
"server_port",
"double_layout",
"width",
"height",
"separator"
)
# Settings window options
SETTINGS_WINDOW = {"TITLE": "Settings",
"WIDTH": 360,
"HEIGHT": 410,
"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)",
"DOUBLE_LAYOUT": "Use double layout",
"NEW_LINE_SEPARATOR": "Newline as split separator",
"CUSTOM_SEPARATOR": "Custom split separator"}
# Fonts that can be selected
AVAILABLE_FONTS = ("arial",
"courier new",
"fixedsys",
"ms sans serif",
"ms serif",
"system",
"times new roman",
"verdana")
+22 -18
View File
@@ -5,19 +5,21 @@ 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):
def ls_connect(ls_socket, call_func, window, server_port):
"""Connects given socket to the livesplit server."""
con_thread = Thread(target=try_connection, args=(ls_socket, call_func, window))
con_thread = Thread(target=try_connection, args=(ls_socket, call_func, window, server_port))
con_thread.start()
def init_socket():
"""Returns a fresh socket"""
return socket.socket()
def try_connection(ls_socket, call_func, window):
def try_connection(ls_socket, call_func, window, server_port):
"""
Tries to connect given socket to ls.
If connection is successful given "call_func"
@@ -25,16 +27,18 @@ def try_connection(ls_socket, call_func, window):
(made to be ran in a separate thread)
"""
try:
ls_socket.connect((config.HOST, config.PORT))
ls_socket.connect((config.HOST, server_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
return False
+270 -197
View File
@@ -1,114 +1,119 @@
import tkinter
from tkinter import messagebox
from tkinter import font
import socket
import os
import sys
import config
import ls_connection as con
import note_reader as noter
import setting_handler
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,
"active_split": -1,
"notes": [],
"server_port": 0,
"force_reset": False,
"double_layout": False,
"settings": {}
}
root = tkinter.Tk()
red_path= os.path.join(
str(os.path.dirname(os.path.realpath(sys.argv[0]))),
config.ICON_FOLDER,
config.ICONS["RED"]
)
green_path= os.path.join(
str(os.path.dirname(os.path.realpath(sys.argv[0]))),
config.ICON_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
con.ls_connect(com_socket, server_found, window)
if runtime_info["force_reset"]:
# Boolean flag to force a connection reset
com_socket = reset_connection(com_socket, window, text1, text2)
runtime_info["force_reset"] = False
elif not runtime_info["ls_connected"]:
# try connecting to ls
con.ls_connect(com_socket, server_found, window, runtime_info["server_port"])
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)
update_notes(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.
@@ -130,16 +135,17 @@ 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):
"""
@@ -149,22 +155,20 @@ 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"]:
if active:
window.tk.call('wm', 'iconphoto', window._w, green_icon)
runtime_info["icon_active"] = True
elif runtime_info["icon_active"]:
else:
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
@@ -174,87 +178,83 @@ 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"]:
set_single_layout(window, box1, box2)
popup.entryconfig(0, label=config.MENU_OPTIONS["DOUBLE"])
else:
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:
notes = noter.get_notes(file)
if file:
notes = noter.get_notes(file, runtime_info["settings"]["separator"])
if notes:
#Notes loaded correctly
# Notes loaded correctly
runtime_info["notes"] = notes
# Save notes to settings
settings = setting_handler.load_settings()
settings["notes"] = file
setting_handler.save_settings(settings)
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.
Displays an info popup window.
if warning is True window has a warning triangle.
"""
if warning:
messagebox.showwarning(info[0], info[1])
else:
messagebox.showinfo(info[0], info[1])
def update_notes(window, text1, text2, index):
def update_notes(text1, text2, index):
"""
Displays notes with the given index in given text widgets.
If index is lower than 0, displays notes for index 0.
@@ -264,179 +264,252 @@ 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
# cant 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)
def change_preview(window, com_socket, text1, text2, move):
"""move is either 1 for next or -1 for previous."""
"""
Chnges notes that are currently displayed.
Move is either 1 for next or -1 for previous.
"""
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):
if runtime_info["big_font"]:
runtime_info["big_font"] = False
menu.entryconfig(1, label=config.MENU_OPTIONS["BIG"])
def menu_open_settings(root_wnd, box1, box2, text1, text2, com_socket):
"""
Opens the settings menu.
"""
setting_handler.edit_settings(root_wnd,
(lambda settings: apply_settings(settings,
root_wnd,
box1, box2,
text1, text2, com_socket)))
def apply_settings(settings, window, box1, box2, text1, text2, com_socket):
"""
Applies the given settings to the given components.
Settings must be a correctly formatted dictionary.
"""
runtime_info["settings"] = settings
# Server port change
if not (runtime_info["server_port"] == int(settings["server_port"])):
runtime_info["server_port"] = int(settings["server_port"])
runtime_info["force_reset"] = True
text_font = (settings["font"], int(settings["font_size"]))
if setting_handler.decode_boolean_setting(settings["double_layout"]):
set_double_layout(window, box1, box2)
else:
runtime_info["big_font"] = True
menu.entryconfig(1, label=config.MENU_OPTIONS["SMALL"])
update_font_size(text_font)
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)
set_single_layout(window, box1, box2)
text1.config(font=text_font)
text2.config(font=text_font)
text1.config(fg=settings["text_color"], bg=settings["background_color"])
text2.config(fg=settings["text_color"], bg=settings["background_color"])
old_note_length = len(runtime_info["notes"])
if settings["notes"] and noter.file_exists(settings["notes"]):
new_notes = noter.get_notes(settings["notes"], settings["separator"])
if new_notes:
# Notes loaded correctly
runtime_info["notes"] = new_notes
new_note_length = len(new_notes)
if not (new_note_length == old_note_length):
show_info(("Notes Loaded",
("Loaded notes with " + str(new_note_length) + " 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 save_geometry_settings(width, height):
"""
Saves given width and height to settigns file.
"""
settings = setting_handler.load_settings()
settings["width"] = str(width)
settings["height"] = str(height)
setting_handler.save_settings(settings)
def do_on_close(root_wnd):
"""
Function that is called when the main tk window is closed.
Saves root_wnd's width and height to the settings file and
then closes the window.
"""
save_geometry_settings(root_wnd.winfo_width(), root_wnd.winfo_height())
root_wnd.destroy()
def init_UI(root):
"""Draws default UI and creates event bindings."""
#Create communication socket
# Create communication socket
com_socket = con.init_socket()
#Graphical components
root.geometry(str(config.DEFAULT_WINDOW["WIDTH"]) + "x" + str(config.DEFAULT_WINDOW["HEIGHT"]))
# Load Settings
settings = setting_handler.load_settings()
runtime_info["server_port"] = int(settings["server_port"])
runtime_info["settings"] = settings
# Graphical components
root.geometry(settings["width"] + "x" + settings["height"])
box1 = tkinter.Frame(root)
box2 = tkinter.Frame(root)
scroll1 = tkinter.Scrollbar(box1)
scroll1 = tkinter.Scrollbar(box1, width=config.SCROLLBAR_WIDTH)
scroll2 = tkinter.Scrollbar(box2, width=config.SCROLLBAR_WIDTH)
scroll1.pack(side=tkinter.RIGHT, fill=tkinter.Y)
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
text_font = font.Font(
family=config.FONT["NAME"],
size=config.FONT["SMALL"]
)
# Set font and color for text
text_font = (settings["font"], int(settings["font_size"]))
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
text1.config(fg=settings["text_color"], bg=settings["background_color"])
text2.config(fg=settings["text_color"], bg=settings["background_color"])
if setting_handler.decode_boolean_setting(settings["double_layout"]):
set_double_layout(root, box1, box2)
else:
set_single_layout(root, box1, box2)
# 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["LOAD"],
command=(lambda: menu_load_notes(root, text1, text2, com_socket))
)
popup.add_command(
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))
)
#Set default window icon and title
label=config.MENU_OPTIONS["SETTINGS"],
command=(lambda: menu_open_settings(root, box1, box2, text1, text2, com_socket))
)
# Set default window icon and title
root.tk.call('wm', 'iconphoto', root._w, red_icon)
update_title(config.DEFAULT_WINDOW["TITLE"], root)
#Event binds
# Check if notes can be loaded from settings
settings = setting_handler.load_settings()
if settings["notes"] and noter.file_exists(settings["notes"]):
notes = noter.get_notes(settings["notes"], settings["separator"])
if notes:
runtime_info["notes"] = notes
update_GUI(root, com_socket, text1, text2)
# Event binds
root.bind("<Configure>", (lambda e: adjust_content(root, box1, box2)))
root.bind("<Button-3>", (lambda e: show_popup(e, popup)))
root.bind("<Right>", (lambda e: right_arrow(root, com_socket, text1, text2)))
root.bind("<Left>", (lambda e: left_arrow(root, com_socket, text1, text2)))
#call update loop
# Window close bind
root.protocol("WM_DELETE_WINDOW", (lambda: do_on_close(root)))
# call update loop
update(root, com_socket, text1, text2)
root.geometry(settings["width"] + "x" + settings["height"])
init_UI(root)
root.mainloop()
root.mainloop()
+49 -33
View File
@@ -5,9 +5,10 @@ import config
"""
NOTE STANDARD FORMATTING
empty newlines separate notes for different splits
It is also possible to set your own split separator in the settings menu
lines that start and end with [ ] are ignored for notes.
these can be used for titles.
(ex. [Split1] is not included in notes)
@@ -15,15 +16,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
@@ -31,37 +32,49 @@ 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()
try:
cur_line = notes_file.readline()
except:
return False
if cur_line:
f_lines.append(cur_line)
else:
keep_reading = False
return f_lines
def encode_notes(note_lines):
def decode_notes(note_lines, separator):
"""
Takes a list containing strings.
Encodes given strings according to the note formatting.
Returns the list containing the notes for every split.
"""
#Check if newline is being used as separator
if separator == config.NEWLINE_CONSTANT:
separator = "" # left after stripping newline
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 is_separator(line):
return (line == separator)
def is_newline(s):
return (s == "\n")
def remove_new_line(line):
if (len(line) >= 1) and (is_newline(line[-1])):
return line[:-1]
@@ -70,27 +83,25 @@ def encode_notes(note_lines):
note_list = []
cur_notes = ""
for line in note_lines:
#remove whitespace at beginning and end
line = line.strip(" ")
if is_newline(line):
for line in note_lines:
line = remove_new_line(line)
if is_separator(line):
if cur_notes:
note_list.append(cur_notes)
cur_notes = ""
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
def get_notes(file_path):
def get_notes(file_path, separator):
"""
Takes a path to a file and returns a list with the notes
in the file encoded according to the note fromatting.
@@ -98,12 +109,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)
note_list = decode_notes(note_lines, separator)
return note_list
@@ -113,10 +124,15 @@ 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
return False
def file_exists(file):
"""Checks if given path leads to an existing file."""
return path.isfile(file)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+425
View File
@@ -0,0 +1,425 @@
import tkinter.colorchooser as colorchooser
from tkinter import messagebox as msgbox
import tkinter
import os
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
)
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():
"""
Tries to load settings from file.
If no working settings-file exist one is created and the default settings are returned.
returns a dictionary with all settings.
"""
# try to open default settings file
try:
settings_file = open(settings_path, "r+")
settings_content = get_file_lines(settings_file)
except:
# File not found
settings_content = set_default_settings()
settings = format_settings(settings_content)
# 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.
"""
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
formats it to a dictionary.
Returns a dictionary with all settings as keys.
"""
SETTING_PART_LENGTH = 2
settings = {}
for row in file_rows:
row = row.strip("\n")
parts = row.split("=", 1)
if len(parts) == SETTING_PART_LENGTH:
# Strip to remove whitespace at end and beginning
settings[parts[0].strip()] = parts[1].strip()
return settings
def get_file_lines(file):
"""
Returns a list containing all the lines of the gicen file.
"""
# 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:
keep_reading = False
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):
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
if not ((settings["double_layout"] == "True") or
(settings["double_layout"] == "False")):
return False
if not validate_pixels(settings["width"]):
return False
if not validate_pixels(settings["height"]):
return False
if not validate_separator(settings["separator"]):
return False
return True
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_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)
layout_label = tkinter.Label(settings_wnd,
text=config.SETTINGS_OPTIONS["DOUBLE_LAYOUT"],
font=config.GUI_FONT)
newline_label = tkinter.Label(settings_wnd,
text=config.SETTINGS_OPTIONS["NEW_LINE_SEPARATOR"],
font=config.GUI_FONT)
separator_label = tkinter.Label(settings_wnd,
text=config.SETTINGS_OPTIONS["CUSTOM_SEPARATOR"],
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]:
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"])
# Double Layout Selection
double_layout = tkinter.BooleanVar()
double_layout_btn = tkinter.Checkbutton(settings_wnd, variable=double_layout)
if decode_boolean_setting(settings["double_layout"]):
double_layout_btn.select()
# Separator selection
separator_entry = tkinter.Entry(settings_wnd, width=14, font=config.GUI_FONT)
def set_separator_active(active):
if active:
separator_entry.configure(state="normal")
else:
separator_entry.configure(state="disabled")
use_newline = tkinter.BooleanVar()
newline_btn = tkinter.Checkbutton(settings_wnd,
variable=use_newline,
command=
(lambda: set_separator_active(not use_newline.get()))
)
if settings["separator"] == config.NEWLINE_CONSTANT:
newline_btn.select()
set_separator_active(False);
else:
separator_entry.insert(0, settings["separator"])
# 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 use_newline.get():
chosen_separator = config.NEWLINE_CONSTANT
else:
chosen_separator = separator_entry.get()
settings["double_layout"] = encode_boolean_setting(double_layout.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 validate_separator(chosen_separator):
msgbox.showerror(config.ERRORS["SEPARATOR"][0], config.ERRORS["SEPARATOR"][1])
errors_found = True
else:
settings["separator"] = chosen_separator
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)
layout_label.place(x=15, y=175)
newline_label.place(x=15, y=215)
separator_label.place(x=15, y=240)
port_label.place(x=15, y=280)
default_port_label.place(x=15, y=305)
font_dropdown.place(x=208, y=15)
font_size_entry.place(x=210, y=55)
text_color.place(x=210, y=95)
bg_color.place(x=210, y=135)
double_layout_btn.place(x=210, y=175)
newline_btn.place(x=210, y=215)
separator_entry.place(x=210, y=240)
port_entry.place(x=210, y=280)
save_btn.place(x=110, y=350)
cancel_btn.place(x=190, y=350)
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:
int(port)
return True
except:
return False
def save_settings(settings):
"""
Saves given settings to the settings file.
"""
file_content = ""
for key in settings.keys():
file_content += key + "=" + settings[key] + "\n"
set_settings_file_content(file_content)
def decode_boolean_setting(setting):
"""
Decodes a boolean string of "True" or "False"
to the coorect boolean value.
"""
return setting == "True"
def encode_boolean_setting(value):
"""
Encodes a boolean to the string "True" or "False".
"""
if value:
return "True"
else:
return "False"
def validate_pixels(pixels):
"""
Checks if given string can be used as a pixel value for height or width.
Height or Width or assumed to never surpass 10000
"""
try:
pixels = int(pixels)
except:
return False
return 0 < pixels < 10000
def validate_separator(separator):
return separator.strip()