# chat.py # # Copyright 2024 Lucas Fryzek # # 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 . # # SPDX-License-Identifier: GPL-3.0-or-later import gi gi.require_version("Spelling", "1") from gi.repository import Adw, Gtk, Gdk, Gio, GObject, GLib, Spelling, GtkSource from .message import WeegtkMessage from .color import Color from weegtk import config from weegtk import netfile import json import os GObject.type_register(GtkSource.View) @Gtk.Template(resource_path='/com/fryzekconcepts/weegtk/gtk/chat.ui') class WeegtkChat(Adw.Bin): __gtype_name__ = 'WeegtkChat' __gsignals__ = { "buffer_input" : ( GObject.SignalFlags.RUN_FIRST, None, (str, str) ) } window = Gtk.Template.Child() messages = Gtk.Template.Child() text_entry = Gtk.Template.Child() scroll_button_revealer = Gtk.Template.Child() def __init__(self, data=None, **kwargs): super().__init__(**kwargs) self.data = data or {} self.model = Gtk.StringList() self.select = Gtk.NoSelection(model=self.model) self.factory = Gtk.SignalListItemFactory() self.factory.connect("setup", self.setup_list_item) self.factory.connect("bind", self.bind_list_item) self.messages.set_model(self.select) self.messages.set_factory(self.factory) self.color = Color(config.color_options(), False) self.auto_scroll = True self.set_sticky(True) adj = self.window.get_vadjustment() adj.connect("value-changed", self.scroll_changes) adj.connect("notify::upper", self.upper_notify) # This is a bit of hack to have the raised color match in the gtk source editor manager = GtkSource.StyleSchemeManager().get_default() scheme = manager.get_scheme('CustomAdwaita') buffer = self.text_entry.get_buffer() buffer.set_style_scheme(scheme) sc_controller = Gtk.ShortcutController(propagation_phase=Gtk.PropagationPhase.CAPTURE) action = Gtk.CallbackAction.new(self.entry_callback, self) trigger = Gtk.ShortcutTrigger.parse_string("Return") shortcut = Gtk.Shortcut(action=action, trigger=trigger) sc_controller.add_shortcut(shortcut) self.add_controller(sc_controller) # Enable spell checking self.checker = Spelling.Checker.get_default() self.adapter = Spelling.TextBufferAdapter.new(self.text_entry.get_buffer(), self.checker) self.extra_menu = self.adapter.get_menu_model() self.text_entry.set_extra_menu(self.extra_menu) self.text_entry.insert_action_group("spelling", self.adapter) self.adapter.set_enabled(True) # TODO figure out why style is not being taken from ui file self.add_css_class("view") def is_at_bottom(self): adj = self.window.get_vadjustment() return (adj.get_value() + adj.get_page_size()) == adj.get_upper() def set_sticky(self, is_sticky): if not is_sticky: self.scroll_button_revealer.set_visible(True) self.scroll_button_revealer.set_reveal_child(not is_sticky) self.sticky = is_sticky def scroll_changes(self, *args): is_at_bottom = self.is_at_bottom() if self.auto_scroll: if is_at_bottom: self.auto_scroll = False self.set_sticky(True) else: self.scroll_bottom() else: self.set_sticky(is_at_bottom) def upper_notify(self, *args): if self.sticky: self.scroll_bottom() @Gtk.Template.Callback() def scroll_bottom(self, *args): n_items = self.model.get_n_items() self.auto_scroll = True if n_items > 0: self.messages.scroll_to(n_items - 1, Gtk.ListScrollFlags.FOCUS) self.window.emit("scroll_child", Gtk.ScrollType.END, False) def nicklist_add_item(self, parent, group, prefix, name, visible): # TODO start handling nicklist pass def nicklist_remove_item(self, parent, group, name): # TODO start handling nicklist pass def nicklist_update_item(self, parent, group, prefix, name, visible): # TODO start handling nicklist pass def nicklist_refresh(self): # TODO start handling nicklist pass def update_prompt(self): # TODO code copied from QWeechat, figure out what I should do with it pass def setup_list_item(self, factory, list_item, *user_data): message = WeegtkMessage() list_item.set_child(message) def bind_list_item(self, factory, list_item, *user_data): text = list_item.get_item().get_string() data = json.loads(text) message = list_item.get_child() message.set_contents(data) def is_chat(self): buf_type = self.data['local_variables']['type'] if 'type' in self.data['local_variables'] else None return buf_type is not None and (buf_type == "private" or buf_type == "channel") def pointer(self): """Return pointer on buffer.""" return self.data.get('__path', [''])[0] def parse_out_colors(self, text): clean_text = self.color.convert(text) return clean_text def display(self, time, prefix, text, forcecolor=None): # TODO using JSON here seems a bit ineffcient # See if its possible to make a model that uses # our own custom struct user = self.parse_out_colors(prefix) msg = self.parse_out_colors(text) count = self.model.get_n_items() msg_type = "message" # TODO figure out if there is a way to check if a message is from the # the system instead of from a user if (len(user) == 0 or user[0] == "=" or user[0] == "-" or user[0] == "[" or user[0] == "<"): msg = "{}{}".format(user, msg) user = "System" msg_type = "system" if count != 0: last = self.model.get_string(count - 1) last_data = json.loads(last) if last_data["username"] == user: if msg_type == "system": msg_type = "system_append" else: msg_type = "message_append" data = { "username": user, "text": [msg], "type": msg_type } self.model.append(json.dumps(data)) def clear(self): # TODO clear chat buffer pass @Gtk.Template.Callback() def entry_activate(self, *args): entry_buffer = self.text_entry.get_buffer() text = entry_buffer.get_text(entry_buffer.get_start_iter(), entry_buffer.get_end_iter(), False) entry_buffer.set_text("", 0) self.emit("buffer_input", self.data['full_name'], text) def entry_callback(self, *args): self.entry_activate() return True def open_file_dialog(self, dialog, result, caller): try: file = dialog.open_finish(result) except GLib.GError: # gtk-dialog-error-quark: Dismissed by user pass else: conf = config.read() file_url = netfile.upload(file.get_path(), conf["upload"]["url"]) entry_buffer = self.text_entry.get_buffer() entry_buffer.insert_at_cursor(file_url, -1) @Gtk.Template.Callback() def attach_file(self, *args): dialog = Gtk.FileDialog() dialog.set_title("Select file to upload") dialog.open(self.get_root(), None, self.open_file_dialog, self)