Files
Modus/modules/launcher/components/emoji.py
T
2025-04-25 06:33:51 +05:45

331 lines
11 KiB
Python

from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.stack import Stack
from fabric.utils import remove_handler, get_relative_path
from gi.repository import Gdk
import utils.icons as icons
import os
import subprocess
import ijson
class EmojiPicker(Box):
def __init__(self, **kwargs):
super().__init__(
name="emoji",
visible=False,
all_visible=False,
**kwargs,
)
self.launcher = kwargs["launcher"]
self.selected_index = -1
self.emojis_per_page = 9 * 3
self.current_page_index = 0
self.filtered_emojis = []
self.total_pages = 0
self._arranger_handler: int = 0
self._all_emojis = self._load_emoji_data()
self.stack = Stack(
name="viewport",
spacing=4,
orientation="v",
transition_type="slide-up-down",
transition_duration=200,
)
self.search_entry = Entry(
name="search-entry",
placeholder="Search Emojis...",
h_expand=True,
notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
on_activate=lambda entry, *_: self.on_search_entry_activate(
entry.get_text()
),
on_key_press_event=self.on_search_entry_key_press,
)
self.search_entry.props.xalign = 0.5
self.header_box = Box(
name="header_box",
spacing=10,
orientation="h",
children=[
self.search_entry,
# Button(
# name="close-button",
# child=Label(name="close-label", markup=icons.cancel),
# tooltip_text="Exit",
# on_clicked=lambda *_: self.close_picker(),
# ),
],
)
self.picker_box = Box(
name="picker-box",
spacing=10,
h_expand=True,
orientation="v",
children=[
self.header_box,
self.stack,
],
)
self.resize_viewport()
self.add(self.picker_box)
self.show_all()
def _load_emoji_data(self):
emoji_data = {}
emoji_file_path = get_relative_path("../../../config/assets/emoji.json")
if not os.path.exists(emoji_file_path):
print(f"Emoji JSON file not found at: {emoji_file_path}")
return {}
with open(emoji_file_path, "r") as f:
for emoji_char, emoji_info in ijson.kvitems(f, ""):
emoji_data[emoji_char] = emoji_info
return emoji_data
def close_picker(self):
self.stack.children = []
self.selected_index = -1
self.launcher.close()
def open_picker(self):
self.search_entry.set_text("")
self.current_page_index = 0
self.arrange_viewport()
self.search_entry.grab_focus()
def arrange_viewport(self, query: str = ""):
remove_handler(self._arranger_handler) if self._arranger_handler else None
self.stack.children = []
self.selected_index = -1
self.current_page_index = 0
self.filtered_emojis = [
(emoji_char, emoji_info)
for emoji_char, emoji_info in self._all_emojis.items()
if query.casefold()
in (
emoji_info.get("name", "") + " " + emoji_info.get("group", "")
).casefold()
]
self.total_pages = (
(len(self.filtered_emojis) + self.emojis_per_page - 1)
// self.emojis_per_page
if self.filtered_emojis
else 0
)
self.load_page(self.current_page_index)
should_resize = not query
if should_resize:
self.resize_viewport()
if query.strip() != "" and self.get_all_emoji_buttons():
self.update_selection(0)
def load_page(self, page_index):
self.update_selection(-1)
page_box = Box(name=f"page-box-{page_index}", orientation="v", spacing=4)
start_index = page_index * self.emojis_per_page
end_index = min(
(page_index + 1) * self.emojis_per_page, len(self.filtered_emojis)
)
page_emojis = self.filtered_emojis[start_index:end_index]
grid_box = Box(name="emoji-grid-box", orientation="v", spacing=2)
row_box = None
for i, (emoji_char, emoji_info) in enumerate(page_emojis):
if i % 9 == 0:
row_box = Box(name="emoji-row-box", orientation="h", spacing=2)
grid_box.add(row_box)
if row_box is not None:
row_box.add(self.bake_emoji_slot(emoji_char, emoji_info))
page_box.add(grid_box)
self.stack.add_named(page_box, f"page-{page_index}")
self.stack.set_visible_child_name(f"page-{page_index}")
page_box.show_all()
buttons = self.get_all_emoji_buttons()
if buttons and self.selected_index != -1:
page_relative_index = self.selected_index % self.emojis_per_page
if page_relative_index < len(buttons):
self.update_selection(page_relative_index)
else:
self.update_selection(len(buttons) - 1)
def resize_viewport(self):
return False
def bake_emoji_slot(self, emoji_char: str, emoji_info: dict, **kwargs) -> Button:
button = Button(
name="emoji-slot-button",
child=Box(
name="emoji-slot-box",
orientation="horizontal",
halign="center",
valign="center",
children=[
Label(
name="emoji-char-label",
label=emoji_char,
use_markup=True,
v_align="center",
h_align="center",
css_name="emoji-char-label",
),
],
),
tooltip_text=emoji_info.get("name", "Unknown"),
on_clicked=lambda *_: (
self.copy_emoji_to_clipboard(emoji_char),
self.close_picker(),
),
**kwargs,
)
return button
def update_selection(self, new_index: int):
buttons = self.get_all_emoji_buttons()
if not buttons:
self.selected_index = -1
return
if self.selected_index != -1 and self.selected_index < len(buttons):
current_button = buttons[self.selected_index]
current_button.get_style_context().remove_class("selected")
if not buttons or current_button not in buttons:
self.selected_index = -1
if 0 <= new_index < len(buttons):
new_button = buttons[new_index]
new_button.get_style_context().add_class("selected")
self.selected_index = new_index
else:
self.selected_index = -1
def get_all_emoji_buttons(self):
buttons = []
current_page = self.stack.get_visible_child()
if current_page and current_page.get_children():
if current_page.get_children()[0].get_children():
for row_box in current_page.get_children()[0].get_children():
buttons.extend(row_box.get_children())
return buttons
def on_search_entry_activate(self, text):
buttons = self.get_all_emoji_buttons()
if buttons:
if self.selected_index != -1:
buttons[self.selected_index].clicked()
elif buttons and text.strip() != "":
buttons[0].clicked()
def on_search_entry_key_press(self, widget, event):
if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right):
self.move_selection_2d(event.keyval)
return True
elif event.keyval == Gdk.KEY_Escape:
self.close_picker()
return True
return False
def move_selection_2d(self, keyval):
buttons = self.get_all_emoji_buttons()
total_items_current_page = len(buttons)
if total_items_current_page == 0:
return
rows = 3
columns = 9
if self.selected_index == -1:
if keyval in (Gdk.KEY_Down, Gdk.KEY_Right):
new_index = 0
elif keyval in (Gdk.KEY_Up, Gdk.KEY_Left):
new_index = total_items_current_page - 1
else:
return
else:
current_index_page = self.selected_index
row = current_index_page // columns
col = current_index_page % columns
if keyval == Gdk.KEY_Right:
new_col = (col + 1) % columns
new_row = row
if new_col == 0:
new_row = row + 1
elif keyval == Gdk.KEY_Left:
new_col = (col - 1) % columns
new_row = row
if new_col == (columns - 1):
new_row = row - 1
elif keyval == Gdk.KEY_Down:
new_row = row + 1
new_col = col
elif keyval == Gdk.KEY_Up:
new_row = row - 1
new_col = col
else:
return
if new_row >= rows:
if self.current_page_index < self.total_pages - 1:
current_col = col # Keep track of current column
self.current_page_index += 1
self.load_page(self.current_page_index)
new_index = current_col # Try to keep the same column
if (
new_index >= total_items_current_page
): # if column is out of bound, select last
new_index = total_items_current_page - 1
self.selected_index = -1
self.update_selection(new_index)
return
else:
new_index = total_items_current_page - 1
elif new_row < 0:
if self.current_page_index > 0:
current_col = col # Keep track of current column
self.current_page_index -= 1
self.load_page(self.current_page_index)
new_index = (
rows - 1
) * columns + current_col # Select last row, same column
if (
new_index >= total_items_current_page
): # if column is out of bound, select last
new_index = total_items_current_page - 1
self.selected_index = -1
self.update_selection(new_index)
return
else:
new_index = 0
else:
new_index = new_row * columns + new_col
if new_index >= total_items_current_page:
new_index = total_items_current_page - 1
if new_index < 0:
new_index = 0
elif new_index >= total_items_current_page:
new_index = total_items_current_page - 1
self.update_selection(new_index)
def copy_emoji_to_clipboard(self, emoji_char: str):
try:
subprocess.run(["wl-copy"], input=emoji_char.encode("utf-8"), check=True)
except subprocess.CalledProcessError as e:
print(f"Clipboard copy failed: {e}")