-
Notifications
You must be signed in to change notification settings - Fork 1
/
gui.py
620 lines (510 loc) · 22.1 KB
/
gui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
import tkinter as tk
import keyboard
from tkinter import ttk
from tkinter import messagebox
import time
class Mainwindow:
def __init__(self, readfile, savefile):
self.delay = .05
self.courselist = []
self.readfile = readfile
self.readfromfile() # Gets the courselist ready based on input from file
self.savefile = savefile
self.root = tk.Tk()
self.readyRoot() # Configures the root window
self.tabControl = ttk.Notebook(self.root)
# Used for the Edit/Delete Tab
self.hotkeyenabled = False
self.tab1 = ttk.Frame(self.tabControl)
# Used for the Go Time tab!
self.tab2 = ttk.Frame(self.tabControl)
self.readytabControl()
# The buttons
self.button1 = tk.Button(self.tab1, text="Edit")
self.button2 = tk.Button(self.tab1, text="Clear")
self.button3 = tk.Button(self.tab1, text="CRN")
self.readyViewButtons()
# The Scrollbar/Notebook combo
self.scrollbar = tk.Scrollbar(self.tab1)
self.scrollbox = tk.Listbox(self.tab1)
self.tab1pad = 50
self.readyscrollBoxBar()
# The Edit Frame (for when you press the edit button)
self.editFrame = tk.Frame(self.tab1)
self.buttonhelp = tk.Button(self.editFrame, text="?") # This button is different because it lives in editFrame.
self.editingcourselist = self.courselist.copy() # This list serves as the course list for editing purposes.
self.editrows = [] # This list will hold the three widgets and an indentation value for each row in the editFrame
self.readyEditFrame() # This creates the widgets that go inside of self.editFrame and grids them there
self.buttonsave = tk.Button(self.tab1, text="Save")
self.buttoncancel = tk.Button(self.tab1, text="Cancel")
self.readyEditButtons()
# Important values used for entering backup classes
self.mainchoices = []
self.backups = []
# Start the program in view mode
self.viewMode()
def readyRoot(self):
"""
A short function that configures the settings for the root window.
:return: None
"""
self.root.title("Awesome Registering GUI")
self.root.columnconfigure(0, weight=1)
self.root.geometry("+700+200")
def readytabControl(self):
"""
Simple function that adds tab1 and tab2 to the tabControl (Notebook) widget.
:return: None
"""
self.tabControl.add(self.tab1, text="View/Edit")
self.tab1.bind("<FocusIn>", lambda function: self.focusTab1())
self.tabControl.add(self.tab2, text="Go Time!")
self.tab2.bind("<FocusIn>", lambda function: self.focusTab2())
tk.Label(self.tab2,
text="""The hotkeys are CURRENTLY ACTIVE. Here's how to use them:
Click into the first box and press the ` button right below the escape key on your keyboard to type your classes.
Additionally, to enter your alternate classes for any given class, press 'alt' and 'shift' followed by the number that corresponds to your class.
e.x. press alt+shift+1 at the same time to enter your alternate for class 1.
NOTE: Doing this once will enter ONLY your FIRST alternate CRN. Doing it again enters your SECOND, then THIRD and so on. They will not loop back to the start.
If you accidentally enter one of your alternate courses before registration, you will need to swap to the View/Edit tab and back again to reset the counter.""",
wraplength=300
).grid(row=0, column=0, sticky="", padx=50, pady=50)
self.tabControl.grid(row=0, column=1, columnspan=1, sticky="ew", padx=10)
def focusTab1(self):
"""
Describes what should happen if Tab1 gets focus. If tab1 gets focus, it checks if the hotkey is enabled. If it
is, then it removes the hotkey and sets self.hotkeyenabled to False (disables the hotkey).
:return: None
"""
if self.hotkeyenabled:
keyboard.remove_hotkey("`")
for numberkey in range(9):
keyboard.remove_hotkey(f"alt+shift+{numberkey+1}")
self.hotkeyenabled = False
def focusTab2(self):
"""
Describes what should happen if Tab2 gets focus. If tab2 gets focus, it checks if the hotkey is enabled. If it's
not then it adds the hotkey and sets self.hotkeyenabled to True.
:return: None
"""
if not self.hotkeyenabled:
keyboard.add_hotkey("`", self.enterCRNs, suppress=True, trigger_on_release=True)
for numberkey in range(9):
keyboard.add_hotkey(f"alt+shift+{numberkey+1}",
self.enterbackup,
args=[numberkey+1],
suppress=True,
trigger_on_release=True)
self.hotkeyenabled = True
# Update the mainchoices and backups lists
self.mainchoices = []
self.backups = []
backupnum = 0
# Add all of the first picks to the mainchoices list.
for course in self.courselist:
if course[2] == 0:
self.mainchoices.append(course)
self.backups.append([1])
backupnum += 1
else:
self.backups[backupnum - 1].append(course)
def readyscrollBoxBar(self):
"""
A simple function that configures the scrollbar and scrollbox (Notebook) in self.
:param externalpadlist: An integer that denote how much external padding to put on the left of the scrollbox
and on the right of the scrollbar.
:return: None
"""
# Find the longest entry in the course list
longest = 20
for course in self.courselist:
# If it's indented, judge its length based off of the indent as well
if course[2] == 1:
if len(course[0]) + 2 > longest:
longest = len(course[0]) + 4
# Otherwise, just judge its length as is
else:
if len(course[0]) > longest:
longest = len(course[0])
# Configure and grid self.scrollbar with set options
self.scrollbar.config(width=20,
command=self.scrollbox.yview)
# Configure and grid self.scrollbox with set options
self.scrollbox.config(height=10,
width=longest,
yscrollcommand=self.scrollbar.set,
font=("Times New Roman", "15")
)
self.listCourses()
def readyViewButtons(self):
"""
A simple function that configures button1, button2, and button3 in self.
:return: None
"""
self.button1.config(font=("Times New Roman", "10"), command=self.editButton)
self.button2.config(font=("Times New Roman", "10"), command=self.clearButton)
self.button3.config(font=("Times New Roman", "10"),
command=self.buttonName)
def readyEditButtons(self):
"""
Simple function that configures the edit buttons.
:return: None
"""
self.buttonsave.config(command=self.saveButton)
self.buttoncancel.config(command=self.cancelEditButton)
self.buttonhelp.config(command=self.helpEditButton)
def readyEditFrame(self):
"""
This function configures the editing tab and gets it ready to display (DOES NOT GRID TO SCREEN).
:return:
"""
self.buttonhelp.grid(row=0, column=0, padx=10)
tk.Label(self.editFrame, text="Name", fg="blue").grid(row=0, column=1)
tk.Label(self.editFrame, text="CRN", fg="green").grid(row=0, column=2)
# This may be able to be removed depending on whether we call the refresh every time we enter editing mode.
# (Edit and clear button)
self.refreshEditingFrame(False)
def refreshEditingFrame(self, updatefromentries=True):
"""
This function updates self.editing course list, delets all entry widgets, makes them all brand new, and then
updates their labels.
:return:
"""
# Update the course list based on changes to the entry widgets
if updatefromentries:
self.updateEditingCourseList()
# Delete all current entry widgets
self.purgeEditRows()
# Make brand new entry widgets
for row in range(len(self.editingcourselist)):
# Make a blank row with the offset and lable befitting of that course and grid them to the frame
self.makeEditingRow(row+1, self.editingcourselist[row][2])
# Add the current classname and CRN into the entry widgets
self.editrows[row][1].insert(0, self.editingcourselist[row][0])
self.editrows[row][2].insert(0, self.editingcourselist[row][1])
# Update the row labels
self.updateEditRowLabels()
def purgeEditRows(self):
"""
Deletes all the widgets that were used in the editing page
:return: None
"""
for rownum in range(len(self.editrows)):
self.editrows[0][0].destroy()
self.editrows[0][1].destroy()
self.editrows[0][2].destroy()
self.editrows.pop(0)
def updateEditingCourseList(self):
"""
This function changes the entrys in self.editrows to reflect the current entries present in the self.editrows
elements.
:return: None
"""
self.editingcourselist = []
for rownum in range(len(self.editrows)):
self.editingcourselist.append(["", "", ""])
self.editingcourselist[rownum][0] = self.editrows[rownum][1].get()
self.editingcourselist[rownum][1] = self.editrows[rownum][2].get()
self.editingcourselist[rownum][2] = self.editrows[rownum][3]
def makeEditingRow(self, rownum, offset=0):
"""
This function makes a blank row inside self.editFrame and grids it there. It also appends the list of the
three widgets to self.editrows.
:param offset: This is a 1 or 0 offsets the column of the widgets
:param rownum: This takes in an integer which decides what row to put the blank row on
:return: None
"""
courserow = [
tk.Label(self.editFrame, text="To be Updated"),
tk.Entry(self.editFrame, fg="blue"),
tk.Entry(self.editFrame, fg="green"),
offset
]
# Bind the Entry widgets to act on tab and enter
courserow[1].bind("<Tab>", lambda function: self.indentEditRow(rownum))
courserow[2].bind("<Tab>", lambda function: self.indentEditRow(rownum))
courserow[1].bind("<Return>", lambda function: self.insertEditRow(rownum+1))
courserow[2].bind("<Return>", lambda function: self.insertEditRow(rownum+1))
courserow[1].bind("<Delete>", lambda function: self.deleteEditRow(rownum))
courserow[2].bind("<Delete>", lambda function: self.deleteEditRow(rownum))
# Grid each of the widgets to the self.editFrame
courserow[0].grid(row=rownum, column=0 + offset)
courserow[1].grid(row=rownum, column=1 + offset)
courserow[2].grid(row=rownum, column=2 + offset)
self.editrows.insert(rownum-1, courserow)
def indentEditRow(self, rownum):
if keyboard.is_pressed("Shift"):
self.editrows[rownum-1][3] = 0
else:
self.editrows[rownum-1][3] = 1
self.refreshEditingFrame()
def updateEditRowLabels(self):
alphabet = "abcdefghijklmnopqrstuvwxyz"
bignumber = 0
smallletter = 0
for row in self.editrows:
# Decide what label to give each row
if row[3] == 0:
bignumber += 1
first = str(bignumber)
second = ""
smallletter = 0
else:
first = str(bignumber)
second = alphabet[smallletter]
if smallletter < 25:
smallletter += 1
row[0].config(text=first + second + ". ")
def insertEditRow(self, rownum):
"""
This function inserts a blank row at the given index and regrids all the entry rows.
:return: None
"""
self.makeEditingRow(rownum, self.editingcourselist[rownum-2][2])
self.refreshEditingFrame()
def deleteEditRow(self, rownum):
"""
Describes what happens when you press the delete button in the Edit view.
:param rownum: This is the row number that you'd like to delete.
:return: None
"""
self.editrows[rownum-1][0].destroy()
self.editrows[rownum-1][1].destroy()
self.editrows[rownum-1][2].destroy()
self.editrows.pop(rownum-1)
self.refreshEditingFrame()
def buttonName(self):
"""
Changes the name of self.button3 to CRN if it was Name, and Name if it was CRN. It then updates the scrollbox.
:return: None
"""
if self.button3["text"] == "Name":
self.button3["text"] = "CRN"
elif self.button3["text"] == "CRN":
self.button3["text"] = "Name"
self.listCourses()
def saveButton(self):
"""
Checks to see if proper edits were made. If they were it saves the edits made to self.courselist and then
updates them everywhere. Then switches to view mode.
:return:
"""
valid = True
for row in self.editrows:
if not self.validateName(row[1].get()):
row[1].config(bg="red")
valid = False
else:
row[1].config(bg="white")
if not self.validateCRN(row[2].get()):
row[2].config(bg="red")
valid = False
else:
row[2].config(bg="white")
if valid:
self.updateEditingCourseList()
self.courselist = self.editingcourselist.copy()
# Save self.courselist to the self.savefile file
self.savetofile()
# Enter viewing mode
self.viewMode()
def validateName(self, name):
"""
This function is used by the save button to validate the name entries. As of now, there is no validation process
for the name entries so this function always returns True. This is a placeholder for future validation if
desired.
:param name: A string that contains the name to be checked
:return: Boolean
"""
return True
def validateCRN(self, crn):
"""
This function validates a suspect CRN and will return True if it followes the 5 number format and False if it
does not.
:param crn: This is the suspect CRN. It can be a string or an integer.
:return: Boolean
"""
valid = True
if len(crn) != 5 or not str(crn).isnumeric():
valid = False
return valid
def cancelEditButton(self):
"""
Describes the behavior of the cancel button.
:return: None
"""
self.purgeEditRows()
self.viewMode()
def helpEditButton(self):
"""
This function describes the behavior of the help button on the editing screen.
:return: None
"""
messagebox.showinfo("Help",
"Enter:\n" +
"Press the enter button to add a new class to the list.\n\n" +
"Delete:\n" +
"Press the delete key (\"del\") to remove a class from the list.\n\n"
"Tab:\n" +
"Press tab to indent a class and mark it as a backup for the class before it.\n\n" +
"Shift+Tab\n" +
"Press shift+tab to remove the indent and makes it a first choice.")
def listCourses(self):
"""
Clears and Inserts the courses into self.scrollbox so it can update based on the state of button3 and the most
recent self.courselist.
:return: None
"""
# Enable the scrollbox for the duration of this function
self.scrollbox.config(state=tk.NORMAL)
if self.button3['text'] == "CRN":
view = 0
else:
view = 1
# Clear out the scrollbox
self.scrollbox.delete(0, tk.END)
number = 0
for course in self.courselist:
if course[2] == 0:
offset = ""
number += 1
else:
offset = "--> "
coursename = f"{number}. " + str(course[view])
if course[view] == "":
self.scrollbox.insert(tk.END, offset + f"{number}. " + "[No Course Name]")
else:
self.scrollbox.insert(tk.END, offset + coursename)
# Disable the scrollbox when you're done
self.scrollbox.config(state=tk.DISABLED)
def editMode(self):
"""
This function sets up the UI to accept user input for classes
:return: None
"""
# Hides the parts of tab1 that are for viewing
self.scrollbox.grid_remove()
self.scrollbar.grid_remove()
self.button1.grid_remove()
self.button2.grid_remove()
self.button3.grid_remove()
# Grids the editing frame
self.editFrame.grid(row=0, column=0, columnspan=4, sticky="e", padx=self.tab1pad)
self.buttonsave.grid(row=1, column=1, sticky="", pady=30)
self.buttoncancel.grid(row=1, column=2, sticky="", pady=30)
def viewMode(self):
"""
This function swaps tab1 from editing mode to viewing mode
:return: None
"""
# Hides the editing widgets
self.editFrame.grid_remove()
self.buttonsave.grid_remove()
self.buttoncancel.grid_remove()
# Grids the viewing widgets
self.button1.grid(row=1, column=1, sticky="", pady=30)
self.button2.grid(row=1, column=2, sticky="", pady=30)
self.button3.grid(row=1, column=3, sticky="", pady=30)
self.scrollbar.grid(row=0, column=5, sticky="nsw", padx=(0, self.tab1pad))
self.listCourses() # List the courses before gridding the scrollbox to the screen
self.scrollbox.grid(row=0, column=0, columnspan=4, sticky="e", padx=(self.tab1pad, 0))
def editButton(self):
"""
This function describes the behavior of the edit button.
:return: None
"""
self.editingcourselist = self.courselist.copy()
self.refreshEditingFrame(False)
self.editMode()
def clearButton(self):
"""
This function describes the behavior of the clear button.
:return: None
"""
self.purgeEditRows()
self.editingcourselist = [["", "", 0]]
self.refreshEditingFrame(False)
self.editMode()
def readfromfile(self):
"""
This will set self.courselist based off the lines in the self.readfile
:return: None
"""
inputfile = open(self.readfile)
courseparts = inputfile.read().split("\n")
inputfile.close()
# Clear and instantiate self.courselist
self.courselist = []
if len(courseparts)//3 != len(courseparts)/3:
print(f"There is some odd problem with the input file. {len(courseparts)} lines is not evenly divisable by 3.")
for coursenum in range(len(courseparts)//3):
self.courselist.append(
[
courseparts[coursenum*3],
courseparts[coursenum * 3 + 1],
int(courseparts[coursenum * 3 + 2])
]
)
def savetofile(self):
"""
This method will save self.courselist to the self.savefile txt file.
:return:
"""
outputfile = open(self.savefile, "w")
for coursenum in range(len(self.courselist)):
outputfile.write(str(self.courselist[coursenum][0]) + "\n")
outputfile.write(str(self.courselist[coursenum][1]) + "\n")
outputfile.write(str(self.courselist[coursenum][2]))
if coursenum != len(self.courselist) - 1:
outputfile.write("\n")
outputfile.close()
def enterCRNs(self):
"""
Describes the behavior of the ` hotkey.
:return: None
"""
# Make the CRN boxes
for course in self.mainchoices:
keyboard.press_and_release("tab")
time.sleep(self.delay)
keyboard.press_and_release("enter")
time.sleep(self.delay)
# Go to the first CRN box
keyboard.press("shift")
time.sleep(self.delay)
for course in self.mainchoices:
keyboard.press_and_release("tab")
time.sleep(self.delay)
keyboard.release("shift")
# Type all of the CRNs
for course in self.mainchoices:
keyboard.write(course[1])
time.sleep(self.delay)
keyboard.press_and_release("tab")
time.sleep(self.delay)
# Press the Add to schedule button
keyboard.press_and_release("tab")
time.sleep(self.delay)
keyboard.press_and_release("tab")
time.sleep(self.delay)
keyboard.press_and_release("enter")
time.sleep(self.delay)
def enterbackup(self, number):
"""
This will type the backup CRN for a particular class based on the number parameter.
:param number: This is the class you want to type your backup for.
:return: None
"""
if number < len(self.backups):
if self.backups[number-1][0] < len(self.backups[number-1]):
# Write the CRN in the first CRN box
keyboard.write((self.backups[number-1][self.backups[number-1][0]][1]))
self.backups[number - 1][0] += 1
else:
print(f"There are no more backups for class {number}.")
else:
print(f"You do not have a main course selection for class {number}.")
def main():
root = Mainwindow("save.txt", "save.txt")
root.root.mainloop()
main()