Created
July 3, 2020 18:41
-
-
Save kirk86/71ee5d5300a8eb34e87a482514e36f42 to your computer and use it in GitHub Desktop.
emojione-picker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# -*- coding: UTF-8 -*- | |
# | |
import os | |
import subprocess | |
import time | |
import socket | |
import sys | |
import json | |
from os.path import expanduser | |
import signal | |
from collections import OrderedDict | |
import re | |
import importlib | |
# Fix enconding in older Python versions | |
importlib.reload(sys) | |
#sys.setdefaultencoding('utf8') | |
# Import GTK | |
from gi import require_version | |
require_version('AppIndicator3', '0.1') | |
require_version('Gtk', '3.0') | |
require_version('Notify', '0.7') | |
from gi.repository import GLib, Gtk, GObject, Gdk, Notify, GdkPixbuf | |
from gi.repository import AppIndicator3 as appindicator | |
# Where is the data? | |
directories = [expanduser("~") + "/.local/share/emojione-picker", "/usr/local/share/emojione-picker", "/usr/share/emojione-picker", os.path.dirname(os.path.realpath(__file__))] | |
for d in directories: | |
if os.path.isdir(d): | |
directory = d; | |
break | |
# Categories definitions | |
categories = ["recent", "people", "food", "nature", "objects", "activity", "travel", "flags", "symbols"] | |
# Settings handling | |
default_settings = settings = { | |
"toned": -1, | |
"notifications": True, | |
"lowend": False, | |
"recent": 20, | |
"paste": False | |
} | |
configpath = expanduser("~") + "/.config/emojione-picker" | |
os.path.isdir(configpath) or os.mkdir(configpath) | |
sockname = '/tmp/emojisocket.' + str(os.getuid()) + os.environ['DISPLAY'] | |
builder = Gtk.Builder() | |
def handle_sock_signal(sock, *args): | |
GLib.io_add_watch(sock, GLib.IO_IN, handle_sock_signal) | |
conn, addr = sock.accept() | |
conn.close() | |
open_search_window('','') | |
def exit(self, w): | |
sys.exit() | |
def open_settings_window(self, w): | |
global settings | |
builder.add_from_file(directory + "/assets/settings.glade") | |
builder.connect_signals(SettingsButtonHandler()) | |
settings_window = builder.get_object("settings_window") | |
settings_window.show_all() | |
settings_window.present() | |
settings_window.grab_focus() | |
# Apply settings | |
builder.get_object("combo_toned").set_active(settings["toned"]+1) | |
builder.get_object("check_notifications").set_active(settings["notifications"]) | |
builder.get_object("check_lowend").set_active(settings["lowend"]) | |
builder.get_object("recent").set_value(settings["recent"]) | |
builder.get_object("check_paste").set_active(settings["paste"]) | |
class SettingsButtonHandler: | |
def onButtonPressed(self, button): | |
apply_settings() | |
button.get_parent_window().destroy() | |
def onToggled(self,button): | |
if builder.get_object("check_paste").get_active() == True: | |
builder.get_object("check_notifications").set_sensitive(False) | |
else: | |
builder.get_object("check_notifications").set_sensitive(True) | |
def apply_settings(): | |
global settings | |
# Get settings from dialog | |
settings = { | |
"toned": builder.get_object("combo_toned").get_active()-1, | |
"notifications": builder.get_object("check_notifications").get_active(), | |
"lowend": builder.get_object("check_lowend").get_active(), | |
"recent": builder.get_object("recent").get_value_as_int(), | |
"paste": builder.get_object("check_paste").get_active() | |
} | |
# Save settings to file | |
save_settings() | |
# Reload app to apply new settings - FIXME: This is ugly and slow, could be much better | |
os.unlink(sockname) | |
os.execv(__file__, sys.argv) | |
def save_settings(): | |
global settings | |
with open(configfile, 'w') as outfile: | |
json.dump(settings, outfile) | |
# Search feature | |
searchbuilder = None | |
search_window = None | |
def open_search_window(self, w): | |
global searchbuilder | |
global search_window | |
# Build window only once | |
try: | |
search_window.show_all() | |
search_window.present() | |
search_window.grab_focus() | |
except: | |
# Build window | |
searchbuilder = Gtk.Builder() | |
searchbuilder.add_from_file(directory + "/assets/chooser.glade") | |
searchbuilder.connect_signals(SearchHandler()) | |
search_window = searchbuilder.get_object("search_window") | |
search_window.show_all() | |
search_window.present() | |
search_window.grab_focus() | |
# Put recent icons by default | |
iconstore = searchbuilder.get_object("iconstore") | |
global sorted_recent, searchresults | |
for i in sorted_recent: | |
emoji_name = sorted_recent[i]["name"] | |
emoji_image = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_recent[i]["unicode"] + ".svg", 24, 24) | |
emoji_code = sorted_recent[i]["unicode"] | |
iconstore.append([emoji_image, emoji_code, emoji_name]) | |
searchresults = [] | |
for i in sorted_recent.keys(): | |
searchresults.append(sorted_recent[i]) | |
return | |
searchresults = None | |
selectionChanged = False | |
class SearchHandler: | |
def onSearchChanged(self, search): | |
global searchresults | |
search = searchbuilder.get_object("search").get_text() | |
iconstore = searchbuilder.get_object("iconstore") | |
iconstore.clear() | |
if search == "": | |
# No search? Put recent icons | |
global sorted_recent | |
for i in sorted_recent: | |
emoji_name = sorted_recent[i]["name"] | |
emoji_image = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_recent[i]["unicode"] + ".svg", 24, 24) | |
emoji_code = sorted_recent[i]["unicode"] | |
iconstore.append([emoji_image, emoji_code, emoji_name]) | |
searchresults = [] | |
for i in sorted_recent.keys(): | |
searchresults.append(sorted_recent[i]) | |
return | |
numfound=0 | |
searchresults = [] | |
for i in sorted_data: | |
match = False | |
for keyword in sorted_data[i]["keywords"]: | |
if keyword.find(search)>-1: | |
match = True | |
if sorted_data[i]["name"].find(search)>-1: | |
match = True | |
if match == True: | |
numfound += 1 | |
if numfound > 50: | |
return | |
searchresults.append(sorted_data[i]) | |
emoji_name = sorted_data[i]["name"] | |
emoji_image = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[i]["unicode"] + ".svg", 24, 24) | |
emoji_code = sorted_data[i]["unicode"] | |
iconstore.append([emoji_image, emoji_code, emoji_name]) | |
def onIconActivated(self, icon, index): | |
global searchresults | |
item_response(self, searchresults[int(index.to_string())]) | |
icon.get_parent_window().hide() | |
def onSelectionChanged(self, data): | |
global selectionChanged | |
selectionChanged = True | |
def onKeyReleased(self, window, event): | |
global selectionChanged | |
if event.keyval == Gdk.KEY_Escape: | |
window.hide() | |
elif event.keyval == Gdk.KEY_Down: | |
searchbuilder.get_object("iconview").grab_focus() | |
elif event.keyval == Gdk.KEY_Up and selectionChanged == False: | |
searchbuilder.get_object("search").grab_focus() | |
selectionChanged = False | |
def onOutFocus(self, window, data): | |
window.hide() | |
# Load settings at startup | |
configfile = configpath + "/settings.json" | |
if os.path.isfile(configfile): | |
with open(configfile) as settings_json_file: | |
try: | |
settings = json.load(settings_json_file) | |
except: | |
save_settings() | |
else: | |
save_settings() | |
for k in default_settings.keys(): | |
if not k in settings: | |
settings[k] = default_settings[k] | |
# Load recent emojis at startup | |
recentfile = configpath + "/recent.json" | |
if os.path.isfile(recentfile): | |
with open(recentfile) as recent_json_file: | |
recent = json.load(recent_json_file) | |
recentindex = 0 | |
for k in recent.keys(): | |
if(int(recent[k]["recent_order"])>recentindex): | |
recentindex = int(recent[k]["recent_order"]) | |
recentindex += 1 | |
else: | |
recent = dict() | |
recentindex = 0 | |
# If using lowend, load lowend information | |
if settings["lowend"] == True: | |
lowendfile = directory + "/assets/lowend.json" | |
if os.path.isfile(lowendfile): | |
with open(lowendfile) as lowend_json_file: | |
lowend = json.load(lowend_json_file) | |
# Refresh recent icons submenu | |
sorted_recent = None | |
def refresh_recent_submenu(): | |
global recent, recent_items, sorted_recent | |
# Rearrange data | |
def orderfunc(tup): | |
key, d = tup | |
return -int(d["recent_order"]) | |
sorted_recent = sorted(recent.items(), key=orderfunc) | |
sorted_recent = OrderedDict(sorted_recent) | |
# Refresh icons | |
i = 0 | |
for key in sorted_recent: | |
if i >= settings["recent"]: | |
break | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_recent[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2]) | |
img = Gtk.Image.new_from_pixbuf(pixbuf) | |
recent_items[i].set_image(img) | |
recent_items[i].set_label(sorted_recent[key]["name"].title()) | |
recent_items[i].show() | |
if ("recent_" + str(i)) in signals.keys(): | |
recent_items[i].disconnect(signals["recent_" + str(i)]) | |
signals["recent_" + str(i)] = recent_items[i].connect("activate", item_response, sorted_recent[key]) | |
i = i + 1 | |
# Type character using xdootool [Experimental] | |
def type_xdotool(text): | |
time.sleep(0.1) | |
subprocess.Popen(["xdotool", "type", text.decode('unicode-escape')]) | |
return False # Glib.idle_add needs this | |
# Click response | |
def item_response(self, w): | |
global recentindex, recent | |
# If this is a toned item with submenu, do nothing | |
try: | |
if self.get_submenu() != None: | |
return | |
except AttributeError: | |
pass | |
# Copy character to clipboard or write it | |
chars = w["unicode"].split("-") | |
output = "" | |
for char in chars: | |
output = output + '\\U' + (char.zfill(8)) | |
if settings["paste"]==True: | |
GLib.idle_add(type_xdotool,output) | |
else: | |
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) | |
#GLib.idle_add(clipboard.set_text,output.decode('unicode-escape'), -1) | |
GLib.idle_add(clipboard.set_text, bytes(output, "utf-8").decode("unicode_escape"), -1) | |
# Show notification | |
if settings["notifications"]==True: | |
if settings["paste"]==False: | |
n = Notify.Notification.new(w["name"].title(), "Emoji is now in the clipboard. Paste it wherever you want!", directory + "/assets/svg/" + w["unicode"] + ".svg") | |
n.show() | |
# Remove item from recent if already present | |
for k in recent.keys(): | |
if recent[k]["emoji_order"] == w["emoji_order"]: | |
del recent[k] | |
# Store item on recent | |
w["recent_order"] = recentindex | |
recentindex = recentindex + 1 | |
recent.update({w["emoji_order"]:w}) | |
# Remove older items if recent is too big | |
if len(recent) > settings["recent"]: | |
biggestindex = 0 | |
for k in recent.keys(): | |
if recent[k]["recent_order"] > biggestindex: | |
biggestindex = recent[k]["recent_order"] | |
nextbiggest = biggestindex | |
for i in range (1, settings["recent"]): | |
nextbiggest_candidate = 0 | |
for k in recent.keys(): | |
if recent[k]["recent_order"] < nextbiggest and recent[k]["recent_order"] > nextbiggest_candidate: | |
nextbiggest_candidate = recent[k]["recent_order"] | |
nextbiggest = nextbiggest_candidate | |
minindex = nextbiggest | |
for k in recent.keys(): | |
if recent[k]["recent_order"] < minindex: | |
del recent[k] | |
# Save recentfile | |
with open(recentfile, 'w') as outfile: | |
json.dump(recent, outfile) | |
# Refresh recent icons submenu | |
GLib.idle_add(refresh_recent_submenu) | |
def get_emoji_group(code): | |
for key in groups_data: | |
if code in groups_data[key]["unicodes"]: | |
return key | |
return False | |
if __name__ == "__main__": | |
try: | |
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
sock.connect(sockname) | |
except: | |
# Calling GObject.threads_init() is not needed for PyGObject 3.10.2+ | |
GObject.threads_init() | |
# Initialize notifications | |
Notify.init("emojione-picker") | |
# Create the main menu | |
menu = Gtk.Menu() | |
# Create indicator | |
ind = appindicator.Indicator.new("emojione-picker", directory + "/assets/icon-default.svg", appindicator.IndicatorCategory.APPLICATION_STATUS) | |
ind.set_status(appindicator.IndicatorStatus.ACTIVE) | |
# If there is a icon present in the theme, use it! | |
try: | |
icons = Gtk.IconTheme.get_default() | |
icon = icons.load_icon("emojione-picker", Gtk.IconSize.MENU, 0) | |
ind.set_icon("emojione-picker") | |
except: | |
pass | |
# Get proper icon sizes | |
iconsizes = Gtk.IconSize.lookup(Gtk.IconSize.MENU) | |
# Create categories items and submenus | |
category_item = {} | |
category_menu = {} | |
for category in categories: | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/categories/" + category + ".svg", iconsizes[1], iconsizes[2]) | |
img = Gtk.Image.new_from_pixbuf(pixbuf) | |
category_item[category] = Gtk.ImageMenuItem(category.title()) | |
item_settings = category_item[category].get_settings() | |
item_settings.set_property('gtk-menu-images', True) | |
GLib.idle_add(category_item[category].set_image, img) | |
category_menu[category] = Gtk.Menu() | |
category_item[category].set_submenu(category_menu[category]); | |
# Load groups and icons definition and rearrange them in order | |
with open(directory + "/assets/groups.json") as groups_file: | |
groups_data = json.load(groups_file) | |
with open(directory + "/assets/emoji.json") as json_file: | |
json_data = json.load(json_file) | |
def orderfunc(tup): | |
key, d = tup | |
return int(d["emoji_order"]) | |
sorted_data = sorted(json_data.items(), key=orderfunc) | |
sorted_data = OrderedDict(sorted_data) | |
# Load icons into menu items | |
tones_re = re.compile('(.*) tone[ ]?\d', re.IGNORECASE) | |
item_groups = {} | |
submenu_groups = {} | |
items = {} | |
tone_groups = {} | |
submenu_tones = {} | |
signals = {} | |
for category in categories: | |
for key in sorted_data: | |
if settings["lowend"] == False or not sorted_data[key]["unicode"] in lowend: | |
if sorted_data[key]["category"] == category: | |
emoji_group = get_emoji_group(sorted_data[key]["unicode"]) | |
if emoji_group != False: | |
# Grouped emoji | |
if not emoji_group in item_groups: | |
# Create group item | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + groups_data[emoji_group]["icon"] + ".svg", iconsizes[1], iconsizes[2]) | |
img = Gtk.Image.new_from_pixbuf(pixbuf) | |
item_groups[emoji_group] = Gtk.ImageMenuItem(groups_data[emoji_group]["name"].title()) | |
item_settings = item_groups[emoji_group].get_settings() | |
item_settings.set_property('gtk-menu-images', True) | |
GLib.idle_add(item_groups[emoji_group].set_image, img) | |
GLib.idle_add(category_menu[category].append, item_groups[emoji_group]) | |
GLib.idle_add(item_groups[emoji_group].show) | |
# Create group submenu | |
submenu_groups[emoji_group] = Gtk.Menu() | |
GLib.idle_add(item_groups[emoji_group].set_submenu, submenu_groups[emoji_group]) | |
if tones_re.match(sorted_data[key]["name"]) == None: | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2]) | |
img = Gtk.Image.new_from_pixbuf(pixbuf) | |
items[sorted_data[key]["unicode"]] = Gtk.ImageMenuItem(sorted_data[key]["name"].title()) | |
item_settings = items[sorted_data[key]["unicode"]].get_settings() | |
item_settings.set_property('gtk-menu-images', True) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img) | |
GLib.idle_add(submenu_groups[emoji_group].append, items[sorted_data[key]["unicode"]]) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].show) | |
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[key]) | |
else: | |
# This won't happen, we are not grouping toned icons | |
pass | |
else: | |
if tones_re.match(sorted_data[key]["name"]) == None: | |
# Not toned emoji (aka simplest case), just add to menu | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2]) | |
img = Gtk.Image.new_from_pixbuf(pixbuf) | |
items[sorted_data[key]["unicode"]] = Gtk.ImageMenuItem(sorted_data[key]["name"].title()) | |
item_settings = items[sorted_data[key]["unicode"]].get_settings() | |
item_settings.set_property('gtk-menu-images', True) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img) | |
GLib.idle_add(category_menu[category].append, items[sorted_data[key]["unicode"]]) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].show) | |
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[key]) | |
else: | |
# Toned emoji | |
if settings["toned"] == -1 and settings["lowend"] == False: | |
# Show all toned emojis in a submenu | |
original_key = key | |
chars = sorted_data[key]["unicode"].split("-") | |
tone_group = chars[0] # FIXME this can be multibyte so taking the first byt isn't right | |
if not tone_group in tone_groups: | |
# Create tone submenu | |
submenu_tones[tone_group] = Gtk.Menu() | |
GLib.idle_add(items[tone_group].set_submenu, submenu_tones[tone_group]) | |
tone_groups[tone_group] = items[tone_group] | |
# Get key from original icon | |
for k in sorted_data: | |
if sorted_data[k]["unicode"] == tone_group: | |
key = k | |
# Append untoned icon to submenu | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2]) | |
img = Gtk.Image.new_from_pixbuf(pixbuf) | |
items[sorted_data[key]["unicode"]] = Gtk.ImageMenuItem(sorted_data[key]["name"].title()) | |
item_settings = items[tone_group].get_settings() | |
item_settings.set_property('gtk-menu-images', True) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img) | |
GLib.idle_add(submenu_tones[tone_group].append, items[sorted_data[key]["unicode"]]) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].show) | |
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[key]) | |
key = original_key | |
# Append toned emoji to toned submenu | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2]) | |
img = Gtk.Image.new_from_pixbuf(pixbuf) | |
items[sorted_data[key]["unicode"]] = Gtk.ImageMenuItem(sorted_data[key]["name"].title()) | |
item_settings = items[sorted_data[key]["unicode"]].get_settings() | |
item_settings.set_property('gtk-menu-images', True) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img) | |
GLib.idle_add(submenu_tones[tone_group].append, items[sorted_data[key]["unicode"]]) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].show) | |
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[key]) | |
else: | |
# Show only one tone for toned emojis | |
if settings["toned"] == 0 or settings["lowend"] == True: | |
# Untoned emojis are already shown by default, so do nothing | |
# When using lowend mode, untoned emojis are enforced | |
pass | |
else: | |
desired_tone = settings["toned"] | |
current_tone = re.search("\d",sorted_data[key]["name"]).group(0) | |
if desired_tone == int(current_tone): | |
# Replace item with desired tone | |
toned_key = key | |
chars = sorted_data[key]["unicode"].split("-") # FIXME this can be multibyte so taking the first byt isn't right | |
untoned_code = chars[0] | |
for k in sorted_data: | |
if sorted_data[k]["unicode"] == untoned_code: | |
key = k | |
# Replace image in item | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[toned_key]["unicode"] + ".svg", iconsizes[1], iconsizes[2]) | |
img = Gtk.Image.new_from_pixbuf(pixbuf) | |
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img) | |
# Replace action | |
items[sorted_data[key]["unicode"]].disconnect(signals[key]) | |
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[toned_key]) | |
# Load icons into recent category | |
recent_items = [] | |
for i in range (0, settings["recent"]): | |
recent_items.append( Gtk.ImageMenuItem()) | |
item_settings = recent_items[i].get_settings() | |
item_settings.set_property('gtk-menu-images', True) | |
category_menu["recent"].append(recent_items[i]) | |
refresh_recent_submenu() | |
# Append categories to main menu | |
for category in categories: | |
GLib.idle_add(menu.append, category_item[category]) | |
GLib.idle_add(category_item[category].show) | |
# Create separator | |
separator = Gtk.SeparatorMenuItem() | |
GLib.idle_add(menu.append,separator) | |
GLib.idle_add(separator.show) | |
# Create search item | |
search_item = Gtk.MenuItem("Search Emoji") | |
GLib.idle_add(menu.append,search_item) | |
GLib.idle_add(search_item.show) | |
search_item.connect("activate", open_search_window, "Search") | |
# Create settings item | |
settings_item = Gtk.MenuItem("Settings...") | |
GLib.idle_add(menu.append,settings_item) | |
GLib.idle_add(settings_item.show) | |
settings_item.connect("activate", open_settings_window, "Settings") | |
# Create another separator | |
separator = Gtk.SeparatorMenuItem() | |
GLib.idle_add(menu.append,separator) | |
GLib.idle_add(separator.show) | |
# Create exit item | |
exit_item = Gtk.MenuItem("Exit") | |
GLib.idle_add(menu.append,exit_item) | |
GLib.idle_add(exit_item.show) | |
exit_item.connect("activate", exit, "Exit") | |
# Associate menu with indicator | |
ind.set_menu(menu) | |
# Listen to socket | |
try: | |
os.unlink(sockname) | |
except OSError: | |
pass | |
try: | |
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
sock.bind(sockname) | |
sock.listen(1) | |
GLib.io_add_watch(sock, GLib.IO_IN, handle_sock_signal) | |
except: | |
print('Could not listen to socket!') | |
# Run server | |
signal.signal(signal.SIGINT, signal.SIG_DFL) | |
Gtk.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment