text_win.py 12.4 KB
Newer Older
1 2 3 4 5 6 7 8
"""
TextWin, the window showing the text messages and info messages in poezio.
Can be locked, scrolled, has a separator, etc…
"""

import logging
import curses
from math import ceil, log10
9
from typing import Optional, List, Union
10

Link Mauve's avatar
Link Mauve committed
11
from poezio.windows.base_wins import Win, FORMAT_CHAR
12
from poezio.ui.funcs import truncate_nick, parse_attrs
mathieui's avatar
mathieui committed
13
from poezio.text_buffer import TextBuffer
14

15 16 17
from poezio import poopt
from poezio.config import config
from poezio.theming import to_curses_attr, get_theme, dump_tuple
mathieui's avatar
mathieui committed
18
from poezio.ui.types import Message, BaseMessage
19
from poezio.ui.render import Line, build_lines, write_pre
20 21

log = logging.getLogger(__name__)
22 23


mathieui's avatar
mathieui committed
24
class TextWin(Win):
25
    __slots__ = ('lines_nb_limit', 'pos', 'built_lines', 'lock', 'lock_buffer',
mathieui's avatar
mathieui committed
26 27
                 'separator_after', 'highlights', 'hl_pos',
                 'nb_of_highlights_after_separator')
28

29
    def __init__(self, lines_nb_limit: Optional[int] = None) -> None:
mathieui's avatar
mathieui committed
30
        Win.__init__(self)
31 32
        if lines_nb_limit is None:
            lines_nb_limit = config.get('max_lines_in_memory')
33
        self.lines_nb_limit = lines_nb_limit  # type: int
34
        self.pos = 0
35
        # Each new message is built and kept here.
36
        # on resize, we rebuild all the messages
37
        self.built_lines = []  # type: List[Union[None, Line]]
38 39

        self.lock = False
40 41
        self.lock_buffer = []  # type: List[Union[None, Line]]
        self.separator_after = None  # type: Optional[Line]
mathieui's avatar
mathieui committed
42 43 44 45 46 47 48 49 50 51 52
        # the Lines of the highlights in that buffer
        self.highlights = []  # type: List[Line]
        # the current HL position in that list NaN means that we’re not on
        # an hl. -1 is a valid position (it's before the first hl of the
        # list. i.e the separator, in the case where there’s no hl before
        # it.)
        self.hl_pos = float('nan')

        # Keep track of the number of hl after the separator.
        # This is useful to make “go to next highlight“ work after a “move to separator”.
        self.nb_of_highlights_after_separator = 0
53

54
    def toggle_lock(self) -> bool:
55 56 57 58 59 60
        if self.lock:
            self.release_lock()
        else:
            self.acquire_lock()
        return self.lock

61
    def acquire_lock(self) -> None:
62 63
        self.lock = True

64
    def release_lock(self) -> None:
65 66 67 68
        for line in self.lock_buffer:
            self.built_lines.append(line)
        self.lock = False

69
    def scroll_up(self, dist: int = 14) -> bool:
70 71 72 73 74 75 76 77
        pos = self.pos
        self.pos += dist
        if self.pos + self.height > len(self.built_lines):
            self.pos = len(self.built_lines) - self.height
            if self.pos < 0:
                self.pos = 0
        return self.pos != pos

78
    def scroll_down(self, dist: int = 14) -> bool:
79 80 81 82 83 84
        pos = self.pos
        self.pos -= dist
        if self.pos <= 0:
            self.pos = 0
        return self.pos != pos

mathieui's avatar
mathieui committed
85
    def build_new_message(self,
mathieui's avatar
mathieui committed
86
                          message: BaseMessage,
87 88 89 90
                          clean: bool = True,
                          highlight: bool = False,
                          timestamp: bool = False,
                          nick_size: int = 10) -> int:
91 92 93 94 95
        """
        Take one message, build it and add it to the list
        Return the number of lines that are built for the given
        message.
        """
mathieui's avatar
mathieui committed
96 97 98 99
        lines = build_lines(
            message, self.width, timestamp=timestamp, nick_size=nick_size
        )
        if isinstance(message, Message) and message.top:
100 101
            for line in lines:
                self.built_lines.insert(0, line)
102
        else:
103 104 105 106
            if self.lock:
                self.lock_buffer.extend(lines)
            else:
                self.built_lines.extend(lines)
107 108
        if not lines or not lines[0]:
            return 0
mathieui's avatar
mathieui committed
109 110 111 112 113
        if highlight:
            self.highlights.append(lines[0])
            self.nb_of_highlights_after_separator += 1
            log.debug("Number of highlights after separator is now %s",
                      self.nb_of_highlights_after_separator)
