Daniel Brötzmann pushed to branch message-selection at gajim / gajim

Commits:

7 changed files:

Changes:

  • gajim/data/style/gajim.css
    ... ... @@ -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;
    

  • gajim/gtk/const.py
    ... ... @@ -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),
    

  • gajim/gtk/control.py
    ... ... @@ -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
     
    

  • gajim/gtk/conversation/message_selection.py
    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')

  • gajim/gtk/conversation/rows/message.py
    ... ... @@ -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)
    

  • gajim/gtk/conversation/rows/widgets.py
    ... ... @@ -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)
    

  • gajim/gtk/conversation/view.py
    ... ... @@ -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: