Daniel Brötzmann pushed to branch message-selection at gajim / gajim
Commits:
-
cdb976dc
by wurstsalat at 2022-10-30T14:27:07+01:00
7 changed files:
- gajim/data/style/gajim.css
- gajim/gtk/const.py
- gajim/gtk/control.py
- + gajim/gtk/conversation/message_selection.py
- gajim/gtk/conversation/rows/message.py
- gajim/gtk/conversation/rows/widgets.py
- gajim/gtk/conversation/view.py
Changes:
| ... | ... | @@ -686,6 +686,15 @@ #GroupchatConfig stacksidebar > scrolledwindow { |
| 686 | 686 | }
|
| 687 | 687 | #GroupchatConfig treeview { padding: 5px; }
|
| 688 | 688 | |
| 689 | +.message-selection-box {
|
|
| 690 | + background-color: alpha(rgb(70, 70, 70), 0.9);
|
|
| 691 | + padding: 12px;
|
|
| 692 | + border-radius: 6px;
|
|
| 693 | +}
|
|
| 694 | +.message-selection-box > label {
|
|
| 695 | + color: rgb(230, 230, 230);
|
|
| 696 | +}
|
|
| 697 | + |
|
| 689 | 698 | .groupchat-state box {
|
| 690 | 699 | background-color: alpha(rgb(70, 70, 70), 0.9);
|
| 691 | 700 | padding: 12px;
|
| ... | ... | @@ -192,6 +192,7 @@ def __str__(self): |
| 192 | 192 | ('input-strike', None, False),
|
| 193 | 193 | ('input-clear', None, False),
|
| 194 | 194 | ('show-emoji-chooser', None, False),
|
| 195 | + ('activate-message-selection', 'u', True),
|
|
| 195 | 196 | ('correct-message', None, False),
|
| 196 | 197 | ('quote', 's', False),
|
| 197 | 198 | ('mention', 's', False),
|
| ... | ... | @@ -23,6 +23,7 @@ |
| 23 | 23 | import logging
|
| 24 | 24 | import time
|
| 25 | 25 | |
| 26 | +from gi.repository import Gio
|
|
| 26 | 27 | from gi.repository import Gtk
|
| 27 | 28 | from gi.repository import GLib
|
| 28 | 29 | |
| ... | ... | @@ -48,8 +49,9 @@ |
| 48 | 49 | from gajim.common.modules.contacts import GroupchatContact
|
| 49 | 50 | from gajim.common.modules.httpupload import HTTPFileTransfer
|
| 50 | 51 | from gajim.common.storage.archive import ConversationRow
|
| 51 | -from gajim.gtk.conversation.view import ConversationView
|
|
| 52 | 52 | |
| 53 | +from gajim.gui.conversation.message_selection import MessageSelection
|
|
| 54 | +from gajim.gui.conversation.view import ConversationView
|
|
| 53 | 55 | from gajim.gui.conversation.jump_to_end_button import JumpToEndButton
|
| 54 | 56 | from gajim.gui.builder import get_builder
|
| 55 | 57 | from gajim.gui.groupchat_roster import GroupchatRoster
|
| ... | ... | @@ -79,6 +81,11 @@ def __init__(self) -> None: |
| 79 | 81 | self._groupchat_state = GroupchatState()
|
| 80 | 82 | self._ui.conv_view_overlay.add_overlay(self._groupchat_state)
|
| 81 | 83 | |
| 84 | + self._message_selection = MessageSelection()
|
|
| 85 | + self._message_selection.connect('copy', self._on_copy_selection)
|
|
| 86 | + self._message_selection.connect('cancel', self._on_cancel_selection)
|
|
| 87 | + self._ui.conv_view_overlay.add_overlay(self._message_selection)
|
|
| 88 | + |
|
| 82 | 89 | self._jump_to_end_button = JumpToEndButton()
|
| 83 | 90 | self._jump_to_end_button.connect('clicked', self._on_jump_to_end)
|
| 84 | 91 | self._ui.conv_view_overlay.add_overlay(self._jump_to_end_button)
|
| ... | ... | @@ -90,6 +97,9 @@ def __init__(self) -> None: |
| 90 | 97 | # Used with encryption plugins
|
| 91 | 98 | self.sendmessage = False
|
| 92 | 99 | |
| 100 | + app.window.get_action('activate-message-selection').connect(
|
|
| 101 | + 'activate', self._on_activate_message_selection)
|
|
| 102 | + |
|
| 93 | 103 | self.widget = cast(Gtk.Box, self._ui.get_object('control_box'))
|
| 94 | 104 | self.widget.show_all()
|
| 95 | 105 | |
| ... | ... | @@ -452,6 +462,22 @@ def _on_autoscroll_changed(self, |
| 452 | 462 | if app.window.is_chat_active(self.contact.account, self.contact.jid):
|
| 453 | 463 | app.window.mark_as_read(self.contact.account, self.contact.jid)
|
| 454 | 464 | |
| 465 | + def _on_activate_message_selection(self,
|
|
| 466 | + _action: Gio.SimpleAction,
|
|
| 467 | + param: GLib.Variant
|
|
| 468 | + ) -> None:
|
|
| 469 | + |
|
| 470 | + log_line_id = param.get_uint32()
|
|
| 471 | + self._scrolled_view.enable_message_selection(log_line_id)
|
|
| 472 | + self._message_selection.set_no_show_all(False)
|
|
| 473 | + self._message_selection.show_all()
|
|
| 474 | + |
|
| 475 | + def _on_copy_selection(self, _widget: MessageSelection) -> None:
|
|
| 476 | + self._scrolled_view.copy_selected_messages()
|
|
| 477 | + |
|
| 478 | + def _on_cancel_selection(self, _widget: MessageSelection) -> None:
|
|
| 479 | + self._scrolled_view.disable_message_selection()
|
|
| 480 | + |
|
| 455 | 481 | def _on_jump_to_end(self, _button: Gtk.Button) -> None:
|
| 456 | 482 | self.reset_view()
|
| 457 | 483 |
| 1 | +# This file is part of Gajim.
|
|
| 2 | +#
|
|
| 3 | +# Gajim is free software; you can redistribute it and/or modify
|
|
| 4 | +# it under the terms of the GNU General Public License as published
|
|
| 5 | +# by the Free Software Foundation; version 3 only.
|
|
| 6 | +#
|
|
| 7 | +# Gajim is distributed in the hope that it will be useful,
|
|
| 8 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 9 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 10 | +# GNU General Public License for more details.
|
|
| 11 | +#
|
|
| 12 | +# You should have received a copy of the GNU General Public License
|
|
| 13 | +# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
| 14 | + |
|
| 15 | +from __future__ import annotations
|
|
| 16 | + |
|
| 17 | +from gi.repository import GObject
|
|
| 18 | +from gi.repository import Gtk
|
|
| 19 | + |
|
| 20 | +from gajim.common.i18n import _
|
|
| 21 | + |
|
| 22 | + |
|
| 23 | +class MessageSelection(Gtk.Grid):
|
|
| 24 | + |
|
| 25 | + __gsignals__ = {
|
|
| 26 | + 'copy': (GObject.SignalFlags.RUN_LAST, None, ()),
|
|
| 27 | + 'cancel': (GObject.SignalFlags.RUN_LAST, None, ()),
|
|
| 28 | + }
|
|
| 29 | + |
|
| 30 | + def __init__(self) -> None:
|
|
| 31 | + Gtk.Grid.__init__(self, row_spacing=18, column_spacing=6)
|
|
| 32 | + self.set_no_show_all(True)
|
|
| 33 | + self.set_halign(Gtk.Align.CENTER)
|
|
| 34 | + self.set_valign(Gtk.Align.END)
|
|
| 35 | + |
|
| 36 | + self.get_style_context().add_class('message-selection-box')
|
|
| 37 | + |
|
| 38 | + label = Gtk.Label(label=_('Click messages to select them'))
|
|
| 39 | + self.attach(label, 0, 0, 2, 1)
|
|
| 40 | + |
|
| 41 | + copy_button = Gtk.Button(label=_('Copy Text'))
|
|
| 42 | + copy_button.get_style_context().add_class('suggested-action')
|
|
| 43 | + copy_button.connect('clicked', self._on_copy_clicked)
|
|
| 44 | + self.attach(copy_button, 0, 1, 1, 1)
|
|
| 45 | + |
|
| 46 | + cancel_button = Gtk.Button(label=_('Cancel'))
|
|
| 47 | + cancel_button.connect('clicked', self._on_cancel_clicked)
|
|
| 48 | + self.attach(cancel_button, 1, 1, 1, 1)
|
|
| 49 | + |
|
| 50 | + def _on_copy_clicked(self, _button: Gtk.Button) -> None:
|
|
| 51 | + self.set_no_show_all(True)
|
|
| 52 | + self.hide()
|
|
| 53 | + self.emit('copy')
|
|
| 54 | + |
|
| 55 | + def _on_cancel_clicked(self, _button: Gtk.Button) -> None:
|
|
| 56 | + self.set_no_show_all(True)
|
|
| 57 | + self.hide()
|
|
| 58 | + self.emit('cancel') |
| ... | ... | @@ -299,17 +299,20 @@ def is_mergeable(self, message: MessageRow) -> bool: |
| 299 | 299 | return False
|
| 300 | 300 | return abs(message.timestamp - self.timestamp) < MERGE_TIMEFRAME
|
| 301 | 301 | |
| 302 | + def get_text(self) -> str:
|
|
| 303 | + return self._message_widget.get_text()
|
|
| 304 | + |
|
| 302 | 305 | def on_copy_message(self, _widget: Gtk.Widget) -> None:
|
| 303 | 306 | time_format = from_one_line(app.settings.get('chat_timestamp_format'))
|
| 304 | 307 | timestamp_formatted = self.timestamp.strftime(time_format)
|
| 305 | 308 | |
| 306 | 309 | clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
| 307 | - text = self._message_widget.get_text()
|
|
| 310 | + text = self.get_text()
|
|
| 308 | 311 | clip.set_text(f'{timestamp_formatted} - {self.name}: {text}', -1)
|
| 309 | 312 | |
| 310 | 313 | def on_quote_message(self, _widget: Gtk.Widget) -> None:
|
| 311 | 314 | app.window.activate_action(
|
| 312 | - 'quote', GLib.Variant('s', self._message_widget.get_text()))
|
|
| 315 | + 'quote', GLib.Variant('s', self.get_text()))
|
|
| 313 | 316 | |
| 314 | 317 | def on_correct_message(self, _widget: Gtk.Widget) -> None:
|
| 315 | 318 | app.window.activate_action('correct-message', None)
|
| ... | ... | @@ -79,7 +79,7 @@ def _on_click(self, _button: Gtk.Button) -> None: |
| 79 | 79 | show_retract = False
|
| 80 | 80 | if isinstance(self._contact, GroupchatContact):
|
| 81 | 81 | if not self._contact.is_joined:
|
| 82 | - self._create_popover(show_retract=False)
|
|
| 82 | + self._create_popover()
|
|
| 83 | 83 | return
|
| 84 | 84 | |
| 85 | 85 | contact = self._contact.get_resource(self._name)
|
| ... | ... | @@ -99,13 +99,14 @@ def _on_click(self, _button: Gtk.Button) -> None: |
| 99 | 99 | show_correction = app.window.is_message_correctable(
|
| 100 | 100 | self._contact, self._row.message_id)
|
| 101 | 101 | |
| 102 | - self._create_popover(show_retract=show_retract,
|
|
| 103 | - show_correction=show_correction)
|
|
| 102 | + self._create_popover(show_retract,
|
|
| 103 | + show_correction)
|
|
| 104 | 104 | |
| 105 | 105 | def _create_popover(self,
|
| 106 | 106 | show_retract: bool = False,
|
| 107 | 107 | show_correction: bool = False
|
| 108 | 108 | ) -> None:
|
| 109 | + |
|
| 109 | 110 | menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
| 110 | 111 | menu_box.get_style_context().add_class('padding-6')
|
| 111 | 112 | |
| ... | ... | @@ -135,6 +136,14 @@ def _create_popover(self, |
| 135 | 136 | 'edit-copy-symbolic', Gtk.IconSize.MENU))
|
| 136 | 137 | menu_box.add(copy_button)
|
| 137 | 138 | |
| 139 | + select_button = Gtk.ModelButton()
|
|
| 140 | + select_button.set_halign(Gtk.Align.START)
|
|
| 141 | + select_button.connect('clicked', self._on_activate_message_selection)
|
|
| 142 | + select_button.set_label(Q_('?Message row action:Select Messages…'))
|
|
| 143 | + select_button.set_image(Gtk.Image.new_from_icon_name(
|
|
| 144 | + 'edit-select-symbolic', Gtk.IconSize.MENU))
|
|
| 145 | + menu_box.add(select_button)
|
|
| 146 | + |
|
| 138 | 147 | if show_correction:
|
| 139 | 148 | correct_button = Gtk.ModelButton()
|
| 140 | 149 | correct_button.set_halign(Gtk.Align.START)
|
| ... | ... | @@ -164,6 +173,11 @@ def _create_popover(self, |
| 164 | 173 | popover.connect('closed', self._on_closed)
|
| 165 | 174 | popover.popup()
|
| 166 | 175 | |
| 176 | + def _on_activate_message_selection(self, _button: Gtk.ModelButton) -> None:
|
|
| 177 | + app.window.activate_action(
|
|
| 178 | + 'activate-message-selection',
|
|
| 179 | + GLib.Variant('u', self._row.log_line_id))
|
|
| 180 | + |
|
| 167 | 181 | @staticmethod
|
| 168 | 182 | def _on_closed(popover: Gtk.Popover) -> None:
|
| 169 | 183 | GLib.idle_add(popover.destroy)
|
| ... | ... | @@ -27,6 +27,7 @@ |
| 27 | 27 | from datetime import datetime
|
| 28 | 28 | from datetime import timedelta
|
| 29 | 29 | |
| 30 | +from gi.repository import Gdk
|
|
| 30 | 31 | from gi.repository import Gtk
|
| 31 | 32 | from gi.repository import GObject
|
| 32 | 33 | from gi.repository import Gio
|
| ... | ... | @@ -41,6 +42,7 @@ |
| 41 | 42 | from gajim.common import types
|
| 42 | 43 | from gajim.common import events
|
| 43 | 44 | from gajim.common.helpers import AdditionalDataDict
|
| 45 | +from gajim.common.helpers import from_one_line
|
|
| 44 | 46 | from gajim.common.helpers import to_user_string
|
| 45 | 47 | from gajim.common.helpers import get_start_of_day
|
| 46 | 48 | from gajim.common.modules.contacts import GroupchatContact
|
| ... | ... | @@ -141,6 +143,33 @@ def __init__(self) -> None: |
| 141 | 143 | app.window.get_action('scroll-view-down').connect(
|
| 142 | 144 | 'activate', self._on_scroll_view)
|
| 143 | 145 | |
| 146 | + def copy_selected_messages(self) -> None:
|
|
| 147 | + time_format = from_one_line(app.settings.get('chat_timestamp_format'))
|
|
| 148 | + selection_text = ''
|
|
| 149 | + for row in cast(list[BaseRow], self._list_box.get_selected_rows()):
|
|
| 150 | + if isinstance(row, MessageRow):
|
|
| 151 | + timestamp_formatted = row.timestamp.strftime(time_format)
|
|
| 152 | + selection_text += (f'{timestamp_formatted} - {row.name}: '
|
|
| 153 | + f'{row.get_text()}\n')
|
|
| 154 | + |
|
| 155 | + clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
|
| 156 | + clip.set_text(selection_text, -1)
|
|
| 157 | + |
|
| 158 | + self.disable_message_selection()
|
|
| 159 | + |
|
| 160 | + def enable_message_selection(self,
|
|
| 161 | + log_line_id: Optional[int]
|
|
| 162 | + ) -> None:
|
|
| 163 | + |
|
| 164 | + self._list_box.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
|
|
| 165 | + if log_line_id is not None:
|
|
| 166 | + row = self.get_row_by_log_line_id(log_line_id)
|
|
| 167 | + self._list_box.select_row(row)
|
|
| 168 | + |
|
| 169 | + def disable_message_selection(self) -> None:
|
|
| 170 | + self._list_box.unselect_all()
|
|
| 171 | + self._list_box.set_selection_mode(Gtk.SelectionMode.NONE)
|
|
| 172 | + |
|
| 144 | 173 | def _on_scroll_view(self,
|
| 145 | 174 | action: Gio.SimpleAction,
|
| 146 | 175 | _param: Literal[None]) -> None:
|