114 115 116 117 118
        if clean:
            while len(self.built_lines) > self.lines_nb_limit:
                self.built_lines.pop(0)
        return len(lines)

119
    def refresh(self) -> None:
mathieui's avatar
mathieui committed
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
        log.debug('Refresh: %s', self.__class__.__name__)
        if self.height <= 0:
            return
        if self.pos == 0:
            lines = self.built_lines[-self.height:]
        else:
            lines = self.built_lines[-self.height - self.pos:-self.pos]
        with_timestamps = config.get("show_timestamps")
        nick_size = config.get("max_nick_length")
        self._win.move(0, 0)
        self._win.erase()
        offset = 0
        for y, line in enumerate(lines):
            if line:
                msg = line.msg
                if line.start_pos == 0:
                    offset = write_pre(msg, self, with_timestamps, nick_size)
                elif y == 0:
                    offset = msg.compute_offset(with_timestamps,
                                                nick_size)
                self.write_text(
                    y, offset,
                    line.prepend + line.msg.txt[line.start_pos:line.end_pos])
            else:
                self.write_line_separator(y)
            if y != self.height - 1:
                self.addstr('\n')
        self._win.attrset(0)
        self._refresh()
149

150
    def write_text(self, y: int, x: int, txt: str) -> None:
151 152 153 154 155
        """
        write the text of a line.
        """
        self.addstr_colored(txt, y, x)

mathieui's avatar
mathieui committed
156
    def resize(self, height: int, width: int, y: int, x: int, room: TextBuffer=None) -> None:
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
        if hasattr(self, 'width'):
            old_width = self.width
        else:
            old_width = None
        self._resize(height, width, y, x)
        if room and self.width != old_width:
            self.rebuild_everything(room)

        # reposition the scrolling after resize
        # (see #2450)
        buf_size = len(self.built_lines)
        if buf_size - self.pos < self.height:
            self.pos = buf_size - self.height
            if self.pos < 0:
                self.pos = 0

mathieui's avatar
mathieui committed
173
    def rebuild_everything(self, room: TextBuffer) -> None:
174 175
        self.built_lines = []
        with_timestamps = config.get('show_timestamps')
mathieui's avatar
mathieui committed
176
        nick_size = config.get('max_nick_length')
177
        for message in room.messages:
mathieui's avatar
mathieui committed
178 179 180 181 182
            self.build_new_message(
                message,
                clean=False,
                timestamp=with_timestamps,
                nick_size=nick_size)
183
            if self.separator_after is message:
mathieui's avatar
mathieui committed
184
                self.built_lines.append(None)
185 186 187
        while len(self.built_lines) > self.lines_nb_limit:
            self.built_lines.pop(0)

mathieui's avatar
mathieui committed
188 189 190 191 192 193 194 195
    def remove_line_separator(self) -> None:
        """
        Remove the line separator
        """
        log.debug('remove_line_separator')
        if None in self.built_lines:
            self.built_lines.remove(None)
            self.separator_after = None
mathieui's avatar
mathieui committed
196

mathieui's avatar
mathieui committed
197 198 199 200 201 202 203 204 205 206 207 208
    def add_line_separator(self, room: TextBuffer = None) -> None:
        """
        add a line separator at the end of messages list
        room is a textbuffer that is needed to get the previous message
        (in case of resize)
        """
        if None not in self.built_lines:
            self.built_lines.append(None)
            self.nb_of_highlights_after_separator = 0
            log.debug("Resetting number of highlights after separator")
            if room and room.messages:
                self.separator_after = room.messages[-1]
209

210

mathieui's avatar
mathieui committed
211 212 213 214 215
    def write_line_separator(self, y) -> None:
        theme = get_theme()
        char = theme.CHAR_NEW_TEXT_SEPARATOR
        self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width,
                     to_curses_attr(theme.COLOR_NEW_TEXT_SEPARATOR))
216

mathieui's avatar
mathieui committed
217 218 219 220
    def __del__(self) -> None:
        log.debug('** TextWin: deleting %s built lines',
                  (len(self.built_lines)))
        del self.built_lines
221

222
    def next_highlight(self) -> None:
223 224 225
        """
        Go to the next highlight in the buffer.
        (depending on which highlight was selected before)
Link Mauve's avatar
Link Mauve committed
226
        if the buffer is already positioned on the last, of if there are no
227 228 229
        highlights, scroll to the end of the buffer.
        """
        log.debug('Going to the next highlight…')
