Skip to content

Commit 6cc4333

Browse files
committed
feature(emojis): add an emojis picker
1 parent 41127af commit 6cc4333

File tree

9 files changed

+1503
-5
lines changed

9 files changed

+1503
-5
lines changed

qchat/gui/dck_qchat.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from qgis.core import Qgis, QgsApplication, QgsJsonExporter, QgsMapLayer, QgsProject
1414
from qgis.gui import QgisInterface, QgsDockWidget
1515
from qgis.PyQt import uic
16-
from qgis.PyQt.QtCore import QPoint, Qt
17-
from qgis.PyQt.QtGui import QCursor, QIcon
16+
from qgis.PyQt.QtCore import QPoint, Qt, QUrl
17+
from qgis.PyQt.QtGui import QCursor, QFont, QFontDatabase, QIcon
1818
from qgis.PyQt.QtWidgets import (
1919
QAction,
2020
QFileDialog,
@@ -43,6 +43,7 @@
4343
QCHAT_MESSAGE_TYPE_TEXT,
4444
QCHAT_NICKNAME_MINLENGTH,
4545
)
46+
from qchat.gui.emojis.fra_emoji_picker_quick import EmojiButtonHandler
4647
from qchat.gui.qchat_tree_widget_items import (
4748
MESSAGE_COLUMN,
4849
QChatAdminTreeWidgetItem,
@@ -104,6 +105,7 @@ def __init__(
104105
self.task_manager = QgsApplication.taskManager()
105106
self.log = PlgLogger().log
106107
self.plg_settings = PlgOptionsManager()
108+
107109
uic.loadUi(Path(__file__).parent / f"{Path(__file__).stem}.ui", self)
108110

109111
# set channel to autoreconnect to when widget will open
@@ -190,6 +192,10 @@ def __init__(
190192
QIcon(QgsApplication.iconPath("mActionDoubleArrowRight.svg"))
191193
)
192194

195+
# emoji picker
196+
self.emoji_handler = EmojiButtonHandler(parent_button=self.btn_emojis_picker)
197+
self.emoji_handler.emoji_selected.connect(self.insert_emoji)
198+
193199
# send image message signal listener
194200
self.btn_send_image.pressed.connect(self.on_send_image_button_clicked)
195201
self.btn_send_image.setIcon(
@@ -956,14 +962,26 @@ def on_send_crs_button_clicked(self) -> None:
956962
self.qchat_ws.send_message(message)
957963

958964
def add_admin_message(self, text: str, timestamp: Optional[int] = None) -> None:
965+
"""Adds an admin message to QTreeWidget chat.
966+
967+
:param text: admin message to insert
968+
:type text: str
969+
:param timestamp: datetime, defaults to None
970+
:type timestamp: Optional[int], optional
959971
"""
960-
Adds an admin message to QTreeWidget chat
961-
"""
972+
962973
item = QChatAdminTreeWidgetItem(self.twg_chat, text, timestamp)
963974
self.add_tree_widget_item(item)
964975

965976
def add_tree_widget_item(self, item: QTreeWidgetItem) -> None:
977+
"""Adds a QTreeWidgetItem to the chat QTreeWidget.
978+
979+
:param item: item to insert
980+
:type item: QTreeWidgetItem
981+
"""
966982
self.twg_chat.addTopLevelItem(item)
983+
if isinstance(item, QChatTextTreeWidgetItem):
984+
item.setFont(MESSAGE_COLUMN, QFont("Noto Color Emoji"))
967985
if self.ckb_autoscroll.isChecked():
968986
self.twg_chat.scrollToItem(item)
969987

@@ -1154,3 +1172,73 @@ def on_send_geojson_layer_to_qchat(self) -> None:
11541172
style=qml_style,
11551173
)
11561174
self.qchat_ws.send_message(message)
1175+
1176+
# -- FONTS --
1177+
def is_font_available(self, font_family: str) -> bool:
1178+
available_fonts = QFontDatabase().families()
1179+
return font_family in available_fonts
1180+
1181+
# -- EMOJIS --
1182+
1183+
def check_emoji_font(self) -> bool:
1184+
"""Check if the font used for displaying emojis is installed. If not, try to
1185+
download it.
1186+
1187+
:return: _description_
1188+
:rtype: _type_
1189+
"""
1190+
if self.is_font_available(font_family=self.plg_settings.font_emoji_family):
1191+
self.log(
1192+
message=self.tr(
1193+
"Required font to display emojis is already installed: {}".format(
1194+
self.plg_settings.font_emoji_family
1195+
)
1196+
),
1197+
push=False,
1198+
log_level=Qgis.MessageLevel.NoLevel,
1199+
)
1200+
return True
1201+
else:
1202+
self.log(
1203+
message="Required font for emojis needs to be installed: {}".format(
1204+
self.plg_settings.font_emoji_family
1205+
),
1206+
push=False,
1207+
)
1208+
font_manager = QgsApplication.fontManager()
1209+
font_manager.fontDownloadErrorOccurred.connect(self.on_font_download_failed)
1210+
auto_downloaded = font_manager.tryToDownloadFontFamily(
1211+
self.plg_settings.font_emoji_family
1212+
)
1213+
if not auto_downloaded:
1214+
self.log(message="not downloaded", log_level=Qgis.MessageLevel.Warning)
1215+
1216+
font_manager.downloadAndInstallFont(
1217+
url=QUrl(self.plg_settings.font_emoji_download_url),
1218+
identifier="qchat-emoji-font",
1219+
)
1220+
1221+
def on_font_download_failed(self, error_message: Optional[str] = None):
1222+
"""Handle pyqtsignal emitted by QgsFontManager when font downloading failed.
1223+
1224+
:param error_message: error message, defaults to None
1225+
:type error_message: Optional[str], optional
1226+
"""
1227+
self.log(
1228+
message=self.tr(
1229+
"Downloading the font {} from {} failed. Since it's required to "
1230+
"correctly display emojis, consider to add it manually to your system. "
1231+
"Trace: {}".format(
1232+
self.EMOJI_FONT_FAMILY, self.EMOJI_FONT_DOWNLOAD_URL, error_message
1233+
)
1234+
)
1235+
)
1236+
1237+
def insert_emoji(self, emoji):
1238+
"""Insert selected emoji at cursor position"""
1239+
cursor_pos = self.lne_message.cursorPosition()
1240+
current_text = self.lne_message.text()
1241+
new_text = current_text[:cursor_pos] + emoji + current_text[cursor_pos:]
1242+
self.lne_message.setText(new_text)
1243+
self.lne_message.setCursorPosition(cursor_pos + len(emoji))
1244+
self.lne_message.setFocus()

qchat/gui/dck_qchat.ui

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>446</width>
9+
<width>467</width>
1010
<height>887</height>
1111
</rect>
1212
</property>
@@ -219,6 +219,11 @@
219219
<property name="text">
220220
<string notr="true">3</string>
221221
</property>
222+
<property name="font">
223+
<font>
224+
<kerning>false</kerning>
225+
</font>
226+
</property>
222227
</column>
223228
</widget>
224229
</item>
@@ -284,11 +289,40 @@
284289
</item>
285290
<item>
286291
<widget class="QgsFilterLineEdit" name="lne_message">
292+
<property name="font">
293+
<font>
294+
<family>Noto Color Emoji</family>
295+
</font>
296+
</property>
297+
<property name="placeholderText">
298+
<string>Type your message here...</string>
299+
</property>
287300
<property name="qgisRelation" stdset="0">
288301
<string notr="true"/>
289302
</property>
290303
</widget>
291304
</item>
305+
<item>
306+
<widget class="QPushButton" name="btn_emojis_picker">
307+
<property name="maximumSize">
308+
<size>
309+
<width>35</width>
310+
<height>35</height>
311+
</size>
312+
</property>
313+
<property name="font">
314+
<font>
315+
<family>Noto Color Emoji</family>
316+
</font>
317+
</property>
318+
<property name="toolTip">
319+
<string>Pick an emoji to insert</string>
320+
</property>
321+
<property name="text">
322+
<string>😀</string>
323+
</property>
324+
</widget>
325+
</item>
292326
<item>
293327
<widget class="QPushButton" name="btn_send">
294328
<property name="cursor">

qchat/gui/emojis/__init__.py

Whitespace-only changes.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# standard library
2+
import json
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
# PyQGIS
7+
from qgis.core import Qgis
8+
from qgis.PyQt import uic
9+
from qgis.PyQt.QtCore import pyqtSignal
10+
from qgis.PyQt.QtGui import QFont, QIcon
11+
from qgis.PyQt.QtWidgets import QDialog, QGridLayout, QPushButton, QScrollArea, QWidget
12+
13+
# plugin
14+
from qchat.__about__ import DIR_PLUGIN_ROOT, __icon_path__
15+
from qchat.toolbelt import PlgLogger, PlgOptionsManager
16+
17+
18+
class FullEmojiPicker(QDialog):
19+
"""Full emoji picker."""
20+
21+
emoji_selected = pyqtSignal(str)
22+
23+
def __init__(self, parent: Optional[QWidget] = None):
24+
super().__init__(parent)
25+
self.log = PlgLogger().log
26+
self.plg_settings = PlgOptionsManager()
27+
28+
# Load UI
29+
uic.loadUi(Path(__file__).parent / f"{Path(__file__).stem}.ui", self)
30+
self.setWindowIcon(QIcon(str(__icon_path__)))
31+
32+
# Load emoji data
33+
self.emoji_categories = self.load_emojis_from_json()
34+
35+
# Setup the picker
36+
self.setup_emoji_tabs()
37+
38+
# Connect search
39+
self.lne_search_box.textChanged.connect(self.filter_emojis)
40+
41+
def load_emojis_from_json(self):
42+
"""Load emoji categories from JSON file"""
43+
try:
44+
with DIR_PLUGIN_ROOT.joinpath("resources/emojis/selection.json").open(
45+
mode="r", encoding="utf-8"
46+
) as file:
47+
data = json.load(file)
48+
49+
# Convert JSON structure
50+
categories = {}
51+
json_categories = data.get("categories", {})
52+
53+
for category_id, category_data in json_categories.items():
54+
category_name = category_data.get("name", category_id.title())
55+
emojis_list = []
56+
57+
for emoji_data in category_data.get("emojis", []):
58+
emojis_list.append(emoji_data.get("emoji", ""))
59+
60+
if emojis_list:
61+
categories[category_name] = emojis_list
62+
63+
return categories
64+
65+
except (FileNotFoundError, json.JSONDecodeError) as err:
66+
self.log(
67+
message=self.tr("Error loading emojis: {}\nUsing defaults.").format(
68+
err
69+
),
70+
log_level=Qgis.MessageLevel.Warning,
71+
duration=3,
72+
push=True,
73+
)
74+
return self.get_default_emojis()
75+
76+
def get_default_emojis(self):
77+
"""Fallback emoji categories"""
78+
return {
79+
"Smileys": ["😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "😇"],
80+
"Gestures": ["👍", "👎", "👌", "✌️", "🤞", "👋", "👏", "🙌", "🙏", "💪"],
81+
}
82+
83+
def setup_emoji_tabs(self):
84+
"""Create tabs with emoji grids"""
85+
for category_name, emojis in self.emoji_categories.items():
86+
emojis_tab = self.create_emoji_tab(emojis)
87+
self.tabWidget.addTab(emojis_tab, category_name.title())
88+
89+
def create_emoji_tab(self, emojis):
90+
"""Create a tab with emoji buttons"""
91+
plg_settings = self.plg_settings.get_plg_settings()
92+
scroll_area = QScrollArea()
93+
scroll_widget = QWidget()
94+
grid_layout = QGridLayout(scroll_widget)
95+
96+
row, col = 0, 0
97+
for emoji in emojis:
98+
if not emoji:
99+
continue
100+
101+
btn = QPushButton(emoji)
102+
btn.setFixedSize(40, 40)
103+
btn.setFont(QFont(plg_settings.font_emoji_family, 14))
104+
btn.clicked.connect(lambda checked, e=emoji: self.emoji_clicked(e))
105+
btn.setToolTip(f"Insert {emoji}")
106+
107+
grid_layout.addWidget(btn, row, col)
108+
col += 1
109+
if col >= 10:
110+
col = 0
111+
row += 1
112+
113+
scroll_area.setWidget(scroll_widget)
114+
scroll_area.setWidgetResizable(True)
115+
return scroll_area
116+
117+
def emoji_clicked(self, emoji):
118+
"""Handle emoji selection"""
119+
self.emoji_selected.emit(emoji)
120+
self.close()
121+
122+
def filter_emojis(self, search_text):
123+
"""Basic emoji filtering"""
124+
if search_text:
125+
self.tabWidget.setCurrentIndex(0)

0 commit comments

Comments
 (0)