tab.py 22.4 KB
Newer Older
1
# Copyright 2010 Le Coz Florent <louiz@louiz.org>
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# Poezio is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Poezio.  If not, see <http://www.gnu.org/licenses/>.

"""
a Tab object is a way to organize various Window (see window.py)
around the screen at once.
A tab is then composed of multiple Window.
Each Tab object has different refresh() and resize() methods, defining of its
Window are displayed, etc
"""

MIN_WIDTH = 50
26
MIN_HEIGHT = 16
27 28 29

import window
import theme
30
import curses
31
from config import config
32
from roster import RosterGroup, roster
33
from contact import Contact, Resource
34 35 36 37

class Tab(object):
    number = 0

38 39
    def __init__(self, stdscr, core):
        self.core = core        # a pointer to core, to access its attributes (ugly?)
40 41 42 43 44 45 46 47
        self.nb = Tab.number
        Tab.number += 1
        self.size = (self.height, self.width) = stdscr.getmaxyx()
        if self.height < MIN_HEIGHT or self.width < MIN_WIDTH:
            self.visible = False
        else:
            self.visible = True

48
    def refresh(self, tabs, informations, roster):
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
        """
        Called on each screen refresh (when something has changed)
        """
        raise NotImplementedError

    def resize(self, stdscr):
        self.size = (self.height, self.width) = stdscr.getmaxyx()
        if self.height < MIN_HEIGHT or self.width < MIN_WIDTH:
            self.visible = False
        else:
            self.visible = True

    def get_color_state(self):
        """
        returns the color that should be used in the GlobalInfoBar
        """
        raise NotImplementedError

    def set_color_state(self, color):
        """
        set the color state
        """
        raise NotImplementedError

    def get_name(self):
        """
        get the name of the tab
        """
        raise NotImplementedError

    def on_input(self, key):
        raise NotImplementedError

    def on_lose_focus(self):
        """
        called when this tab loses the focus.
        """
        raise NotImplementedError

    def on_gain_focus(self):
        """
        called when this tab gains the focus.
        """
        raise NotImplementedError

    def add_message(self):
        """
        Adds a message in the tab.
        If the tab cannot add a message in itself (for example
        FormTab, where text is not intented to be appened), it returns False.
        If the tab can, it returns True
        """
        raise NotImplementedError

    def on_scroll_down(self):
        """
        Defines what happens when we scrol down
        """
        raise NotImplementedError

    def on_scroll_up(self):
        """
        Defines what happens when we scrol down
        """
        raise NotImplementedError

    def on_info_win_size_changed(self, size, stdscr):
        """
        Called when the window with the informations is resized
        """
        raise NotImplementedError

121 122 123 124 125 126 127 128
    def just_before_refresh(self):
        """
        Method called just before the screen refresh.
        Particularly useful to move the cursor at the
        correct position.
        """
        raise NotImplementedError

129 130 131 132 133
class InfoTab(Tab):
    """
    The information tab, used to display global informations
    when using a anonymous account
    """
134 135
    def __init__(self, stdscr, core, name):
        Tab.__init__(self, stdscr, core)
