mirror of
https://github.com/ApfelTeeSaft/SplitNotes.git
synced 2026-08-26 19:33:39 +00:00
Initial files
Added initial files that were created without version control. A number of .py files and a resource folder with a couple of icons.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import tkinter
|
||||
|
||||
tkinter.messagebox.showinfo("hi!")
|
||||
@@ -0,0 +1,50 @@
|
||||
#CONFIG FILE WITH CONSTANTS
|
||||
|
||||
DEBUG = True
|
||||
|
||||
#Livesplit connection
|
||||
HOST = "localhost"
|
||||
PORT = 16834
|
||||
|
||||
#In network communication, time out after this time. (in seconds)
|
||||
COM_TIMEOUT = 0.5
|
||||
|
||||
#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": 200, "HEIGHT": 500, "TITLE": "SplitNotes"}
|
||||
|
||||
#Color Scheme
|
||||
COLOR_SCHEME = {}
|
||||
|
||||
#Default Welcome Message
|
||||
DEFAULT_MSG = "Right Click to Open Notes."
|
||||
|
||||
#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"}
|
||||
|
||||
#Popup menu options
|
||||
MENU_OPTIONS = {
|
||||
"SINGLE": "Set Single Layout",
|
||||
"DOUBLE": "Set Double Layout",
|
||||
"LOAD": "Load Notes"
|
||||
}
|
||||
|
||||
#Error messages
|
||||
ERRORS = {"NOTES_EMPTY": ("Error", "Notes empty or can not be loaded!")}
|
||||
|
||||
#Max file size for notes
|
||||
MAX_FILE_SIZE = 1000000000 #1 Giga-Byte
|
||||
|
||||
#TO be added to title to alert user that timer is running
|
||||
RUNNING_ALERT = "RUNNING"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
ls refers to LiveSplit in the entire document.
|
||||
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
|
||||
|
||||
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.
|
||||
If connection is successful given "call_func"
|
||||
is called with window as argument.
|
||||
(made to be ran in a separate thread)
|
||||
"""
|
||||
try:
|
||||
ls_socket.connect((config.HOST, config.PORT))
|
||||
except:
|
||||
return False
|
||||
|
||||
call_func(window)
|
||||
|
||||
def close_socket(socket):
|
||||
"""Closes given socket."""
|
||||
socket.close()
|
||||
|
||||
def check_connection(ls_socket):
|
||||
"""
|
||||
Check so connection between socket and livesplit
|
||||
is still active and working.
|
||||
Returns boolean
|
||||
"""
|
||||
if send_to_ls(ls_socket, "best_possible"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def send_to_ls(ls_socket, command):
|
||||
"""
|
||||
Sends given command to ls using given socket.
|
||||
If connected is False, tries to send without socket being connected to ls.
|
||||
Returns the response, or False if an error occurs.
|
||||
Check config.LS_COMMANDS for avaiable commands.
|
||||
"""
|
||||
|
||||
try:
|
||||
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:
|
||||
return (ls_socket.recv(1000)).decode("utf-8")
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_split_index(ls_socket):
|
||||
"""
|
||||
Returns the index of the active split in livesplit.
|
||||
Returns -1 if timer is not yet started.
|
||||
(First split is 0)
|
||||
|
||||
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):
|
||||
"""
|
||||
Returns name of the active split in livesplit.
|
||||
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
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
import tkinter
|
||||
from tkinter import messagebox
|
||||
|
||||
import socket
|
||||
import os
|
||||
|
||||
import config
|
||||
import ls_connection as con
|
||||
import note_reader as noter
|
||||
|
||||
runtime_info = {
|
||||
"ls_connected": False,
|
||||
"timer_running": False,
|
||||
"icon_active": False,
|
||||
"active_split": -1,
|
||||
"notes": [],
|
||||
"double_layout": False
|
||||
}
|
||||
|
||||
root = tkinter.Tk()
|
||||
|
||||
red_path= os.path.join(
|
||||
str(os.path.dirname(os.path.realpath(__file__))),
|
||||
config.ICON_FOLDER,
|
||||
config.ICONS["RED"]
|
||||
)
|
||||
green_path= os.path.join(
|
||||
str(os.path.dirname(os.path.realpath(__file__))),
|
||||
config.ICON_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
|
||||
connection_ok = con.ls_connect(com_socket, server_found, window)
|
||||
else:
|
||||
#is_connected
|
||||
if runtime_info["notes"]:
|
||||
#notes loaded
|
||||
|
||||
#get index of current split
|
||||
new_index = con.get_split_index(com_socket)
|
||||
|
||||
if isinstance(new_index, bool):
|
||||
#Connection error
|
||||
test_connection(com_socket)
|
||||
else:
|
||||
#index retrieved succesfully
|
||||
if new_index == -1:
|
||||
#timer not running
|
||||
if runtime_info["timer_running"]:
|
||||
runtime_info["timer_running"] = False
|
||||
runtime_info["active_split"] = new_index
|
||||
update_notes(window, text1, text2, new_index)
|
||||
set_title_notes(window, 0)
|
||||
else:
|
||||
#timer is running
|
||||
runtime_info["timer_running"] = True
|
||||
|
||||
if not runtime_info["active_split"] == new_index:
|
||||
#new split, need to update
|
||||
|
||||
#update notes
|
||||
update_notes(window, text1, text2, new_index)
|
||||
|
||||
#set new window title
|
||||
new_split = con.get_split_name(com_socket)
|
||||
|
||||
if new_split:
|
||||
set_title_notes(window, new_index, new_split)
|
||||
else:
|
||||
#connection error
|
||||
set_title_notes(window, new_index)
|
||||
test_connection(com_socket)
|
||||
|
||||
runtime_info["active_split"] = new_index
|
||||
|
||||
else:
|
||||
#notes not yet loaded
|
||||
if not con.check_connection(com_socket):
|
||||
#connection lost
|
||||
runtime_info["ls_connected"] = False
|
||||
update_icon(False, window)
|
||||
com_socket = con.init_socket()
|
||||
|
||||
|
||||
|
||||
#self looping
|
||||
window.after(int(config.POLLING_TIME * 1000), update, window, com_socket, text1, text2)
|
||||
|
||||
|
||||
def test_connection(com_socket):
|
||||
if con.check_connection(com_socket):
|
||||
return True
|
||||
else:
|
||||
reset_connection(com_socket)
|
||||
return False
|
||||
|
||||
|
||||
def reset_connection(com_socket):
|
||||
runtime_info["ls_connected"] = False
|
||||
com_socket = con.init_socket()
|
||||
|
||||
|
||||
def server_found(window):
|
||||
"""
|
||||
Executes correct settings for when
|
||||
ls connection has been established.
|
||||
"""
|
||||
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"]:
|
||||
window.tk.call('wm', 'iconphoto', window._w, green_icon)
|
||||
runtime_info["icon_active"] = True
|
||||
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
|
||||
layout and size of window.
|
||||
"""
|
||||
if runtime_info["double_layout"]:
|
||||
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):
|
||||
"""Menu selected load notes option."""
|
||||
load_notes(window, text1, text2)
|
||||
|
||||
|
||||
def load_notes(window, text1, text2):
|
||||
"""
|
||||
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 notes:
|
||||
#Notes loaded correctly
|
||||
runtime_info["notes"] = notes
|
||||
|
||||
split_c = len(notes)
|
||||
show_info(("Notes Loaded", ("Loaded notes with " + str(split_c) + " splits.")))
|
||||
|
||||
update_notes(window, text1, text2, runtime_info["active_split"])
|
||||
set_title_notes(window, 0, split_name = False)
|
||||
else:
|
||||
show_info(config.ERRORS["NOTES_EMPTY"], True)
|
||||
|
||||
|
||||
def show_info(info, warning = False):
|
||||
"""
|
||||
Displays an infor 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):
|
||||
"""
|
||||
Displays notes with the given index in given text widgets.
|
||||
If index is lower than 0, displays notes for index 0.
|
||||
If index is higher than the highest index there are
|
||||
notes for the text widgets are left empty.
|
||||
|
||||
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
|
||||
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, text1, text2):
|
||||
"""Event handler for right arrow key."""
|
||||
change_preview(window, text1, text2, 1)
|
||||
|
||||
|
||||
def left_arrow(window, text1, text2):
|
||||
"""Event handler for left arrow key."""
|
||||
change_preview(window, text1, text2, -1)
|
||||
|
||||
|
||||
def change_preview(window, text1, text2, move):
|
||||
"""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
|
||||
|
||||
update_notes(window, text1, text2, index)
|
||||
runtime_info["active_split"] = index
|
||||
|
||||
if index < 0:
|
||||
index = 0
|
||||
set_title_notes(window, index)
|
||||
|
||||
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
|
||||
title += " - " + disp_index
|
||||
|
||||
if split_name:
|
||||
title += " - " + split_name
|
||||
|
||||
if runtime_info["timer_running"]:
|
||||
title += " - " + config.RUNNING_ALERT
|
||||
|
||||
update_title(title, window)
|
||||
|
||||
def init_UI(root):
|
||||
"""Draws default UI and creates event bindings."""
|
||||
|
||||
#Graphical components
|
||||
root.geometry(str(config.DEFAULT_WINDOW["WIDTH"]) + "x" + str(config.DEFAULT_WINDOW["HEIGHT"]))
|
||||
|
||||
box1 = tkinter.Frame(root)
|
||||
box2 = tkinter.Frame(root)
|
||||
|
||||
scroll1 = tkinter.Scrollbar(box1)
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
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_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
|
||||
popup.add_command(
|
||||
label=config.MENU_OPTIONS["LOAD"],
|
||||
command=(lambda: menu_load_notes(root, 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
|
||||
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, text1, text2)))
|
||||
root.bind("<Left>", (lambda e: left_arrow(root, text1, text2)))
|
||||
|
||||
#call update loop
|
||||
com_socket = con.init_socket()
|
||||
update(root, com_socket, text1, text2)
|
||||
|
||||
|
||||
init_UI(root)
|
||||
|
||||
root.mainloop()
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import tkinter.filedialog as file_dia
|
||||
import os.path as path
|
||||
|
||||
import config
|
||||
|
||||
"""
|
||||
NOTE STANDARD FORMATTING
|
||||
|
||||
empty newlines separate notes for different splits
|
||||
|
||||
lines that start and end with [ ] are ignored for notes.
|
||||
these can be used for titles.
|
||||
(ex. [Split1] is not included in notes)
|
||||
|
||||
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
|
||||
if path.getsize(file_path) > config.MAX_FILE_SIZE:
|
||||
return False
|
||||
|
||||
try:
|
||||
notes_file = open(file_path, "r")
|
||||
except:
|
||||
return False
|
||||
|
||||
#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]
|
||||
else:
|
||||
return line
|
||||
|
||||
note_list = []
|
||||
cur_notes = ""
|
||||
|
||||
for line in note_lines:
|
||||
#remove whitespace at beginning and end
|
||||
line = line.strip(" ")
|
||||
|
||||
if is_newline(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
|
||||
|
||||
if cur_notes:
|
||||
note_list.append(cur_notes)
|
||||
|
||||
return note_list
|
||||
|
||||
|
||||
def get_notes(file_path):
|
||||
"""
|
||||
Takes a path to a file and returns a list with the notes
|
||||
in the file encoded according to the note fromatting.
|
||||
|
||||
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
|
||||
|
||||
|
||||
def select_file():
|
||||
"""
|
||||
Opens a file select window.
|
||||
Returns False upon no file selection.
|
||||
Otherwise returns absolute path to selected file.
|
||||
"""
|
||||
|
||||
file = file_dia.askopenfilename()
|
||||
|
||||
if file:
|
||||
return file
|
||||
else:
|
||||
return False
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Reference in New Issue
Block a user