Commit 79716f42 authored by Philipp Hörist's avatar Philipp Hörist
Browse files

Rework emoticon menu

parent 0d067137
emoticonsdir = $(pkgdatadir)/data/emoticons
nobase_dist_emoticons_DATA = \
$(srcdir)/*/*.png \
$(srcdir)/*/*.gif \
$(srcdir)/*/emoticons.py
$(srcdir)/*/LICENSE \
$(srcdir)/*/emoticons_theme.py
MAINTAINERCLEANFILES = Makefile.in
......@@ -664,23 +664,17 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton" id="emoticons_button">
<object class="GtkMenuButton" id="emoticons_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="has_tooltip">True</property>
<property name="tooltip_markup" translatable="yes">Show a list of emoticons (Alt+M)</property>
<property name="tooltip_text" translatable="yes">Show a list of emoticons (Alt+M)</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="emoticons_button_image">
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="stock">gtk-missing-image</property>
<property name="icon_size">1</property>
<property name="icon_name">face-smile</property>
</object>
</child>
</object>
......
......@@ -224,20 +224,17 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton" id="emoticons_button">
<object class="GtkMenuButton" id="emoticons_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="has_tooltip">True</property>
<property name="tooltip_text" translatable="yes">Show a list of emoticons (Alt+M)</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="emoticons_button_image">
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-missing-image</property>
<property name="icon_size">1</property>
<property name="icon_name">face-smile</property>
</object>
</child>
</object>
......
......@@ -4,4 +4,11 @@
#ChatControl-AuthenticationButton { padding-top: 0px; padding-bottom: 0px}
/* VCardWindow */
.VCard-GtkLinkButton { padding-left: 5px; border-left: none; }
\ No newline at end of file
.VCard-GtkLinkButton { padding-left: 5px; border-left: none; }
popover#EmoticonPopover button { background: none; border: none; box-shadow:none; padding: 0px;}
popover#EmoticonPopover button > label { font-size: 24px; }
popover#EmoticonPopover flowboxchild > label { font-size: 24px; }
popover#EmoticonPopover notebook label { font-size: 24px; }
popover#EmoticonPopover flowbox { padding-left: 5px; padding-right: 6px; }
popover#EmoticonPopover flowboxchild { padding-top: 5px; padding-bottom: 5px; }
......@@ -102,6 +102,8 @@ def __init__(self, parent_win, contact, acct, session, resource=None):
self.handlers[id_] = self.actions_button
self._formattings_button = self.xml.get_object('formattings_button')
self.emoticons_button = self.xml.get_object('emoticons_button')
self.toggle_emoticons()
self._add_to_roster_button = self.xml.get_object(
'add_to_roster_button')
......@@ -325,8 +327,6 @@ def _update_toolbar(self):
if (gajim.connections[self.account].connected > 1 and not \
self.TYPE_ID == 'pm') or (self.contact.show != 'offline' and \
self.TYPE_ID == 'pm'):
emoticons_button = self.xml.get_object('emoticons_button')
emoticons_button.set_sensitive(True)
send_button = self.xml.get_object('send_button')
send_button.set_sensitive(True)
# Formatting
......@@ -1663,15 +1663,11 @@ def got_connected(self):
if contact:
self.contact = contact
self.draw_banner()
emoticons_button = self.xml.get_object('emoticons_button')
emoticons_button.set_sensitive(True)
send_button = self.xml.get_object('send_button')
send_button.set_sensitive(True)
def got_disconnected(self):
# Emoticons button
emoticons_button = self.xml.get_object('emoticons_button')
emoticons_button.set_sensitive(False)
send_button = self.xml.get_object('send_button')
send_button.set_sensitive(False)
# Add to roster
......
......@@ -43,6 +43,7 @@
import notify
import re
import emoticons
from common import events
from common import gajim
from common import helpers
......@@ -263,12 +264,6 @@ def __init__(self, type_id, parent_win, widget_name, contact, acct,
id_ = widget.connect('clicked', self._on_history_menuitem_activate)
self.handlers[id_] = widget
# when/if we do XHTML we will put formatting buttons back
widget = self.xml.get_object('emoticons_button')
widget.set_sensitive(False)
id_ = widget.connect('clicked', self.on_emoticons_button_clicked)
self.handlers[id_] = widget
# Create banner and connect signals
widget = self.xml.get_object('banner_eventbox')
id_ = widget.connect('button-press-event',
......@@ -364,13 +359,7 @@ def __init__(self, type_id, parent_win, widget_name, contact, acct,
self.received_history_pos = 0
self.orig_msg = None
# Emoticons menu
# set image no matter if user wants at this time emoticons or not
# (so toggle works ok)
img = self.xml.get_object('emoticons_button_image')
img.set_from_file(os.path.join(gajim.DATA_DIR, 'emoticons', 'static',
'smile.png'))
self.toggle_emoticons()
self.set_emoticon_popover()
# Attach speller
if gajim.config.get('use_speller') and HAS_GTK_SPELL:
......@@ -568,6 +557,7 @@ def _on_send_button_clicked(self, widget):
When send button is pressed: send the current message
"""
message_buffer = self.msg_textview.get_buffer()
emoticons.replace_with_codepoint(message_buffer)
start_iter = message_buffer.get_start_iter()
end_iter = message_buffer.get_end_iter()
message = message_buffer.get_text(start_iter, end_iter, False)
......@@ -588,12 +578,6 @@ def _conv_textview_key_press_event(self, widget, event):
self.parent_win.notebook.event(event)
return True
def show_emoticons_menu(self):
if not gajim.config.get('emoticons_theme'):
return
gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
gajim.interface.emoticons_menu.popup(None, None, None, None, 1, 0)
def _on_message_textview_key_press_event(self, widget, event):
if event.keyval == Gdk.KEY_space:
self.space_pressed = True
......@@ -683,6 +667,7 @@ def _on_message_textview_key_press_event(self, widget, event):
event.keyval == Gdk.KEY_KP_Enter: # ENTER
message_textview = widget
message_buffer = message_textview.get_buffer()
emoticons.replace_with_codepoint(message_buffer)
start_iter, end_iter = message_buffer.get_bounds()
message = message_buffer.get_text(start_iter, end_iter, False)
xhtml = self.msg_textview.get_xhtml()
......@@ -1012,31 +997,26 @@ def print_conversation_line(self, text, kind, name, tim,
def toggle_emoticons(self):
"""
Hide show emoticons_button and make sure emoticons_menu is always there
when needed
Hide show emoticons_button
"""
emoticons_button = self.xml.get_object('emoticons_button')
if gajim.config.get('emoticons_theme'):
emoticons_button.show()
emoticons_button.set_no_show_all(False)
self.emoticons_button.set_no_show_all(False)
self.emoticons_button.show()
else:
emoticons_button.hide()
emoticons_button.set_no_show_all(True)
def append_emoticon(self, str_):
buffer_ = self.msg_textview.get_buffer()
if buffer_.get_char_count():
buffer_.insert_at_cursor(' %s ' % str_)
else: # we are the beginning of buffer
buffer_.insert_at_cursor('%s ' % str_)
self.msg_textview.grab_focus()
def on_emoticons_button_clicked(self, widget):
"""
Popup emoticons menu
"""
gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
gajim.interface.popup_emoticons_under_button(widget, self.parent_win)
self.emoticons_button.set_no_show_all(True)
self.emoticons_button.hide()
def set_emoticon_popover(self):
if not gajim.config.get('emoticons_theme'):
return
if not self.parent_win:
return
popover = emoticons.get_popover()
popover.set_callbacks(self.msg_textview)
emoticons_button = self.xml.get_object('emoticons_button')
emoticons_button.set_popover(popover)
def on_color_menuitem_activate(self, widget):
color_dialog = Gtk.ColorChooserDialog(None, self.parent_win.window)
......@@ -1139,6 +1119,7 @@ def on_minimize_menuitem_toggled(self, widget):
def set_control_active(self, state):
if state:
self.set_emoticon_popover()
jid = self.contact.jid
if self.was_at_the_end:
# we are at the end
......
......@@ -657,7 +657,7 @@ def on_emoticons_combobox_changed(self, widget):
else:
gajim.config.set('emoticons_theme', emot_theme)
gajim.interface.init_emoticons(need_reload = True)
gajim.interface.init_emoticons()
gajim.interface.make_regexps()
self.toggle_emoticons()
......
......@@ -48,6 +48,7 @@
from common import i18n
from calendar import timegm
from common.fuzzyclock import FuzzyClock
import emoticons
from htmltextview import HtmlTextView
from common.exceptions import GajimGeneralException
......@@ -952,22 +953,17 @@ def print_special_text(self, special_text, other_tags, graphics=True,
end_iter = iter_
else:
end_iter = buffer_.get_end_iter()
if gajim.config.get('emoticons_theme') and \
possible_emot_ascii_caps in gajim.interface.emoticons.keys() and graphics:
# it's an emoticon
emot_ascii = possible_emot_ascii_caps
anchor = buffer_.create_child_anchor(end_iter)
img = TextViewImage(anchor,
GLib.markup_escape_text(special_text))
animations = gajim.interface.emoticons_animations
if not emot_ascii in animations:
animations[emot_ascii] = GdkPixbuf.PixbufAnimation.new_from_file(
gajim.interface.emoticons[emot_ascii])
img.set_from_animation(animations[emot_ascii])
img.show()
self.images.append(img)
# add with possible animation
self.tv.add_child_at_anchor(img, anchor)
if gajim.config.get('emoticons_theme') and graphics:
pixbuf = emoticons.get_pixbuf(possible_emot_ascii_caps)
if pixbuf:
# it's an emoticon
anchor = buffer_.create_child_anchor(end_iter)
img = TextViewImage(anchor,
GLib.markup_escape_text(special_text))
img.set_from_pixbuf(pixbuf)
img.show()
self.images.append(img)
self.tv.add_child_at_anchor(img, anchor)
elif special_text.startswith('www.') or \
special_text.startswith('ftp.') or \
text_is_valid_uri and not is_xhtml_link:
......
# -*- coding:utf-8 -*-
#
# Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
import importlib.util as imp
from collections import OrderedDict
from gi.repository import GdkPixbuf, Gtk, GLib
MODIFIER_MAX_CHILDREN_PER_LINE = 6
MAX_CHILDREN_PER_LINE = 10
MIN_HEIGHT = 200
pixbufs = dict()
codepoints = dict()
popover_instance = None
log = logging.getLogger('gajim.emoticons')
class SubPixbuf:
height = 24
width = 24
columns = 20
def __init__(self, path):
self.cur_column = 0
self.src_x = 0
self.src_y = 0
self.atlas = GdkPixbuf.Pixbuf.new_from_file(path)
def get_pixbuf(self):
self.src_x = self.cur_column * self.width
subpixbuf = self.atlas.new_subpixbuf(self.src_x, self.src_y, self.width, self.height)
if self.cur_column == self.columns - 1:
self.src_y += self.width
self.cur_column = 0
else:
self.cur_column += 1
return subpixbuf
def load(path):
theme_path = os.path.join(path, 'emoticons_theme.py')
spec = imp.spec_from_file_location("emoticons_theme.py", theme_path)
try:
theme = imp.module_from_spec(spec)
spec.loader.exec_module(theme)
except FileNotFoundError:
log.exception('Emoticons theme not found')
return
if not theme.use_image:
# Use Font to display emoticons
set_popover(theme.emoticons, False)
return True
try:
sub = SubPixbuf(os.path.join(path, 'emoticons.png'))
except GLib.GError:
log.exception('Error while creating subpixbuf')
return False
def add_emoticon(codepoint_, sub, mod_list=None):
pix = sub.get_pixbuf()
for alternate in codepoint_:
codepoints[alternate.upper()] = pix
if pix not in pixbufs:
pixbufs[pix] = alternate.upper()
if mod_list is not None:
mod_list.append(pix)
else:
pixbuf_list.append(pix)
popover_dict = OrderedDict()
try:
for category in theme.emoticons:
if not theme.emoticons[category]:
# Empty category
continue
pixbuf_list = []
for filename, codepoint_ in theme.emoticons[category]:
if codepoint_ is None:
# Category image
pixbuf_list.append(sub.get_pixbuf())
continue
if not filename:
# We have an emoticon with a modifier
mod_list = []
for _, mod_codepoint in codepoint_:
add_emoticon(mod_codepoint, sub, mod_list)
pixbuf_list.append(mod_list)
else:
add_emoticon(codepoint_, sub)
popover_dict[category] = pixbuf_list
except Exception:
log.exception('Error while loading emoticon theme')
return
set_popover(popover_dict, True)
return True
def set_popover(popover_dict, use_image):
global popover_instance
popover_instance = EmoticonPopover(popover_dict, use_image)
def get_popover():
return popover_instance
def get_pixbuf(codepoint_):
try:
return codepoints[codepoint_]
except KeyError:
return None
def get_codepoint(pixbuf_):
try:
return pixbufs[pixbuf_]
except KeyError:
return None
def replace_with_codepoint(buffer_):
if not pixbufs:
# We use font emoticons
return
iter_ = buffer_.get_start_iter()
pix = iter_.get_pixbuf()
def replace(pix):
if pix:
emote = get_codepoint(pix)
if not emote:
return
iter_2 = iter_.copy()
iter_2.forward_char()
buffer_.delete(iter_, iter_2)
buffer_.insert(iter_, emote)
replace(pix)
while iter_.forward_char():
pix = iter_.get_pixbuf()
replace(pix)
class EmoticonPopover(Gtk.Popover):
def __init__(self, emoji_dict, use_image):
super().__init__()
self.set_name('EmoticonPopover')
self.text_widget = None
self.use_image = use_image
notebook = Gtk.Notebook()
self.add(notebook)
self.handler_id = self.connect('key_press_event', self.on_key_press)
for category in emoji_dict:
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_min_content_height(MIN_HEIGHT)
flowbox = Gtk.FlowBox()
flowbox.set_max_children_per_line(MAX_CHILDREN_PER_LINE)
flowbox.connect('child_activated', self.on_emoticon_press)
scrolled_window.add(flowbox)
# Use first entry as a label for the notebook page
if self.use_image:
cat_image = Gtk.Image()
cat_image.set_from_pixbuf(emoji_dict[category][0])
notebook.append_page(scrolled_window, cat_image)
else:
notebook.append_page(scrolled_window, Gtk.Label(label=emoji_dict[category][0]))
# Populate the category with emojis
for pix in emoji_dict[category][1:]:
if isinstance(pix, list):
widget = self.add_emoticon_modifier(pix)
else:
if self.use_image:
widget = Gtk.Image()
widget.set_from_pixbuf(pix)
else:
widget = Gtk.Label(pix)
flowbox.add(widget)
notebook.show_all()
def add_emoticon_modifier(self, pixbuf_list):
button = Gtk.MenuButton()
button.set_relief(Gtk.ReliefStyle.NONE)
if self.use_image:
# We use the first item of the list as image for the button
button.get_child().set_from_pixbuf(pixbuf_list[0])
else:
button.remove(button.get_child())
label = Gtk.Label(pixbuf_list[0])
button.add(label)
button.connect('button-press-event', self.on_modifier_press)
popover = Gtk.Popover()
popover.set_name('EmoticonPopover')
popover.connect('key_press_event', self.on_key_press)
flowbox = Gtk.FlowBox()
flowbox.set_size_request(200, -1)
flowbox.set_max_children_per_line(MODIFIER_MAX_CHILDREN_PER_LINE)
flowbox.connect('child_activated', self.on_emoticon_press)
popover.add(flowbox)
for pix in pixbuf_list[1:]:
if self.use_image:
widget = Gtk.Image()
widget.set_from_pixbuf(pix)
else:
widget = Gtk.Label(pix)
flowbox.add(widget)
flowbox.show_all()
button.set_popover(popover)
return button
def set_callbacks(self, widget):
self.text_widget = widget
# Because the handlers getting disconnected when on_destroy() is called
# we connect them again
if self.handler_id:
self.disconnect(self.handler_id)
self.handler_id = self.connect('key_press_event', self.on_key_press)
def on_key_press(self, widget, event):
self.text_widget.grab_focus()
def on_modifier_press(self, button, event):
if event.button == 3:
button.get_popover().show()
button.get_popover().get_child().unselect_all()
if event.button == 1:
button.get_parent().emit('activate')
if self.use_image:
self.append_emoticon(button.get_child().get_pixbuf())
else:
self.append_emoticon(button.get_child().get_text())
return True
def on_emoticon_press(self, flowbox, child):
GLib.timeout_add(100, flowbox.unselect_all)
if isinstance(child.get_child(), Gtk.MenuButton):
return
if self.use_image:
self.append_emoticon(child.get_child().get_pixbuf())
else:
self.append_emoticon(child.get_child().get_text())
def append_emoticon(self, pix):
buffer_ = self.text_widget.get_buffer()
if buffer_.get_char_count():
buffer_.insert_at_cursor(' ')
insert_mark = buffer_.get_insert()
insert_iter = buffer_.get_iter_at_mark(insert_mark)
if self.use_image:
buffer_.insert_pixbuf(insert_iter, pix)
else:
buffer_.insert(insert_iter, pix)
buffer_.insert_at_cursor(' ')
else: # we are the beginning of buffer
insert_mark = buffer_.get_insert()
insert_iter = buffer_.get_iter_at_mark(insert_mark)
if self.use_image:
buffer_.insert_pixbuf(insert_iter, pix)
else:
buffer_.insert(insert_iter, pix)
buffer_.insert_at_cursor(' ')
def do_destroy(self):
# Remove the references we hold to other objects
self.text_widget = None
# Even though we dont destroy the Popover, handlers are getting
# still disconnected, which makes the handler_id invalid
# FIXME: find out how we can prevent handlers getting disconnected
self.handler_id = None
# Never destroy, creating a new EmoticonPopover is expensive
return True
......@@ -304,6 +304,9 @@ def __init__(self, parent_win, contact, acct, is_continued=False):
self.on_actions_button_clicked)
self.handlers[id_] = self.actions_button
self.emoticons_button = self.xml.get_object('emoticons_button')
self.toggle_emoticons()
widget = self.xml.get_object('change_nick_button')
widget.set_sensitive(False)
id_ = widget.connect('clicked', self._on_change_nick_menuitem_activate)
......@@ -1429,8 +1432,6 @@ def got_connected(self):
send_button = self.xml.get_object('send_button')
send_button.set_sensitive(True)
emoticons_button = self.xml.get_object('emoticons_button')
emoticons_button.set_sensitive(True)
formattings_button = self.xml.get_object('formattings_button')
formattings_button.set_sensitive(True)