fmt.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. # Text formatting abstractions
  2. # Note -- this module is obsolete, it's too slow anyway
  3. import string
  4. import Para
  5. # A formatter back-end object has one method that is called by the formatter:
  6. # addpara(p), where p is a paragraph object. For example:
  7. # Formatter back-end to do nothing at all with the paragraphs
  8. class NullBackEnd:
  9. #
  10. def __init__(self):
  11. pass
  12. #
  13. def addpara(self, p):
  14. pass
  15. #
  16. def bgn_anchor(self, id):
  17. pass
  18. #
  19. def end_anchor(self, id):
  20. pass
  21. # Formatter back-end to collect the paragraphs in a list
  22. class SavingBackEnd(NullBackEnd):
  23. #
  24. def __init__(self):
  25. self.paralist = []
  26. #
  27. def addpara(self, p):
  28. self.paralist.append(p)
  29. #
  30. def hitcheck(self, h, v):
  31. hits = []
  32. for p in self.paralist:
  33. if p.top <= v <= p.bottom:
  34. for id in p.hitcheck(h, v):
  35. if id not in hits:
  36. hits.append(id)
  37. return hits
  38. #
  39. def extract(self):
  40. text = ''
  41. for p in self.paralist:
  42. text = text + (p.extract())
  43. return text
  44. #
  45. def extractpart(self, long1, long2):
  46. if long1 > long2: long1, long2 = long2, long1
  47. para1, pos1 = long1
  48. para2, pos2 = long2
  49. text = ''
  50. while para1 < para2:
  51. ptext = self.paralist[para1].extract()
  52. text = text + ptext[pos1:]
  53. pos1 = 0
  54. para1 = para1 + 1
  55. ptext = self.paralist[para2].extract()
  56. return text + ptext[pos1:pos2]
  57. #
  58. def whereis(self, d, h, v):
  59. total = 0
  60. for i in range(len(self.paralist)):
  61. p = self.paralist[i]
  62. result = p.whereis(d, h, v)
  63. if result is not None:
  64. return i, result
  65. return None
  66. #
  67. def roundtowords(self, long1, long2):
  68. i, offset = long1
  69. text = self.paralist[i].extract()
  70. while offset > 0 and text[offset-1] != ' ': offset = offset-1
  71. long1 = i, offset
  72. #
  73. i, offset = long2
  74. text = self.paralist[i].extract()
  75. n = len(text)
  76. while offset < n-1 and text[offset] != ' ': offset = offset+1
  77. long2 = i, offset
  78. #
  79. return long1, long2
  80. #
  81. def roundtoparagraphs(self, long1, long2):
  82. long1 = long1[0], 0
  83. long2 = long2[0], len(self.paralist[long2[0]].extract())
  84. return long1, long2
  85. # Formatter back-end to send the text directly to the drawing object
  86. class WritingBackEnd(NullBackEnd):
  87. #
  88. def __init__(self, d, width):
  89. self.d = d
  90. self.width = width
  91. self.lineno = 0
  92. #
  93. def addpara(self, p):
  94. self.lineno = p.render(self.d, 0, self.lineno, self.width)
  95. # A formatter receives a stream of formatting instructions and assembles
  96. # these into a stream of paragraphs on to a back-end. The assembly is
  97. # parametrized by a text measurement object, which must match the output
  98. # operations of the back-end. The back-end is responsible for splitting
  99. # paragraphs up in lines of a given maximum width. (This is done because
  100. # in a windowing environment, when the window size changes, there is no
  101. # need to redo the assembly into paragraphs, but the splitting into lines
  102. # must be done taking the new window size into account.)
  103. # Formatter base class. Initialize it with a text measurement object,
  104. # which is used for text measurements, and a back-end object,
  105. # which receives the completed paragraphs. The formatting methods are:
  106. # setfont(font)
  107. # setleftindent(nspaces)
  108. # setjust(type) where type is 'l', 'c', 'r', or 'lr'
  109. # flush()
  110. # vspace(nlines)
  111. # needvspace(nlines)
  112. # addword(word, nspaces)
  113. class BaseFormatter:
  114. #
  115. def __init__(self, d, b):
  116. # Drawing object used for text measurements
  117. self.d = d
  118. #
  119. # BackEnd object receiving completed paragraphs
  120. self.b = b
  121. #
  122. # Parameters of the formatting model
  123. self.leftindent = 0
  124. self.just = 'l'
  125. self.font = None
  126. self.blanklines = 0
  127. #
  128. # Parameters derived from the current font
  129. self.space = d.textwidth(' ')
  130. self.line = d.lineheight()
  131. self.ascent = d.baseline()
  132. self.descent = self.line - self.ascent
  133. #
  134. # Parameter derived from the default font
  135. self.n_space = self.space
  136. #
  137. # Current paragraph being built
  138. self.para = None
  139. self.nospace = 1
  140. #
  141. # Font to set on the next word
  142. self.nextfont = None
  143. #
  144. def newpara(self):
  145. return Para.Para()
  146. #
  147. def setfont(self, font):
  148. if font is None: return
  149. self.font = self.nextfont = font
  150. d = self.d
  151. d.setfont(font)
  152. self.space = d.textwidth(' ')
  153. self.line = d.lineheight()
  154. self.ascent = d.baseline()
  155. self.descent = self.line - self.ascent
  156. #
  157. def setleftindent(self, nspaces):
  158. self.leftindent = int(self.n_space * nspaces)
  159. if self.para:
  160. hang = self.leftindent - self.para.indent_left
  161. if hang > 0 and self.para.getlength() <= hang:
  162. self.para.makehangingtag(hang)
  163. self.nospace = 1
  164. else:
  165. self.flush()
  166. #
  167. def setrightindent(self, nspaces):
  168. self.rightindent = int(self.n_space * nspaces)
  169. if self.para:
  170. self.para.indent_right = self.rightindent
  171. self.flush()
  172. #
  173. def setjust(self, just):
  174. self.just = just
  175. if self.para:
  176. self.para.just = self.just
  177. #
  178. def flush(self):
  179. if self.para:
  180. self.b.addpara(self.para)
  181. self.para = None
  182. if self.font is not None:
  183. self.d.setfont(self.font)
  184. self.nospace = 1
  185. #
  186. def vspace(self, nlines):
  187. self.flush()
  188. if nlines > 0:
  189. self.para = self.newpara()
  190. tuple = None, '', 0, 0, 0, int(nlines*self.line), 0
  191. self.para.words.append(tuple)
  192. self.flush()
  193. self.blanklines = self.blanklines + nlines
  194. #
  195. def needvspace(self, nlines):
  196. self.flush() # Just to be sure
  197. if nlines > self.blanklines:
  198. self.vspace(nlines - self.blanklines)
  199. #
  200. def addword(self, text, space):
  201. if self.nospace and not text:
  202. return
  203. self.nospace = 0
  204. self.blanklines = 0
  205. if not self.para:
  206. self.para = self.newpara()
  207. self.para.indent_left = self.leftindent
  208. self.para.just = self.just
  209. self.nextfont = self.font
  210. space = int(space * self.space)
  211. self.para.words.append((self.nextfont, text,
  212. self.d.textwidth(text), space, space,
  213. self.ascent, self.descent))
  214. self.nextfont = None
  215. #
  216. def bgn_anchor(self, id):
  217. if not self.para:
  218. self.nospace = 0
  219. self.addword('', 0)
  220. self.para.bgn_anchor(id)
  221. #
  222. def end_anchor(self, id):
  223. if not self.para:
  224. self.nospace = 0
  225. self.addword('', 0)
  226. self.para.end_anchor(id)
  227. # Measuring object for measuring text as viewed on a tty
  228. class NullMeasurer:
  229. #
  230. def __init__(self):
  231. pass
  232. #
  233. def setfont(self, font):
  234. pass
  235. #
  236. def textwidth(self, text):
  237. return len(text)
  238. #
  239. def lineheight(self):
  240. return 1
  241. #
  242. def baseline(self):
  243. return 0
  244. # Drawing object for writing plain ASCII text to a file
  245. class FileWriter:
  246. #
  247. def __init__(self, fp):
  248. self.fp = fp
  249. self.lineno, self.colno = 0, 0
  250. #
  251. def setfont(self, font):
  252. pass
  253. #
  254. def text(self, (h, v), str):
  255. if not str: return
  256. if '\n' in str:
  257. raise ValueError, 'can\'t write \\n'
  258. while self.lineno < v:
  259. self.fp.write('\n')
  260. self.colno, self.lineno = 0, self.lineno + 1
  261. while self.lineno > v:
  262. # XXX This should never happen...
  263. self.fp.write('\033[A') # ANSI up arrow
  264. self.lineno = self.lineno - 1
  265. if self.colno < h:
  266. self.fp.write(' ' * (h - self.colno))
  267. elif self.colno > h:
  268. self.fp.write('\b' * (self.colno - h))
  269. self.colno = h
  270. self.fp.write(str)
  271. self.colno = h + len(str)
  272. # Formatting class to do nothing at all with the data
  273. class NullFormatter(BaseFormatter):
  274. #
  275. def __init__(self):
  276. d = NullMeasurer()
  277. b = NullBackEnd()
  278. BaseFormatter.__init__(self, d, b)
  279. # Formatting class to write directly to a file
  280. class WritingFormatter(BaseFormatter):
  281. #
  282. def __init__(self, fp, width):
  283. dm = NullMeasurer()
  284. dw = FileWriter(fp)
  285. b = WritingBackEnd(dw, width)
  286. BaseFormatter.__init__(self, dm, b)
  287. self.blanklines = 1
  288. #
  289. # Suppress multiple blank lines
  290. def needvspace(self, nlines):
  291. BaseFormatter.needvspace(self, min(1, nlines))
  292. # A "FunnyFormatter" writes ASCII text with a twist: *bold words*,
  293. # _italic text_ and _underlined words_, and `quoted text'.
  294. # It assumes that the fonts are 'r', 'i', 'b', 'u', 'q': (roman,
  295. # italic, bold, underline, quote).
  296. # Moreover, if the font is in upper case, the text is converted to
  297. # UPPER CASE.
  298. class FunnyFormatter(WritingFormatter):
  299. #
  300. def flush(self):
  301. if self.para: finalize(self.para)
  302. WritingFormatter.flush(self)
  303. # Surrounds *bold words* and _italic text_ in a paragraph with
  304. # appropriate markers, fixing the size (assuming these characters'
  305. # width is 1).
  306. openchar = \
  307. {'b':'*', 'i':'_', 'u':'_', 'q':'`', 'B':'*', 'I':'_', 'U':'_', 'Q':'`'}
  308. closechar = \
  309. {'b':'*', 'i':'_', 'u':'_', 'q':'\'', 'B':'*', 'I':'_', 'U':'_', 'Q':'\''}
  310. def finalize(para):
  311. oldfont = curfont = 'r'
  312. para.words.append(('r', '', 0, 0, 0, 0)) # temporary, deleted at end
  313. for i in range(len(para.words)):
  314. fo, te, wi = para.words[i][:3]
  315. if fo is not None: curfont = fo
  316. if curfont != oldfont:
  317. if closechar.has_key(oldfont):
  318. c = closechar[oldfont]
  319. j = i-1
  320. while j > 0 and para.words[j][1] == '': j = j-1
  321. fo1, te1, wi1 = para.words[j][:3]
  322. te1 = te1 + c
  323. wi1 = wi1 + len(c)
  324. para.words[j] = (fo1, te1, wi1) + \
  325. para.words[j][3:]
  326. if openchar.has_key(curfont) and te:
  327. c = openchar[curfont]
  328. te = c + te
  329. wi = len(c) + wi
  330. para.words[i] = (fo, te, wi) + \
  331. para.words[i][3:]
  332. if te: oldfont = curfont
  333. else: oldfont = 'r'
  334. if curfont in string.uppercase:
  335. te = string.upper(te)
  336. para.words[i] = (fo, te, wi) + para.words[i][3:]
  337. del para.words[-1]
  338. # Formatter back-end to draw the text in a window.
  339. # This has an option to draw while the paragraphs are being added,
  340. # to minimize the delay before the user sees anything.
  341. # This manages the entire "document" of the window.
  342. class StdwinBackEnd(SavingBackEnd):
  343. #
  344. def __init__(self, window, drawnow):
  345. self.window = window
  346. self.drawnow = drawnow
  347. self.width = window.getwinsize()[0]
  348. self.selection = None
  349. self.height = 0
  350. window.setorigin(0, 0)
  351. window.setdocsize(0, 0)
  352. self.d = window.begindrawing()
  353. SavingBackEnd.__init__(self)
  354. #
  355. def finish(self):
  356. self.d.close()
  357. self.d = None
  358. self.window.setdocsize(0, self.height)
  359. #
  360. def addpara(self, p):
  361. self.paralist.append(p)
  362. if self.drawnow:
  363. self.height = \
  364. p.render(self.d, 0, self.height, self.width)
  365. else:
  366. p.layout(self.width)
  367. p.left = 0
  368. p.top = self.height
  369. p.right = self.width
  370. p.bottom = self.height + p.height
  371. self.height = p.bottom
  372. #
  373. def resize(self):
  374. self.window.change((0, 0), (self.width, self.height))
  375. self.width = self.window.getwinsize()[0]
  376. self.height = 0
  377. for p in self.paralist:
  378. p.layout(self.width)
  379. p.left = 0
  380. p.top = self.height
  381. p.right = self.width
  382. p.bottom = self.height + p.height
  383. self.height = p.bottom
  384. self.window.change((0, 0), (self.width, self.height))
  385. self.window.setdocsize(0, self.height)
  386. #
  387. def redraw(self, area):
  388. d = self.window.begindrawing()
  389. (left, top), (right, bottom) = area
  390. d.erase(area)
  391. d.cliprect(area)
  392. for p in self.paralist:
  393. if top < p.bottom and p.top < bottom:
  394. v = p.render(d, p.left, p.top, p.right)
  395. if self.selection:
  396. self.invert(d, self.selection)
  397. d.close()
  398. #
  399. def setselection(self, new):
  400. if new:
  401. long1, long2 = new
  402. pos1 = long1[:3]
  403. pos2 = long2[:3]
  404. new = pos1, pos2
  405. if new != self.selection:
  406. d = self.window.begindrawing()
  407. if self.selection:
  408. self.invert(d, self.selection)
  409. if new:
  410. self.invert(d, new)
  411. d.close()
  412. self.selection = new
  413. #
  414. def getselection(self):
  415. return self.selection
  416. #
  417. def extractselection(self):
  418. if self.selection:
  419. a, b = self.selection
  420. return self.extractpart(a, b)
  421. else:
  422. return None
  423. #
  424. def invert(self, d, region):
  425. long1, long2 = region
  426. if long1 > long2: long1, long2 = long2, long1
  427. para1, pos1 = long1
  428. para2, pos2 = long2
  429. while para1 < para2:
  430. self.paralist[para1].invert(d, pos1, None)
  431. pos1 = None
  432. para1 = para1 + 1
  433. self.paralist[para2].invert(d, pos1, pos2)
  434. #
  435. def search(self, prog):
  436. import re, string
  437. if type(prog) is type(''):
  438. prog = re.compile(string.lower(prog))
  439. if self.selection:
  440. iold = self.selection[0][0]
  441. else:
  442. iold = -1
  443. hit = None
  444. for i in range(len(self.paralist)):
  445. if i == iold or i < iold and hit:
  446. continue
  447. p = self.paralist[i]
  448. text = string.lower(p.extract())
  449. match = prog.search(text)
  450. if match:
  451. a, b = match.group(0)
  452. long1 = i, a
  453. long2 = i, b
  454. hit = long1, long2
  455. if i > iold:
  456. break
  457. if hit:
  458. self.setselection(hit)
  459. i = hit[0][0]
  460. p = self.paralist[i]
  461. self.window.show((p.left, p.top), (p.right, p.bottom))
  462. return 1
  463. else:
  464. return 0
  465. #
  466. def showanchor(self, id):
  467. for i in range(len(self.paralist)):
  468. p = self.paralist[i]
  469. if p.hasanchor(id):
  470. long1 = i, 0
  471. long2 = i, len(p.extract())
  472. hit = long1, long2
  473. self.setselection(hit)
  474. self.window.show(
  475. (p.left, p.top), (p.right, p.bottom))
  476. break
  477. # GL extensions
  478. class GLFontCache:
  479. #
  480. def __init__(self):
  481. self.reset()
  482. self.setfont('')
  483. #
  484. def reset(self):
  485. self.fontkey = None
  486. self.fonthandle = None
  487. self.fontinfo = None
  488. self.fontcache = {}
  489. #
  490. def close(self):
  491. self.reset()
  492. #
  493. def setfont(self, fontkey):
  494. if fontkey == '':
  495. fontkey = 'Times-Roman 12'
  496. elif ' ' not in fontkey:
  497. fontkey = fontkey + ' 12'
  498. if fontkey == self.fontkey:
  499. return
  500. if self.fontcache.has_key(fontkey):
  501. handle = self.fontcache[fontkey]
  502. else:
  503. import string
  504. i = string.index(fontkey, ' ')
  505. name, sizestr = fontkey[:i], fontkey[i:]
  506. size = eval(sizestr)
  507. key1 = name + ' 1'
  508. key = name + ' ' + `size`
  509. # NB key may differ from fontkey!
  510. if self.fontcache.has_key(key):
  511. handle = self.fontcache[key]
  512. else:
  513. if self.fontcache.has_key(key1):
  514. handle = self.fontcache[key1]
  515. else:
  516. import fm
  517. handle = fm.findfont(name)
  518. self.fontcache[key1] = handle
  519. handle = handle.scalefont(size)
  520. self.fontcache[fontkey] = \
  521. self.fontcache[key] = handle
  522. self.fontkey = fontkey
  523. if self.fonthandle != handle:
  524. self.fonthandle = handle
  525. self.fontinfo = handle.getfontinfo()
  526. handle.setfont()
  527. class GLMeasurer(GLFontCache):
  528. #
  529. def textwidth(self, text):
  530. return self.fonthandle.getstrwidth(text)
  531. #
  532. def baseline(self):
  533. return self.fontinfo[6] - self.fontinfo[3]
  534. #
  535. def lineheight(self):
  536. return self.fontinfo[6]
  537. class GLWriter(GLFontCache):
  538. #
  539. # NOTES:
  540. # (1) Use gl.ortho2 to use X pixel coordinates!
  541. #
  542. def text(self, (h, v), text):
  543. import gl, fm
  544. gl.cmov2i(h, v + self.fontinfo[6] - self.fontinfo[3])
  545. fm.prstr(text)
  546. #
  547. def setfont(self, fontkey):
  548. oldhandle = self.fonthandle
  549. GLFontCache.setfont(fontkey)
  550. if self.fonthandle != oldhandle:
  551. handle.setfont()
  552. class GLMeasurerWriter(GLMeasurer, GLWriter):
  553. pass
  554. class GLBackEnd(SavingBackEnd):
  555. #
  556. def __init__(self, wid):
  557. import gl
  558. gl.winset(wid)
  559. self.wid = wid
  560. self.width = gl.getsize()[1]
  561. self.height = 0
  562. self.d = GLMeasurerWriter()
  563. SavingBackEnd.__init__(self)
  564. #
  565. def finish(self):
  566. pass
  567. #
  568. def addpara(self, p):
  569. self.paralist.append(p)
  570. self.height = p.render(self.d, 0, self.height, self.width)
  571. #
  572. def redraw(self):
  573. import gl
  574. gl.winset(self.wid)
  575. width = gl.getsize()[1]
  576. if width != self.width:
  577. setdocsize = 1
  578. self.width = width
  579. for p in self.paralist:
  580. p.top = p.bottom = None
  581. d = self.d
  582. v = 0
  583. for p in self.paralist:
  584. v = p.render(d, 0, v, width)