136 137 138 139 140 141 142 143 144 145 146 147
        self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.text_win = window.TextWin(self.height-2, self.width, 0, 0, stdscr, self.visible)
        self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible)
        self.name = name
        self.color_state = theme.COLOR_TAB_NORMAL

    def resize(self, stdscr):
        Tab.resize(self, stdscr)
        self.tab_win.resize(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.text_win.resize(self.height-2, self.width, 0, 0, stdscr, self.visible)
        self.input.resize(1, self.width, self.height-1, 0, stdscr, self.visible)

148
    def refresh(self, tabs, informations, _):
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
        self.text_win.refresh(informations)
        self.tab_win.refresh(tabs, tabs[0])
        self.input.refresh()

    def get_name(self):
        return self.name

    def get_color_state(self):
        return self.color_state

    def set_color_state(self, color):
        return

    def on_input(self, key):
        return self.input.do_command(key)

    def on_lose_focus(self):
        self.color_state = theme.COLOR_TAB_NORMAL

    def on_gain_focus(self):
        self.color_state = theme.COLOR_TAB_CURRENT
170
        curses.curs_set(0)
171 172 173 174 175 176 177 178 179 180

    def on_scroll_up(self):
        pass

    def on_scroll_down(self):
        pass

    def on_info_win_size_changed(self, size, stdscr):
        return

181 182 183
    def just_before_refresh(self):
        return

184 185 186 187 188
class MucTab(Tab):
    """
    The tab containing a multi-user-chat room.
    It contains an userlist, an input, a topic, an information and a chat zone
    """
189
    def __init__(self, stdscr, core, room):
190 191 192 193 194
        """
        room is a Room object
        The stdscr is passed to know the size of the
        terminal
        """
195
        Tab.__init__(self, stdscr, core)
196 197
        self._room = room
        self.topic_win = window.Topic(1, self.width, 0, 0, stdscr, self.visible)
198
        self.text_win = window.TextWin(self.height-4-self.core.information_win_size, (self.width//10)*9, 1, 0, stdscr, self.visible)
199 200
        self.v_separator = window.VerticalSeparator(self.height-3, 1, 1, 9*(self.width//10), stdscr, self.visible)
        self.user_win = window.UserList(self.height-3, (self.width//10), 1, 9*(self.width//10)+1, stdscr, self.visible)
201 202
        self.info_header = window.MucInfoWin(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win = window.TextWin(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
203 204 205 206 207 208 209 210 211 212
        self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible)

    def resize(self, stdscr):
        """
        Resize the whole window. i.e. all its sub-windows
        """
        Tab.resize(self, stdscr)
        text_width = (self.width//10)*9
        self.topic_win.resize(1, self.width, 0, 0, stdscr, self.visible)
213
        self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, stdscr, self.visible)
214 215
        self.v_separator.resize(self.height-3, 1, 1, 9*(self.width//10), stdscr, self.visible)
        self.user_win.resize(self.height-3, self.width-text_width-1, 1, text_width+1, stdscr, self.visible)
216 217
        self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
218 219 220
        self.tab_win.resize(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.input.resize(1, self.width, self.height-1, 0, stdscr, self.visible)

221
    def refresh(self, tabs, informations, _):
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        self.topic_win.refresh(self._room.topic)
        self.text_win.refresh(self._room)
        self.v_separator.refresh()
        self.user_win.refresh(self._room.users)
        self.info_header.refresh(self._room)
        self.info_win.refresh(informations)
        self.tab_win.refresh(tabs, tabs[0])
        self.input.refresh()

    def on_input(self, key):
        self.key_func = {
            "\t": self.completion,
            "^I": self.completion,
            "KEY_BTAB": self.last_words_completion,
            }
        if key in self.key_func:
            return self.key_func[key]()
        return self.input.do_command(key)

    def completion(self):
        """
        Called when Tab is pressed, complete the nickname in the input
        """
        compare_users = lambda x: x.last_talked
        self.input.auto_completion([user.nick for user in sorted(self._room.users, key=compare_users, reverse=True)])

    def last_words_completion(self):
        """
        Complete the input with words recently said
        """
        # build the list of the recent words
        char_we_dont_want = [',', '(', ')', '.']
        words = list()
        for msg in self._room.messages[:-40:-1]:
            if not msg:
                continue
            for char in char_we_dont_want:
                msg.txt.replace(char, ' ')
            for word in msg.txt.split():
                if len(word) > 5:
                    words.append(word)
        self.input.auto_completion(words, False)

    def get_color_state(self):
        """
        """
        return self._room.color_state

    def set_color_state(self, color):
        """
        """
        self._room.set_color_state(color)

    def get_name(self):
        """
        """
        return self._room.name

    def get_room(self):
        return self._room

    def on_lose_focus(self):
        self._room.set_color_state(theme.COLOR_TAB_NORMAL)
        self._room.remove_line_separator()
286
        self._room.add_line_separator()
287 288 289

    def on_gain_focus(self):
        self._room.set_color_state(theme.COLOR_TAB_CURRENT)
290
        curses.curs_set(1)
291 292 293 294 295 296 297

    def on_scroll_up(self):
        self._room.scroll_up(self.text_win.height-1)

    def on_scroll_down(self):
        self._room.scroll_down(self.text_win.height-1)

298
    def on_info_win_size_changed(self, stdscr):
299
        text_width = (self.width//10)*9
300 301 302
        self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, stdscr, self.visible)
        self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
303

304 305 306
    def just_before_refresh(self):
        self.input.move_cursor_to_pos()

307 308 309 310
class PrivateTab(Tab):
    """
    The tab containg a private conversation (someone from a MUC)
    """
311 312
    def __init__(self, stdscr, core, room):
        Tab.__init__(self, stdscr, core)
313
        self._room = room
314 315 316
        self.text_win = window.TextWin(self.height-3-self.core.information_win_size, self.width, 0, 0, stdscr, self.visible)
        self.info_header = window.PrivateInfoWin(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win = window.TextWin(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
317 318 319 320
        self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible)

    def resize(self, stdscr):
321
        Tab.resize(self, stdscr)
322 323 324
        self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, stdscr, self.visible)
        self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
325 326 327
        self.tab_win.resize(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.input.resize(1, self.width, self.height-1, 0, stdscr, self.visible)

328
    def refresh(self, tabs, informations, _):
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
        self.text_win.refresh(self._room)
        self.info_header.refresh(self._room)
        self.info_win.refresh(informations)
        self.tab_win.refresh(tabs, tabs[0])
        self.input.refresh()

    def get_color_state(self):
        if self._room.color_state == theme.COLOR_TAB_NORMAL or\
                self._room.color_state == theme.COLOR_TAB_CURRENT:
            return self._room.color_state
        return theme.COLOR_TAB_PRIVATE

    def set_color_state(self, color):
        self._room.color_state = color

    def get_name(self):
        return self._room.name

    def on_input(self, key):
        return self.input.do_command(key)

    def on_lose_focus(self):
        self._room.set_color_state(theme.COLOR_TAB_NORMAL)
        self._room.remove_line_separator()
353
        self._room.add_line_separator()
354 355 356

    def on_gain_focus(self):
        self._room.set_color_state(theme.COLOR_TAB_CURRENT)
357
        curses.curs_set(1)
358 359 360 361 362 363 364

    def on_scroll_up(self):
        self._room.scroll_up(self.text_win.height-1)

    def on_scroll_down(self):
        self._room.scroll_down(self.text_win.height-1)

365 366 367 368
    def on_info_win_size_changed(self, stdscr):
        self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, stdscr, self.visible)
        self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
369 370 371

    def get_room(self):
        return self._room
372

373 374 375
    def just_before_refresh(self):
        return

376 377
class RosterInfoTab(Tab):
    """
378
    A tab, splitted in two, containing the roster and infos
379
    """
380
    def __init__(self, stdscr, core):
381 382 383 384 385 386 387 388 389
        self.single_key_commands = {
            "^J": self.on_enter,
            "^M": self.on_enter,
            "\n": self.on_enter,
            ' ': self.on_space,
            "/": self.on_slash,
            "KEY_UP": self.move_cursor_up,
            "KEY_DOWN": self.move_cursor_down,
            "o": self.toggle_offline_show,
390
            "^F": self.start_search,
391
            }
392
        Tab.__init__(self, stdscr, core)
393 394 395 396 397 398
        self.name = "Roster"
        roster_width = self.width//2
        info_width = self.width-roster_width-1
        self.v_separator = window.VerticalSeparator(self.height-2, 1, 0, roster_width, stdscr, self.visible)
        self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.info_win = window.TextWin(self.height-2, info_width, 0, roster_width+1, stdscr, self.visible)
399 400
        self.roster_win = window.RosterWin(self.height-2-3, roster_width, 0, 0, stdscr, self.visible)
        self.contact_info_win = window.ContactInfoWin(3, roster_width, self.height-2-3, 0, stdscr, self.visible)
401
        self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible, False, "Enter commands with “/”. “o”: toggle offline show")
402 403 404 405 406 407 408 409 410
        self.set_color_state(theme.COLOR_TAB_NORMAL)

    def resize(self, stdscr):
        Tab.resize(self, stdscr)
        roster_width = self.width//2
        info_width = self.width-roster_width-1
        self.v_separator.resize(self.height-2, 1, 0, roster_width, stdscr, self.visible)
        self.tab_win.resize(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.info_win.resize(self.height-2, info_width, 0, roster_width+1, stdscr, self.visible)
411 412
        self.roster_win.resize(self.height-2-3, roster_width, 0, 0, stdscr, self.visible)
        self.contact_info_win.resize(3, roster_width, self.height-2-3, 0, stdscr, self.visible)
413 414
        self.input.resize(1, self.width, self.height-1, 0, stdscr, self.visible)

415
    def refresh(self, tabs, informations, roster):
416
        self.v_separator.refresh()
417
        self.roster_win.refresh(roster)
418
        self.contact_info_win.refresh(self.roster_win.get_selected_row())
419 420 421 422 423 424 425 426 427 428 429 430 431 432
        self.info_win.refresh(informations)
        self.tab_win.refresh(tabs, tabs[0])
        self.input.refresh()

    def get_name(self):
        return self.name

    def get_color_state(self):
        return self._color_state

    def set_color_state(self, color):
        self._color_state = color

    def on_input(self, key):
433 434
        if self.input.input_mode:
            ret = self.input.do_command(key)
435
            roster._contact_filter = (jid_and_name_match, self.input.text)
436
            # if the input is empty, go back to command mode
437
            if self.input.is_empty() and not self.input._instructions:
438 439 440
                self.input.input_mode = False
                curses.curs_set(0)
                self.input.rewrite_text()
441 442
            if self.input._instructions:
                return True
443 444 445 446 447 448 449 450 451 452 453 454 455 456
            return ret
        if key in self.single_key_commands:
            return self.single_key_commands[key]()

    def toggle_offline_show(self):
        """
        Show or hide offline contacts
        """
        option = 'roster_show_offline'
        if config.get(option, 'false') == 'false':
            config.set_and_save(option, 'true')
        else:
            config.set_and_save(option, 'false')
        return True
457

458 459 460 461 462 463 464
    def on_slash(self):
        """
        '/' is pressed, we enter "input mode"
        """
        self.input.input_mode = True
        curses.curs_set(1)
        self.on_input("/") # we add the slash
465 466 467 468 469 470

    def on_lose_focus(self):
        self._color_state = theme.COLOR_TAB_NORMAL

    def on_gain_focus(self):
        self._color_state = theme.COLOR_TAB_CURRENT
471
        curses.curs_set(0)
472 473 474 475

    def add_message(self):
        return False

476
    def move_cursor_down(self):
477
        self.roster_win.move_cursor_down()
478
        return True
479

480
    def move_cursor_up(self):
481
        self.roster_win.move_cursor_up()
482 483 484 485 486 487 488 489 490
        return True

    def on_scroll_down(self):
        # Scroll info win
        pass

    def on_scroll_up(self):
        # Scroll info down
        pass
491

492 493 494
    def on_info_win_size_changed(self, _, __):
        pass

495 496 497 498 499 500
    def on_space(self):
        selected_row = self.roster_win.get_selected_row()
        if isinstance(selected_row, RosterGroup) or\
                isinstance(selected_row, Contact):
            selected_row.toggle_folded()
            return True
501

502
    def on_enter(self):
503 504
        selected_row = self.roster_win.get_selected_row()
        return selected_row
505

506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
    def start_search(self):
        """
        Start the search. The input should appear with a short instruction
        in it.
        """
        curses.curs_set(1)
        roster._contact_filter = (jid_and_name_match, self.input.text)
        self.input.input_mode = True
        self.input.start_command(self.on_search_terminate, self.on_search_terminate, '[search]')
        return True

    def on_search_terminate(self, txt):
        curses.curs_set(0)
        roster._contact_filter = None
        return True

522 523 524
    def just_before_refresh(self):
        return

525 526 527 528
class ConversationTab(Tab):
    """
    The tab containg a normal conversation (someone from our roster)
    """
529
    def __init__(self, stdscr, core, text_buffer, jid):
530
        Tab.__init__(self, stdscr, core)
531 532 533 534 535
        self._text_buffer = text_buffer
        self.color_state = theme.COLOR_TAB_NORMAL
        self._name = jid        # a conversation tab is linked to one specific full jid OR bare jid
        self.text_win = window.TextWin(self.height-4-self.core.information_win_size, self.width, 1, 0, stdscr, self.visible)
        self.upper_bar = window.ConversationStatusMessageWin(1, self.width, 0, 0, stdscr, self.visible)
536 537
        self.info_header = window.ConversationInfoWin(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win = window.TextWin(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
538 539 540 541 542
        self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible)

    def resize(self, stdscr):
        Tab.resize(self, stdscr)
543
        self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, stdscr, self.visible)
544
        self.upper_bar.resize(1, self.width, 0, 0, stdscr, self.visible)
545 546
        self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
547 548 549
        self.tab_win.resize(1, self.width, self.height-2, 0, stdscr, self.visible)
        self.input.resize(1, self.width, self.height-1, 0, stdscr, self.visible)

550
    def refresh(self, tabs, informations, roster):
551 552 553
        self.text_win.refresh(self._text_buffer)
        self.upper_bar.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name()))
        self.info_header.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name()), self._text_buffer)
554 555 556 557 558
        self.info_win.refresh(informations)
        self.tab_win.refresh(tabs, tabs[0])
        self.input.refresh()

    def get_color_state(self):
559 560 561
        if self.color_state == theme.COLOR_TAB_NORMAL or\
                self.color_state == theme.COLOR_TAB_CURRENT:
            return self.color_state
562 563 564
        return theme.COLOR_TAB_PRIVATE

    def set_color_state(self, color):
565
        self.color_state = color
566 567

    def get_name(self):
568
        return self._name
569 570 571 572 573

    def on_input(self, key):
        return self.input.do_command(key)

    def on_lose_focus(self):
574 575 576
        self.set_color_state(theme.COLOR_TAB_NORMAL)
        self._text_buffer.remove_line_separator()
        self._text_buffer.add_line_separator()
577 578

    def on_gain_focus(self):
579 580
        self.set_color_state(theme.COLOR_TAB_CURRENT)
        curses.curs_set(1)
581 582

    def on_scroll_up(self):
583
        self._text_buffer.scroll_up(self.text_win.height-1)
584 585

    def on_scroll_down(self):
586
        self._text_buffer.scroll_down(self.text_win.height-1)
587

588 589 590 591
    def on_info_win_size_changed(self, stdscr):
        self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, stdscr, self.visible)
        self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible)
        self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible)
592 593

    def get_room(self):
594
        return self._text_buffer
595 596 597

    def just_before_refresh(self):
        return
598 599 600 601 602 603 604 605 606 607

def jid_and_name_match(contact, txt):
    """
    A function used to know if a contact in the roster should
    be shown in the roster
    """
    # TODO: search in nickname, and use libdiff
    if txt in contact.get_bare_jid():
        return True
    return False