#!/usr/bin/python2.7 # -*- coding: UTF-8 -*- # import os import thread import sys from time import sleep import json from os.path import expanduser import signal from collections import OrderedDict import re # Fix enconding in older Python versions 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", "foods", "nature", "objects", "activity", "travel", "flags", "symbols"] # Settings handling default_settings = settings = { "toned": -1, "notifications": True, "lowend": False, "recent": 20 } configpath = expanduser("~") + "/.config/emojione-picker" os.path.isdir(configpath) or os.mkdir(configpath) builder = Gtk.Builder() 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"]) class SettingsButtonHandler: def onButtonPressed(self, button): apply_settings() button.get_parent_window().destroy() 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() } # Save settings to file save_settings() # Reload app to apply new settings - FIXME: This is ugly and slow, could be much better os.execv(__file__, sys.argv) def save_settings(): global settings with open(configfile, 'w') as outfile: json.dump(settings, outfile) # Search feature searchbuilder = Gtk.Builder() def open_search_window(self, w): 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 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().destroy() def onKeyReleased(self, window, event): if event.keyval == Gdk.KEY_Escape: window.destroy() elif event.keyval == Gdk.KEY_Down: searchbuilder.get_object("iconview").grab_focus() def onOutFocus(self, window, data): window.destroy() # 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 # 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 chars = w["unicode"].split("-") output = "" for char in chars: output = output + '\\U' + (char.zfill(8)) clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) GLib.idle_add(clipboard.set_text,output.decode('unicode-escape'), -1) # Show notification if settings["notifications"]==True: 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 try: for k in recent.keys(): if recent[k]["emoji_order"] == w["emoji_order"]: del recent[k] except ValueError: pass # 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__": # 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.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") # Associate menu with indicator ind.set_menu(menu) # Gtk main - call after first update signal.signal(signal.SIGINT, signal.SIG_DFL) Gtk.main()