-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpingmote.py
315 lines (273 loc) · 11.9 KB
/
pingmote.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
'''
pingmote: a cross-platform Python global emote picker to quickly insert custom images/gifs
Author: David Chen
'''
import PySimpleGUI as sg
import json
import pyperclip
import keyboard
import os
import platform
from psgtray import SystemTray
from pathlib import Path
from time import sleep
from math import ceil
from config import *
SYSTEM = platform.system() # Windows, Linux, Darwin (Mac OS)
class PingMote():
def __init__(self):
# Load frequencies from json for frequents section
self.clean_frequencies()
self.frequencies = self.load_frequencies()
self.frequents = self.get_frequents(self.frequencies)
# Load links and file paths
if GITHUB_URL:
self.filename_to_link = self.get_github_links()
else:
self.filename_to_link = self.load_links()
# Setup
self.window = None
self.hidden = True
self.window_location = WINDOW_LOCATION
self.setup_hardware()
if CUSTOM_HOTKEY_HANDLER:
keyboard.hook(self.custom_hotkey)
self.setup_gui()
if TRAY_ICON:
self.setup_tray()
self.create_window_gui()
def setup_gui(self):
sg.theme('LightBrown1') # Use this as base theme
button_color = sg.theme_button_color()
sg.theme_background_color(GUI_BG_COLOR)
sg.theme_text_element_background_color(GUI_BG_COLOR)
sg.theme_text_color('white')
sg.theme_button_color((button_color[0], GUI_BG_COLOR))
sg.theme_border_width(0)
self.layout_gui()
def layout_gui(self):
""" Layout GUI, then build a window and hide it """
print('loading layout...')
self.layout = []
if SHOW_FREQUENTS:
if SHOW_LABELS:
self.layout.append([sg.Text('Frequently Used'),
sg.Button('Hide', button_color=('black', 'orange'))])
self.layout.append([sg.HorizontalSeparator()])
self.layout += self.layout_frequents_section()
self.layout += self.layout_main_section()
if self.window: # close old window before opening new (for rebuilds)
self.window.close()
no_titlebar = SYSTEM == 'Windows'
self.window = sg.Window('Emote Picker', self.layout, location=self.window_location,
keep_on_top=True, no_titlebar=no_titlebar, grab_anywhere=True, finalize=True)
if SYSTEM == 'Darwin': # Mac hacky fix for blank hidden windows
# read the window once, allows for hiding
self.window.read(timeout=10)
self.hide_gui()
print('ready - window created and hidden')
def setup_tray(self):
""" Set up psgtray SystemTray """
self.system_tray = SystemTray(menu=['_', [
'Toggle', 'Settings', 'Exit']], icon=ICON, window=self.window, single_click_events=True)
self.system_tray.show_message('Ready', 'Window created and hidden')
def layout_frequents_section(self):
""" Return a list of frequent emotes """
return self.list_to_table([
sg.Button('', key=img_name, image_filename=IMAGE_PATH /
img_name, image_subsample=2, tooltip=img_name)
for img_name in self.frequents
])
def layout_main_section(self):
""" Return a list of main section emotes.
If SEPARATE_GIFS is True, split into static and emoji sections
"""
main_section = []
statics, gifs = [], []
for img in sorted(IMAGE_PATH.iterdir()):
if SHOW_FREQUENTS and img.name in self.frequents: # don't show same image in both sections
continue
button = sg.Button(
'', key=img.name, image_filename=img, image_subsample=2, tooltip=img.name)
if SEPARATE_GIFS:
if img.suffix == '.png':
statics.append(button)
else: # gif
gifs.append(button)
else:
main_section.append(button)
if SEPARATE_GIFS:
combined = []
if SHOW_LABELS:
combined.append([sg.Text('Images')])
combined.append([sg.HorizontalSeparator()])
combined += self.list_to_table(statics)
if SHOW_LABELS:
combined.append([sg.Text('GIFs')])
combined.append([sg.HorizontalSeparator()])
combined += self.list_to_table(gifs)
return combined
return self.list_to_table(main_section)
def create_window_gui(self):
""" Run the event loop for the GUI, listening for clicks """
# Event loop
try:
while True:
event, values = self.window.read()
# self.system_tray.show_message(event, values) # for debugging
if event == self.system_tray.key:
event = values[event]
if event in ('Exit', sg.WINDOW_CLOSED):
break
elif event in ('Hide', 'Toggle', sg.EVENT_SYSTEM_TRAY_ICON_DOUBLE_CLICKED, sg.EVENT_SYSTEM_TRAY_ICON_ACTIVATED):
self.on_activate()
elif event == 'Settings':
self.system_tray.show_message(
'Settings', 'Please edit settings in config.py')
elif event in self.filename_to_link:
self.on_select(event)
else:
self.system_tray.show_message(
'ERROR', f'NOT FOUND: selection event = {event}')
except Exception as e:
sg.popup('Pingmote - error in event loop - CLOSING', e)
self.system_tray.close()
self.window.close()
def on_select(self, event):
""" Paste selected image link """
self.hide_gui()
if event not in self.filename_to_link: # link missing
print('Error: Link missing -', event)
return
if AUTO_PASTE:
if PRESERVE_CLIPBOARD: # write text with pynput
self.paste_selection(event)
else: # copy to clipboard then paste
self.copy_to_clipboard(event)
self.paste_link()
if AUTO_ENTER:
self.keyboard_enter()
else:
self.copy_to_clipboard(event)
self.window_location = self.window.current_location() # remember window position
self.update_frequencies(event) # update count for chosen image
def copy_to_clipboard(self, filename):
""" Given an an image, copy the image link to clipboard """
pyperclip.copy(self.filename_to_link[filename])
def paste_selection(self, filename):
""" Use keyboard to write the link instead of copy paste """
keyboard.write(self.filename_to_link[filename])
def paste_link(self):
""" Press ctrl + v to paste """
sleep(SLEEP_TIME) # wait a bit if needed
paste_cmd = 'command+v' if SYSTEM == 'Darwin' else 'ctrl+v'
keyboard.send(paste_cmd)
def keyboard_enter(self):
""" Hit enter on keyboard to send pasted link """
sleep(SLEEP_TIME)
keyboard.send('enter')
def update_frequencies(self, filename):
""" Increment chosen image's counter in frequencies.json
Rebuilds GUI if layout changes (frequents section changes)
"""
if filename not in self.frequencies:
self.frequencies[filename] = 0
self.frequencies[filename] += 1
self.write_frequencies(self.frequencies)
prev_frequents = self.frequents
self.frequents = self.get_frequents(
self.frequencies) # update frequents list
if self.frequents != prev_frequents: # frequents list has changed, update layout
self.layout_gui()
def clean_frequencies(self):
""" Clean frequencies.json on file changes """
frequencies = self.load_frequencies()
filenames = {img_path.name for img_path in IMAGE_PATH.iterdir()}
for file in list(frequencies):
if file not in filenames:
del frequencies[file] # remove key, file not present
self.write_frequencies(frequencies)
def load_links(self):
""" Load image links from links.txt """
with open(MAIN_PATH / 'assets' / 'links.txt') as f:
links = f.read().splitlines()
return {link.rsplit('/', 1)[-1]: link for link in links}
def load_frequencies(self):
""" Load the frequencies dictionary from frequencies.json """
with open(MAIN_PATH / 'assets' / 'frequencies.json', 'r') as f:
return json.load(f)
def write_frequencies(self, frequencies):
""" Write new frequencies to frequencies.json """
with open(MAIN_PATH / 'assets' / 'frequencies.json', 'w') as f:
json.dump(frequencies, f, indent=4)
def get_frequents(self, frequencies):
""" Get the images used most frequently """
# sort in descending order by frequency
desc_frequencies = sorted(
frequencies.items(), key=lambda x: x[-1], reverse=True)
return [img for img, _ in desc_frequencies[:NUM_FREQUENT]]
def get_github_links(self):
""" If a GITHUB_URL is provided, use raw GitHub links instead of an alternate provider like PostImages
NOTE: this current implementation of storing a dict mapping filenames to links is pretty bad, since all the links are prefixed with the same thing. I'm only currently using this for compatibility with the original image hoster method.
"""
github_raw_prefix = 'https://raw.githubusercontent.com/'
user_repo = GITHUB_URL.split('github.com/')[-1]
url = github_raw_prefix + user_repo + '/master/assets/resized/'
# ex) https://raw.githubusercontent.com/dchen327/pingmote/master/assets/resized/
return {
img.name: url + img.name
for img in sorted(IMAGE_PATH.iterdir())
}
def list_to_table(self, a, num_cols=NUM_COLS):
""" Given a list a, convert it to rows and columns
ex) a = [1, 2, 3, 4, 5], num_cols = 2
returns: [[1, 2], [3, 4], [5]]
"""
return [a[i * num_cols:i * num_cols + num_cols] for i in range(ceil(len(a) / num_cols))]
def setup_hardware(self):
""" Create mouse controller, setup hotkeys """
if CUSTOM_HOTKEY_HANDLER:
self.hotkeys = {
SHORTCUT: self.on_activate,
KILL_SHORTCUT: self.kill_all,
}
else:
keyboard.add_hotkey(SHORTCUT, self.on_activate)
keyboard.add_hotkey(KILL_SHORTCUT, self.kill_all)
def custom_hotkey(self, event):
""" Hook and react to hotkeys with custom handler """
try:
pressed_keys = [e.name.lower()
for e in keyboard._pressed_events.values()]
except AttributeError: # Fn might return as None
pressed_keys = []
for hotkey, func in self.hotkeys.items():
pressed = all(
key in pressed_keys
for key in hotkey.split('+')
)
if pressed:
func()
def hide_gui(self):
self.window.hide()
self.hidden = True
if SYSTEM == 'Darwin': # Unfocus Python to allow for pasting
keyboard.send('command+tab')
def show_gui(self):
self.window.un_hide()
self.window.force_focus() # force window to be focused
self.hidden = False
def on_activate(self):
""" When hotkey is activated, toggle the GUI """
if self.hidden:
self.show_gui()
else:
self.hide_gui()
def kill_all(self):
""" Kill the script in case it's frozen or buggy """
print('exit program')
self.system_tray.close()
self.window.close()
os._exit(1) # exit the entire program
if __name__ == '__main__':
pingmote = PingMote()