-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgui.py
198 lines (177 loc) · 6.71 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
import sys
import threading
import tkinter as tk
from artistwordcloud.cloud_creation import cloud_hook
from copy import deepcopy
from multiprocessing import freeze_support
from pathvalidate import sanitize_filename
from PIL import ImageTk, Image
from tkinter import ttk, messagebox, filedialog
from ttkthemes import ThemedTk
from typing import Optional
from wordcloud import WordCloud
current_cloud: WordCloud
next_cloud: WordCloud
thread: threading.Thread
class TextRedirector:
"""
Directs console output from stdout to a text widget provided in the constructor.
"""
def __init__(self, text_widget):
self.text_widget = text_widget
def write(self, string):
self.text_widget.insert(tk.END, string)
self.text_widget.see(tk.END) # Scroll to the end
def flush(self):
pass # Required for file-like object compatibility
def threaded_generation(artist: str, album: Optional[str] = None):
"""
Thread wrapper for cloud_hook
"""
global next_cloud
next_cloud = cloud_hook(artist, album)
if next_cloud is not None:
print("Word cloud complete!")
else:
print(f"Error with query: {album if album else 'All songs'} by {artist}")
return
def check_thread():
"""
Periodically checks thread for completion, after which it displays current word cloud
or gives error for invalid artist or album
"""
if thread.is_alive():
window.after(100, check_thread)
else:
global current_cloud
if next_cloud:
current_cloud = next_cloud
display_cloud()
window.nametowidget("button_frame_wrapper.button_frame.save_button")[
"state"
] = tk.NORMAL
else:
messagebox.showerror(
"Could not find query",
"Artist and/or album could not be found on Genius, ensure that both are spelled correctly."
"\n\nIf the problem persists and the artist is in another script, check Genius to find the specific spelling.",
)
window.nametowidget("button_frame_wrapper.button_frame.submit_button")[
"state"
] = tk.NORMAL
def save_cloud(artist: str, album: Optional[str]):
# Avoid race conditions by copying current cloud immediately
cloud_copy = deepcopy(current_cloud)
try:
file_path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[
("PNG", "*.png"),
("JPEG", "*.jpg"),
("Bitmap", "*.bmp"),
("GIF", "*.gif"),
("WebP", "*.webp"),
],
confirmoverwrite=True,
initialfile=f"{sanitize_filename(artist)}{'_' if album else ''}{sanitize_filename(album)}.png",
)
if file_path:
print(f"Saving file to {file_path}")
cloud_copy.to_file(file_path)
print("Successfully saved file!")
else:
print("Cancelled saving file")
except OSError:
messagebox.showerror("Error Saving File", "File could not be saved.")
def get_cloud(artist: str, album: Optional[str]):
"""
Initiates thread for building word cloud
Calls check_queue on a delay to retrieve word cloud
"""
global thread
thread = threading.Thread(target=threaded_generation, args=(artist, album))
thread.start()
window.nametowidget("button_frame_wrapper.button_frame.submit_button")["state"] = (
tk.DISABLED
)
window.after(1000, check_thread)
def display_cloud(event=None):
"""
Displays word cloud based on window size
This is called from cloud completion and window resizing, so code should be performant wherever possible
event is unused but required for tkinter
"""
if current_cloud is not None:
cloud_frame = window.nametowidget("cloud_frame")
size = min(cloud_frame.winfo_width(), cloud_frame.winfo_height())
wc_image = current_cloud.to_image().resize(
(size, size), Image.Resampling.LANCZOS
)
tk_image = ImageTk.PhotoImage(wc_image)
# Remove old clouds
if not cloud_frame.winfo_children():
image = ttk.Label(cloud_frame, name="image")
image.pack()
image = window.nametowidget("cloud_frame.image")
image.config(image=tk_image)
image.image = tk_image
def set_up_gui() -> tk.Tk:
"""
Creates layout for GUI as well as setting styles and features for widgets
"""
# Prevent error on start-up
global current_cloud
current_cloud = None
# Create window
root = ThemedTk(theme="equilux")
root.title("WordCloud")
root.geometry("900x700")
# Create Frames
text_frame = ttk.Frame(root)
text_frame.pack(side=tk.BOTTOM, fill=tk.X)
button_frame_wrapper = ttk.Frame(root, name="button_frame_wrapper")
button_frame_wrapper.pack(side=tk.BOTTOM, fill=tk.X)
button_frame = ttk.Frame(button_frame_wrapper, name="button_frame")
button_frame.pack(side=tk.BOTTOM, anchor=tk.CENTER)
entry_frame_wrapper = ttk.Frame(root, name="entry_frame_wrapper")
entry_frame_wrapper.pack(side=tk.BOTTOM, fill=tk.X)
entry_frame = ttk.Frame(entry_frame_wrapper, name="entry_frame")
entry_frame.pack(side=tk.BOTTOM, anchor=tk.CENTER)
cloud_frame = ttk.Frame(root, name="cloud_frame")
cloud_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, pady=(0, 5), padx=5)
# For changing size of cloud
cloud_frame.bind("<Configure>", display_cloud)
# Create content
artist_entry_label = ttk.Label(entry_frame, text="Enter an artist:")
artist_entry = ttk.Entry(entry_frame, width=30)
album_entry_label = ttk.Label(entry_frame, text="(Optional) Enter an album:")
album_entry = ttk.Entry(entry_frame, width=30)
submit_button = ttk.Button(
button_frame,
text="Submit",
command=lambda: get_cloud(artist_entry.get(), album_entry.get()),
name="submit_button",
)
save_button = ttk.Button(
button_frame,
text="Save as...",
command=lambda: save_cloud(artist_entry.get(), album_entry.get()),
name="save_button",
)
save_button["state"] = tk.DISABLED
text_output = tk.Text(text_frame, wrap=tk.WORD, height=6, width=75)
# Fill frames
artist_entry_label.pack(side=tk.LEFT, padx=2)
artist_entry.pack(side=tk.LEFT, padx=5)
album_entry_label.pack(side=tk.LEFT, padx=2)
album_entry.pack(side=tk.LEFT, padx=5)
submit_button.pack(side=tk.LEFT, padx=5)
save_button.pack(side=tk.LEFT, padx=40, ipadx=10)
text_output.pack(pady=20)
# Redirect output to text widget
sys.stdout = TextRedirector(text_output)
return root
if __name__ == "__main__":
freeze_support()
window = set_up_gui()
window.mainloop()