1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# Urwid Text Layout classes 5# Copyright (C) 2004-2011 Ian Ward 6# 7# This library is free software; you can redistribute it and/or 8# modify it under the terms of the GNU Lesser General Public 9# License as published by the Free Software Foundation; either 10# version 2.1 of the License, or (at your option) any later version. 11# 12# This library is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15# Lesser General Public License for more details. 16# 17# You should have received a copy of the GNU Lesser General Public 18# License along with this library; if not, write to the Free Software 19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20# 21# Urwid web site: http://excess.org/urwid/ 22 23from __future__ import division, print_function 24 25from urwid.util import calc_width, calc_text_pos, calc_trim_text, is_wide_char, \ 26 move_prev_char, move_next_char 27from urwid.compat import bytes, PYTHON3, B, xrange 28 29class TextLayout: 30 def supports_align_mode(self, align): 31 """Return True if align is a supported align mode.""" 32 return True 33 def supports_wrap_mode(self, wrap): 34 """Return True if wrap is a supported wrap mode.""" 35 return True 36 def layout(self, text, width, align, wrap ): 37 """ 38 Return a layout structure for text. 39 40 :param text: string in current encoding or unicode string 41 :param width: number of screen columns available 42 :param align: align mode for text 43 :param wrap: wrap mode for text 44 45 Layout structure is a list of line layouts, one per output line. 46 Line layouts are lists than may contain the following tuples: 47 48 * (column width of text segment, start offset, end offset) 49 * (number of space characters to insert, offset or None) 50 * (column width of insert text, offset, "insert text") 51 52 The offset in the last two tuples is used to determine the 53 attribute used for the inserted spaces or text respectively. 54 The attribute used will be the same as the attribute at that 55 text offset. If the offset is None when inserting spaces 56 then no attribute will be used. 57 """ 58 raise NotImplementedError("This function must be overridden by a real" 59 " text layout class. (see StandardTextLayout)") 60 61class CanNotDisplayText(Exception): 62 pass 63 64class StandardTextLayout(TextLayout): 65 def __init__(self):#, tab_stops=(), tab_stop_every=8): 66 pass 67 #""" 68 #tab_stops -- list of screen column indexes for tab stops 69 #tab_stop_every -- repeated interval for following tab stops 70 #""" 71 #assert tab_stop_every is None or type(tab_stop_every)==int 72 #if not tab_stops and tab_stop_every: 73 # self.tab_stops = (tab_stop_every,) 74 #self.tab_stops = tab_stops 75 #self.tab_stop_every = tab_stop_every 76 def supports_align_mode(self, align): 77 """Return True if align is 'left', 'center' or 'right'.""" 78 return align in ('left', 'center', 'right') 79 def supports_wrap_mode(self, wrap): 80 """Return True if wrap is 'any', 'space', 'clip' or 'ellipsis'.""" 81 return wrap in ('any', 'space', 'clip', 'ellipsis') 82 def layout(self, text, width, align, wrap ): 83 """Return a layout structure for text.""" 84 try: 85 segs = self.calculate_text_segments( text, width, wrap ) 86 return self.align_layout( text, width, segs, wrap, align ) 87 except CanNotDisplayText: 88 return [[]] 89 90 def pack(self, maxcol, layout): 91 """ 92 Return a minimal maxcol value that would result in the same 93 number of lines for layout. layout must be a layout structure 94 returned by self.layout(). 95 """ 96 maxwidth = 0 97 assert layout, "huh? empty layout?: "+repr(layout) 98 for l in layout: 99 lw = line_width(l) 100 if lw >= maxcol: 101 return maxcol 102 maxwidth = max(maxwidth, lw) 103 return maxwidth 104 105 def align_layout( self, text, width, segs, wrap, align ): 106 """Convert the layout segs to an aligned layout.""" 107 out = [] 108 for l in segs: 109 sc = line_width(l) 110 if sc == width or align=='left': 111 out.append(l) 112 continue 113 114 if align == 'right': 115 out.append([(width-sc, None)] + l) 116 continue 117 assert align == 'center' 118 out.append([((width-sc+1) // 2, None)] + l) 119 return out 120 121 122 def calculate_text_segments(self, text, width, wrap): 123 """ 124 Calculate the segments of text to display given width screen 125 columns to display them. 126 127 text - unicode text or byte string to display 128 width - number of available screen columns 129 wrap - wrapping mode used 130 131 Returns a layout structure without alignment applied. 132 """ 133 nl, nl_o, sp_o = "\n", "\n", " " 134 if PYTHON3 and isinstance(text, bytes): 135 nl = B(nl) # can only find bytes in python3 bytestrings 136 nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value 137 sp_o = ord(sp_o) 138 b = [] 139 p = 0 140 if wrap in ('clip', 'ellipsis'): 141 # no wrapping to calculate, so it's easy. 142 while p<=len(text): 143 n_cr = text.find(nl, p) 144 if n_cr == -1: 145 n_cr = len(text) 146 sc = calc_width(text, p, n_cr) 147 148 # trim line to max width if needed, add ellipsis if trimmed 149 if wrap == 'ellipsis' and sc > width: 150 trimmed = True 151 spos, n_end, pad_left, pad_right = calc_trim_text(text, p, n_cr, 0, width-1) 152 # pad_left should be 0, because the start_col parameter was 0 (no trimming on the left) 153 # similarly spos should not be changed from p 154 assert pad_left == 0 155 assert spos == p 156 sc = width - 1 - pad_right 157 else: 158 trimmed = False 159 n_end = n_cr 160 pad_right = 0 161 162 l = [] 163 if p!=n_end: 164 l += [(sc, p, n_end)] 165 if trimmed: 166 l += [(1, n_end, u'…'.encode("utf-8"))] 167 l += [(pad_right,n_end)] 168 b.append(l) 169 p = n_cr+1 170 return b 171 172 173 while p<=len(text): 174 # look for next eligible line break 175 n_cr = text.find(nl, p) 176 if n_cr == -1: 177 n_cr = len(text) 178 sc = calc_width(text, p, n_cr) 179 if sc == 0: 180 # removed character hint 181 b.append([(0,n_cr)]) 182 p = n_cr+1 183 continue 184 if sc <= width: 185 # this segment fits 186 b.append([(sc,p,n_cr), 187 # removed character hint 188 (0,n_cr)]) 189 190 p = n_cr+1 191 continue 192 pos, sc = calc_text_pos( text, p, n_cr, width ) 193 if pos == p: # pathological width=1 double-byte case 194 raise CanNotDisplayText( 195 "Wide character will not fit in 1-column width") 196 if wrap == 'any': 197 b.append([(sc,p,pos)]) 198 p = pos 199 continue 200 assert wrap == 'space' 201 if text[pos] == sp_o: 202 # perfect space wrap 203 b.append([(sc,p,pos), 204 # removed character hint 205 (0,pos)]) 206 p = pos+1 207 continue 208 if is_wide_char(text, pos): 209 # perfect next wide 210 b.append([(sc,p,pos)]) 211 p = pos 212 continue 213 prev = pos 214 while prev > p: 215 prev = move_prev_char(text, p, prev) 216 if text[prev] == sp_o: 217 sc = calc_width(text,p,prev) 218 l = [(0,prev)] 219 if p!=prev: 220 l = [(sc,p,prev)] + l 221 b.append(l) 222 p = prev+1 223 break 224 if is_wide_char(text,prev): 225 # wrap after wide char 226 next = move_next_char(text, prev, pos) 227 sc = calc_width(text,p,next) 228 b.append([(sc,p,next)]) 229 p = next 230 break 231 else: 232 # unwrap previous line space if possible to 233 # fit more text (we're breaking a word anyway) 234 if b and (len(b[-1]) == 2 or ( len(b[-1])==1 235 and len(b[-1][0])==2 )): 236 # look for removed space above 237 if len(b[-1]) == 1: 238 [(h_sc, h_off)] = b[-1] 239 p_sc = 0 240 p_off = p_end = h_off 241 else: 242 [(p_sc, p_off, p_end), 243 (h_sc, h_off)] = b[-1] 244 if (p_sc < width and h_sc==0 and 245 text[h_off] == sp_o): 246 # combine with previous line 247 del b[-1] 248 p = p_off 249 pos, sc = calc_text_pos( 250 text, p, n_cr, width ) 251 b.append([(sc,p,pos)]) 252 # check for trailing " " or "\n" 253 p = pos 254 if p < len(text) and ( 255 text[p] in (sp_o, nl_o)): 256 # removed character hint 257 b[-1].append((0,p)) 258 p += 1 259 continue 260 261 262 # force any char wrap 263 b.append([(sc,p,pos)]) 264 p = pos 265 return b 266 267 268 269###################################### 270# default layout object to use 271default_layout = StandardTextLayout() 272###################################### 273 274 275class LayoutSegment: 276 def __init__(self, seg): 277 """Create object from line layout segment structure""" 278 279 assert type(seg) == tuple, repr(seg) 280 assert len(seg) in (2,3), repr(seg) 281 282 self.sc, self.offs = seg[:2] 283 284 assert type(self.sc) == int, repr(self.sc) 285 286 if len(seg)==3: 287 assert type(self.offs) == int, repr(self.offs) 288 assert self.sc > 0, repr(seg) 289 t = seg[2] 290 if type(t) == bytes: 291 self.text = t 292 self.end = None 293 else: 294 assert type(t) == int, repr(t) 295 self.text = None 296 self.end = t 297 else: 298 assert len(seg) == 2, repr(seg) 299 if self.offs is not None: 300 assert self.sc >= 0, repr(seg) 301 assert type(self.offs)==int 302 self.text = self.end = None 303 304 def subseg(self, text, start, end): 305 """ 306 Return a "sub-segment" list containing segment structures 307 that make up a portion of this segment. 308 309 A list is returned to handle cases where wide characters 310 need to be replaced with a space character at either edge 311 so two or three segments will be returned. 312 """ 313 if start < 0: start = 0 314 if end > self.sc: end = self.sc 315 if start >= end: 316 return [] # completely gone 317 if self.text: 318 # use text stored in segment (self.text) 319 spos, epos, pad_left, pad_right = calc_trim_text( 320 self.text, 0, len(self.text), start, end ) 321 return [ (end-start, self.offs, bytes().ljust(pad_left) + 322 self.text[spos:epos] + bytes().ljust(pad_right)) ] 323 elif self.end: 324 # use text passed as parameter (text) 325 spos, epos, pad_left, pad_right = calc_trim_text( 326 text, self.offs, self.end, start, end ) 327 l = [] 328 if pad_left: 329 l.append((1,spos-1)) 330 l.append((end-start-pad_left-pad_right, spos, epos)) 331 if pad_right: 332 l.append((1,epos)) 333 return l 334 else: 335 # simple padding adjustment 336 return [(end-start,self.offs)] 337 338 339def line_width( segs ): 340 """ 341 Return the screen column width of one line of a text layout structure. 342 343 This function ignores any existing shift applied to the line, 344 represented by an (amount, None) tuple at the start of the line. 345 """ 346 sc = 0 347 seglist = segs 348 if segs and len(segs[0])==2 and segs[0][1]==None: 349 seglist = segs[1:] 350 for s in seglist: 351 sc += s[0] 352 return sc 353 354def shift_line( segs, amount ): 355 """ 356 Return a shifted line from a layout structure to the left or right. 357 segs -- line of a layout structure 358 amount -- screen columns to shift right (+ve) or left (-ve) 359 """ 360 assert type(amount)==int, repr(amount) 361 362 if segs and len(segs[0])==2 and segs[0][1]==None: 363 # existing shift 364 amount += segs[0][0] 365 if amount: 366 return [(amount,None)]+segs[1:] 367 return segs[1:] 368 369 if amount: 370 return [(amount,None)]+segs 371 return segs 372 373 374def trim_line( segs, text, start, end ): 375 """ 376 Return a trimmed line of a text layout structure. 377 text -- text to which this layout structure applies 378 start -- starting screen column 379 end -- ending screen column 380 """ 381 l = [] 382 x = 0 383 for seg in segs: 384 sc = seg[0] 385 if start or sc < 0: 386 if start >= sc: 387 start -= sc 388 x += sc 389 continue 390 s = LayoutSegment(seg) 391 if x+sc >= end: 392 # can all be done at once 393 return s.subseg( text, start, end-x ) 394 l += s.subseg( text, start, sc ) 395 start = 0 396 x += sc 397 continue 398 if x >= end: 399 break 400 if x+sc > end: 401 s = LayoutSegment(seg) 402 l += s.subseg( text, 0, end-x ) 403 break 404 l.append( seg ) 405 return l 406 407 408 409def calc_line_pos( text, line_layout, pref_col ): 410 """ 411 Calculate the closest linear position to pref_col given a 412 line layout structure. Returns None if no position found. 413 """ 414 closest_sc = None 415 closest_pos = None 416 current_sc = 0 417 418 if pref_col == 'left': 419 for seg in line_layout: 420 s = LayoutSegment(seg) 421 if s.offs is not None: 422 return s.offs 423 return 424 elif pref_col == 'right': 425 for seg in line_layout: 426 s = LayoutSegment(seg) 427 if s.offs is not None: 428 closest_pos = s 429 s = closest_pos 430 if s is None: 431 return 432 if s.end is None: 433 return s.offs 434 return calc_text_pos( text, s.offs, s.end, s.sc-1)[0] 435 436 for seg in line_layout: 437 s = LayoutSegment(seg) 438 if s.offs is not None: 439 if s.end is not None: 440 if (current_sc <= pref_col and 441 pref_col < current_sc + s.sc): 442 # exact match within this segment 443 return calc_text_pos( text, 444 s.offs, s.end, 445 pref_col - current_sc )[0] 446 elif current_sc <= pref_col: 447 closest_sc = current_sc + s.sc - 1 448 closest_pos = s 449 450 if closest_sc is None or ( abs(pref_col-current_sc) 451 < abs(pref_col-closest_sc) ): 452 # this screen column is closer 453 closest_sc = current_sc 454 closest_pos = s.offs 455 if current_sc > closest_sc: 456 # we're moving past 457 break 458 current_sc += s.sc 459 460 if closest_pos is None or type(closest_pos) == int: 461 return closest_pos 462 463 # return the last positions in the segment "closest_pos" 464 s = closest_pos 465 return calc_text_pos( text, s.offs, s.end, s.sc-1)[0] 466 467def calc_pos( text, layout, pref_col, row ): 468 """ 469 Calculate the closest linear position to pref_col and row given a 470 layout structure. 471 """ 472 473 if row < 0 or row >= len(layout): 474 raise Exception("calculate_pos: out of layout row range") 475 476 pos = calc_line_pos( text, layout[row], pref_col ) 477 if pos is not None: 478 return pos 479 480 rows_above = list(xrange(row-1,-1,-1)) 481 rows_below = list(xrange(row+1,len(layout))) 482 while rows_above and rows_below: 483 if rows_above: 484 r = rows_above.pop(0) 485 pos = calc_line_pos(text, layout[r], pref_col) 486 if pos is not None: return pos 487 if rows_below: 488 r = rows_below.pop(0) 489 pos = calc_line_pos(text, layout[r], pref_col) 490 if pos is not None: return pos 491 return 0 492 493 494def calc_coords( text, layout, pos, clamp=1 ): 495 """ 496 Calculate the coordinates closest to position pos in text with layout. 497 498 text -- raw string or unicode string 499 layout -- layout structure applied to text 500 pos -- integer position into text 501 clamp -- ignored right now 502 """ 503 closest = None 504 y = 0 505 for line_layout in layout: 506 x = 0 507 for seg in line_layout: 508 s = LayoutSegment(seg) 509 if s.offs is None: 510 x += s.sc 511 continue 512 if s.offs == pos: 513 return x,y 514 if s.end is not None and s.offs<=pos and s.end>pos: 515 x += calc_width( text, s.offs, pos ) 516 return x,y 517 distance = abs(s.offs - pos) 518 if s.end is not None and s.end<pos: 519 distance = pos - (s.end-1) 520 if closest is None or distance < closest[0]: 521 closest = distance, (x,y) 522 x += s.sc 523 y += 1 524 525 if closest: 526 return closest[1] 527 return 0,0 528