mathieui's avatar
mathieui committed
230 231
        if (not self.highlights or self.hl_pos != self.hl_pos
                or self.hl_pos >= len(self.highlights) - 1):
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
            self.hl_pos = float('nan')
            self.pos = 0
            return
        hl_size = len(self.highlights) - 1
        if self.hl_pos < hl_size:
            self.hl_pos += 1
        else:
            self.hl_pos = hl_size
        log.debug("self.hl_pos = %s", self.hl_pos)
        hl = self.highlights[self.hl_pos]
        pos = None
        while not pos:
            try:
                pos = self.built_lines.index(hl)
            except ValueError:
mathieui's avatar
mathieui committed
247
                self.highlights = self.highlights[self.hl_pos + 1:]
248 249 250 251 252 253 254 255 256 257
                if not self.highlights:
                    self.hl_pos = float('nan')
                    self.pos = 0
                    return
                self.hl_pos = 0
                hl = self.highlights[0]
        self.pos = len(self.built_lines) - pos - self.height
        if self.pos < 0 or self.pos >= len(self.built_lines):
            self.pos = 0

258
    def previous_highlight(self) -> None:
259 260 261
        """
        Go to the previous highlight in the buffer.
        (depending on which highlight was selected before)
Link Mauve's avatar
Link Mauve committed
262
        if the buffer is already positioned on the first, or if there are no
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
        highlights, scroll to the end of the buffer.
        """
        log.debug('Going to the previous highlight…')
        if not self.highlights or self.hl_pos <= 0:
            self.hl_pos = float('nan')
            self.pos = 0
            return
        if self.hl_pos != self.hl_pos:
            self.hl_pos = len(self.highlights) - 1
        else:
            self.hl_pos -= 1
        log.debug("self.hl_pos = %s", self.hl_pos)
        hl = self.highlights[self.hl_pos]
        pos = None
        while not pos:
            try:
                pos = self.built_lines.index(hl)
            except ValueError:
mathieui's avatar
mathieui committed
281
                self.highlights = self.highlights[self.hl_pos + 1:]
282 283 284 285 286 287 288 289 290 291
                if not self.highlights:
                    self.hl_pos = float('nan')
                    self.pos = 0
                    return
                self.hl_pos = 0
                hl = self.highlights[0]
        self.pos = len(self.built_lines) - pos - self.height
        if self.pos < 0 or self.pos >= len(self.built_lines):
            self.pos = 0

292
    def scroll_to_separator(self) -> None:
293
        """
294 295
        Scroll to the first message after the separator.  If no
        separator is present, scroll to the first message of the window
296 297
        """
        if None in self.built_lines:
mathieui's avatar
mathieui committed
298 299
            self.pos = len(self.built_lines) - self.built_lines.index(
                None) - self.height + 1
300 301 302 303 304 305 306 307 308
            if self.pos < 0:
                self.pos = 0
        else:
            self.pos = len(self.built_lines) - self.height + 1
        # Chose a proper position (not too high)
        self.scroll_up(0)
        # Make “next highlight” work afterwards. This makes it easy to
        # review all the highlights since the separator was placed, in
        # the correct order.
mathieui's avatar
mathieui committed
309 310
        self.hl_pos = len(
            self.highlights) - self.nb_of_highlights_after_separator - 1
311 312
        log.debug("self.hl_pos = %s", self.hl_pos)

313
    def modify_message(self, old_id, message) -> None:
314 315 316 317
        """
        Find a message, and replace it with a new one
        (instead of rebuilding everything in order to correct a message)
        """
318
        with_timestamps = config.get('show_timestamps')
mathieui's avatar
mathieui committed
319
        nick_size = config.get('max_nick_length')
mathieui's avatar
mathieui committed
320
        for i in range(len(self.built_lines) - 1, -1, -1):
mathieui's avatar
mathieui committed
321 322
            current = self.built_lines[i]
            if current is not None and current.msg.identifier == old_id:
323
                index = i
mathieui's avatar
mathieui committed
324 325 326 327 328
                while (
                        index >= 0
                        and current is not None
                        and current.msg.identifier == old_id
                        ):
329 330
                    self.built_lines.pop(index)
                    index -= 1
mathieui's avatar
mathieui committed
331
                current = self.built_lines[index]
332
                index += 1
mathieui's avatar
mathieui committed
333 334 335
                lines = build_lines(
                    message, self.width, timestamp=with_timestamps, nick_size=nick_size
                )
336 337 338 339
                for line in lines:
                    self.built_lines.insert(index, line)
                    index += 1
                break