Daniel Brötzmann pushed to branch message-selection at gajim / gajim
Commits: a3cd8882 by wurstsalat at 2022-10-30T15:02:41+01:00 fix: AvatarBox: Only react to clicks for GroupchatContacts
- - - - - 4351a5a6 by wurstsalat at 2022-10-30T15:52:04+01:00 feat: Add action to copy multiple messages
Fixes #10975
- - - - -
10 changed files:
- gajim/data/style/gajim.css - gajim/gtk/const.py - gajim/gtk/control.py - + gajim/gtk/conversation/message_selection.py - gajim/gtk/conversation/message_widget.py - gajim/gtk/conversation/plain_widget.py - gajim/gtk/conversation/rows/base.py - gajim/gtk/conversation/rows/message.py - gajim/gtk/conversation/rows/widgets.py - gajim/gtk/conversation/view.py
Changes:
===================================== gajim/data/style/gajim.css ===================================== @@ -686,6 +686,15 @@ #GroupchatConfig stacksidebar > scrolledwindow { } #GroupchatConfig treeview { padding: 5px; }
+.message-selection-box { + background-color: alpha(rgb(70, 70, 70), 0.9); + padding: 12px; + border-radius: 6px; +} +.message-selection-box > label { + color: rgb(230, 230, 230); +} + .groupchat-state box { background-color: alpha(rgb(70, 70, 70), 0.9); padding: 12px;
===================================== gajim/gtk/const.py ===================================== @@ -192,6 +192,7 @@ def __str__(self): ('input-strike', None, False), ('input-clear', None, False), ('show-emoji-chooser', None, False), + ('activate-message-selection', 'u', True), ('correct-message', None, False), ('quote', 's', False), ('mention', 's', False),
===================================== gajim/gtk/control.py ===================================== @@ -23,6 +23,7 @@ import logging import time
+from gi.repository import Gio from gi.repository import Gtk from gi.repository import GLib
@@ -48,8 +49,9 @@ from gajim.common.modules.contacts import GroupchatContact from gajim.common.modules.httpupload import HTTPFileTransfer from gajim.common.storage.archive import ConversationRow -from gajim.gtk.conversation.view import ConversationView
+from gajim.gui.conversation.message_selection import MessageSelection +from gajim.gui.conversation.view import ConversationView from gajim.gui.conversation.jump_to_end_button import JumpToEndButton from gajim.gui.builder import get_builder from gajim.gui.groupchat_roster import GroupchatRoster @@ -79,6 +81,11 @@ def __init__(self) -> None: self._groupchat_state = GroupchatState() self._ui.conv_view_overlay.add_overlay(self._groupchat_state)
+ self._message_selection = MessageSelection() + self._message_selection.connect('copy', self._on_copy_selection) + self._message_selection.connect('cancel', self._on_cancel_selection) + self._ui.conv_view_overlay.add_overlay(self._message_selection) + self._jump_to_end_button = JumpToEndButton() self._jump_to_end_button.connect('clicked', self._on_jump_to_end) self._ui.conv_view_overlay.add_overlay(self._jump_to_end_button) @@ -90,6 +97,9 @@ def __init__(self) -> None: # Used with encryption plugins self.sendmessage = False
+ app.window.get_action('activate-message-selection').connect( + 'activate', self._on_activate_message_selection) + self.widget = cast(Gtk.Box, self._ui.get_object('control_box')) self.widget.show_all()
@@ -156,6 +166,9 @@ def switch_contact(self, contact: Union[BareContact, self._groupchat_state.switch_contact(contact) self._roster.switch_contact(contact)
+ self._message_selection.set_no_show_all(True) + self._message_selection.hide() + self._register_events()
if isinstance(contact, GroupchatParticipant): @@ -452,6 +465,22 @@ def _on_autoscroll_changed(self, if app.window.is_chat_active(self.contact.account, self.contact.jid): app.window.mark_as_read(self.contact.account, self.contact.jid)
+ def _on_activate_message_selection(self, + _action: Gio.SimpleAction, + param: GLib.Variant + ) -> None: + + log_line_id = param.get_uint32() + self._scrolled_view.enable_message_selection(log_line_id) + self._message_selection.set_no_show_all(False) + self._message_selection.show_all() + + def _on_copy_selection(self, _widget: MessageSelection) -> None: + self._scrolled_view.copy_selected_messages() + + def _on_cancel_selection(self, _widget: MessageSelection) -> None: + self._scrolled_view.disable_message_selection() + def _on_jump_to_end(self, _button: Gtk.Button) -> None: self.reset_view()
===================================== gajim/gtk/conversation/message_selection.py ===================================== @@ -0,0 +1,58 @@ +# This file is part of Gajim. +# +# Gajim 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; version 3 only. +# +# Gajim 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 Gajim. If not, see http://www.gnu.org/licenses/. + +from __future__ import annotations + +from gi.repository import GObject +from gi.repository import Gtk + +from gajim.common.i18n import _ + + +class MessageSelection(Gtk.Grid): + + __gsignals__ = { + 'copy': (GObject.SignalFlags.RUN_LAST, None, ()), + 'cancel': (GObject.SignalFlags.RUN_LAST, None, ()), + } + + def __init__(self) -> None: + Gtk.Grid.__init__(self, row_spacing=18, column_spacing=6) + self.set_no_show_all(True) + self.set_halign(Gtk.Align.CENTER) + self.set_valign(Gtk.Align.END) + + self.get_style_context().add_class('message-selection-box') + + label = Gtk.Label(label=_('Click messages to select them')) + self.attach(label, 0, 0, 2, 1) + + copy_button = Gtk.Button(label=_('Copy Text')) + copy_button.get_style_context().add_class('suggested-action') + copy_button.connect('clicked', self._on_copy_clicked) + self.attach(copy_button, 0, 1, 1, 1) + + cancel_button = Gtk.Button(label=_('Cancel')) + cancel_button.connect('clicked', self._on_cancel_clicked) + self.attach(cancel_button, 1, 1, 1, 1) + + def _on_copy_clicked(self, _button: Gtk.Button) -> None: + self.set_no_show_all(True) + self.hide() + self.emit('copy') + + def _on_cancel_clicked(self, _button: Gtk.Button) -> None: + self.set_no_show_all(True) + self.hide() + self.emit('cancel')
===================================== gajim/gtk/conversation/message_widget.py ===================================== @@ -51,6 +51,11 @@ def get_text(self) -> str: return self._content.text return self._action_phrase_text
+ def set_selectable(self, selectable: bool) -> None: + for widget in self.get_children(): + if isinstance(widget, PlainWidget): + widget.set_selectable(selectable) + def add_with_styling(self, text: str, nickname: Optional[str] = None) -> None:
===================================== gajim/gtk/conversation/plain_widget.py ===================================== @@ -68,6 +68,10 @@ def __init__(self, account: str, selectable: bool) -> None: self._text_widget = MessageLabel(self._account, selectable) self.add(self._text_widget)
+ def set_selectable(self, selectable: bool) -> None: + if isinstance(self._text_widget, MessageLabel): + self._text_widget.set_selectable(selectable) + def add_content(self, block: PlainBlock) -> None: self._text_widget.print_text_with_styling(block)
===================================== gajim/gtk/conversation/rows/base.py ===================================== @@ -39,6 +39,7 @@ def __init__(self, account: str, widget: Optional[str] = None) -> None: self.text: str = '' self._merged: bool = False
+ self.set_selectable(False) self.set_can_focus(False) self.get_style_context().add_class('conversation-row')
===================================== gajim/gtk/conversation/rows/message.py ===================================== @@ -92,6 +92,8 @@ def __init__(self, self.additional_data = additional_data self.display_marking = display_marking
+ self.set_selectable(True) + self._account = account self._contact = contact
@@ -198,6 +200,14 @@ def __init__(self,
self.show_all()
+ def enable_selection_mode(self) -> None: + if isinstance(self._message_widget, MessageWidget): + self._message_widget.set_selectable(False) + + def disable_selection_mode(self) -> None: + if isinstance(self._message_widget, MessageWidget): + self._message_widget.set_selectable(True) + def _add_security_label(self, display_marking: Optional[Displaymarking] ) -> None: @@ -299,17 +309,20 @@ def is_mergeable(self, message: MessageRow) -> bool: return False return abs(message.timestamp - self.timestamp) < MERGE_TIMEFRAME
+ def get_text(self) -> str: + return self._message_widget.get_text() + def on_copy_message(self, _widget: Gtk.Widget) -> None: time_format = from_one_line(app.settings.get('chat_timestamp_format')) timestamp_formatted = self.timestamp.strftime(time_format)
clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - text = self._message_widget.get_text() + text = self.get_text() clip.set_text(f'{timestamp_formatted} - {self.name}: {text}', -1)
def on_quote_message(self, _widget: Gtk.Widget) -> None: app.window.activate_action( - 'quote', GLib.Variant('s', self._message_widget.get_text())) + 'quote', GLib.Variant('s', self.get_text()))
def on_correct_message(self, _widget: Gtk.Widget) -> None: app.window.activate_action('correct-message', None)
===================================== gajim/gtk/conversation/rows/widgets.py ===================================== @@ -79,7 +79,7 @@ def _on_click(self, _button: Gtk.Button) -> None: show_retract = False if isinstance(self._contact, GroupchatContact): if not self._contact.is_joined: - self._create_popover(show_retract=False) + self._create_popover() return
contact = self._contact.get_resource(self._name) @@ -99,13 +99,14 @@ def _on_click(self, _button: Gtk.Button) -> None: show_correction = app.window.is_message_correctable( self._contact, self._row.message_id)
- self._create_popover(show_retract=show_retract, - show_correction=show_correction) + self._create_popover(show_retract, + show_correction)
def _create_popover(self, show_retract: bool = False, show_correction: bool = False ) -> None: + menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) menu_box.get_style_context().add_class('padding-6')
@@ -135,6 +136,14 @@ def _create_popover(self, 'edit-copy-symbolic', Gtk.IconSize.MENU)) menu_box.add(copy_button)
+ select_button = Gtk.ModelButton() + select_button.set_halign(Gtk.Align.START) + select_button.connect('clicked', self._on_activate_message_selection) + select_button.set_label(Q_('?Message row action:Select Messages…')) + select_button.set_image(Gtk.Image.new_from_icon_name( + 'edit-select-symbolic', Gtk.IconSize.MENU)) + menu_box.add(select_button) + if show_correction: correct_button = Gtk.ModelButton() correct_button.set_halign(Gtk.Align.START) @@ -164,6 +173,11 @@ def _create_popover(self, popover.connect('closed', self._on_closed) popover.popup()
+ def _on_activate_message_selection(self, _button: Gtk.ModelButton) -> None: + app.window.activate_action( + 'activate-message-selection', + GLib.Variant('u', self._row.log_line_id)) + @staticmethod def _on_closed(popover: Gtk.Popover) -> None: GLib.idle_add(popover.destroy) @@ -293,6 +307,9 @@ def _on_avatar_clicked(self, ) -> int:
if event.type == Gdk.EventType.BUTTON_PRESS: + if not isinstance(self._contact, GroupchatContact): + return Gdk.EVENT_STOP + if event.button == 1: app.window.activate_action('mention', GLib.Variant('s', name)) elif event.button == 3:
===================================== gajim/gtk/conversation/view.py ===================================== @@ -27,6 +27,7 @@ from datetime import datetime from datetime import timedelta
+from gi.repository import Gdk from gi.repository import Gtk from gi.repository import GObject from gi.repository import Gio @@ -41,6 +42,7 @@ from gajim.common import types from gajim.common import events from gajim.common.helpers import AdditionalDataDict +from gajim.common.helpers import from_one_line from gajim.common.helpers import to_user_string from gajim.common.helpers import get_start_of_day from gajim.common.modules.contacts import GroupchatContact @@ -141,6 +143,42 @@ def __init__(self) -> None: app.window.get_action('scroll-view-down').connect( 'activate', self._on_scroll_view)
+ def copy_selected_messages(self) -> None: + time_format = from_one_line(app.settings.get('chat_timestamp_format')) + selection_text = '' + for row in cast(list[BaseRow], self._list_box.get_selected_rows()): + if isinstance(row, MessageRow): + timestamp_formatted = row.timestamp.strftime(time_format) + selection_text += (f'{timestamp_formatted} - {row.name}: ' + f'{row.get_text()}\n') + + clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clip.set_text(selection_text, -1) + + self.disable_message_selection() + + def enable_message_selection(self, + log_line_id: Optional[int] + ) -> None: + + self._list_box.set_selection_mode(Gtk.SelectionMode.MULTIPLE) + + if log_line_id is not None: + row = self.get_row_by_log_line_id(log_line_id) + self._list_box.select_row(row) + + for row in self.iter_rows(): + if isinstance(row, MessageRow): + row.enable_selection_mode() + + def disable_message_selection(self) -> None: + self._list_box.unselect_all() + self._list_box.set_selection_mode(Gtk.SelectionMode.NONE) + + for row in self.iter_rows(): + if isinstance(row, MessageRow): + row.disable_selection_mode() + def _on_scroll_view(self, action: Gio.SimpleAction, _param: Literal[None]) -> None: @@ -187,6 +225,8 @@ def switch_contact(self, contact: ChatContactT) -> None: self._block_signals = True self._reset()
+ self.disable_message_selection() + self._read_marker_row = ReadMarkerRow(self._contact) self._list_box.add(self._read_marker_row)
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/5a1c168d44b44ba4eec3ac0bed7a4da2...