Daniel Brötzmann pushed to branch pinned-chat-dnd at gajim / gajim

Commits:

27 changed files:

Changes:

  • data/gajim-remote.1
    ... ... @@ -53,8 +53,6 @@ Sends custom XML
    53 53
     Changes the priority of account or accounts
    
    54 54
     .Ss show_next_pending_event
    
    55 55
     Pops up a window with the next pending event
    
    56
    -.Ss toggle_ipython
    
    57
    -Shows or hides the ipython window
    
    58 56
     .Ss toggle_roster_appearance
    
    59 57
     Shows or hides the roster window
    
    60 58
     .Sh EXAMPLES
    

  • gajim/common/app.py
    ... ... @@ -75,7 +75,6 @@
    75 75
     connections: dict[str, types.Client] = {}
    
    76 76
     avatar_cache: dict[str, dict[str, Any]] = {}
    
    77 77
     bob_cache: dict[str, bytes] = {}
    
    78
    -ipython_window = None
    
    79 78
     app = None  # type: GajimApplication
    
    80 79
     window = None  # type: MainWindow
    
    81 80
     commands = None  # type: ChatCommands
    

  • gajim/common/commands.py
    ... ... @@ -52,12 +52,15 @@ def split_argument_string(string: str) -> list[str]:
    52 52
         return result
    
    53 53
     
    
    54 54
     
    
    55
    -def get_usage_from_command(cmd: argparse.ArgumentParser) -> str:
    
    55
    +def get_usage_from_command(cmd: argparse.ArgumentParser,
    
    56
    +                           command_name: str
    
    57
    +                           ) -> str:
    
    58
    +
    
    56 59
         with io.StringIO() as output:
    
    57 60
             cmd.print_usage(file=output)
    
    58 61
             usage = output.getvalue()
    
    59 62
     
    
    60
    -    usage = usage.split('[-h] ')[1]
    
    63
    +    usage = usage.split(f'ChatCommands {command_name}')[1]
    
    61 64
         return usage.strip()
    
    62 65
     
    
    63 66
     
    
    ... ... @@ -69,7 +72,7 @@ class CommandError(Exception):
    69 72
         pass
    
    70 73
     
    
    71 74
     
    
    72
    -class CommandNotFoundError(Exception):
    
    75
    +class CommandFailed(Exception):
    
    73 76
         pass
    
    74 77
     
    
    75 78
     
    
    ... ... @@ -82,7 +85,7 @@ class ChatCommands(Observable):
    82 85
         def __init__(self) -> None:
    
    83 86
             Observable.__init__(self)
    
    84 87
             self._parser = ArgumentParser(prog='ChatCommands')
    
    85
    -        self._sub_parser = self._parser.add_subparsers()
    
    88
    +        self._sub_parser = self._parser.add_subparsers(title='Commands')
    
    86 89
             self._commands: dict[str, tuple[list[str], str]] = {}
    
    87 90
     
    
    88 91
             self._create_commands()
    
    ... ... @@ -95,6 +98,16 @@ def get_commands(self, used_in: str) -> list[tuple[str, str]]:
    95 98
             commands.sort(key=operator.itemgetter(0))
    
    96 99
             return commands
    
    97 100
     
    
    101
    +    def _generate_help(self, used_in: str) -> str:
    
    102
    +        help_text = 'Commands:\n\n'
    
    103
    +        commands = self.get_commands(used_in)
    
    104
    +        for command in commands:
    
    105
    +            name, usage = command
    
    106
    +            if name == 'help':
    
    107
    +                continue
    
    108
    +            help_text += f'  {name:<15} {usage}\n'
    
    109
    +        return help_text
    
    110
    +
    
    98 111
         def make_parser(self,
    
    99 112
                         command_name: str,
    
    100 113
                         callback: Callable[..., Any],
    
    ... ... @@ -104,9 +117,11 @@ def make_parser(self,
    104 117
             it with the command name.
    
    105 118
             '''
    
    106 119
     
    
    107
    -        parser = self._sub_parser.add_parser(command_name, **kwargs)
    
    108
    -        parser.set_defaults(command_name=command_name)
    
    109
    -        self.connect(command_name, callback)
    
    120
    +        parser = self._sub_parser.add_parser(command_name,
    
    121
    +                                             add_help=False,
    
    122
    +                                             **kwargs)
    
    123
    +        parser.set_defaults(command_name=command_name,
    
    124
    +                            exec=callback)
    
    110 125
             return parser
    
    111 126
     
    
    112 127
         def add_command(self,
    
    ... ... @@ -115,7 +130,7 @@ def add_command(self,
    115 130
                         cmd: argparse.ArgumentParser
    
    116 131
                         ) -> None:
    
    117 132
     
    
    118
    -        usage = get_usage_from_command(cmd)
    
    133
    +        usage = get_usage_from_command(cmd, command_name)
    
    119 134
             self._commands[command_name] = (used_in, usage)
    
    120 135
     
    
    121 136
         def parse(self, type_: str, arg_string: str) -> None:
    
    ... ... @@ -124,13 +139,33 @@ def parse(self, type_: str, arg_string: str) -> None:
    124 139
             command_name = arg_list[0]
    
    125 140
             command = self._commands.get(command_name)
    
    126 141
             if command is None or type_ not in command[0]:
    
    127
    -            raise CommandNotFoundError(_('Unknown command: %s' % command_name))
    
    142
    +            self.notify('command-not-found',
    
    143
    +                        _('Unknown command: %s' % command_name))
    
    144
    +            raise CommandFailed
    
    145
    +
    
    146
    +        try:
    
    147
    +            args = self._parser.parse_args(arg_list)
    
    148
    +        except ArgumentParserError as error:
    
    149
    +            self.notify('command-error', str(error))
    
    150
    +            raise CommandFailed
    
    151
    +
    
    152
    +        args.used_in = type_
    
    153
    +
    
    154
    +        try:
    
    155
    +            result = args.exec(args)
    
    156
    +        except CommandError as error:
    
    157
    +            self.notify('command-error', str(error))
    
    158
    +            raise CommandFailed
    
    128 159
     
    
    129
    -        args = self._parser.parse_args(arg_list)
    
    160
    +        if result is None:
    
    161
    +            result = _('Command executed successfully')
    
    130 162
     
    
    131
    -        self.notify(args.command_name, args)
    
    163
    +        self.notify('command-result', result)
    
    132 164
     
    
    133 165
         def _create_commands(self) -> None:
    
    166
    +        parser = self.make_parser('help', self._help_command)
    
    167
    +        self.add_command('help', ['chat', 'groupchat', 'pm'], parser)
    
    168
    +
    
    134 169
             parser = self.make_parser('status', self._status_command)
    
    135 170
             parser.add_argument('status',
    
    136 171
                                 choices=['online', 'away', 'xa', 'dnd'])
    
    ... ... @@ -164,12 +199,10 @@ def _create_commands(self) -> None:
    164 199
                                 choices=['moderator', 'participant', 'visitor'])
    
    165 200
             self.add_command('role', ['groupchat'], parser)
    
    166 201
     
    
    167
    -    def _status_command(self,
    
    168
    -                        chat_commands: Any,
    
    169
    -                        signal_name: str,
    
    170
    -                        args: Any
    
    171
    -                        ) -> None:
    
    202
    +    def _help_command(self, args: Any) -> str:
    
    203
    +        return self._generate_help(args.used_in)
    
    172 204
     
    
    205
    +    def _status_command(self, args: Any) -> None:
    
    173 206
             for client in app.get_clients():
    
    174 207
                 if not app.settings.get_account_setting(client.account,
    
    175 208
                                                         'sync_with_global_status'):
    
    ... ... @@ -189,12 +222,7 @@ def _check_if_joined(self) -> GroupchatContact:
    189 222
                                      'joined this group chat'))
    
    190 223
             return contact
    
    191 224
     
    
    192
    -    def _invite_command(self,
    
    193
    -                        chat_commands: Any,
    
    194
    -                        signal_name: str,
    
    195
    -                        args: Any
    
    196
    -                        ) -> None:
    
    197
    -
    
    225
    +    def _invite_command(self, args: Any) -> None:
    
    198 226
             contact = self._check_if_joined()
    
    199 227
     
    
    200 228
             try:
    
    ... ... @@ -242,20 +270,10 @@ def _change_affiliation(self,
    242 270
                 {jid: {'affiliation': affiliation,
    
    243 271
                        'reason': reason}})
    
    244 272
     
    
    245
    -    def _ban_command(self,
    
    246
    -                     chat_commands: Any,
    
    247
    -                     signal_name: str,
    
    248
    -                     args: Any
    
    249
    -                     ) -> None:
    
    250
    -
    
    273
    +    def _ban_command(self, args: Any) -> None:
    
    251 274
             self._change_affiliation(args.who, 'outcast', args.reason)
    
    252 275
     
    
    253
    -    def _affiliate_command(self,
    
    254
    -                           chat_commands: Any,
    
    255
    -                           signal_name: str,
    
    256
    -                           args: Any
    
    257
    -                           ) -> None:
    
    258
    -
    
    276
    +    def _affiliate_command(self, args: Any) -> None:
    
    259 277
             self._change_affiliation(args.who, args.affiliation, None)
    
    260 278
     
    
    261 279
         def _change_role(self, nick: str, role: str, reason: Optional[str]) -> None:
    
    ... ... @@ -278,18 +296,8 @@ def _change_role(self, nick: str, role: str, reason: Optional[str]) -> None:
    278 296
                                               role,
    
    279 297
                                               reason)
    
    280 298
     
    
    281
    -    def _kick_command(self,
    
    282
    -                      chat_commands: Any,
    
    283
    -                      signal_name: str,
    
    284
    -                      args: Any
    
    285
    -                      ) -> None:
    
    286
    -
    
    299
    +    def _kick_command(self, args: Any) -> None:
    
    287 300
             self._change_role(args.who, 'none', args.reason)
    
    288 301
     
    
    289
    -    def _role_command(self,
    
    290
    -                      chat_commands: Any,
    
    291
    -                      signal_name: str,
    
    292
    -                      args: Any
    
    293
    -                      ) -> None:
    
    294
    -
    
    302
    +    def _role_command(self, args: Any) -> None:
    
    295 303
             self._change_role(args.who, args.role, None)

  • gajim/common/modules/chatstates.py
    ... ... @@ -36,6 +36,8 @@
    36 36
     from gajim.common.const import ClientState
    
    37 37
     from gajim.common.structs import OutgoingMessage
    
    38 38
     from gajim.common.modules.base import BaseModule
    
    39
    +from gajim.common.modules.contacts import GroupchatParticipant
    
    40
    +from gajim.common.modules.contacts import BareContact
    
    39 41
     
    
    40 42
     INACTIVE_AFTER = 60
    
    41 43
     PAUSED_AFTER = 10
    
    ... ... @@ -270,9 +272,9 @@ def set_chatstate(self, contact: types.ChatContactT, state: State) -> None:
    270 272
             if setting == 'disabled':
    
    271 273
                 # Send a last 'active' state after user disabled chatstates
    
    272 274
                 if current_state is not None:
    
    273
    -                self._log.info('Disabled for %s', contact.jid)
    
    275
    +                self._log.info('Disabled for %s', contact)
    
    274 276
                     self._log.info('Send last state: %-10s - %s',
    
    275
    -                               State.ACTIVE, contact.jid)
    
    277
    +                               State.ACTIVE, contact)
    
    276 278
     
    
    277 279
                     self._send_chatstate(contact, State.ACTIVE)
    
    278 280
     
    
    ... ... @@ -281,22 +283,23 @@ def set_chatstate(self, contact: types.ChatContactT, state: State) -> None:
    281 283
                 self._last_keyboard_activity.pop(contact.jid, None)
    
    282 284
                 return
    
    283 285
     
    
    284
    -        if not contact.is_groupchat:
    
    285
    -            # Don’t leak presence to contacts
    
    286
    -            # which are not allowed to see our status
    
    287
    -            if not contact.is_pm_contact:
    
    288
    -                # TODO
    
    289
    -                # if contact and contact.sub in ('to', 'none'):
    
    290
    -                #     self._log.info('Contact not subscribed: %s', contact.jid)
    
    291
    -                #     return
    
    292
    -                pass
    
    286
    +        if isinstance(contact, BareContact):
    
    287
    +            if not contact.is_subscribed:
    
    288
    +                self._log.debug('Contact not subscribed: %s', contact)
    
    289
    +                return
    
    293 290
     
    
    294 291
                 if not contact.is_available:
    
    295
    -                self._log.info('Contact offline: %s', contact.jid)
    
    292
    +                self._log.debug('Contact offline: %s', contact)
    
    296 293
                     return
    
    297 294
     
    
    298
    -            if not contact.supports(Namespace.CHATSTATES):
    
    299
    -                self._log.info('Chatstates not supported: %s', contact.jid)
    
    295
    +        elif isinstance(contact, GroupchatParticipant):
    
    296
    +            if not contact.is_available:
    
    297
    +                self._log.debug('Contact offline: %s', contact)
    
    298
    +                return
    
    299
    +
    
    300
    +        else:
    
    301
    +            if not contact.is_joined:
    
    302
    +                self._log.debug('Groupchat not joined: %s', contact)
    
    300 303
                     return
    
    301 304
     
    
    302 305
             if state in (State.ACTIVE, State.COMPOSING):
    
    ... ... @@ -309,7 +312,7 @@ def set_chatstate(self, contact: types.ChatContactT, state: State) -> None:
    309 312
             if current_state == state:
    
    310 313
                 return
    
    311 314
     
    
    312
    -        self._log.info('Send: %-10s - %s', state, contact.jid)
    
    315
    +        self._log.info('Send: %-10s - %s', state, contact)
    
    313 316
     
    
    314 317
             self._send_chatstate(contact, state)
    
    315 318
     
    

  • gajim/common/setting_values.py
    ... ... @@ -164,7 +164,7 @@ class _ACCOUNT_DEFAULT:
    164 164
     ]
    
    165 165
     
    
    166 166
     AllSettings = Literal[BoolSettings, IntSettings, StringSettings]
    
    167
    -AllSettingsT = Union[str, int, bool]
    
    167
    +AllSettingsT = Union[str, int, bool, list[str]]
    
    168 168
     
    
    169 169
     APP_SETTINGS = {
    
    170 170
         'always_english_wikipedia': False,
    
    ... ... @@ -508,15 +508,24 @@ class _ACCOUNT_DEFAULT:
    508 508
     
    
    509 509
     AllWorkspaceSettings = Literal[StringWorkspaceSettings, 'open_chats']
    
    510 510
     
    
    511
    -OpenChatSettingT = list[tuple[str, JID, str, bool]]
    
    512
    -AllWorkspaceSettingsT = Union[str, OpenChatSettingT]
    
    511
    +
    
    512
    +class OpenChatSettingDetails(TypedDict):
    
    513
    +    account: str
    
    514
    +    jid: JID
    
    515
    +    type: str
    
    516
    +    pinned: bool
    
    517
    +    position: int
    
    518
    +
    
    519
    +
    
    520
    +OpenChatsSettingT = list[OpenChatSettingDetails]
    
    521
    +AllWorkspaceSettingsT = Union[str, OpenChatsSettingT]
    
    513 522
     
    
    514 523
     
    
    515 524
     class WorkspaceSettings(TypedDict):
    
    516 525
         name: str
    
    517 526
         color: str
    
    518 527
         avatar_sha: str
    
    519
    -    open_chats: OpenChatSettingT
    
    528
    +    open_chats: OpenChatsSettingT
    
    520 529
     
    
    521 530
     
    
    522 531
     WORKSPACE_SETTINGS: WorkspaceSettings = {
    

  • gajim/common/settings.py
    ... ... @@ -56,7 +56,7 @@
    56 56
     from gajim.common.setting_values import StringGroupChatSettings
    
    57 57
     from gajim.common.setting_values import AllWorkspaceSettings
    
    58 58
     from gajim.common.setting_values import AllWorkspaceSettingsT
    
    59
    -from gajim.common.setting_values import OpenChatSettingT
    
    59
    +from gajim.common.setting_values import OpenChatsSettingT
    
    60 60
     from gajim.common.setting_values import StringWorkspaceSettings
    
    61 61
     from gajim.common.setting_values import ACCOUNT_SETTINGS
    
    62 62
     from gajim.common.setting_values import PROXY_SETTINGS
    
    ... ... @@ -82,6 +82,8 @@
    82 82
     
    
    83 83
     log = logging.getLogger('gajim.c.settings')
    
    84 84
     
    
    85
    +CURRENT_USER_VERSION = 2
    
    86
    +
    
    85 87
     CREATE_SQL = '''
    
    86 88
         CREATE TABLE settings (
    
    87 89
                 name TEXT UNIQUE,
    
    ... ... @@ -100,10 +102,11 @@
    100 102
         INSERT INTO settings(name, settings) VALUES ('plugins', '{}');
    
    101 103
         INSERT INTO settings(name, settings) VALUES ('workspaces', '%s');
    
    102 104
     
    
    103
    -    PRAGMA user_version=1;
    
    105
    +    PRAGMA user_version=%s;
    
    104 106
         ''' % (json.dumps(STATUS_PRESET_EXAMPLES),
    
    105 107
                json.dumps(PROXY_EXAMPLES),
    
    106
    -           json.dumps(INITAL_WORKSPACE))
    
    108
    +           json.dumps(INITAL_WORKSPACE),
    
    109
    +           CURRENT_USER_VERSION)
    
    107 110
     
    
    108 111
     
    
    109 112
     _SignalCallable = Callable[[Any, str, Optional[str], Optional[JID]], Any]
    
    ... ... @@ -217,6 +220,7 @@ def init(self) -> None:
    217 220
                 self._commit()
    
    218 221
             self._migrate_database()
    
    219 222
             self._load_app_overrides()
    
    223
    +        self._commit()
    
    220 224
     
    
    221 225
         @staticmethod
    
    222 226
         def _setup_installation_defaults() -> None:
    
    ... ... @@ -310,6 +314,8 @@ def _migrate_database(self) -> None:
    310 314
                 self._con.close()
    
    311 315
                 log.exception('Error')
    
    312 316
                 sys.exit()
    
    317
    +        else:
    
    318
    +            self._commit_settings('workspaces')
    
    313 319
     
    
    314 320
         def _migrate(self) -> None:
    
    315 321
             version = self._get_user_version()
    
    ... ... @@ -320,6 +326,21 @@ def _migrate(self) -> None:
    320 326
                 self._settings['workspaces'] = INITAL_WORKSPACE
    
    321 327
                 self._set_user_version(1)
    
    322 328
     
    
    329
    +        if version < 2:
    
    330
    +            # Migrate open chats tuple to dict
    
    331
    +            for workspace in self._settings['workspaces'].values():
    
    332
    +                open_chats = []
    
    333
    +                for open_chat in workspace.get('open_chats', []):
    
    334
    +                    account, jid, type_, pinned = open_chat
    
    335
    +                    open_chats.append({'account': account,
    
    336
    +                                       'jid': jid,
    
    337
    +                                       'type': type_,
    
    338
    +                                       'pinned': pinned,
    
    339
    +                                       'position': -1})
    
    340
    +
    
    341
    +                workspace['open_chats'] = open_chats
    
    342
    +            self._set_user_version(2)
    
    343
    +
    
    323 344
         def _migrate_old_config(self) -> None:
    
    324 345
             config_file = configpaths.get('CONFIG_FILE')
    
    325 346
             if not config_file.exists():
    
    ... ... @@ -585,6 +606,12 @@ def set_app_setting(self,
    585 606
                             value: Optional[int]) -> None:
    
    586 607
             ...
    
    587 608
     
    
    609
    +    @overload
    
    610
    +    def set_app_setting(self,
    
    611
    +                        setting: Literal['workspace_order'],
    
    612
    +                        value: list[str]) -> None:
    
    613
    +        ...
    
    614
    +
    
    588 615
         def set_app_setting(self,
    
    589 616
                             setting: str,
    
    590 617
                             value: Optional[AllSettingsT]) -> None:
    
    ... ... @@ -1175,7 +1202,7 @@ def set_workspace_setting(self,
    1175 1202
         def set_workspace_setting(self,
    
    1176 1203
                                   workspace_id: str,
    
    1177 1204
                                   setting: Literal['open_chats'],
    
    1178
    -                              value: list[tuple[str, JID, str, bool]]
    
    1205
    +                              value: OpenChatsSettingT
    
    1179 1206
                                   ) -> None:
    
    1180 1207
             ...
    
    1181 1208
     
    
    ... ... @@ -1202,7 +1229,7 @@ def set_workspace_setting(self,
    1202 1229
         def get_workspace_setting(self,
    
    1203 1230
                                   workspace_id: str,
    
    1204 1231
                                   setting: Literal['open_chats']
    
    1205
    -                              ) -> OpenChatSettingT:
    
    1232
    +                              ) -> OpenChatsSettingT:
    
    1206 1233
             ...
    
    1207 1234
     
    
    1208 1235
         @overload
    

  • gajim/common/types.py
    ... ... @@ -25,9 +25,12 @@
    25 25
     from gi.repository import GdkPixbuf
    
    26 26
     
    
    27 27
     import nbxmpp
    
    28
    +from nbxmpp.const import PresenceShow
    
    28 29
     from nbxmpp.protocol import JID
    
    29 30
     from nbxmpp.structs import BookmarkData
    
    30 31
     
    
    32
    +from gajim.common.const import PresenceShowExt
    
    33
    +
    
    31 34
     if TYPE_CHECKING:
    
    32 35
         from gajim.common.client import Client
    
    33 36
         from nbxmpp.client import Client as xmppClient  # noqa: F401
    
    ... ... @@ -49,7 +52,6 @@
    49 52
                          GroupchatContact,
    
    50 53
                          GroupchatParticipant]
    
    51 54
     
    
    52
    -
    
    53 55
     InterfaceT = Union['Interface']
    
    54 56
     PluginManagerT = Union['PluginManager']
    
    55 57
     PluginRepositoryT = Union['PluginRepository']
    
    ... ... @@ -78,3 +80,5 @@
    78 80
     ChatContactT = Union['BareContact', 'GroupchatContact', 'GroupchatParticipant']
    
    79 81
     OneOnOneContactT = Union['BareContact', 'GroupchatParticipant']
    
    80 82
     GroupchatContactT = Union['GroupchatContact']
    
    83
    +
    
    84
    +PresenceShowT = Union[PresenceShowExt, PresenceShow]

  • gajim/data/other/shortcuts.json
    ... ... @@ -5,7 +5,6 @@
    5 5
         "app.plugins": ["<Primary>E"],
    
    6 6
         "app.xml-console": ["<Primary><Shift>X"],
    
    7 7
         "app.file-transfer": ["<Primary>T"],
    
    8
    -    "app.ipython": ["<Primary><Alt>I"],
    
    9 8
         "app.start-chat::": ["<Primary>N"],
    
    10 9
         "app.create-groupchat::": ["<Primary>G"],
    
    11 10
         "win.change-nickname": ["<Primary><Shift>N"],
    

  • gajim/dev/__init__.py deleted

  • gajim/dev/ipython_view.py deleted
    1
    -#!/usr/bin/python
    
    2
    -
    
    3
    -# Copyright (C) 2008-2014 Yann Leboulanger <asterix AT lagaule.org>
    
    4
    -#
    
    5
    -# This file is part of Gajim.
    
    6
    -#
    
    7
    -# Gajim is free software; you can redistribute it and/or modify
    
    8
    -# it under the terms of the GNU General Public License as published
    
    9
    -# by the Free Software Foundation; version 3 only.
    
    10
    -#
    
    11
    -# Gajim is distributed in the hope that it will be useful,
    
    12
    -# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    13
    -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    14
    -# GNU General Public License for more details.
    
    15
    -#
    
    16
    -# You should have received a copy of the GNU General Public License
    
    17
    -# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    18
    -#
    
    19
    -# Copyright (c) 2007, IBM Corporation
    
    20
    -# All rights reserved.
    
    21
    -
    
    22
    -# Redistribution and use in source and binary forms, with or without
    
    23
    -# modification, are permitted provided that the following conditions are met:
    
    24
    -
    
    25
    -# * Redistributions of source code must retain the above copyright notice,
    
    26
    -#   this list of conditions and the following disclaimer.
    
    27
    -# * Redistributions in binary form must reproduce the above copyright notice,
    
    28
    -#   this list of conditions and the following disclaimer in the documentation
    
    29
    -#   and/or other materials provided with the distribution.
    
    30
    -# * Neither the name of the IBM Corporation nor the names of its contributors
    
    31
    -#   may be used to endorse or promote products derived from this software
    
    32
    -#   without specific prior written permission.
    
    33
    -
    
    34
    -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    
    35
    -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    
    36
    -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    
    37
    -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
    
    38
    -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    
    39
    -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    
    40
    -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    
    41
    -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    
    42
    -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    
    43
    -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    
    44
    -# POSSIBILITY OF SUCH DAMAGE.
    
    45
    -
    
    46
    -'''
    
    47
    -Provides IPython console widget
    
    48
    -
    
    49
    -@author: Eitan Isaacson
    
    50
    -@organization: IBM Corporation
    
    51
    -@copyright: Copyright (c) 2007 IBM Corporation
    
    52
    -@license: BSD
    
    53
    -
    
    54
    -All rights reserved. This program and the accompanying materials are made
    
    55
    -available under the terms of the BSD which accompanies this distribution, and
    
    56
    -is available at U{http://www.opensource.org/licenses/bsd-license.php}
    
    57
    -'''
    
    58
    -
    
    59
    -import re
    
    60
    -import sys
    
    61
    -import os
    
    62
    -from io import StringIO
    
    63
    -from functools import reduce
    
    64
    -
    
    65
    -from gi.repository import Gtk
    
    66
    -from gi.repository import Gdk
    
    67
    -from gi.repository import GObject
    
    68
    -from gi.repository import GLib
    
    69
    -from gi.repository import Pango
    
    70
    -
    
    71
    -from pygments.token import Token
    
    72
    -
    
    73
    -import IPython
    
    74
    -from IPython.core.displayhook import DisplayHook
    
    75
    -from IPython.core.display_trap import DisplayTrap
    
    76
    -
    
    77
    -
    
    78
    -class MyPromptDisplayHook(DisplayHook):
    
    79
    -    def __init__(self, shell, view):
    
    80
    -        DisplayHook.__init__(self, shell=shell)
    
    81
    -        self.view = view
    
    82
    -
    
    83
    -    def write_output_prompt(self):
    
    84
    -        tokens = self.shell.prompts.out_prompt_tokens()
    
    85
    -        self.view.write('\n')
    
    86
    -        self.view.write(tokens)
    
    87
    -
    
    88
    -
    
    89
    -class IterableIPShell:
    
    90
    -    '''
    
    91
    -    Create an IPython instance. Does not start a blocking event loop,
    
    92
    -    instead allow single iterations. This allows embedding in GTK
    
    93
    -    without blockage
    
    94
    -
    
    95
    -    @ivar IP: IPython instance.
    
    96
    -    @type IP: IPython.iplib.InteractiveShell
    
    97
    -    @ivar iter_more: Indicates if the line executed was a complete command,
    
    98
    -    or we should wait for more.
    
    99
    -    @type iter_more: integer
    
    100
    -    @ivar history_level: The place in history where we currently are
    
    101
    -    when pressing up/down.
    
    102
    -    @type history_level: integer
    
    103
    -    @ivar complete_sep: Separation delimiters for completion function.
    
    104
    -    @type complete_sep: _sre.SRE_Pattern
    
    105
    -    '''
    
    106
    -    def __init__(self, argv=None, user_ns=None, user_global_ns=None, cin=None,
    
    107
    -                 cout=None, cerr=None, input_func=None):
    
    108
    -        '''
    
    109
    -        @param argv: Command line options for IPython
    
    110
    -        @type argv: list
    
    111
    -        @param user_ns: User namespace.
    
    112
    -        @type user_ns: dictionary
    
    113
    -        @param user_global_ns: User global namespace.
    
    114
    -        @type user_global_ns: dictionary.
    
    115
    -        @param cin: Console standard input.
    
    116
    -        @type cin: IO stream
    
    117
    -        @param cout: Console standard output.
    
    118
    -        @type cout: IO stream
    
    119
    -        @param cerr: Console standard error.
    
    120
    -        @type cerr: IO stream
    
    121
    -        @param input_func: Replacement for builtin raw_input()
    
    122
    -        @type input_func: function
    
    123
    -        '''
    
    124
    -        if argv is None:
    
    125
    -            argv = []
    
    126
    -
    
    127
    -        io = IPython.utils.io
    
    128
    -        if input_func:
    
    129
    -            IPython.terminal.interactiveshell.raw_input_original = input_func
    
    130
    -
    
    131
    -        # This is to get rid of the blockage that occurs during
    
    132
    -        # IPython.Shell.InteractiveShell.user_setup()
    
    133
    -        io.raw_input = lambda x: None
    
    134
    -
    
    135
    -        os.environ['TERM'] = 'dumb'
    
    136
    -        excepthook = sys.excepthook
    
    137
    -
    
    138
    -        from traitlets.config.loader import Config
    
    139
    -
    
    140
    -        cfg = Config()
    
    141
    -        cfg.InteractiveShell.colors = 'Linux'
    
    142
    -
    
    143
    -        # InteractiveShell's __init__ gets a reference of stdout and stderr
    
    144
    -        # so we save the standard here to revert it after init
    
    145
    -        old_stdout, old_stderr = sys.stdout, sys.stderr
    
    146
    -        sys.stdout, sys.stderr = cout, cerr
    
    147
    -
    
    148
    -        # InteractiveShell inherits from SingletonConfigurable so use instance()
    
    149
    -        self.IP = IPython.terminal.embed.InteractiveShellEmbed.instance(
    
    150
    -            config=cfg, user_ns=user_ns, user_module=user_global_ns)
    
    151
    -
    
    152
    -        # Set back stdout and stderr to what it was before
    
    153
    -        sys.stdout, sys.stderr = old_stdout, old_stderr
    
    154
    -
    
    155
    -        self.IP.system = lambda cmd: self.shell(self.IP.var_expand(cmd),
    
    156
    -                                                header='IPython system call: ',
    
    157
    -                                                local_ns=user_ns)
    
    158
    -
    
    159
    -        self.IP.raw_input = input_func
    
    160
    -        sys.excepthook = excepthook
    
    161
    -        self.iter_more = 0
    
    162
    -        self.history_level = 0
    
    163
    -        self.complete_sep = re.compile(r'[\s\{\}\[\]\(\)]')
    
    164
    -        self.updateNamespace({'exit': lambda: None})
    
    165
    -        self.updateNamespace({'quit': lambda: None})
    
    166
    -
    
    167
    -        # Workaround for updating namespace with sys.modules
    
    168
    -        #
    
    169
    -        self.__update_namespace()
    
    170
    -        self.lines = []
    
    171
    -
    
    172
    -    def __update_namespace(self):
    
    173
    -        '''
    
    174
    -        Update self.IP namespace for autocompletion with sys.modules
    
    175
    -        '''
    
    176
    -        for k, v in sys.modules.items():
    
    177
    -            if '.' not in k:
    
    178
    -                self.IP.user_ns.update({k: v})
    
    179
    -
    
    180
    -    def execute(self):
    
    181
    -        '''
    
    182
    -        Execute the current line provided by the shell object
    
    183
    -        '''
    
    184
    -        self.history_level = 0
    
    185
    -        orig_stdout = sys.stdout
    
    186
    -        sys.stdout = IPython.utils.io.stdout
    
    187
    -
    
    188
    -        orig_stdin = sys.stdin
    
    189
    -        sys.stdin = IPython.utils.io.stdin
    
    190
    -        self.prompt = self.generatePrompt(self.iter_more)
    
    191
    -
    
    192
    -        self.IP.hooks.pre_prompt_hook()
    
    193
    -        if self.iter_more:
    
    194
    -            try:
    
    195
    -                self.prompt = self.generatePrompt(True)
    
    196
    -            except Exception:
    
    197
    -                self.IP.showtraceback()
    
    198
    -            if self.IP.autoindent:
    
    199
    -                self.IP.rl_do_indent = True
    
    200
    -
    
    201
    -        try:
    
    202
    -            self.IP.raw_input(self.prompt)
    
    203
    -        except KeyboardInterrupt:
    
    204
    -            self.IP.write('\nKeyboardInterrupt\n')
    
    205
    -            self.IP.input_splitter.reset()
    
    206
    -        except Exception:
    
    207
    -            self.IP.showtraceback()
    
    208
    -        else:
    
    209
    -            self.lines.append(self.IP.raw_input(self.prompt))
    
    210
    -            self.iter_more = self.IP.check_complete(
    
    211
    -                '\n'.join(self.lines))[0] == 'incomplete'
    
    212
    -
    
    213
    -            self.prompt = self.generatePrompt(self.iter_more)
    
    214
    -            if not self.iter_more:
    
    215
    -                source_raw = '\n'.join(self.lines)
    
    216
    -                self.lines = []
    
    217
    -
    
    218
    -                self.IP.run_cell(source_raw, store_history=True)
    
    219
    -                self.IP.rl_do_indent = False
    
    220
    -            else:
    
    221
    -                # TODO: Auto-indent
    
    222
    -                #
    
    223
    -                self.IP.rl_do_indent = True
    
    224
    -
    
    225
    -        sys.stdout = orig_stdout
    
    226
    -        sys.stdin = orig_stdin
    
    227
    -
    
    228
    -    def generatePrompt(self, is_continuation):
    
    229
    -        '''
    
    230
    -        Generate prompt depending on is_continuation value
    
    231
    -
    
    232
    -        @param is_continuation
    
    233
    -        @type is_continuation: boolean
    
    234
    -
    
    235
    -        @return: The prompt string representation
    
    236
    -        @rtype: string
    
    237
    -
    
    238
    -        '''
    
    239
    -        # TODO: Update to IPython 5.x and later
    
    240
    -        prompt = 'In [%d]: ' % self.IP.execution_count
    
    241
    -
    
    242
    -        return prompt
    
    243
    -
    
    244
    -    def historyBack(self):
    
    245
    -        '''
    
    246
    -        Provide one history command back
    
    247
    -
    
    248
    -        @return: The command string.
    
    249
    -        @rtype: string
    
    250
    -        '''
    
    251
    -        self.history_level -= 1
    
    252
    -        return self._getHistory()
    
    253
    -
    
    254
    -    def historyForward(self):
    
    255
    -        '''
    
    256
    -        Provide one history command forward
    
    257
    -
    
    258
    -        @return: The command string.
    
    259
    -        @rtype: string
    
    260
    -        '''
    
    261
    -        self.history_level += 1
    
    262
    -        return self._getHistory()
    
    263
    -
    
    264
    -    def _getHistory(self):
    
    265
    -        '''
    
    266
    -        Get the command string of the current history level
    
    267
    -
    
    268
    -        @return: Historic command string.
    
    269
    -        @rtype: string
    
    270
    -        '''
    
    271
    -        try:
    
    272
    -            rv = self.IP.user_ns['In'][self.history_level].strip('\n')
    
    273
    -        except IndexError:
    
    274
    -            self.history_level = 0
    
    275
    -            rv = ''
    
    276
    -        return rv
    
    277
    -
    
    278
    -    def updateNamespace(self, ns_dict):
    
    279
    -        '''
    
    280
    -        Add the current dictionary to the shell namespace
    
    281
    -
    
    282
    -        @param ns_dict: A dictionary of symbol-values.
    
    283
    -        @type ns_dict: dictionary
    
    284
    -        '''
    
    285
    -        self.IP.user_ns.update(ns_dict)
    
    286
    -
    
    287
    -    def complete(self, line):
    
    288
    -        '''
    
    289
    -        Returns an auto completed line and/or possibilities for completion
    
    290
    -
    
    291
    -        @param line: Given line so far.
    
    292
    -        @type line: string
    
    293
    -
    
    294
    -        @return: Line completed as for as possible,
    
    295
    -        and possible further completions.
    
    296
    -        @rtype: tuple
    
    297
    -        '''
    
    298
    -        split_line = self.complete_sep.split(line)
    
    299
    -        if split_line[-1]:
    
    300
    -            possibilities = self.IP.complete(split_line[-1])
    
    301
    -        else:
    
    302
    -            completed = line
    
    303
    -            possibilities = ['', []]
    
    304
    -        if possibilities:
    
    305
    -            def _commonPrefix(str1, str2):
    
    306
    -                '''
    
    307
    -                Reduction function. returns common prefix of two given strings.
    
    308
    -
    
    309
    -                @param str1: First string.
    
    310
    -                @type str1: string
    
    311
    -                @param str2: Second string
    
    312
    -                @type str2: string
    
    313
    -
    
    314
    -                @return: Common prefix to both strings.
    
    315
    -                @rtype: string
    
    316
    -                '''
    
    317
    -                for i in range(len(str1)):
    
    318
    -                    if not str2.startswith(str1[:i + 1]):
    
    319
    -                        return str1[:i]
    
    320
    -                return str1
    
    321
    -
    
    322
    -            if possibilities[1]:
    
    323
    -                common_prefix = \
    
    324
    -                    reduce(_commonPrefix, possibilities[1]) or line[-1]
    
    325
    -                completed = line[:-len(split_line[-1])] + common_prefix
    
    326
    -            else:
    
    327
    -                completed = line
    
    328
    -        else:
    
    329
    -            completed = line
    
    330
    -        return completed, possibilities[1]
    
    331
    -
    
    332
    -
    
    333
    -class ConsoleView(Gtk.TextView):
    
    334
    -    '''
    
    335
    -    Specialized text view for console-like workflow
    
    336
    -
    
    337
    -    @cvar ANSI_COLORS: Mapping of terminal colors to X11 names.
    
    338
    -    @type ANSI_COLORS: dictionary
    
    339
    -
    
    340
    -    @ivar text_buffer: Widget's text buffer.
    
    341
    -    @type text_buffer: Gtk.TextBuffer
    
    342
    -    @ivar color_pat: Regex of terminal color pattern
    
    343
    -    @type color_pat: _sre.SRE_Pattern
    
    344
    -    @ivar mark: Scroll mark for automatic scrolling on input.
    
    345
    -    @type mark: Gtk.TextMark
    
    346
    -    @ivar line_start: Start of command line mark.
    
    347
    -    @type line_start: Gtk.TextMark
    
    348
    -    '''
    
    349
    -
    
    350
    -    ANSI_COLORS = {'0;30': 'Black', '0;31': 'Red',
    
    351
    -                   '0;32': 'Green', '0;33': 'Brown',
    
    352
    -                   '0;34': 'Blue', '0;35': 'Purple',
    
    353
    -                   '0;36': 'Cyan', '0;37': 'LightGray',
    
    354
    -                   '1;30': 'DarkGray', '1;31': 'DarkRed',
    
    355
    -                   '1;32': 'SeaGreen', '1;33': 'Yellow',
    
    356
    -                   '1;34': 'LightBlue', '1;35': 'MediumPurple',
    
    357
    -                   '1;36': 'LightCyan', '1;37': 'White'}
    
    358
    -
    
    359
    -    def __init__(self):
    
    360
    -        '''
    
    361
    -        Initialize console view
    
    362
    -        '''
    
    363
    -        GObject.GObject.__init__(self)
    
    364
    -        self.override_font(Pango.FontDescription('Mono'))
    
    365
    -        self.set_cursor_visible(True)
    
    366
    -        self.text_buffer = self.get_buffer()
    
    367
    -        self.mark = self.text_buffer.create_mark(
    
    368
    -            'scroll_mark',
    
    369
    -            self.text_buffer.get_end_iter(),
    
    370
    -            False)
    
    371
    -        for code, color in self.ANSI_COLORS.items():
    
    372
    -            self.text_buffer.create_tag(code,
    
    373
    -                                        foreground=color,
    
    374
    -                                        weight=700)
    
    375
    -        self.text_buffer.create_tag('0')
    
    376
    -        self.text_buffer.create_tag('notouch', editable=False)
    
    377
    -        self.color_pat = re.compile(r'\x01?\x1b\[(.*?)m\x02?')
    
    378
    -
    
    379
    -        self.style_dict = {
    
    380
    -            Token.Prompt: '0;32',
    
    381
    -            Token.PromptNum: '1;32',
    
    382
    -            Token.OutPrompt: '0;31',
    
    383
    -            Token.OutPromptNum: '1;31',
    
    384
    -        }
    
    385
    -
    
    386
    -        self.line_start = \
    
    387
    -            self.text_buffer.create_mark('line_start',
    
    388
    -                                         self.text_buffer.get_end_iter(),
    
    389
    -                                         True)
    
    390
    -        self.connect('key-press-event', self.onKeyPress)
    
    391
    -
    
    392
    -    def write(self, text, editable=False):
    
    393
    -        if isinstance(text, str):
    
    394
    -            GLib.idle_add(self._write, text, editable)
    
    395
    -        else:
    
    396
    -            GLib.idle_add(self._write5, text, editable)
    
    397
    -
    
    398
    -    def _write5(self, text, editable=False):
    
    399
    -        '''
    
    400
    -        Write given text to buffer
    
    401
    -
    
    402
    -        @param text: Text to append.
    
    403
    -        @type text: list of (token: string)
    
    404
    -        @param editable: If true, added text is editable.
    
    405
    -        @type editable: boolean
    
    406
    -        '''
    
    407
    -        start_mark = self.text_buffer.create_mark(
    
    408
    -            None, self.text_buffer.get_end_iter(), True)
    
    409
    -
    
    410
    -        for token, segment in text:
    
    411
    -            tag = self.style_dict[token]
    
    412
    -            self.text_buffer.insert_with_tags_by_name(
    
    413
    -                self.text_buffer.get_end_iter(), segment, tag)
    
    414
    -        if not editable:
    
    415
    -            self.text_buffer.apply_tag_by_name(
    
    416
    -                'notouch',
    
    417
    -                self.text_buffer.get_iter_at_mark(start_mark),
    
    418
    -                self.text_buffer.get_end_iter())
    
    419
    -        self.text_buffer.delete_mark(start_mark)
    
    420
    -        self.scroll_mark_onscreen(self.mark)
    
    421
    -
    
    422
    -    def _write(self, text, editable=False):
    
    423
    -        '''
    
    424
    -        Write given text to buffer
    
    425
    -
    
    426
    -        @param text: Text to append.
    
    427
    -        @type text: string
    
    428
    -        @param editable: If true, added text is editable.
    
    429
    -        @type editable: boolean
    
    430
    -        '''
    
    431
    -        if isinstance(text, list):
    
    432
    -            self._write5(text, editable)
    
    433
    -            return
    
    434
    -        segments = self.color_pat.split(text)
    
    435
    -        segment = segments.pop(0)
    
    436
    -        start_mark = self.text_buffer.create_mark(
    
    437
    -            None, self.text_buffer.get_end_iter(), True)
    
    438
    -        self.text_buffer.insert(self.text_buffer.get_end_iter(), segment)
    
    439
    -
    
    440
    -        if segments:
    
    441
    -            ansi_tags = self.color_pat.findall(text)
    
    442
    -            for tag in ansi_tags:
    
    443
    -                i = segments.index(tag)
    
    444
    -                self.text_buffer.insert_with_tags_by_name(
    
    445
    -                    self.text_buffer.get_end_iter(), segments[i + 1], str(tag))
    
    446
    -                segments.pop(i)
    
    447
    -        if not editable:
    
    448
    -            self.text_buffer.apply_tag_by_name(
    
    449
    -                'notouch',
    
    450
    -                self.text_buffer.get_iter_at_mark(start_mark),
    
    451
    -                self.text_buffer.get_end_iter())
    
    452
    -
    
    453
    -        self.text_buffer.delete_mark(start_mark)
    
    454
    -        self.scroll_mark_onscreen(self.mark)
    
    455
    -
    
    456
    -    def showPrompt(self, prompt):
    
    457
    -        GLib.idle_add(self._showPrompt, prompt)
    
    458
    -
    
    459
    -    def _showPrompt(self, prompt):
    
    460
    -        '''
    
    461
    -        Print prompt at start of line
    
    462
    -
    
    463
    -        @param prompt: Prompt to print.
    
    464
    -        @type prompt: string
    
    465
    -        '''
    
    466
    -        self._write(prompt)
    
    467
    -        self.text_buffer.move_mark(self.line_start,
    
    468
    -                                   self.text_buffer.get_end_iter())
    
    469
    -
    
    470
    -    def changeLine(self, text):
    
    471
    -        GLib.idle_add(self._changeLine, text)
    
    472
    -
    
    473
    -    def _changeLine(self, text):
    
    474
    -        '''
    
    475
    -        Replace currently entered command line with given text
    
    476
    -
    
    477
    -        @param text: Text to use as replacement.
    
    478
    -        @type text: string
    
    479
    -        '''
    
    480
    -        iter_ = self.text_buffer.get_iter_at_mark(self.line_start)
    
    481
    -        iter_.forward_to_line_end()
    
    482
    -        self.text_buffer.delete(
    
    483
    -            self.text_buffer.get_iter_at_mark(self.line_start), iter_)
    
    484
    -        self._write(text, True)
    
    485
    -
    
    486
    -    def getCurrentLine(self):
    
    487
    -        '''
    
    488
    -        Get text in current command line
    
    489
    -
    
    490
    -        @return: Text of current command line.
    
    491
    -        @rtype: string
    
    492
    -        '''
    
    493
    -        rv = self.text_buffer.get_slice(
    
    494
    -            self.text_buffer.get_iter_at_mark(self.line_start),
    
    495
    -            self.text_buffer.get_end_iter(), False)
    
    496
    -        return rv
    
    497
    -
    
    498
    -    def showReturned(self, text):
    
    499
    -        GLib.idle_add(self._showReturned, text)
    
    500
    -
    
    501
    -    def _showReturned(self, text):
    
    502
    -        '''
    
    503
    -        Show returned text from last command and print new prompt
    
    504
    -
    
    505
    -        @param text: Text to show.
    
    506
    -        @type text: string
    
    507
    -        '''
    
    508
    -        iter_ = self.text_buffer.get_iter_at_mark(self.line_start)
    
    509
    -        iter_.forward_to_line_end()
    
    510
    -        self.text_buffer.apply_tag_by_name(
    
    511
    -            'notouch',
    
    512
    -            self.text_buffer.get_iter_at_mark(self.line_start),
    
    513
    -            iter_)
    
    514
    -        self._write('\n' + text)
    
    515
    -        if text:
    
    516
    -            self._write('\n')
    
    517
    -        self._showPrompt(self.prompt)
    
    518
    -        self.text_buffer.move_mark(
    
    519
    -            self.line_start, self.text_buffer.get_end_iter())
    
    520
    -        self.text_buffer.place_cursor(self.text_buffer.get_end_iter())
    
    521
    -
    
    522
    -        if self.IP.rl_do_indent:
    
    523
    -            indentation = self.IP.input_splitter.indent_spaces * ' '
    
    524
    -            self.text_buffer.insert_at_cursor(indentation)
    
    525
    -
    
    526
    -    def onKeyPress(self, _widget, event):
    
    527
    -        '''
    
    528
    -        Key press callback used for correcting behavior for console-like
    
    529
    -        interfaces. For example 'home' should go to prompt, not to beginning of
    
    530
    -        line
    
    531
    -
    
    532
    -        @param widget: Widget that key press occurred in.
    
    533
    -        @type widget: Gtk.Widget
    
    534
    -        @param event: Event object
    
    535
    -        @type event: Gdk.Event
    
    536
    -
    
    537
    -        @return: Return True if event should not trickle.
    
    538
    -        @rtype: boolean
    
    539
    -        '''
    
    540
    -        insert_mark = self.text_buffer.get_insert()
    
    541
    -        insert_iter = self.text_buffer.get_iter_at_mark(insert_mark)
    
    542
    -        selection_mark = self.text_buffer.get_selection_bound()
    
    543
    -        selection_iter = self.text_buffer.get_iter_at_mark(selection_mark)
    
    544
    -        start_iter = self.text_buffer.get_iter_at_mark(self.line_start)
    
    545
    -        if event.keyval == Gdk.KEY_Home:
    
    546
    -            if event.get_state() == 0:
    
    547
    -                self.text_buffer.place_cursor(start_iter)
    
    548
    -                return True
    
    549
    -            if event.get_state() == Gdk.ModifierType.SHIFT_MASK:
    
    550
    -                self.text_buffer.move_mark(insert_mark, start_iter)
    
    551
    -                return True
    
    552
    -
    
    553
    -        if event.keyval == Gdk.KEY_Left:
    
    554
    -            insert_iter.backward_cursor_position()
    
    555
    -            if not insert_iter.editable(True):
    
    556
    -                return True
    
    557
    -        elif not event.string:
    
    558
    -            pass
    
    559
    -        elif (start_iter.compare(insert_iter) <= 0 and
    
    560
    -                start_iter.compare(selection_iter) <= 0):
    
    561
    -            pass
    
    562
    -        elif (start_iter.compare(insert_iter) > 0 and
    
    563
    -                start_iter.compare(selection_iter) > 0):
    
    564
    -            self.text_buffer.place_cursor(start_iter)
    
    565
    -        elif insert_iter.compare(selection_iter) < 0:
    
    566
    -            self.text_buffer.move_mark(insert_mark, start_iter)
    
    567
    -        elif insert_iter.compare(selection_iter) > 0:
    
    568
    -            self.text_buffer.move_mark(selection_mark, start_iter)
    
    569
    -
    
    570
    -        return self.onKeyPressExtend(event)
    
    571
    -
    
    572
    -    def onKeyPressExtend(self, event):
    
    573
    -        '''
    
    574
    -        For some reason we can't extend onKeyPress directly (bug #500900)
    
    575
    -        '''
    
    576
    -
    
    577
    -
    
    578
    -class IPythonView(ConsoleView, IterableIPShell):
    
    579
    -    '''
    
    580
    -    Sub-class of both modified IPython shell and L{ConsoleView} this makes
    
    581
    -    a GTK IPython console.
    
    582
    -    '''
    
    583
    -    def __init__(self):
    
    584
    -        '''
    
    585
    -        Initialize. Redirect I/O to console
    
    586
    -        '''
    
    587
    -        ConsoleView.__init__(self)
    
    588
    -        self.cout = StringIO()
    
    589
    -        IterableIPShell.__init__(self, cout=self.cout, cerr=self.cout,
    
    590
    -                                 input_func=self.raw_input)
    
    591
    -
    
    592
    -        displayhook = MyPromptDisplayHook(shell=self.IP, view=self)
    
    593
    -        self.IP.displayhook = displayhook
    
    594
    -        self.IP.display_trap = DisplayTrap(hook=displayhook)
    
    595
    -
    
    596
    -        self.interrupt = False
    
    597
    -        self.execute()
    
    598
    -        self.prompt = self.generatePrompt(False)
    
    599
    -        self.cout.truncate(0)
    
    600
    -        self.showPrompt(self.prompt)
    
    601
    -
    
    602
    -    def prompt_for_code(self):
    
    603
    -        # IPython 5.0 calls prompt_for_code instead of raw_input
    
    604
    -        return self.raw_input(self)
    
    605
    -
    
    606
    -    def raw_input(self, _prompt=''):
    
    607
    -        '''
    
    608
    -        Custom raw_input() replacement. Gets current line from console buffer
    
    609
    -
    
    610
    -        @param prompt: Prompt to print. Here for compatibility as replacement.
    
    611
    -        @type prompt: string
    
    612
    -
    
    613
    -        @return: The current command line text.
    
    614
    -        @rtype: string
    
    615
    -        '''
    
    616
    -        if self.interrupt:
    
    617
    -            self.interrupt = False
    
    618
    -            raise KeyboardInterrupt
    
    619
    -        return self.getCurrentLine()
    
    620
    -
    
    621
    -    def onKeyPressExtend(self, event):
    
    622
    -        '''
    
    623
    -        Key press callback with plenty of shell goodness, like history,
    
    624
    -        autocompletions, etc
    
    625
    -
    
    626
    -        @param widget: Widget that key press occurred in.
    
    627
    -        @type widget: Gtk.Widget
    
    628
    -        @param event: Event object.
    
    629
    -        @type event: Gdk.Event
    
    630
    -
    
    631
    -        @return: True if event should not trickle.
    
    632
    -        @rtype: boolean
    
    633
    -        '''
    
    634
    -        if (event.get_state() & Gdk.ModifierType.CONTROL_MASK and
    
    635
    -                event.keyval == 99):
    
    636
    -            self.interrupt = True
    
    637
    -            self._processLine()
    
    638
    -            return True
    
    639
    -        if event.keyval == Gdk.KEY_Return:
    
    640
    -            self._processLine()
    
    641
    -            return True
    
    642
    -        if event.keyval == Gdk.KEY_Up:
    
    643
    -            self.changeLine(self.historyBack())
    
    644
    -            return True
    
    645
    -        if event.keyval == Gdk.KEY_Down:
    
    646
    -            self.changeLine(self.historyForward())
    
    647
    -            return True
    
    648
    -        if event.keyval == Gdk.KEY_Tab:
    
    649
    -            if not self.getCurrentLine().strip():
    
    650
    -                return False
    
    651
    -            completed, possibilities = self.complete(self.getCurrentLine())
    
    652
    -            if len(possibilities) > 1:
    
    653
    -                slice_ = self.getCurrentLine()
    
    654
    -                self.write('\n')
    
    655
    -                for symbol in possibilities:
    
    656
    -                    self.write(symbol + '\n')
    
    657
    -                self.showPrompt(self.prompt)
    
    658
    -            self.changeLine(completed or slice_)
    
    659
    -            return True
    
    660
    -
    
    661
    -    def _processLine(self):
    
    662
    -        '''
    
    663
    -        Process current command line
    
    664
    -        '''
    
    665
    -        self.history_pos = 0
    
    666
    -        self.execute()
    
    667
    -        rv = self.cout.getvalue()
    
    668
    -        if rv:
    
    669
    -            rv = rv.strip('\n')
    
    670
    -        self.showReturned(rv)
    
    671
    -        self.cout.truncate(0)
    
    672
    -        self.cout.seek(0)

  • gajim/gtk/application.py
    ... ... @@ -165,13 +165,6 @@ def __init__(self):
    165 165
                 GLib.OptionArg.NONE,
    
    166 166
                 _('Show all warnings'))
    
    167 167
     
    
    168
    -        self.add_main_option(
    
    169
    -            'ipython',
    
    170
    -            ord('i'),
    
    171
    -            GLib.OptionFlags.NONE,
    
    172
    -            GLib.OptionArg.NONE,
    
    173
    -            _('Open IPython shell'))
    
    174
    -
    
    175 168
             self.add_main_option(
    
    176 169
                 'gdebug',
    
    177 170
                 0,
    
    ... ... @@ -345,7 +338,6 @@ def _command_line(self,
    345 338
             options = command_line.get_options_dict()
    
    346 339
     
    
    347 340
             remote_commands = [
    
    348
    -            ('ipython', None),
    
    349 341
                 ('start-chat', GLib.Variant('s', '')),
    
    350 342
                 ('show', None)
    
    351 343
             ]
    
    ... ... @@ -417,7 +409,6 @@ def _connect_app_actions(self) -> None:
    417 409
                 ('join-support-chat', self._on_join_support_chat),
    
    418 410
                 ('about', self._on_about_action),
    
    419 411
                 ('faq', self._on_faq_action),
    
    420
    -            ('ipython', self._on_ipython_action),
    
    421 412
                 ('start-chat', self._on_new_chat_action),
    
    422 413
                 ('accounts', self._on_accounts_action),
    
    423 414
                 ('add-contact', self._on_add_contact_action),
    
    ... ... @@ -742,18 +733,6 @@ def _on_mark_as_read_action(self,
    742 733
     
    
    743 734
             app.window.mark_as_read(params.account, params.jid)
    
    744 735
     
    
    745
    -    @staticmethod
    
    746
    -    def _on_ipython_action(_action: Gio.SimpleAction,
    
    747
    -                           _param: Optional[GLib.Variant]) -> None:
    
    748
    -        '''
    
    749
    -        Show/hide the ipython window
    
    750
    -        '''
    
    751
    -        win = cast(Gtk.Window, app.ipython_window)
    
    752
    -        if win and win.is_visible():
    
    753
    -            win.present()
    
    754
    -        else:
    
    755
    -            app.interface.create_ipython_window()
    
    756
    -
    
    757 736
         @staticmethod
    
    758 737
         def _on_open_mail_action(_action: Gio.SimpleAction,
    
    759 738
                                  param: GLib.Variant) -> None:
    

  • gajim/gtk/avatar.py
    ... ... @@ -52,8 +52,8 @@
    52 52
     log = logging.getLogger('gajim.gui.avatar')
    
    53 53
     
    
    54 54
     
    
    55
    -AvatarCacheT = dict[JID, dict[tuple[int, int, Optional[str]],
    
    56
    -                              cairo.ImageSurface]]
    
    55
    +AvatarCacheT = dict[Union[JID, str], dict[tuple[int, int, Optional[str]],
    
    56
    +                                          cairo.ImageSurface]]
    
    57 57
     
    
    58 58
     
    
    59 59
     def generate_avatar(letters: str,
    
    ... ... @@ -102,7 +102,7 @@ def generate_avatar(letters: str,
    102 102
         context.set_operator(cairo.Operator.OVER)
    
    103 103
         context.show_text(letters)
    
    104 104
     
    
    105
    -    return context.get_target()
    
    105
    +    return context.get_target()  # pyright: ignore
    
    106 106
     
    
    107 107
     
    
    108 108
     @lru_cache(maxsize=None)
    
    ... ... @@ -132,7 +132,8 @@ def make_workspace_avatar(letter: str,
    132 132
     
    
    133 133
     
    
    134 134
     def add_status_to_avatar(surface: cairo.ImageSurface,
    
    135
    -                         show: str) -> cairo.ImageSurface:
    
    135
    +                         show: str
    
    136
    +                         ) -> cairo.ImageSurface:
    
    136 137
     
    
    137 138
         width = surface.get_width()
    
    138 139
         height = surface.get_height()
    
    ... ... @@ -179,11 +180,15 @@ def add_status_to_avatar(surface: cairo.ImageSurface,
    179 180
             context.set_line_width(clip_radius / 4)
    
    180 181
             context.stroke()
    
    181 182
     
    
    182
    -    return context.get_target()
    
    183
    +    return context.get_target()  # pyright: ignore
    
    183 184
     
    
    184 185
     
    
    185 186
     @lru_cache(maxsize=128)
    
    186
    -def get_show_circle(show, size: int, scale: int) -> cairo.ImageSurface:
    
    187
    +def get_show_circle(show: Union[str, types.PresenceShowT],
    
    188
    +                    size: int,
    
    189
    +                    scale: int
    
    190
    +                    ) -> cairo.ImageSurface:
    
    191
    +
    
    187 192
         if not isinstance(show, str):
    
    188 193
             show = show.value
    
    189 194
     
    
    ... ... @@ -212,7 +217,7 @@ def get_show_circle(show, size: int, scale: int) -> cairo.ImageSurface:
    212 217
             context.set_line_width(size / 10)
    
    213 218
             context.stroke()
    
    214 219
     
    
    215
    -    return context.get_target()
    
    220
    +    return context.get_target()  # pyright: ignore
    
    216 221
     
    
    217 222
     
    
    218 223
     def fit(surface: cairo.ImageSurface, size: int) -> cairo.ImageSurface:
    
    ... ... @@ -235,7 +240,7 @@ def fit(surface: cairo.ImageSurface, size: int) -> cairo.ImageSurface:
    235 240
         context.scale(factor, factor)
    
    236 241
         context.set_source_surface(square_surface, 0, 0)
    
    237 242
         context.paint()
    
    238
    -    return context.get_target()
    
    243
    +    return context.get_target()  # pyright: ignore
    
    239 244
     
    
    240 245
     
    
    241 246
     def square(surface: cairo.ImageSurface, size: int) -> cairo.ImageSurface:
    
    ... ... @@ -259,7 +264,7 @@ def square(surface: cairo.ImageSurface, size: int) -> cairo.ImageSurface:
    259 264
     
    
    260 265
         context.set_source_surface(surface, x_pos, y_pos)
    
    261 266
         context.paint()
    
    262
    -    return context.get_target()
    
    267
    +    return context.get_target()  # pyright: ignore
    
    263 268
     
    
    264 269
     
    
    265 270
     def clip(surface: cairo.ImageSurface, mode: str) -> cairo.ImageSurface:
    
    ... ... @@ -289,7 +294,7 @@ def clip_circle(surface: cairo.ImageSurface) -> cairo.ImageSurface:
    289 294
         context.clip()
    
    290 295
         context.paint()
    
    291 296
     
    
    292
    -    return context.get_target()
    
    297
    +    return context.get_target()  # pyright: ignore
    
    293 298
     
    
    294 299
     
    
    295 300
     def round_corners(surface: cairo.ImageSurface) -> cairo.ImageSurface:
    
    ... ... @@ -320,7 +325,7 @@ def round_corners(surface: cairo.ImageSurface) -> cairo.ImageSurface:
    320 325
     
    
    321 326
         context.paint()
    
    322 327
     
    
    323
    -    return context.get_target()
    
    328
    +    return context.get_target()  # pyright: ignore
    
    324 329
     
    
    325 330
     
    
    326 331
     def convert_to_greyscale(surface: cairo.ImageSurface) -> cairo.ImageSurface:
    
    ... ... @@ -333,14 +338,14 @@ def convert_to_greyscale(surface: cairo.ImageSurface) -> cairo.ImageSurface:
    333 338
         context.set_source_rgba(1, 1, 1, 0.5)
    
    334 339
         context.rectangle(0, 0, surface.get_width(), surface.get_height())
    
    335 340
         context.fill()
    
    336
    -    return context.get_target()
    
    341
    +    return context.get_target()  # pyright: ignore
    
    337 342
     
    
    338 343
     
    
    339 344
     class AvatarStorage(metaclass=Singleton):
    
    340 345
         def __init__(self):
    
    341 346
             self._cache: AvatarCacheT = defaultdict(dict)
    
    342 347
     
    
    343
    -    def invalidate_cache(self, jid: JID) -> None:
    
    348
    +    def invalidate_cache(self, jid: Union[JID, str]) -> None:
    
    344 349
             self._cache.pop(jid, None)
    
    345 350
     
    
    346 351
         def get_pixbuf(self,
    
    ... ... @@ -373,6 +378,7 @@ def get_surface(self,
    373 378
     
    
    374 379
             if transport_icon is not None:
    
    375 380
                 surface = load_icon_surface(transport_icon, size, scale)
    
    381
    +            assert surface is not None
    
    376 382
                 if show is not None:
    
    377 383
                     surface = add_status_to_avatar(surface, show)
    
    378 384
                 self._cache[jid][(size, scale, show)] = surface
    
    ... ... @@ -412,11 +418,12 @@ def get_muc_surface(self,
    412 418
     
    
    413 419
             if transport_icon is not None:
    
    414 420
                 surface = load_icon_surface(transport_icon, size, scale)
    
    415
    -            self._cache[jid][(size, scale)] = surface
    
    421
    +            assert surface is not None
    
    422
    +            self._cache[jid][(size, scale, None)] = surface
    
    416 423
                 return surface
    
    417 424
     
    
    418 425
             if not default:
    
    419
    -            surface = self._cache[jid].get((size, scale))
    
    426
    +            surface = self._cache[jid].get((size, scale, None))
    
    420 427
                 if surface is not None:
    
    421 428
                     return surface
    
    422 429
     
    
    ... ... @@ -425,7 +432,7 @@ def get_muc_surface(self,
    425 432
                     surface = self.surface_from_filename(avatar_sha, size, scale)
    
    426 433
                     if surface is not None:
    
    427 434
                         surface = clip(surface, style)
    
    428
    -                    self._cache[jid][(size, scale)] = surface
    
    435
    +                    self._cache[jid][(size, scale, None)] = surface
    
    429 436
                         return surface
    
    430 437
     
    
    431 438
                     # avatar_sha set, but image is missing
    
    ... ... @@ -436,7 +443,7 @@ def get_muc_surface(self,
    436 443
             name = get_groupchat_name(con, jid)
    
    437 444
             letter = self._generate_letter(name)
    
    438 445
             surface = generate_default_avatar(letter, str(jid), size, scale, style)
    
    439
    -        self._cache[jid][(size, scale)] = surface
    
    446
    +        self._cache[jid][(size, scale, None)] = surface
    
    440 447
             return surface
    
    441 448
     
    
    442 449
         def get_workspace_surface(self,
    
    ... ... @@ -444,7 +451,7 @@ def get_workspace_surface(self,
    444 451
                                   size: int,
    
    445 452
                                   scale: int) -> Optional[cairo.ImageSurface]:
    
    446 453
     
    
    447
    -        surface = self._cache[workspace_id].get((size, scale))
    
    454
    +        surface = self._cache[workspace_id].get((size, scale, None))
    
    448 455
             if surface is not None:
    
    449 456
                 return surface
    
    450 457
     
    
    ... ... @@ -465,7 +472,7 @@ def get_workspace_surface(self,
    465 472
             letter = name[:1].upper()
    
    466 473
             surface = make_workspace_avatar(
    
    467 474
                 letter, rgba_to_float(rgba), size, scale)
    
    468
    -        self._cache[workspace_id][(size, scale)] = surface
    
    475
    +        self._cache[workspace_id][(size, scale, None)] = surface
    
    469 476
             return surface
    
    470 477
     
    
    471 478
         @staticmethod
    
    ... ... @@ -483,6 +490,7 @@ def _load_for_publish(path: str) -> Optional[tuple[bool, bytes]]:
    483 490
                                              height,
    
    484 491
                                              GdkPixbuf.InterpType.BILINEAR)
    
    485 492
     
    
    493
    +        assert pixbuf is not None
    
    486 494
             return pixbuf.save_to_bufferv('png', [], [])
    
    487 495
     
    
    488 496
         @staticmethod
    

  • gajim/gtk/avatar_selector.py
    ... ... @@ -178,7 +178,9 @@ def _scale_for_publish(pixbuf: GdkPixbuf.Pixbuf
    178 178
                 return scaled_pixbuf, width, height
    
    179 179
             return pixbuf, width, height
    
    180 180
     
    
    181
    -    def get_avatar_surface(self) -> Optional[tuple[cairo.Surface, int, int]]:
    
    181
    +    def get_avatar_surface(self) -> Optional[tuple[cairo.ImageSurface,
    
    182
    +                                                   int,
    
    183
    +                                                   int]]:
    
    182 184
             pixbuf = self._crop_area.get_pixbuf()
    
    183 185
             if pixbuf is None:
    
    184 186
                 return None
    

  • gajim/gtk/chat_banner.py
    ... ... @@ -236,12 +236,12 @@ def _update_avatar(self) -> None:
    236 236
             scale = app.window.get_scale_factor()
    
    237 237
             assert self._contact
    
    238 238
             surface = self._contact.get_avatar(AvatarSize.CHAT, scale)
    
    239
    -        assert isinstance(surface, cairo.Surface)
    
    239
    +        assert isinstance(surface, cairo.ImageSurface)
    
    240 240
             self._ui.avatar_image.set_from_surface(surface)
    
    241 241
     
    
    242 242
         def _update_content(self) -> None:
    
    243
    -        if self._client is None or self._contact is None:
    
    244
    -            return
    
    243
    +        assert self._contact is not None
    
    244
    +        assert self._client is not None
    
    245 245
     
    
    246 246
             name = self._contact.name
    
    247 247
     
    

  • gajim/gtk/chat_list.py
    ... ... @@ -26,6 +26,7 @@
    26 26
     from gi.repository import Gio
    
    27 27
     from gi.repository import Gdk
    
    28 28
     from gi.repository import GLib
    
    29
    +from gi.repository import GObject
    
    29 30
     from gi.repository import Gtk
    
    30 31
     import cairo
    
    31 32
     
    
    ... ... @@ -47,6 +48,7 @@
    47 48
     from gajim.common.helpers import AdditionalDataDict
    
    48 49
     from gajim.common.preview_helpers import filename_from_uri
    
    49 50
     from gajim.common.preview_helpers import guess_simple_file_type
    
    51
    +from gajim.common.setting_values import OpenChatsSettingT
    
    50 52
     from gajim.common.types import ChatContactT
    
    51 53
     from gajim.common.types import OneOnOneContactT
    
    52 54
     from gajim.common.modules.contacts import BareContact
    
    ... ... @@ -65,6 +67,13 @@
    65 67
     
    
    66 68
     
    
    67 69
     class ChatList(Gtk.ListBox, EventHelper):
    
    70
    +
    
    71
    +    __gsignals__ = {
    
    72
    +        'chat-order-changed': (GObject.SignalFlags.RUN_LAST,
    
    73
    +                               None,
    
    74
    +                               ()),
    
    75
    +    }
    
    76
    +
    
    68 77
         def __init__(self, workspace_id: str) -> None:
    
    69 78
             Gtk.ListBox.__init__(self)
    
    70 79
             EventHelper.__init__(self)
    
    ... ... @@ -94,12 +103,19 @@ def __init__(self, workspace_id: str) -> None:
    94 103
                 entries,
    
    95 104
                 Gdk.DragAction.MOVE)
    
    96 105
     
    
    106
    +        self._drag_row: Optional[ChatRow] = None
    
    107
    +        self._row_before: Optional[ChatRow] = None
    
    108
    +        self._row_after: Optional[ChatRow] = None
    
    109
    +        self._chat_order: list[ChatRow] = []
    
    110
    +
    
    97 111
             self.register_events([
    
    98 112
                 ('account-enabled', ged.GUI2, self._on_account_changed),
    
    99 113
                 ('account-disabled', ged.GUI2, self._on_account_changed),
    
    100 114
                 ('bookmarks-received', ged.GUI1, self._on_bookmarks_received),
    
    101 115
             ])
    
    102 116
     
    
    117
    +        self.connect('drag-motion', self._on_drag_motion)
    
    118
    +        self.connect('drag-data-received', self._on_drag_data_received)
    
    103 119
             self.connect('destroy', self._on_destroy)
    
    104 120
     
    
    105 121
             self._timer_id = GLib.timeout_add_seconds(60, self._update_timer)
    
    ... ... @@ -146,6 +162,103 @@ def emit_unread_changed(self) -> None:
    146 162
                            self._workspace_id,
    
    147 163
                            count)
    
    148 164
     
    
    165
    +    def _get_row_before(self, row: ChatRow) -> Optional[ChatRow]:
    
    166
    +        row = cast(ChatRow, self.get_row_at_index(row.get_index() - 1))
    
    167
    +        return row
    
    168
    +
    
    169
    +    def _get_row_after(self, row: ChatRow) -> Optional[ChatRow]:
    
    170
    +        row = cast(ChatRow, self.get_row_at_index(row.get_index() + 1))
    
    171
    +        return row
    
    172
    +
    
    173
    +    def _get_last_row(self) -> ChatRow:
    
    174
    +        row = cast(ChatRow, self.get_row_at_index(len(self.get_children())))
    
    175
    +        return row
    
    176
    +
    
    177
    +    def set_drag_row(self, row: ChatRow) -> None:
    
    178
    +        self._drag_row = row
    
    179
    +
    
    180
    +    def _on_drag_motion(self,
    
    181
    +                        _widget: Gtk.Widget,
    
    182
    +                        _drag_context: Gdk.DragContext,
    
    183
    +                        _x_coord: int,
    
    184
    +                        y_coord: int,
    
    185
    +                        _time: int
    
    186
    +                        ) -> bool:
    
    187
    +
    
    188
    +        row = cast(ChatRow, self.get_row_at_y(y_coord))
    
    189
    +        if row:
    
    190
    +            alloc = row.get_allocation()
    
    191
    +            hover_row_y = alloc.y
    
    192
    +            hover_row_height = alloc.height
    
    193
    +
    
    194
    +            if y_coord < hover_row_y + hover_row_height / 2:
    
    195
    +                self._row_before = self._get_row_before(row)
    
    196
    +                self._row_after = row
    
    197
    +            else:
    
    198
    +                self._row_before = row
    
    199
    +                self._row_after = self._get_row_after(row)
    
    200
    +        else:
    
    201
    +            self._row_before = self._get_last_row()
    
    202
    +            self._row_after = None
    
    203
    +
    
    204
    +        if self._drag_row in (self._row_before, self._row_after):
    
    205
    +            return False
    
    206
    +
    
    207
    +        return True
    
    208
    +
    
    209
    +    def _on_drag_data_received(self,
    
    210
    +                               _widget: Gtk.Widget,
    
    211
    +                               _drag_context: Gdk.DragContext,
    
    212
    +                               _x_coord: int,
    
    213
    +                               y_coord: int,
    
    214
    +                               selection_data: Gtk.SelectionData,
    
    215
    +                               _info: int,
    
    216
    +                               _time: int
    
    217
    +                               ) -> None:
    
    218
    +
    
    219
    +        item_type = selection_data.get_data_type().name()
    
    220
    +        if item_type == 'CHAT_LIST_ITEM':
    
    221
    +            self._change_pinned_order()
    
    222
    +        else:
    
    223
    +            log.debug('Unknown item type dropped')
    
    224
    +
    
    225
    +    def _change_pinned_order(self) -> None:
    
    226
    +        assert self._drag_row is not None
    
    227
    +
    
    228
    +        if not self._drag_row.is_pinned:
    
    229
    +            log.debug('Dragged row is not pinned')
    
    230
    +            return
    
    231
    +
    
    232
    +        if self._drag_row in (self._row_before, self._row_after):
    
    233
    +            log.debug('Dragged row dropped on self')
    
    234
    +            return
    
    235
    +
    
    236
    +        if self._row_after is None:
    
    237
    +            log.debug('Dragged row dropped in empty space')
    
    238
    +            return
    
    239
    +
    
    240
    +        self._chat_order.remove(self._drag_row)
    
    241
    +
    
    242
    +        if self._row_before is None:
    
    243
    +            self._chat_order.insert(0, self._drag_row)
    
    244
    +        else:
    
    245
    +            if self._row_before.position < self._drag_row.position:
    
    246
    +                offset = 1
    
    247
    +            else:
    
    248
    +                offset = 0
    
    249
    +            self._chat_order.insert(
    
    250
    +                self._row_before.position + offset, self._drag_row)
    
    251
    +
    
    252
    +        for row in self._chat_order:
    
    253
    +            row.position = self._chat_order.index(row)
    
    254
    +
    
    255
    +        self._row_before = None
    
    256
    +        self._row_after = None
    
    257
    +        self._drag_row = None
    
    258
    +
    
    259
    +        self.emit('chat-order-changed')
    
    260
    +        self.invalidate_sort()
    
    261
    +
    
    149 262
         def _on_destroy(self, _widget: Gtk.Widget) -> None:
    
    150 263
             GLib.source_remove(self._timer_id)
    
    151 264
     
    
    ... ... @@ -198,9 +311,11 @@ def _sort_func(self, row1: ChatRow, row2: ChatRow) -> int:
    198 311
                 log.debug('Mouseover active, don’t sort rows')
    
    199 312
                 return 0
    
    200 313
     
    
    201
    -        # Don’t sort pinned rows themselves
    
    314
    +        # Sort pinned rows according to stored order
    
    202 315
             if row1.is_pinned and row2.is_pinned:
    
    203
    -            return 0
    
    316
    +            if row1.position > row2.position:
    
    317
    +                return 1
    
    318
    +            return -1
    
    204 319
     
    
    205 320
             # Sort pinned rows to top
    
    206 321
             if row1.is_pinned > row2.is_pinned:
    
    ... ... @@ -250,14 +365,30 @@ def get_chat_type(self, account: str, jid: JID) -> Optional[str]:
    250 365
                 return row.type
    
    251 366
             return None
    
    252 367
     
    
    253
    -    def add_chat(self, account: str, jid: JID, type_: str,
    
    254
    -                 pinned: bool = False) -> None:
    
    255
    -        if self._chats.get((account, jid)) is not None:
    
    368
    +    def add_chat(self,
    
    369
    +                 account: str,
    
    370
    +                 jid: JID,
    
    371
    +                 type_: str,
    
    372
    +                 pinned: bool,
    
    373
    +                 position: int
    
    374
    +                 ) -> None:
    
    375
    +
    
    376
    +        key = (account, jid)
    
    377
    +        if self._chats.get(key) is not None:
    
    256 378
                 # Chat is already in the List
    
    257 379
                 return
    
    258 380
     
    
    259
    -        row = ChatRow(self._workspace_id, account, jid, type_, pinned)
    
    260
    -        self._chats[(account, jid)] = row
    
    381
    +        row = ChatRow(self._workspace_id,
    
    382
    +                      account,
    
    383
    +                      jid,
    
    384
    +                      type_,
    
    385
    +                      pinned,
    
    386
    +                      position)
    
    387
    +
    
    388
    +        self._chats[key] = row
    
    389
    +        if position != -1:
    
    390
    +            self._chat_order.insert(position, row)
    
    391
    +
    
    261 392
             self.add(row)
    
    262 393
     
    
    263 394
         def select_chat(self, account: str, jid: JID) -> None:
    
    ... ... @@ -333,11 +464,27 @@ def select_chat_number(self, number: int) -> None:
    333 464
         def toggle_chat_pinned(self, account: str, jid: JID) -> None:
    
    334 465
             row = self._chats[(account, jid)]
    
    335 466
             row.toggle_pinned()
    
    467
    +
    
    468
    +        self._chat_order.clear()
    
    469
    +        chat_rows = cast(list[ChatRow], self.get_children())
    
    470
    +        for row in chat_rows:
    
    471
    +            if row.is_pinned:
    
    472
    +                row.position = chat_rows.index(row)
    
    473
    +                self._chat_order.append(row)
    
    474
    +            else:
    
    475
    +                row.position = -1
    
    476
    +
    
    336 477
             self.invalidate_sort()
    
    337 478
     
    
    338
    -    def remove_chat(self, account: str, jid: JID,
    
    339
    -                    emit_unread: bool = True) -> None:
    
    479
    +    def remove_chat(self,
    
    480
    +                    account: str,
    
    481
    +                    jid: JID,
    
    482
    +                    emit_unread: bool = True
    
    483
    +                    ) -> None:
    
    484
    +
    
    340 485
             row = self._chats.pop((account, jid))
    
    486
    +        if row.position != -1:
    
    487
    +            self._chat_order.remove(row)
    
    341 488
             self.remove(row)
    
    342 489
             row.destroy()
    
    343 490
             if emit_unread:
    
    ... ... @@ -359,10 +506,15 @@ def get_selected_chat(self) -> Optional[ChatRow]:
    359 506
         def contains_chat(self, account: str, jid: JID) -> bool:
    
    360 507
             return self._chats.get((account, jid)) is not None
    
    361 508
     
    
    362
    -    def get_open_chats(self) -> list[tuple[str, JID, str, bool]]:
    
    363
    -        open_chats: list[tuple[str, JID, str, bool]] = []
    
    364
    -        for key, value in self._chats.items():
    
    365
    -            open_chats.append(key + (value.type, value.is_pinned))
    
    509
    +    def get_open_chats(self) -> OpenChatsSettingT:
    
    510
    +        open_chats: OpenChatsSettingT = []
    
    511
    +        for key, row in self._chats.items():
    
    512
    +            account, jid = key
    
    513
    +            open_chats.append({'account': account,
    
    514
    +                               'jid': jid,
    
    515
    +                               'type': row.type,
    
    516
    +                               'pinned': row.is_pinned,
    
    517
    +                               'position': row.position})
    
    366 518
             return open_chats
    
    367 519
     
    
    368 520
         def update_time(self) -> None:
    
    ... ... @@ -536,14 +688,22 @@ def _on_bookmarks_received(self, _event: events.BookmarksReceived) -> None:
    536 688
     
    
    537 689
     
    
    538 690
     class ChatRow(Gtk.ListBoxRow):
    
    539
    -    def __init__(self, workspace_id: str, account: str, jid: JID, type_: str,
    
    540
    -                 pinned: bool) -> None:
    
    691
    +    def __init__(self,
    
    692
    +                 workspace_id: str,
    
    693
    +                 account: str,
    
    694
    +                 jid: JID,
    
    695
    +                 type_: str,
    
    696
    +                 pinned: bool,
    
    697
    +                 position: int
    
    698
    +                 ) -> None:
    
    699
    +
    
    541 700
             Gtk.ListBoxRow.__init__(self)
    
    542 701
     
    
    543 702
             self.account = account
    
    544 703
             self.jid = jid
    
    545 704
             self.workspace_id = workspace_id
    
    546 705
             self.type = type_
    
    706
    +        self._position = position
    
    547 707
     
    
    548 708
             self.active_label = ActiveHeader()
    
    549 709
             self.conversations_label = ConversationsHeader()
    
    ... ... @@ -705,6 +865,14 @@ def header(self, type_: Optional[RowHeaderType]) -> None:
    705 865
         def is_pinned(self) -> bool:
    
    706 866
             return self._pinned
    
    707 867
     
    
    868
    +    @property
    
    869
    +    def position(self) -> int:
    
    870
    +        return self._position
    
    871
    +
    
    872
    +    @position.setter
    
    873
    +    def position(self, position: int) -> None:
    
    874
    +        self._position = position
    
    875
    +
    
    708 876
         def _on_button_press(self,
    
    709 877
                              _widget: Gtk.Widget,
    
    710 878
                              event: Gdk.EventButton
    
    ... ... @@ -735,6 +903,10 @@ def _on_drag_begin(self,
    735 903
                            widget: Gtk.Widget,
    
    736 904
                            drag_context: Gdk.DragContext
    
    737 905
                            ) -> None:
    
    906
    +
    
    907
    +        chat_list = cast(ChatList, self.get_parent())
    
    908
    +        chat_list.set_drag_row(self)
    
    909
    +
    
    738 910
             # Use rendered ChatListRow as drag icon
    
    739 911
             alloc = self.get_allocation()
    
    740 912
             surface = cairo.ImageSurface(
    

  • gajim/gtk/chat_list_stack.py
    ... ... @@ -14,7 +14,6 @@
    14 14
     
    
    15 15
     from __future__ import annotations
    
    16 16
     
    
    17
    -from typing import Dict
    
    18 17
     from typing import Optional
    
    19 18
     from typing import cast
    
    20 19
     
    
    ... ... @@ -66,7 +65,7 @@ def __init__(self,
    66 65
             self.set_vexpand(True)
    
    67 66
             self.set_vhomogeneous(False)
    
    68 67
     
    
    69
    -        self._chat_lists: Dict[str, ChatList] = {}
    
    68
    +        self._chat_lists: dict[str, ChatList] = {}
    
    70 69
     
    
    71 70
             self._last_visible_child_name: str = 'default'
    
    72 71
     
    
    ... ... @@ -153,6 +152,7 @@ def _on_search_changed(self, search_entry: Gtk.SearchEntry) -> None:
    153 152
         def add_chat_list(self, workspace_id: str) -> ChatList:
    
    154 153
             chat_list = ChatList(workspace_id)
    
    155 154
             chat_list.connect('row-selected', self._on_row_selected)
    
    155
    +        chat_list.connect('chat-order-changed', self._on_chat_order_changed)
    
    156 156
     
    
    157 157
             self._chat_lists[workspace_id] = chat_list
    
    158 158
             self.add_named(chat_list, workspace_id)
    
    ... ... @@ -161,8 +161,10 @@ def add_chat_list(self, workspace_id: str) -> ChatList:
    161 161
         def remove_chat_list(self, workspace_id: str) -> None:
    
    162 162
             chat_list = self._chat_lists[workspace_id]
    
    163 163
             self.remove(chat_list)
    
    164
    -        for account, jid, _type, _pinned in chat_list.get_open_chats():
    
    165
    -            self.remove_chat(workspace_id, account, jid)
    
    164
    +        for open_chat in chat_list.get_open_chats():
    
    165
    +            self.remove_chat(workspace_id,
    
    166
    +                             open_chat['account'],
    
    167
    +                             open_chat['jid'])
    
    166 168
     
    
    167 169
             self._chat_lists.pop(workspace_id)
    
    168 170
             chat_list.destroy()
    
    ... ... @@ -177,6 +179,12 @@ def _on_row_selected(self,
    177 179
     
    
    178 180
             self.emit('chat-selected', row.workspace_id, row.account, row.jid)
    
    179 181
     
    
    182
    +    def _on_chat_order_changed(self,
    
    183
    +                               chat_list: ChatList
    
    184
    +                               ) -> None:
    
    185
    +
    
    186
    +        self.store_open_chats(chat_list.workspace_id)
    
    187
    +
    
    180 188
         def show_chat_list(self, workspace_id: str) -> None:
    
    181 189
             cur_workspace_id = self.get_visible_child_name()
    
    182 190
             if cur_workspace_id == workspace_id:
    
    ... ... @@ -187,12 +195,19 @@ def show_chat_list(self, workspace_id: str) -> None:
    187 195
     
    
    188 196
             self.set_visible_child_name(workspace_id)
    
    189 197
     
    
    190
    -    def add_chat(self, workspace_id: str, account: str, jid: JID, type_: str,
    
    191
    -                 pinned: bool = False) -> None:
    
    198
    +    def add_chat(self,
    
    199
    +                 workspace_id: str,
    
    200
    +                 account: str,
    
    201
    +                 jid: JID,
    
    202
    +                 type_: str,
    
    203
    +                 pinned: bool,
    
    204
    +                 position: int
    
    205
    +                 ) -> None:
    
    206
    +
    
    192 207
             chat_list = self._chat_lists.get(workspace_id)
    
    193 208
             if chat_list is None:
    
    194 209
                 chat_list = self.add_chat_list(workspace_id)
    
    195
    -        chat_list.add_chat(account, jid, type_, pinned)
    
    210
    +        chat_list.add_chat(account, jid, type_, pinned, position)
    
    196 211
     
    
    197 212
         def select_chat(self, account: str, jid: JID) -> None:
    
    198 213
             chat_list = self.find_chat(account, jid)
    
    ... ... @@ -235,7 +250,8 @@ def _move_chat_to_workspace(self,
    235 250
             current_chatlist.remove_chat(params.account, params.jid)
    
    236 251
     
    
    237 252
             new_chatlist = self.get_chatlist(workspace_id)
    
    238
    -        new_chatlist.add_chat(params.account, params.jid, type_)
    
    253
    +        new_chatlist.add_chat(params.account, params.jid, type_, False, -1)
    
    254
    +
    
    239 255
             self.store_open_chats(current_chatlist.workspace_id)
    
    240 256
             self.store_open_chats(workspace_id)
    
    241 257
     
    
    ... ... @@ -251,31 +267,38 @@ def remove_chat(self, workspace_id: str, account: str, jid: JID) -> None:
    251 267
             chat_list = self._chat_lists[workspace_id]
    
    252 268
             type_ = chat_list.get_chat_type(account, jid)
    
    253 269
     
    
    254
    -        def _leave(is_checked: bool) -> None:
    
    255
    -            if is_checked:
    
    270
    +        def _leave(not_ask_again: bool) -> None:
    
    271
    +            if not_ask_again:
    
    256 272
                     app.settings.set('confirm_close_muc', False)
    
    273
    +            _remove()
    
    257 274
     
    
    275
    +        def _remove() -> None:
    
    258 276
                 chat_list.remove_chat(account, jid, emit_unread=False)
    
    259 277
                 self.store_open_chats(workspace_id)
    
    260 278
                 self.emit('chat-removed', account, jid, type_)
    
    261 279
     
    
    262
    -        if type_ == 'groupchat' and app.settings.get('confirm_close_muc'):
    
    263
    -            client = app.get_client(account)
    
    264
    -            contact = client.get_module('Contacts').get_group_chat_contact(jid)
    
    265
    -            ConfirmationCheckDialog(
    
    266
    -                _('Leave Group Chat'),
    
    267
    -                _('Are you sure you want to leave this group chat?'),
    
    268
    -                _('If you close this chat, you will leave '
    
    269
    -                  '"%s".') % contact.name,
    
    270
    -                _('_Do not ask me again'),
    
    271
    -                [DialogButton.make('Cancel'),
    
    272
    -                 DialogButton.make('Accept',
    
    273
    -                                   text=_('_Leave'),
    
    274
    -                                   callback=_leave)],
    
    275
    -                transient_for=app.window).show()
    
    280
    +        if type_ != 'groupchat' or not app.settings.get('confirm_close_muc'):
    
    281
    +            _remove()
    
    282
    +            return
    
    283
    +
    
    284
    +        client = app.get_client(account)
    
    285
    +        contact = client.get_module('Contacts').get_group_chat_contact(jid)
    
    286
    +
    
    287
    +        if contact.is_not_joined:
    
    288
    +            _remove()
    
    276 289
                 return
    
    277 290
     
    
    278
    -        _leave(True)
    
    291
    +        ConfirmationCheckDialog(
    
    292
    +            _('Leave Group Chat'),
    
    293
    +            _('Are you sure you want to leave this group chat?'),
    
    294
    +            _('If you close this chat, you will leave '
    
    295
    +              '"%s".') % contact.name,
    
    296
    +            _('_Do not ask me again'),
    
    297
    +            [DialogButton.make('Cancel'),
    
    298
    +             DialogButton.make('Accept',
    
    299
    +                               text=_('_Leave'),
    
    300
    +                               callback=_leave)],
    
    301
    +            transient_for=app.window).show()
    
    279 302
     
    
    280 303
         def remove_chats_for_account(self, account: str) -> None:
    
    281 304
             for workspace_id, chat_list in self._chat_lists.items():
    

  • gajim/gtk/chat_page.py
    ... ... @@ -221,6 +221,7 @@ def add_chat_for_workspace(self,
    221 221
                                    jid: JID,
    
    222 222
                                    type_: str,
    
    223 223
                                    pinned: bool = False,
    
    224
    +                               position: int = -1,
    
    224 225
                                    select: bool = False) -> None:
    
    225 226
     
    
    226 227
             client = app.get_client(account)
    
    ... ... @@ -240,7 +241,7 @@ def add_chat_for_workspace(self,
    240 241
                 return
    
    241 242
     
    
    242 243
             self._chat_list_stack.add_chat(workspace_id, account, jid, type_,
    
    243
    -                                       pinned)
    
    244
    +                                       pinned, position)
    
    244 245
     
    
    245 246
             if self._startup_finished:
    
    246 247
                 if select:
    
    ... ... @@ -252,15 +253,17 @@ def load_workspace_chats(self, workspace_id: str) -> None:
    252 253
                                                             'open_chats')
    
    253 254
     
    
    254 255
             active_accounts = app.settings.get_active_accounts()
    
    255
    -        for account, jid, type_, pinned in open_chats:
    
    256
    +        for open_chat in open_chats:
    
    257
    +            account = open_chat['account']
    
    256 258
                 if account not in active_accounts:
    
    257 259
                     continue
    
    258 260
     
    
    259 261
                 self.add_chat_for_workspace(workspace_id,
    
    260 262
                                             account,
    
    261
    -                                        jid,
    
    262
    -                                        type_,
    
    263
    -                                        pinned=pinned)
    
    263
    +                                        open_chat['jid'],
    
    264
    +                                        open_chat['type'],
    
    265
    +                                        pinned=open_chat['pinned'],
    
    266
    +                                        position=open_chat['position'])
    
    264 267
     
    
    265 268
         def is_chat_active(self, account: str, jid: JID) -> bool:
    
    266 269
             return self._chat_list_stack.is_chat_active(account, jid)
    

  • gajim/gtk/chat_stack.py
    ... ... @@ -34,6 +34,7 @@
    34 34
     from gajim.common import events
    
    35 35
     from gajim.common import helpers
    
    36 36
     from gajim.common import preview_helpers
    
    37
    +from gajim.common.commands import ChatCommands
    
    37 38
     from gajim.common.i18n import _
    
    38 39
     from gajim.common.const import CallType
    
    39 40
     from gajim.common.modules.contacts import BareContact
    
    ... ... @@ -76,10 +77,10 @@ def __init__(self):
    76 77
             self._chat_banner = ChatBanner()
    
    77 78
             self._chat_control = ChatControl()
    
    78 79
             self._message_action_box = MessageActionsBox()
    
    79
    -        self._message_action_box.connect('command-error',
    
    80
    -                                         self._on_command_error)
    
    81
    -        self._message_action_box.connect('command-not-found',
    
    82
    -                                         self._on_command_not_found)
    
    80
    +
    
    81
    +        app.commands.connect('command-error', self._on_command_signal)
    
    82
    +        app.commands.connect('command-not-found', self._on_command_signal)
    
    83
    +        app.commands.connect('command-result', self._on_command_signal)
    
    83 84
     
    
    84 85
             box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    
    85 86
             box.add(self._chat_banner)
    
    ... ... @@ -741,18 +742,14 @@ def clear(self) -> None:
    741 742
             self._message_action_box.clear()
    
    742 743
             self._chat_control.clear()
    
    743 744
     
    
    744
    -    def _on_command_error(self,
    
    745
    -                          message_actions_box: MessageActionsBox,
    
    746
    -                          error: str) -> None:
    
    747
    -
    
    748
    -        self._chat_control.add_command_output(error, True)
    
    749
    -
    
    750
    -    def _on_command_not_found(self,
    
    751
    -                              message_actions_box: MessageActionsBox,
    
    752
    -                              error: str
    
    753
    -                              ) -> None:
    
    745
    +    def _on_command_signal(self,
    
    746
    +                           chat_commands: ChatCommands,
    
    747
    +                           signal_name: str,
    
    748
    +                           text: str
    
    749
    +                           ) -> None:
    
    754 750
     
    
    755
    -        self._chat_control.add_command_output(error, True)
    
    751
    +        is_error = signal_name != 'command-result'
    
    752
    +        self._chat_control.add_command_output(text, is_error)
    
    756 753
     
    
    757 754
     
    
    758 755
     class ChatPlaceholderBox(Gtk.Box):
    

  • gajim/gtk/const.py
    ... ... @@ -171,7 +171,6 @@ def __str__(self):
    171 171
         ('file-transfer', None),
    
    172 172
         ('forget-groupchat', 'a{sv}'),
    
    173 173
         ('groupchat-join', 'as'),
    
    174
    -    ('ipython', None),
    
    175 174
         ('join-support-chat', None),
    
    176 175
         ('manage-proxies', None),
    
    177 176
         ('open-link', 'as'),
    

  • gajim/gtk/main.py
    ... ... @@ -305,7 +305,7 @@ def _add_actions(self) -> None:
    305 305
             actions = [
    
    306 306
                 ('add-workspace', 's', self._add_workspace),
    
    307 307
                 ('edit-workspace', 's', self._edit_workspace),
    
    308
    -            ('remove-workspace', None, self._remove_workspace),
    
    308
    +            ('remove-workspace', 's', self._remove_workspace),
    
    309 309
                 ('activate-workspace', 's', self._activate_workspace),
    
    310 310
                 ('add-chat', 'a{sv}', self._add_chat),
    
    311 311
                 ('add-group-chat', 'as', self._add_group_chat),
    
    ... ... @@ -616,9 +616,12 @@ def _edit_workspace(self,
    616 616
     
    
    617 617
         def _remove_workspace(self,
    
    618 618
                               _action: Gio.SimpleAction,
    
    619
    -                          _param: Optional[GLib.Variant]) -> None:
    
    619
    +                          param: GLib.Variant) -> None:
    
    620
    +
    
    621
    +        workspace_id = param.get_string() or None
    
    622
    +        if workspace_id is None:
    
    623
    +            workspace_id = self.get_active_workspace()
    
    620 624
     
    
    621
    -        workspace_id = self.get_active_workspace()
    
    622 625
             if workspace_id is not None:
    
    623 626
                 self.remove_workspace(workspace_id)
    
    624 627
     
    

  • gajim/gtk/menus.py
    ... ... @@ -574,6 +574,15 @@ def get_format_menu() -> GajimMenu:
    574 574
         return GajimMenu.from_list(menuitems)
    
    575 575
     
    
    576 576
     
    
    577
    +def get_workspace_menu(workspace_id: str) -> GajimMenu:
    
    578
    +    menuitems: MenuItemListT = [
    
    579
    +        (_('Edit…'), 'win.edit-workspace', f'"{workspace_id}"'),
    
    580
    +        (_('Remove'), 'win.remove-workspace', f'"{workspace_id}"'),
    
    581
    +    ]
    
    582
    +
    
    583
    +    return GajimMenu.from_list(menuitems)
    
    584
    +
    
    585
    +
    
    577 586
     def escape_mnemonic(label: Optional[str]) -> Optional[str]:
    
    578 587
         if label is None:
    
    579 588
             return None
    

  • gajim/gtk/message_actions_box.py
    ... ... @@ -37,9 +37,7 @@
    37 37
     from gajim.common import app
    
    38 38
     from gajim.common import events
    
    39 39
     from gajim.common import ged
    
    40
    -from gajim.common.commands import ArgumentParserError
    
    41
    -from gajim.common.commands import CommandError
    
    42
    -from gajim.common.commands import CommandNotFoundError
    
    40
    +from gajim.common.commands import CommandFailed
    
    43 41
     from gajim.common.i18n import _
    
    44 42
     from gajim.common.client import Client
    
    45 43
     from gajim.common.const import SimpleClientState
    
    ... ... @@ -67,12 +65,6 @@
    67 65
     
    
    68 66
     
    
    69 67
     class MessageActionsBox(Gtk.Grid, ged.EventHelper):
    
    70
    -
    
    71
    -    __gsignals__ = {
    
    72
    -        'command-error': (GObject.SignalFlags.RUN_FIRST, None, (str,)),
    
    73
    -        'command-not-found': (GObject.SignalFlags.RUN_FIRST, None, (str,))
    
    74
    -    }
    
    75
    -
    
    76 68
         def __init__(self) -> None:
    
    77 69
             Gtk.Grid.__init__(self)
    
    78 70
             ged.EventHelper.__init__(self)
    
    ... ... @@ -544,10 +536,8 @@ def _on_msg_textview_key_press_event(self,
    544 536
                         not message.startswith('/me ')):
    
    545 537
                     try:
    
    546 538
                         app.commands.parse(self._contact.type_string, message)
    
    547
    -                except (ArgumentParserError, CommandError) as error:
    
    548
    -                    self.emit('command-error', error)
    
    549
    -                except CommandNotFoundError as error:
    
    550
    -                    self.emit('command-not-found', error)
    
    539
    +                except CommandFailed:
    
    540
    +                    pass
    
    551 541
                     else:
    
    552 542
                         self.msg_textview.clear()
    
    553 543
                     return True
    

  • gajim/gtk/profile.py
    ... ... @@ -94,7 +94,7 @@ def __init__(self, account: str) -> None:
    94 94
             self._add_actions()
    
    95 95
     
    
    96 96
             self._avatar_selector: Optional[AvatarSelector] = None
    
    97
    -        self._current_avatar: Optional[cairo.Surface] = None
    
    97
    +        self._current_avatar: Optional[cairo.ImageSurface] = None
    
    98 98
             self._current_vcard: Optional[VCard] = None
    
    99 99
             self._avatar_nick_public: Optional[bool] = None
    
    100 100
     
    

  • gajim/gtk/util.py
    ... ... @@ -106,7 +106,7 @@ def load_icon_surface(
    106 106
             size: int = 16,
    
    107 107
             scale: Optional[int] = None,
    
    108 108
             flags: Gtk.IconLookupFlags = Gtk.IconLookupFlags.FORCE_SIZE
    
    109
    -) -> Optional[cairo.Surface]:
    
    109
    +) -> Optional[cairo.ImageSurface]:
    
    110 110
     
    
    111 111
         icon_info = load_icon_info(icon_name, size, scale, flags)
    
    112 112
         if icon_info is None:
    

  • gajim/gtk/workspace_side_bar.py
    ... ... @@ -15,7 +15,6 @@
    15 15
     from __future__ import annotations
    
    16 16
     
    
    17 17
     from typing import Any
    
    18
    -from typing import List
    
    19 18
     from typing import Optional
    
    20 19
     from typing import cast
    
    21 20
     
    
    ... ... @@ -23,8 +22,6 @@
    23 22
     import pickle
    
    24 23
     
    
    25 24
     from gi.repository import Gdk
    
    26
    -from gi.repository import Gio
    
    27
    -from gi.repository import GLib
    
    28 25
     from gi.repository import Gtk
    
    29 26
     from nbxmpp.protocol import JID
    
    30 27
     
    
    ... ... @@ -34,6 +31,7 @@
    34 31
     
    
    35 32
     from .chat_list_stack import ChatListStack
    
    36 33
     from .chat_page import ChatPage
    
    34
    +from .menus import get_workspace_menu
    
    37 35
     from .structs import ChatListEntryParam
    
    38 36
     from .util import open_window
    
    39 37
     
    
    ... ... @@ -323,7 +321,7 @@ def _popup_menu(self, _widget: Gtk.Widget, event: Gdk.EventButton) -> None:
    323 321
             if event.button != 3:  # right click
    
    324 322
                 return
    
    325 323
     
    
    326
    -        menu = self._get_workspace_menu()
    
    324
    +        menu = get_workspace_menu(self.workspace_id)
    
    327 325
     
    
    328 326
             rectangle = Gdk.Rectangle()
    
    329 327
             rectangle.x = int(event.x)
    
    ... ... @@ -336,20 +334,6 @@ def _popup_menu(self, _widget: Gtk.Widget, event: Gdk.EventButton) -> None:
    336 334
             popover.set_pointing_to(rectangle)
    
    337 335
             popover.popup()
    
    338 336
     
    
    339
    -    def _get_workspace_menu(self) -> Gio.Menu:
    
    340
    -        menu_items: List[Any] = [
    
    341
    -            ('edit-workspace', _('Edit…')),
    
    342
    -        ]
    
    343
    -        menu = Gio.Menu()
    
    344
    -        for item in menu_items:
    
    345
    -            action, label = item
    
    346
    -            action = f'win.{action}'
    
    347
    -            menuitem = Gio.MenuItem.new(label, action)
    
    348
    -            variant = GLib.Variant('s', self.workspace_id)
    
    349
    -            menuitem.set_action_and_target_value(action, variant)
    
    350
    -            menu.append_item(menuitem)
    
    351
    -        return menu
    
    352
    -
    
    353 337
         def update_avatar(self) -> None:
    
    354 338
             self._image.update()
    
    355 339
     
    

  • gajim/gui_interface.py
    ... ... @@ -38,13 +38,11 @@
    38 38
     from typing import Optional
    
    39 39
     from typing import Union
    
    40 40
     
    
    41
    -import os
    
    42 41
     import sys
    
    43 42
     import time
    
    44 43
     import logging
    
    45 44
     from functools import partial
    
    46 45
     from threading import Thread
    
    47
    -from importlib.util import find_spec
    
    48 46
     
    
    49 47
     from gi.repository import Gtk
    
    50 48
     from gi.repository import GLib
    
    ... ... @@ -678,47 +676,6 @@ def save_avatar(self, data: bytes) -> Optional[str]:
    678 676
         def avatar_exists(self, filename: str) -> bool:
    
    679 677
             return app.app.avatar_storage.get_avatar_path(filename) is not None
    
    680 678
     
    
    681
    -    @staticmethod
    
    682
    -    def create_ipython_window() -> None:
    
    683
    -        # Check if IPython is installed
    
    684
    -        ipython = find_spec('IPython')
    
    685
    -        is_installed = ipython is not None
    
    686
    -        if not is_installed:
    
    687
    -            # Abort early to avoid tracebacks
    
    688
    -            print('IPython is not installed')
    
    689
    -            return
    
    690
    -        try:
    
    691
    -            from gajim.dev.ipython_view import IPythonView
    
    692
    -        except ImportError:
    
    693
    -            print('ipython_view not found')
    
    694
    -            return
    
    695
    -        from gi.repository import Pango
    
    696
    -
    
    697
    -        if os.name == 'nt':
    
    698
    -            font = 'Lucida Console 9'
    
    699
    -        else:
    
    700
    -            font = 'Luxi Mono 10'
    
    701
    -
    
    702
    -        window = Gtk.Window()
    
    703
    -        window.set_title(_('Gajim: IPython Console'))
    
    704
    -        window.set_size_request(750, 550)
    
    705
    -        window.set_resizable(True)
    
    706
    -        sw = Gtk.ScrolledWindow()
    
    707
    -        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    
    708
    -        view = IPythonView()
    
    709
    -        view.override_font(Pango.FontDescription(font))
    
    710
    -        view.set_wrap_mode(Gtk.WrapMode.CHAR)
    
    711
    -        sw.add(view)
    
    712
    -        window.add(sw)
    
    713
    -        window.show_all()
    
    714
    -
    
    715
    -        def _on_delete(win, _event):
    
    716
    -            win.hide()
    
    717
    -            return True
    
    718
    -        window.connect('delete_event', _on_delete)
    
    719
    -        view.updateNamespace({'gajim': app})
    
    720
    -        app.ipython_window = window
    
    721
    -
    
    722 679
         def run(self, _application: Gtk.Application) -> None:
    
    723 680
             # Creating plugin manager
    
    724 681
             from gajim import plugins
    

  • pyrightconfig.json
    ... ... @@ -15,15 +15,13 @@
    15 15
             ".ci/*",
    
    16 16
             "scripts/*",
    
    17 17
             "win/*",
    
    18
    -        "gajim/gajim.py",
    
    19
    -        "gajim/gajim_remote.py",
    
    20
    -        "gajim/common/dbus/*",
    
    21 18
             "gajim/common/application.py",
    
    22 19
             "gajim/common/call_manager.py",
    
    23 20
             "gajim/common/cert_store.py",
    
    24 21
             "gajim/common/commands.py",
    
    25 22
             "gajim/common/configpaths.py",
    
    26 23
             "gajim/common/const.py",
    
    24
    +        "gajim/common/dbus/*",
    
    27 25
             "gajim/common/events.py",
    
    28 26
             "gajim/common/exceptions.py",
    
    29 27
             "gajim/common/filetransfer.py",
    
    ... ... @@ -34,15 +32,16 @@
    34 32
             "gajim/common/modules/chat_markers.py",
    
    35 33
             "gajim/common/modules/pep.py",
    
    36 34
             "gajim/common/modules/register.py",
    
    37
    -        "gajim/common/modules/vcard_temp.py",
    
    38 35
             "gajim/common/modules/vcard4.py",
    
    36
    +        "gajim/common/modules/vcard_temp.py",
    
    39 37
             "gajim/common/passwords.py",
    
    40 38
             "gajim/common/regex.py",
    
    41 39
             "gajim/common/setting_values.py",
    
    42 40
             "gajim/common/sound.py",
    
    43 41
             "gajim/common/storage/",
    
    44 42
             "gajim/common/task_manager.py",
    
    45
    -        "gajim/gtk/conversation",
    
    43
    +        "gajim/gajim.py",
    
    44
    +        "gajim/gajim_remote.py",
    
    46 45
             "gajim/gtk/about.py",
    
    47 46
             "gajim/gtk/account_page.py",
    
    48 47
             "gajim/gtk/account_side_bar.py",
    
    ... ... @@ -51,6 +50,7 @@
    51 50
             "gajim/gtk/app_side_bar.py",
    
    52 51
             "gajim/gtk/application.py",
    
    53 52
             "gajim/gtk/apply_button_box.py",
    
    53
    +        "gajim/gtk/avatar.py",
    
    54 54
             "gajim/gtk/avatar_selector.py",
    
    55 55
             "gajim/gtk/blocking.py",
    
    56 56
             "gajim/gtk/bookmarks.py",
    
    ... ... @@ -62,8 +62,8 @@
    62 62
             "gajim/gtk/chat_banner.py",
    
    63 63
             "gajim/gtk/chat_filter.py",
    
    64 64
             "gajim/gtk/chat_function_page.py",
    
    65
    -        "gajim/gtk/chat_list_stack.py",
    
    66 65
             "gajim/gtk/chat_list.py",
    
    66
    +        "gajim/gtk/chat_list_stack.py",
    
    67 67
             "gajim/gtk/chat_page.py",
    
    68 68
             "gajim/gtk/chat_stack.py",
    
    69 69
             "gajim/gtk/const.py",
    
    ... ... @@ -71,6 +71,7 @@
    71 71
             "gajim/gtk/contact_settings.py",
    
    72 72
             "gajim/gtk/contacts_flowbox.py",
    
    73 73
             "gajim/gtk/control.py",
    
    74
    +        "gajim/gtk/conversation/*",
    
    74 75
             "gajim/gtk/dialogs.py",
    
    75 76
             "gajim/gtk/emoji_data_gtk.py",
    
    76 77
             "gajim/gtk/exception.py",
    
    ... ... @@ -83,8 +84,8 @@
    83 84
             "gajim/gtk/groupchat_invitation.py",
    
    84 85
             "gajim/gtk/groupchat_inviter.py",
    
    85 86
             "gajim/gtk/groupchat_join.py",
    
    86
    -        "gajim/gtk/groupchat_nick_completion.py",
    
    87 87
             "gajim/gtk/groupchat_nick.py",
    
    88
    +        "gajim/gtk/groupchat_nick_completion.py",
    
    88 89
             "gajim/gtk/groupchat_outcasts.py",
    
    89 90
             "gajim/gtk/groupchat_roster.py",
    
    90 91
             "gajim/gtk/groupchat_settings.py",
    
    ... ... @@ -92,20 +93,20 @@
    92 93
             "gajim/gtk/gstreamer.py",
    
    93 94
             "gajim/gtk/history_export.py",
    
    94 95
             "gajim/gtk/history_sync.py",
    
    95
    -        "gajim/gtk/main_stack.py",
    
    96 96
             "gajim/gtk/main.py",
    
    97
    +        "gajim/gtk/main_stack.py",
    
    97 98
             "gajim/gtk/mam_preferences.py",
    
    98 99
             "gajim/gtk/manage_sounds.py",
    
    99 100
             "gajim/gtk/menus.py",
    
    100 101
             "gajim/gtk/message_actions_box.py",
    
    101 102
             "gajim/gtk/message_input.py",
    
    102
    -        "gajim/gtk/notification_manager.py",
    
    103 103
             "gajim/gtk/notification.py",
    
    104
    +        "gajim/gtk/notification_manager.py",
    
    104 105
             "gajim/gtk/password_dialog.py",
    
    105 106
             "gajim/gtk/pep_config.py",
    
    106 107
             "gajim/gtk/plugins.py",
    
    107
    -        "gajim/gtk/preview_audio.py",
    
    108 108
             "gajim/gtk/preview.py",
    
    109
    +        "gajim/gtk/preview_audio.py",
    
    109 110
             "gajim/gtk/proxies.py",
    
    110 111
             "gajim/gtk/remove_account.py",
    
    111 112
             "gajim/gtk/resource_selector.py",
    
    ... ... @@ -123,6 +124,7 @@
    123 124
             "gajim/gtk/util.py",
    
    124 125
             "gajim/gtk/video_preview.py",
    
    125 126
             "gajim/gtk/workspace_dialog.py",
    
    127
    +        "gajim/gtk/workspace_side_bar.py",
    
    126 128
             "gajim/gtk/xml_console.py",
    
    127 129
             "gajim/plugins/gajimplugin.py",
    
    128 130
             "gajim/plugins/helpers.py",