Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20031218072017.3018: * @file leoFileCommands.py 

4#@@first 

5"""Classes relating to reading and writing .leo files.""" 

6#@+<< imports >> 

7#@+node:ekr.20050405141130: ** << imports >> (leoFileCommands) 

8import binascii 

9from collections import defaultdict 

10from contextlib import contextmanager 

11import difflib 

12import hashlib 

13import io 

14import json 

15import os 

16import pickle 

17import shutil 

18import sqlite3 

19import tempfile 

20import time 

21from typing import Dict 

22import zipfile 

23import xml.etree.ElementTree as ElementTree 

24import xml.sax 

25import xml.sax.saxutils 

26from leo.core import leoGlobals as g 

27from leo.core import leoNodes 

28#@-<< imports >> 

29PRIVAREA = '---begin-private-area---' 

30#@+others 

31#@+node:ekr.20150509194827.1: ** cmd (decorator) 

32def cmd(name): 

33 """Command decorator for the FileCommands class.""" 

34 return g.new_cmd_decorator(name, ['c', 'fileCommands',]) 

35#@+node:ekr.20210316035506.1: ** commands (leoFileCommands.py) 

36#@+node:ekr.20180708114847.1: *3* dump-clone-parents 

37@g.command('dump-clone-parents') 

38def dump_clone_parents(event): 

39 """Print the parent vnodes of all cloned vnodes.""" 

40 c = event.get('c') 

41 if not c: 

42 return 

43 print('dump-clone-parents...') 

44 d = c.fileCommands.gnxDict 

45 for gnx in d: 

46 v = d.get(gnx) 

47 if len(v.parents) > 1: 

48 print(v.h) 

49 g.printObj(v.parents) 

50#@+node:ekr.20210309114903.1: *3* dump-gnx-dict 

51@g.command('dump-gnx-dict') 

52def dump_gnx_dict(event): 

53 """Dump c.fileCommands.gnxDict.""" 

54 c = event.get('c') 

55 if not c: 

56 return 

57 d = c.fileCommands.gnxDict 

58 g.printObj(d, tag='gnxDict') 

59#@+node:ekr.20060918164811: ** class BadLeoFile 

60class BadLeoFile(Exception): 

61 

62 def __init__(self, message): 

63 self.message = message 

64 super().__init__(message) 

65 

66 def __str__(self): 

67 return "Bad Leo File:" + self.message 

68#@+node:ekr.20180602062323.1: ** class FastRead 

69class FastRead: 

70 

71 nativeVnodeAttributes = ( 

72 'a', 

73 'descendentTnodeUnknownAttributes', 

74 'descendentVnodeUnknownAttributes', 

75 'expanded', 'marks', 't', 

76 # 'tnodeList', # Removed in Leo 4.7. 

77 ) 

78 

79 def __init__(self, c, gnx2vnode): 

80 self.c = c 

81 self.gnx2vnode = gnx2vnode 

82 

83 #@+others 

84 #@+node:ekr.20180604110143.1: *3* fast.readFile 

85 def readFile(self, theFile, path): 

86 """Read the file, change splitter ratiors, and return its hidden vnode.""" 

87 s = theFile.read() 

88 v, g_element = self.readWithElementTree(path, s) 

89 if not v: # #1510. 

90 return None 

91 self.scanGlobals(g_element) 

92 # #1047: only this method changes splitter sizes. 

93 # 

94 # #1111: ensure that all outlines have at least one node. 

95 if not v.children: 

96 new_vnode = leoNodes.VNode(context=self.c) 

97 new_vnode.h = 'newHeadline' 

98 v.children = [new_vnode] 

99 return v 

100 

101 #@+node:ekr.20210316035646.1: *3* fast.readFileFromClipboard 

102 def readFileFromClipboard(self, s): 

103 """ 

104 Recreate a file from a string s, and return its hidden vnode. 

105 

106 Unlike readFile above, this does not affect splitter sizes. 

107 """ 

108 v, g_element = self.readWithElementTree(path=None, s=s) 

109 if not v: # #1510. 

110 return None 

111 # 

112 # #1111: ensure that all outlines have at least one node. 

113 if not v.children: 

114 new_vnode = leoNodes.VNode(context=self.c) 

115 new_vnode.h = 'newHeadline' 

116 v.children = [new_vnode] 

117 return v 

118 #@+node:ekr.20180602062323.7: *3* fast.readWithElementTree & helpers 

119 # #1510: https://en.wikipedia.org/wiki/Valid_characters_in_XML. 

120 translate_dict = {z: None for z in range(20) if chr(z) not in '\t\r\n'} 

121 

122 def readWithElementTree(self, path, s): 

123 

124 contents = g.toUnicode(s) 

125 table = contents.maketrans(self.translate_dict) # type:ignore #1510. 

126 contents = contents.translate(table) # #1036, #1046. 

127 try: 

128 xroot = ElementTree.fromstring(contents) 

129 except Exception as e: 

130 # #970: Report failure here. 

131 if path: 

132 message = f"bad .leo file: {g.shortFileName(path)}" 

133 else: 

134 message = 'The clipboard is not a vaild .leo file' 

135 g.es_print('\n' + message, color='red') 

136 g.es_print(g.toUnicode(e)) 

137 print('') 

138 return None, None # #1510: Return a tuple. 

139 g_element = xroot.find('globals') 

140 v_elements = xroot.find('vnodes') 

141 t_elements = xroot.find('tnodes') 

142 gnx2body, gnx2ua = self.scanTnodes(t_elements) 

143 hidden_v = self.scanVnodes(gnx2body, self.gnx2vnode, gnx2ua, v_elements) 

144 self.handleBits() 

145 return hidden_v, g_element 

146 #@+node:ekr.20180624125321.1: *4* fast.handleBits (reads c.db) 

147 def handleBits(self): 

148 """Restore the expanded and marked bits from c.db.""" 

149 c, fc = self.c, self.c.fileCommands 

150 expanded = c.db.get('expanded') 

151 marked = c.db.get('marked') 

152 expanded = expanded.split(',') if expanded else [] 

153 marked = marked.split(',') if marked else [] 

154 fc.descendentExpandedList = expanded 

155 fc.descendentMarksList = marked 

156 #@+node:ekr.20180606041211.1: *4* fast.resolveUa & helper 

157 def resolveUa(self, attr, val, kind=None): # Kind is for unit testing. 

158 """Parse an unknown attribute in a <v> or <t> element.""" 

159 try: 

160 val = g.toEncodedString(val) 

161 except Exception: 

162 g.es_print('unexpected exception converting hexlified string to string') 

163 g.es_exception() 

164 return None 

165 # Leave string attributes starting with 'str_' alone. 

166 if attr.startswith('str_'): 

167 if isinstance(val, (str, bytes)): 

168 return g.toUnicode(val) 

169 try: 

170 binString = binascii.unhexlify(val) 

171 # Throws a TypeError if val is not a hex string. 

172 except Exception: 

173 # Assume that Leo 4.1 or above wrote the attribute. 

174 if g.unitTesting: 

175 assert kind == 'raw', f"unit test failed: kind={kind}" 

176 else: 

177 g.trace(f"can not unhexlify {attr}={val}") 

178 return val 

179 try: 

180 # No change needed to support protocols. 

181 val2 = pickle.loads(binString) 

182 return val2 

183 except Exception: 

184 try: 

185 val2 = pickle.loads(binString, encoding='bytes') 

186 val2 = self.bytesToUnicode(val2) 

187 return val2 

188 except Exception: 

189 g.trace(f"can not unpickle {attr}={val}") 

190 return val 

191 #@+node:ekr.20180606044154.1: *5* fast.bytesToUnicode 

192 def bytesToUnicode(self, ob): 

193 """ 

194 Recursively convert bytes objects in strings / lists / dicts to str 

195 objects, thanks to TNT 

196 http://stackoverflow.com/questions/22840092 

197 Needed for reading Python 2.7 pickles in Python 3.4. 

198 """ 

199 # This is simpler than using isinstance. 

200 # pylint: disable=unidiomatic-typecheck 

201 t = type(ob) 

202 if t in (list, tuple): 

203 l = [str(i, 'utf-8') if type(i) is bytes else i for i in ob] 

204 l = [self.bytesToUnicode(i) 

205 if type(i) in (list, tuple, dict) else i 

206 for i in l] 

207 ro = tuple(l) if t is tuple else l 

208 elif t is dict: 

209 byte_keys = [i for i in ob if type(i) is bytes] 

210 for bk in byte_keys: 

211 v = ob[bk] 

212 del ob[bk] 

213 ob[str(bk, 'utf-8')] = v 

214 for k in ob: 

215 if type(ob[k]) is bytes: 

216 ob[k] = str(ob[k], 'utf-8') 

217 elif type(ob[k]) in (list, tuple, dict): 

218 ob[k] = self.bytesToUnicode(ob[k]) 

219 ro = ob 

220 elif t is bytes: # TNB added this clause 

221 ro = str(ob, 'utf-8') 

222 else: 

223 ro = ob 

224 return ro 

225 #@+node:ekr.20180605062300.1: *4* fast.scanGlobals & helper 

226 def scanGlobals(self, g_element): 

227 """Get global data from the cache, with reasonable defaults.""" 

228 c = self.c 

229 d = self.getGlobalData() 

230 windowSize = g.app.loadManager.options.get('windowSize') 

231 windowSpot = g.app.loadManager.options.get('windowSpot') 

232 if windowSize is not None: 

233 h, w = windowSize # checked in LM.scanOption. 

234 else: 

235 w, h = d.get('width'), d.get('height') 

236 if windowSpot is None: 

237 x, y = d.get('left'), d.get('top') 

238 else: 

239 y, x = windowSpot # #1263: (top, left) 

240 if 'size' in g.app.debug: 

241 g.trace(w, h, x, y, c.shortFileName()) 

242 # c.frame may be a NullFrame. 

243 c.frame.setTopGeometry(w, h, x, y) 

244 r1, r2 = d.get('r1'), d.get('r2') 

245 c.frame.resizePanesToRatio(r1, r2) 

246 frameFactory = getattr(g.app.gui, 'frameFactory', None) 

247 if not frameFactory: 

248 return 

249 assert frameFactory is not None 

250 mf = frameFactory.masterFrame 

251 if g.app.start_minimized: 

252 mf.showMinimized() 

253 elif g.app.start_maximized: 

254 # #1189: fast.scanGlobals calls showMaximized later. 

255 mf.showMaximized() 

256 elif g.app.start_fullscreen: 

257 mf.showFullScreen() 

258 else: 

259 mf.show() 

260 #@+node:ekr.20180708060437.1: *5* fast.getGlobalData 

261 def getGlobalData(self): 

262 """Return a dict containing all global data.""" 

263 c = self.c 

264 try: 

265 window_pos = c.db.get('window_position') 

266 r1 = float(c.db.get('body_outline_ratio', '0.5')) 

267 r2 = float(c.db.get('body_secondary_ratio', '0.5')) 

268 top, left, height, width = window_pos 

269 return { 

270 'top': int(top), 

271 'left': int(left), 

272 'height': int(height), 

273 'width': int(width), 

274 'r1': r1, 

275 'r2': r2, 

276 } 

277 except Exception: 

278 pass 

279 # Use reasonable defaults. 

280 return { 

281 'top': 50, 'left': 50, 

282 'height': 500, 'width': 800, 

283 'r1': 0.5, 'r2': 0.5, 

284 } 

285 #@+node:ekr.20180602062323.8: *4* fast.scanTnodes 

286 def scanTnodes(self, t_elements): 

287 

288 gnx2body: Dict[str, str] = {} 

289 gnx2ua: Dict[str, dict] = defaultdict(dict) 

290 for e in t_elements: 

291 # First, find the gnx. 

292 gnx = e.attrib['tx'] 

293 gnx2body[gnx] = e.text or '' 

294 # Next, scan for uA's for this gnx. 

295 for key, val in e.attrib.items(): 

296 if key != 'tx': 

297 gnx2ua[gnx][key] = self.resolveUa(key, val) 

298 return gnx2body, gnx2ua 

299 #@+node:ekr.20180602062323.9: *4* fast.scanVnodes & helper 

300 def scanVnodes(self, gnx2body, gnx2vnode, gnx2ua, v_elements): 

301 

302 c, fc = self.c, self.c.fileCommands 

303 #@+<< define v_element_visitor >> 

304 #@+node:ekr.20180605102822.1: *5* << define v_element_visitor >> 

305 def v_element_visitor(parent_e, parent_v): 

306 """Visit the given element, creating or updating the parent vnode.""" 

307 for e in parent_e: 

308 assert e.tag in ('v', 'vh'), e.tag 

309 if e.tag == 'vh': 

310 parent_v._headString = g.toUnicode(e.text or '') 

311 continue 

312 # #1581: Attempt to handle old Leo outlines. 

313 try: 

314 gnx = e.attrib['t'] 

315 v = gnx2vnode.get(gnx) 

316 except KeyError: 

317 # g.trace('no "t" attrib') 

318 gnx = None 

319 v = None 

320 if v: 

321 # A clone 

322 parent_v.children.append(v) 

323 v.parents.append(parent_v) 

324 # The body overrides any previous body text. 

325 body = g.toUnicode(gnx2body.get(gnx) or '') 

326 assert isinstance(body, str), body.__class__.__name__ 

327 v._bodyString = body 

328 else: 

329 #@+<< Make a new vnode, linked to the parent >> 

330 #@+node:ekr.20180605075042.1: *6* << Make a new vnode, linked to the parent >> 

331 v = leoNodes.VNode(context=c, gnx=gnx) 

332 gnx2vnode[gnx] = v 

333 parent_v.children.append(v) 

334 v.parents.append(parent_v) 

335 body = g.toUnicode(gnx2body.get(gnx) or '') 

336 assert isinstance(body, str), body.__class__.__name__ 

337 v._bodyString = body 

338 v._headString = 'PLACE HOLDER' 

339 #@-<< Make a new vnode, linked to the parent >> 

340 #@+<< handle all other v attributes >> 

341 #@+node:ekr.20180605075113.1: *6* << handle all other v attributes >> 

342 # FastRead.nativeVnodeAttributes defines the native attributes of <v> elements. 

343 d = e.attrib 

344 s = d.get('descendentTnodeUnknownAttributes') 

345 if s: 

346 aDict = fc.getDescendentUnknownAttributes(s, v=v) 

347 if aDict: 

348 fc.descendentTnodeUaDictList.append(aDict) 

349 s = d.get('descendentVnodeUnknownAttributes') 

350 if s: 

351 aDict = fc.getDescendentUnknownAttributes(s, v=v) 

352 if aDict: 

353 fc.descendentVnodeUaDictList.append((v, aDict),) 

354 # 

355 # Handle vnode uA's 

356 uaDict = gnx2ua[gnx] # A defaultdict(dict) 

357 for key, val in d.items(): 

358 if key not in self.nativeVnodeAttributes: 

359 uaDict[key] = self.resolveUa(key, val) 

360 if uaDict: 

361 v.unknownAttributes = uaDict 

362 #@-<< handle all other v attributes >> 

363 # Handle all inner elements. 

364 v_element_visitor(e, v) 

365 

366 #@-<< define v_element_visitor >> 

367 # 

368 # Create the hidden root vnode. 

369 

370 gnx = 'hidden-root-vnode-gnx' 

371 hidden_v = leoNodes.VNode(context=c, gnx=gnx) 

372 hidden_v._headString = '<hidden root vnode>' 

373 gnx2vnode[gnx] = hidden_v 

374 # 

375 # Traverse the tree of v elements. 

376 v_element_visitor(v_elements, hidden_v) 

377 return hidden_v 

378 #@-others 

379#@+node:ekr.20160514120347.1: ** class FileCommands 

380class FileCommands: 

381 """A class creating the FileCommands subcommander.""" 

382 #@+others 

383 #@+node:ekr.20090218115025.4: *3* fc.Birth 

384 #@+node:ekr.20031218072017.3019: *4* fc.ctor 

385 def __init__(self, c): 

386 """Ctor for FileCommands class.""" 

387 self.c = c 

388 self.frame = c.frame 

389 self.nativeTnodeAttributes = ('tx',) 

390 self.nativeVnodeAttributes = ( 

391 'a', 

392 'descendentTnodeUnknownAttributes', 

393 'descendentVnodeUnknownAttributes', # New in Leo 4.5. 

394 'expanded', 'marks', 't', 

395 # 'tnodeList', # Removed in Leo 4.7. 

396 ) 

397 self.initIvars() 

398 #@+node:ekr.20090218115025.5: *4* fc.initIvars 

399 def initIvars(self): 

400 """Init ivars of the FileCommands class.""" 

401 # General... 

402 c = self.c 

403 self.mFileName = "" 

404 self.fileDate = -1 

405 self.leo_file_encoding = c.config.new_leo_file_encoding 

406 # For reading... 

407 self.checking = False # True: checking only: do *not* alter the outline. 

408 self.descendentExpandedList = [] 

409 self.descendentMarksList = [] 

410 self.forbiddenTnodes = [] 

411 self.descendentTnodeUaDictList = [] 

412 self.descendentVnodeUaDictList = [] 

413 self.ratio = 0.5 

414 self.currentVnode = None 

415 # For writing... 

416 self.read_only = False 

417 self.rootPosition = None 

418 self.outputFile = None 

419 self.openDirectory = None 

420 self.usingClipboard = False 

421 self.currentPosition = None 

422 # New in 3.12... 

423 self.copiedTree = None 

424 # fc.gnxDict is never re-inited. 

425 self.gnxDict = {} # Keys are gnx strings. Values are vnodes. 

426 self.vnodesDict = {} # keys are gnx strings; values are ignored 

427 #@+node:ekr.20210316042224.1: *3* fc: Commands 

428 #@+node:ekr.20031218072017.2012: *4* fc.writeAtFileNodes 

429 @cmd('write-at-file-nodes') 

430 def writeAtFileNodes(self, event=None): 

431 """Write all @file nodes in the selected outline.""" 

432 c = self.c 

433 c.endEditing() 

434 c.init_error_dialogs() 

435 c.atFileCommands.writeAll(all=True) 

436 c.raise_error_dialogs(kind='write') 

437 #@+node:ekr.20031218072017.3050: *4* fc.write-outline-only 

438 @cmd('write-outline-only') 

439 def writeOutlineOnly(self, event=None): 

440 """Write the entire outline without writing any derived files.""" 

441 c = self.c 

442 c.endEditing() 

443 self.writeOutline(fileName=self.mFileName) 

444 

445 #@+node:ekr.20031218072017.1666: *4* fc.writeDirtyAtFileNodes 

446 @cmd('write-dirty-at-file-nodes') 

447 def writeDirtyAtFileNodes(self, event=None): 

448 """Write all changed @file Nodes.""" 

449 c = self.c 

450 c.endEditing() 

451 c.init_error_dialogs() 

452 c.atFileCommands.writeAll(dirty=True) 

453 c.raise_error_dialogs(kind='write') 

454 #@+node:ekr.20031218072017.2013: *4* fc.writeMissingAtFileNodes 

455 @cmd('write-missing-at-file-nodes') 

456 def writeMissingAtFileNodes(self, event=None): 

457 """Write all @file nodes for which the corresponding external file does not exist.""" 

458 c = self.c 

459 c.endEditing() 

460 c.atFileCommands.writeMissing(c.p) 

461 #@+node:ekr.20210316034350.1: *3* fc: File Utils 

462 #@+node:ekr.20031218072017.3047: *4* fc.createBackupFile 

463 def createBackupFile(self, fileName): 

464 """ 

465 Create a closed backup file and copy the file to it, 

466 but only if the original file exists. 

467 """ 

468 if g.os_path_exists(fileName): 

469 fd, backupName = tempfile.mkstemp(text=False) 

470 f = open(fileName, 'rb') # rb is essential. 

471 s = f.read() 

472 f.close() 

473 try: 

474 try: 

475 os.write(fd, s) 

476 finally: 

477 os.close(fd) 

478 ok = True 

479 except Exception: 

480 g.error('exception creating backup file') 

481 g.es_exception() 

482 ok, backupName = False, None 

483 if not ok and self.read_only: 

484 g.error("read only") 

485 else: 

486 ok, backupName = True, None 

487 return ok, backupName 

488 #@+node:ekr.20050404190914.2: *4* fc.deleteBackupFile 

489 def deleteBackupFile(self, fileName): 

490 try: 

491 os.remove(fileName) 

492 except Exception: 

493 if self.read_only: 

494 g.error("read only") 

495 g.error("exception deleting backup file:", fileName) 

496 g.es_exception(full=False) 

497 #@+node:ekr.20100119145629.6108: *4* fc.handleWriteLeoFileException 

498 def handleWriteLeoFileException(self, fileName, backupName, f): 

499 """Report an exception. f is an open file, or None.""" 

500 # c = self.c 

501 g.es("exception writing:", fileName) 

502 g.es_exception(full=True) 

503 if f: 

504 f.close() 

505 # Delete fileName. 

506 if fileName and g.os_path_exists(fileName): 

507 self.deleteBackupFile(fileName) 

508 # Rename backupName to fileName. 

509 if backupName and g.os_path_exists(backupName): 

510 g.es("restoring", fileName, "from", backupName) 

511 # No need to create directories when restoring. 

512 src, dst = backupName, fileName 

513 try: 

514 shutil.move(src, dst) 

515 except Exception: 

516 g.error('exception renaming', src, 'to', dst) 

517 g.es_exception(full=False) 

518 else: 

519 g.error('backup file does not exist!', repr(backupName)) 

520 #@+node:ekr.20040324080359.1: *4* fc.isReadOnly 

521 def isReadOnly(self, fileName): 

522 # self.read_only is not valid for Save As and Save To commands. 

523 if g.os_path_exists(fileName): 

524 try: 

525 if not os.access(fileName, os.W_OK): 

526 g.error("can not write: read only:", fileName) 

527 return True 

528 except Exception: 

529 pass # os.access() may not exist on all platforms. 

530 return False 

531 #@+node:ekr.20210315031535.1: *4* fc.openOutlineForWriting 

532 def openOutlineForWriting(self, fileName): 

533 """Open a .leo file for writing. Return the open file, or None.""" 

534 try: 

535 f = open(fileName, 'wb') # Always use binary mode. 

536 except Exception: 

537 g.es(f"can not open {fileName}") 

538 g.es_exception() 

539 f = None 

540 return f 

541 #@+node:ekr.20031218072017.3045: *4* fc.setDefaultDirectoryForNewFiles 

542 def setDefaultDirectoryForNewFiles(self, fileName): 

543 """Set c.openDirectory for new files for the benefit of leoAtFile.scanAllDirectives.""" 

544 c = self.c 

545 if not c.openDirectory: 

546 theDir = g.os_path_dirname(fileName) 

547 if theDir and g.os_path_isabs(theDir) and g.os_path_exists(theDir): 

548 c.openDirectory = c.frame.openDirectory = theDir 

549 #@+node:ekr.20031218072017.1554: *4* fc.warnOnReadOnlyFiles 

550 def warnOnReadOnlyFiles(self, fileName): 

551 # os.access may not exist on all platforms. 

552 try: 

553 self.read_only = not os.access(fileName, os.W_OK) 

554 except AttributeError: 

555 self.read_only = False 

556 except UnicodeError: 

557 self.read_only = False 

558 if self.read_only and not g.unitTesting: 

559 g.error("read only:", fileName) 

560 #@+node:ekr.20031218072017.3020: *3* fc: Reading 

561 #@+node:ekr.20031218072017.1559: *4* fc: Paste 

562 #@+node:ekr.20080410115129.1: *5* fc.checkPaste 

563 def checkPaste(self, parent, p): 

564 """Return True if p may be pasted as a child of parent.""" 

565 if not parent: 

566 return True 

567 parents = list(parent.self_and_parents()) 

568 for p in p.self_and_subtree(copy=False): 

569 for z in parents: 

570 if p.v == z.v: 

571 g.warning('Invalid paste: nodes may not descend from themselves') 

572 return False 

573 return True 

574 #@+node:ekr.20180709205603.1: *5* fc.getLeoOutlineFromClipBoard 

575 def getLeoOutlineFromClipboard(self, s): 

576 """Read a Leo outline from string s in clipboard format.""" 

577 c = self.c 

578 current = c.p 

579 if not current: 

580 g.trace('no c.p') 

581 return None 

582 self.initReadIvars() 

583 # Save the hidden root's children. 

584 old_children = c.hiddenRootNode.children 

585 # Save and clear gnxDict. 

586 oldGnxDict = self.gnxDict 

587 self.gnxDict = {} 

588 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True) 

589 # This encoding must match the encoding used in outline_to_clipboard_string. 

590 hidden_v = FastRead(c, self.gnxDict).readFileFromClipboard(s) 

591 v = hidden_v.children[0] 

592 v.parents = [] 

593 # Restore the hidden root's children 

594 c.hiddenRootNode.children = old_children 

595 if not v: 

596 return g.es("the clipboard is not valid ", color="blue") 

597 # Create the position. 

598 p = leoNodes.Position(v) 

599 # Do *not* adjust links when linking v. 

600 if current.hasChildren() and current.isExpanded(): 

601 p._linkCopiedAsNthChild(current, 0) 

602 else: 

603 p._linkCopiedAfter(current) 

604 assert not p.isCloned(), g.objToString(p.v.parents) 

605 self.gnxDict = oldGnxDict 

606 self.reassignAllIndices(p) 

607 c.selectPosition(p) 

608 self.initReadIvars() 

609 return p 

610 

611 getLeoOutline = getLeoOutlineFromClipboard # for compatibility 

612 #@+node:ekr.20180709205640.1: *5* fc.getLeoOutlineFromClipBoardRetainingClones 

613 def getLeoOutlineFromClipboardRetainingClones(self, s): 

614 """Read a Leo outline from string s in clipboard format.""" 

615 c = self.c 

616 current = c.p 

617 if not current: 

618 return g.trace('no c.p') 

619 self.initReadIvars() 

620 # Save the hidden root's children. 

621 old_children = c.hiddenRootNode.children 

622 # All pasted nodes should already have unique gnx's. 

623 ni = g.app.nodeIndices 

624 for v in c.all_unique_nodes(): 

625 ni.check_gnx(c, v.fileIndex, v) 

626 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True) 

627 # This encoding must match the encoding used in outline_to_clipboard_string. 

628 hidden_v = FastRead(c, self.gnxDict).readFileFromClipboard(s) 

629 v = hidden_v.children[0] 

630 v.parents.remove(hidden_v) 

631 # Restore the hidden root's children 

632 c.hiddenRootNode.children = old_children 

633 if not v: 

634 return g.es("the clipboard is not valid ", color="blue") 

635 # Create the position. 

636 p = leoNodes.Position(v) 

637 # Do *not* adjust links when linking v. 

638 if current.hasChildren() and current.isExpanded(): 

639 if not self.checkPaste(current, p): 

640 return None 

641 p._linkCopiedAsNthChild(current, 0) 

642 else: 

643 if not self.checkPaste(current.parent(), p): 

644 return None 

645 p._linkCopiedAfter(current) 

646 # Fix #862: paste-retaining-clones can corrupt the outline. 

647 self.linkChildrenToParents(p) 

648 c.selectPosition(p) 

649 self.initReadIvars() 

650 return p 

651 #@+node:ekr.20180424123010.1: *5* fc.linkChildrenToParents 

652 def linkChildrenToParents(self, p): 

653 """ 

654 Populate the parent links in all children of p. 

655 """ 

656 for child in p.children(): 

657 if not child.v.parents: 

658 child.v.parents.append(p.v) 

659 self.linkChildrenToParents(child) 

660 #@+node:ekr.20180425034856.1: *5* fc.reassignAllIndices 

661 def reassignAllIndices(self, p): 

662 """Reassign all indices in p's subtree.""" 

663 ni = g.app.nodeIndices 

664 for p2 in p.self_and_subtree(copy=False): 

665 v = p2.v 

666 index = ni.getNewIndex(v) 

667 if 'gnx' in g.app.debug: 

668 g.trace('**reassigning**', index, v) 

669 #@+node:ekr.20060919104836: *4* fc: Read Top-level 

670 #@+node:ekr.20031218072017.1553: *5* fc.getLeoFile (read switch) 

671 def getLeoFile(self, 

672 theFile, 

673 fileName, 

674 readAtFileNodesFlag=True, 

675 silent=False, 

676 checkOpenFiles=True, 

677 ): 

678 """ 

679 Read a .leo file. 

680 The caller should follow this with a call to c.redraw(). 

681 """ 

682 fc, c = self, self.c 

683 t1 = time.time() 

684 c.clearChanged() # May be set when reading @file nodes. 

685 fc.warnOnReadOnlyFiles(fileName) 

686 fc.checking = False 

687 fc.mFileName = c.mFileName 

688 fc.initReadIvars() 

689 recoveryNode = None 

690 try: 

691 c.loading = True # disable c.changed 

692 if not silent and checkOpenFiles: 

693 # Don't check for open file when reverting. 

694 g.app.checkForOpenFile(c, fileName) 

695 # 

696 # Read the .leo file and create the outline. 

697 if fileName.endswith('.db'): 

698 v = fc.retrieveVnodesFromDb(theFile) or fc.initNewDb(theFile) 

699 elif fileName.endswith('.leojs'): 

700 v = fc.read_leojs(theFile, fileName) 

701 readAtFileNodesFlag = False # Suppress post-processing. 

702 else: 

703 v = FastRead(c, self.gnxDict).readFile(theFile, fileName) 

704 if v: 

705 c.hiddenRootNode = v 

706 if v: 

707 c.setFileTimeStamp(fileName) 

708 if readAtFileNodesFlag: 

709 recoveryNode = fc.readExternalFiles(fileName) 

710 finally: 

711 p = recoveryNode or c.p or c.lastTopLevel() 

712 # lastTopLevel is a better fallback, imo. 

713 c.selectPosition(p) 

714 c.redraw_later() 

715 # Delay the second redraw until idle time. 

716 # This causes a slight flash, but corrects a hangnail. 

717 c.checkOutline() 

718 # Must be called *after* ni.end_holding. 

719 c.loading = False 

720 # reenable c.changed 

721 if not isinstance(theFile, sqlite3.Connection): 

722 theFile.close() 

723 # Fix bug https://bugs.launchpad.net/leo-editor/+bug/1208942 

724 # Leo holding directory/file handles after file close? 

725 if c.changed: 

726 fc.propagateDirtyNodes() 

727 fc.initReadIvars() 

728 t2 = time.time() 

729 g.es(f"read outline in {t2 - t1:2.2f} seconds") 

730 return v, c.frame.ratio 

731 #@+node:ekr.20031218072017.2297: *5* fc.openLeoFile 

732 def openLeoFile(self, theFile, fileName, readAtFileNodesFlag=True, silent=False): 

733 """ 

734 Open a Leo file. 

735 

736 readAtFileNodesFlag: False when reading settings files. 

737 silent: True when creating hidden commanders. 

738 """ 

739 c, frame = self.c, self.c.frame 

740 # Set c.openDirectory 

741 theDir = g.os_path_dirname(fileName) 

742 if theDir: 

743 c.openDirectory = c.frame.openDirectory = theDir 

744 # Get the file. 

745 self.gnxDict = {} # #1437 

746 ok, ratio = self.getLeoFile( 

747 theFile, fileName, 

748 readAtFileNodesFlag=readAtFileNodesFlag, 

749 silent=silent, 

750 ) 

751 if ok: 

752 frame.resizePanesToRatio(ratio, frame.secondary_ratio) 

753 return ok 

754 #@+node:ekr.20120212220616.10537: *5* fc.readExternalFiles & helper 

755 def readExternalFiles(self, fileName): 

756 """ 

757 Read all external files. 

758  

759 A helper for fc.getLeoFile. 

760 """ 

761 c, fc = self.c, self 

762 c.atFileCommands.readAll(c.rootPosition()) 

763 recoveryNode = fc.handleNodeConflicts() 

764 # 

765 # Do this after reading external files. 

766 # The descendent nodes won't exist unless we have read 

767 # the @thin nodes! 

768 fc.restoreDescendentAttributes() 

769 fc.setPositionsFromVnodes() 

770 return recoveryNode 

771 #@+node:ekr.20100205060712.8314: *6* fc.handleNodeConflicts 

772 def handleNodeConflicts(self): 

773 """Create a 'Recovered Nodes' node for each entry in c.nodeConflictList.""" 

774 c = self.c 

775 if not c.nodeConflictList: 

776 return None 

777 if not c.make_node_conflicts_node: 

778 s = f"suppressed {len(c.nodeConflictList)} node conflicts" 

779 g.es(s, color='red') 

780 g.pr('\n' + s + '\n') 

781 return None 

782 # Create the 'Recovered Nodes' node. 

783 last = c.lastTopLevel() 

784 root = last.insertAfter() 

785 root.setHeadString('Recovered Nodes') 

786 root.expand() 

787 # For each conflict, create one child and two grandchildren. 

788 for bunch in c.nodeConflictList: 

789 tag = bunch.get('tag') or '' 

790 gnx = bunch.get('gnx') or '' 

791 fn = bunch.get('fileName') or '' 

792 b1, h1 = bunch.get('b_old'), bunch.get('h_old') 

793 b2, h2 = bunch.get('b_new'), bunch.get('h_new') 

794 root_v = bunch.get('root_v') or '' 

795 child = root.insertAsLastChild() 

796 h = f'Recovered node "{h1}" from {g.shortFileName(fn)}' 

797 child.setHeadString(h) 

798 if b1 == b2: 

799 lines = [ 

800 'Headline changed...', 

801 f"{tag} gnx: {gnx} root: {(root_v and root.v)!r}", 

802 f"old headline: {h1}", 

803 f"new headline: {h2}", 

804 ] 

805 child.setBodyString('\n'.join(lines)) 

806 else: 

807 line1 = f"{tag} gnx: {gnx} root: {root_v and root.v!r}\nDiff...\n" 

808 d = difflib.Differ().compare(g.splitLines(b1), g.splitLines(b2)) 

809 # 2017/06/19: reverse comparison order. 

810 diffLines = [z for z in d] 

811 lines = [line1] 

812 lines.extend(diffLines) 

813 # There is less need to show trailing newlines because 

814 # we don't report changes involving only trailing newlines. 

815 child.setBodyString(''.join(lines)) 

816 n1 = child.insertAsNthChild(0) 

817 n2 = child.insertAsNthChild(1) 

818 n1.setHeadString('old:' + h1) 

819 n1.setBodyString(b1) 

820 n2.setHeadString('new:' + h2) 

821 n2.setBodyString(b2) 

822 return root 

823 #@+node:ekr.20031218072017.3030: *5* fc.readOutlineOnly 

824 def readOutlineOnly(self, theFile, fileName): 

825 c = self.c 

826 # Set c.openDirectory 

827 theDir = g.os_path_dirname(fileName) 

828 if theDir: 

829 c.openDirectory = c.frame.openDirectory = theDir 

830 ok, ratio = self.getLeoFile(theFile, fileName, readAtFileNodesFlag=False) 

831 c.redraw() 

832 c.frame.deiconify() 

833 junk, junk, secondary_ratio = self.frame.initialRatios() 

834 c.frame.resizePanesToRatio(ratio, secondary_ratio) 

835 return ok 

836 #@+node:vitalije.20170630152841.1: *5* fc.retrieveVnodesFromDb & helpers 

837 def retrieveVnodesFromDb(self, conn): 

838 """ 

839 Recreates tree from the data contained in table vnodes. 

840 

841 This method follows behavior of readSaxFile. 

842 """ 

843 

844 c, fc = self.c, self 

845 sql = '''select gnx, head, 

846 body, 

847 children, 

848 parents, 

849 iconVal, 

850 statusBits, 

851 ua from vnodes''' 

852 vnodes = [] 

853 try: 

854 for row in conn.execute(sql): 

855 (gnx, h, b, children, parents, iconVal, statusBits, ua) = row 

856 try: 

857 ua = pickle.loads(g.toEncodedString(ua)) 

858 except ValueError: 

859 ua = None 

860 v = leoNodes.VNode(context=c, gnx=gnx) 

861 v._headString = h 

862 v._bodyString = b 

863 v.children = children.split() 

864 v.parents = parents.split() 

865 v.iconVal = iconVal 

866 v.statusBits = statusBits 

867 v.u = ua 

868 vnodes.append(v) 

869 except sqlite3.Error as er: 

870 if er.args[0].find('no such table') < 0: 

871 # there was an error raised but it is not the one we expect 

872 g.internalError(er) 

873 # there is no vnodes table 

874 return None 

875 

876 rootChildren = [x for x in vnodes if 'hidden-root-vnode-gnx' in x.parents] 

877 if not rootChildren: 

878 g.trace('there should be at least one top level node!') 

879 return None 

880 

881 findNode = lambda x: fc.gnxDict.get(x, c.hiddenRootNode) 

882 

883 # let us replace every gnx with the corresponding vnode 

884 for v in vnodes: 

885 v.children = [findNode(x) for x in v.children] 

886 v.parents = [findNode(x) for x in v.parents] 

887 c.hiddenRootNode.children = rootChildren 

888 (w, h, x, y, r1, r2, encp) = fc.getWindowGeometryFromDb(conn) 

889 c.frame.setTopGeometry(w, h, x, y) 

890 c.frame.resizePanesToRatio(r1, r2) 

891 p = fc.decodePosition(encp) 

892 c.setCurrentPosition(p) 

893 return rootChildren[0] 

894 #@+node:vitalije.20170815162307.1: *6* fc.initNewDb 

895 def initNewDb(self, conn): 

896 """ Initializes tables and returns None""" 

897 c, fc = self.c, self 

898 v = leoNodes.VNode(context=c) 

899 c.hiddenRootNode.children = [v] 

900 (w, h, x, y, r1, r2, encp) = fc.getWindowGeometryFromDb(conn) 

901 c.frame.setTopGeometry(w, h, x, y) 

902 c.frame.resizePanesToRatio(r1, r2) 

903 c.sqlite_connection = conn 

904 fc.exportToSqlite(c.mFileName) 

905 return v 

906 #@+node:vitalije.20170630200802.1: *6* fc.getWindowGeometryFromDb 

907 def getWindowGeometryFromDb(self, conn): 

908 geom = (600, 400, 50, 50, 0.5, 0.5, '') 

909 keys = ('width', 'height', 'left', 'top', 

910 'ratio', 'secondary_ratio', 

911 'current_position') 

912 try: 

913 d = dict( 

914 conn.execute( 

915 '''select * from extra_infos 

916 where name in (?, ?, ?, ?, ?, ?, ?)''', 

917 keys, 

918 ).fetchall(), 

919 ) 

920 # mypy complained that geom must be a tuple, not a generator. 

921 geom = tuple(d.get(*x) for x in zip(keys, geom)) # type:ignore 

922 except sqlite3.OperationalError: 

923 pass 

924 return geom 

925 #@+node:vitalije.20170831154734.1: *5* fc.setReferenceFile 

926 def setReferenceFile(self, fileName): 

927 c = self.c 

928 for v in c.hiddenRootNode.children: 

929 if v.h == PRIVAREA: 

930 v.b = fileName 

931 break 

932 else: 

933 v = c.rootPosition().insertBefore().v 

934 v.h = PRIVAREA 

935 v.b = fileName 

936 c.redraw() 

937 g.es('set reference file:', g.shortFileName(fileName)) 

938 #@+node:vitalije.20170831144643.1: *5* fc.updateFromRefFile 

939 def updateFromRefFile(self): 

940 """Updates public part of outline from the specified file.""" 

941 c, fc = self.c, self 

942 #@+others 

943 #@+node:vitalije.20170831144827.2: *6* function: get_ref_filename 

944 def get_ref_filename(): 

945 for v in priv_vnodes(): 

946 return g.splitLines(v.b)[0].strip() 

947 #@+node:vitalije.20170831144827.4: *6* function: pub_vnodes 

948 def pub_vnodes(): 

949 for v in c.hiddenRootNode.children: 

950 if v.h == PRIVAREA: 

951 break 

952 yield v 

953 #@+node:vitalije.20170831144827.5: *6* function: priv_vnodes 

954 def priv_vnodes(): 

955 pub = True 

956 for v in c.hiddenRootNode.children: 

957 if v.h == PRIVAREA: 

958 pub = False 

959 if pub: 

960 continue 

961 yield v 

962 #@+node:vitalije.20170831144827.6: *6* function: pub_gnxes 

963 def sub_gnxes(children): 

964 for v in children: 

965 yield v.gnx 

966 for gnx in sub_gnxes(v.children): 

967 yield gnx 

968 

969 def pub_gnxes(): 

970 return sub_gnxes(pub_vnodes()) 

971 

972 def priv_gnxes(): 

973 return sub_gnxes(priv_vnodes()) 

974 #@+node:vitalije.20170831144827.7: *6* function: restore_priv 

975 def restore_priv(prdata, topgnxes): 

976 vnodes = [] 

977 for row in prdata: 

978 (gnx, h, b, children, parents, iconVal, statusBits, ua) = row 

979 v = leoNodes.VNode(context=c, gnx=gnx) 

980 v._headString = h 

981 v._bodyString = b 

982 v.children = children 

983 v.parents = parents 

984 v.iconVal = iconVal 

985 v.statusBits = statusBits 

986 v.u = ua 

987 vnodes.append(v) 

988 pv = lambda x: fc.gnxDict.get(x, c.hiddenRootNode) 

989 for v in vnodes: 

990 v.children = [pv(x) for x in v.children] 

991 v.parents = [pv(x) for x in v.parents] 

992 for gnx in topgnxes: 

993 v = fc.gnxDict[gnx] 

994 c.hiddenRootNode.children.append(v) 

995 if gnx in pubgnxes: 

996 v.parents.append(c.hiddenRootNode) 

997 #@+node:vitalije.20170831144827.8: *6* function: priv_data 

998 def priv_data(gnxes): 

999 dbrow = lambda v: ( 

1000 v.gnx, 

1001 v.h, 

1002 v.b, 

1003 [x.gnx for x in v.children], 

1004 [x.gnx for x in v.parents], 

1005 v.iconVal, 

1006 v.statusBits, 

1007 v.u 

1008 ) 

1009 return tuple(dbrow(fc.gnxDict[x]) for x in gnxes) 

1010 #@+node:vitalije.20170831144827.9: *6* function: nosqlite_commander 

1011 @contextmanager 

1012 def nosqlite_commander(fname): 

1013 oldname = c.mFileName 

1014 conn = getattr(c, 'sqlite_connection', None) 

1015 c.sqlite_connection = None 

1016 c.mFileName = fname 

1017 yield c 

1018 if c.sqlite_connection: 

1019 c.sqlite_connection.close() 

1020 c.mFileName = oldname 

1021 c.sqlite_connection = conn 

1022 #@-others 

1023 pubgnxes = set(pub_gnxes()) 

1024 privgnxes = set(priv_gnxes()) 

1025 privnodes = priv_data(privgnxes - pubgnxes) 

1026 toppriv = [v.gnx for v in priv_vnodes()] 

1027 fname = get_ref_filename() 

1028 with nosqlite_commander(fname): 

1029 theFile = open(fname, 'rb') 

1030 fc.initIvars() 

1031 fc.getLeoFile(theFile, fname, checkOpenFiles=False) 

1032 restore_priv(privnodes, toppriv) 

1033 c.redraw() 

1034 #@+node:ekr.20210316043902.1: *5* fc.read_leojs & helpers 

1035 def read_leojs(self, theFile, fileName): 

1036 """Read a JSON (.leojs) file and create the outline.""" 

1037 c = self.c 

1038 s = theFile.read() 

1039 try: 

1040 d = json.loads(s) 

1041 except Exception: 

1042 g.trace(f"Error reading .leojs file: {fileName}") 

1043 g.es_exception() 

1044 return None 

1045 # 

1046 # Get the top-level dicts. 

1047 tnodes_dict = d.get('tnodes') 

1048 vnodes_list = d.get('vnodes') 

1049 if not tnodes_dict: 

1050 g.trace(f"Bad .leojs file: no tnodes dict: {fileName}") 

1051 return None 

1052 if not vnodes_list: 

1053 g.trace(f"Bad .leojs file: no vnodes list: {fileName}") 

1054 return None 

1055 # 

1056 # Define function: create_vnode_from_dicts. 

1057 #@+others 

1058 #@+node:ekr.20210317155137.1: *6* function: create_vnode_from_dicts 

1059 def create_vnode_from_dicts(i, parent_v, v_dict): 

1060 """Create a new vnode as the i'th child of the parent vnode.""" 

1061 # 

1062 # Get the gnx. 

1063 gnx = v_dict.get('gnx') 

1064 if not gnx: 

1065 g.trace(f"Bad .leojs file: no gnx in v_dict: {fileName}") 

1066 g.printObj(v_dict) 

1067 return 

1068 # 

1069 # Create the vnode. 

1070 assert len(parent_v.children) == i, (i, parent_v, parent_v.children) 

1071 v = leoNodes.VNode(context=c, gnx=gnx) 

1072 parent_v.children.append(v) 

1073 v._headString = v_dict.get('vh', '') 

1074 v._bodyString = tnodes_dict.get(gnx, '') 

1075 # 

1076 # Recursively create the children. 

1077 for i2, v_dict2 in enumerate(v_dict.get('children', [])): 

1078 create_vnode_from_dicts(i2, v, v_dict2) 

1079 #@+node:ekr.20210318125522.1: *6* function: scan_leojs_globals 

1080 def scan_leojs_globals(json_d): 

1081 """Set the geometries from the globals dict.""" 

1082 

1083 def toInt(x, default): 

1084 try: 

1085 return int(x) 

1086 except Exception: 

1087 return default 

1088 

1089 # Priority 1: command-line args 

1090 windowSize = g.app.loadManager.options.get('windowSize') 

1091 windowSpot = g.app.loadManager.options.get('windowSpot') 

1092 # 

1093 # Priority 2: The cache. 

1094 db_top, db_left, db_height, db_width = c.db.get('window_position', (None, None, None, None)) 

1095 # 

1096 # Priority 3: The globals dict in the .leojs file. 

1097 # Leo doesn't write the globals element, but leoInteg might. 

1098 d = json_d.get('globals', {}) 

1099 # 

1100 # height & width 

1101 height, width = windowSize or (None, None) 

1102 if height is None: 

1103 height, width = d.get('height'), d.get('width') 

1104 if height is None: 

1105 height, width = db_height, db_width 

1106 height, width = toInt(height, 500), toInt(width, 800) 

1107 # 

1108 # top, left. 

1109 top, left = windowSpot or (None, None) 

1110 if top is None: 

1111 top, left = d.get('top'), d.get('left') 

1112 if top is None: 

1113 top, left = db_top, db_left 

1114 top, left = toInt(top, 50), toInt(left, 50) 

1115 # 

1116 # r1, r2. 

1117 r1 = float(c.db.get('body_outline_ratio', '0.5')) 

1118 r2 = float(c.db.get('body_secondary_ratio', '0.5')) 

1119 if 'size' in g.app.debug: 

1120 g.trace(width, height, left, top, c.shortFileName()) 

1121 # c.frame may be a NullFrame. 

1122 c.frame.setTopGeometry(width, height, left, top) 

1123 c.frame.resizePanesToRatio(r1, r2) 

1124 frameFactory = getattr(g.app.gui, 'frameFactory', None) 

1125 if not frameFactory: 

1126 return 

1127 assert frameFactory is not None 

1128 mf = frameFactory.masterFrame 

1129 if g.app.start_minimized: 

1130 mf.showMinimized() 

1131 elif g.app.start_maximized: 

1132 # #1189: fast.scanGlobals calls showMaximized later. 

1133 mf.showMaximized() 

1134 elif g.app.start_fullscreen: 

1135 mf.showFullScreen() 

1136 else: 

1137 mf.show() 

1138 #@-others 

1139 # 

1140 # Start the recursion by creating the top-level vnodes. 

1141 c.hiddenRootNode.children = [] # Necessary. 

1142 parent_v = c.hiddenRootNode 

1143 for i, v_dict in enumerate(vnodes_list): 

1144 create_vnode_from_dicts(i, parent_v, v_dict) 

1145 scan_leojs_globals(d) 

1146 return c.hiddenRootNode.children[0] 

1147 #@+node:ekr.20060919133249: *4* fc: Read Utils 

1148 # Methods common to both the sax and non-sax code. 

1149 #@+node:ekr.20061006104837.1: *5* fc.archivedPositionToPosition 

1150 def archivedPositionToPosition(self, s): 

1151 """Convert an archived position (a string) to a position.""" 

1152 return self.c.archivedPositionToPosition(s) 

1153 #@+node:ekr.20040701065235.1: *5* fc.getDescendentAttributes 

1154 def getDescendentAttributes(self, s, tag=""): 

1155 """s is a list of gnx's, separated by commas from a <v> or <t> element. 

1156 Parses s into a list. 

1157 

1158 This is used to record marked and expanded nodes. 

1159 """ 

1160 gnxs = s.split(',') 

1161 result = [gnx for gnx in gnxs if len(gnx) > 0] 

1162 return result 

1163 #@+node:EKR.20040627114602: *5* fc.getDescendentUnknownAttributes 

1164 # Pre Leo 4.5 Only @thin vnodes had the descendentTnodeUnknownAttributes field. 

1165 # New in Leo 4.5: @thin & @shadow vnodes have descendentVnodeUnknownAttributes field. 

1166 

1167 def getDescendentUnknownAttributes(self, s, v=None): 

1168 """Unhexlify and unpickle t/v.descendentUnknownAttribute field.""" 

1169 try: 

1170 # Changed in version 3.2: Accept only bytestring or bytearray objects as input. 

1171 s = g.toEncodedString(s) # 2011/02/22 

1172 bin = binascii.unhexlify(s) 

1173 # Throws a TypeError if val is not a hex string. 

1174 val = pickle.loads(bin) 

1175 return val 

1176 except Exception: 

1177 g.es_exception() 

1178 g.trace('Can not unpickle', type(s), v and v.h, s[:40]) 

1179 return None 

1180 #@+node:vitalije.20180304190953.1: *5* fc.getPos/VnodeFromClipboard 

1181 def getPosFromClipboard(self, s): 

1182 """A utility called from init_tree_abbrev.""" 

1183 v = self.getVnodeFromClipboard(s) 

1184 return leoNodes.Position(v) 

1185 

1186 def getVnodeFromClipboard(self, s): 

1187 """Called only from getPosFromClipboard.""" 

1188 c = self.c 

1189 self.initReadIvars() 

1190 oldGnxDict = self.gnxDict 

1191 self.gnxDict = {} # Fix #943 

1192 try: 

1193 # This encoding must match the encoding used in outline_to_clipboard_string. 

1194 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True) 

1195 v = FastRead(c, {}).readFileFromClipboard(s) 

1196 if not v: 

1197 return g.es("the clipboard is not valid ", color="blue") 

1198 finally: 

1199 self.gnxDict = oldGnxDict 

1200 return v 

1201 #@+node:ekr.20060919142200.1: *5* fc.initReadIvars 

1202 def initReadIvars(self): 

1203 self.descendentTnodeUaDictList = [] 

1204 self.descendentVnodeUaDictList = [] 

1205 self.descendentExpandedList = [] 

1206 self.descendentMarksList = [] 

1207 # 2011/12/10: never re-init this dict. 

1208 # self.gnxDict = {} 

1209 self.c.nodeConflictList = [] # 2010/01/05 

1210 self.c.nodeConflictFileName = None # 2010/01/05 

1211 #@+node:ekr.20100124110832.6212: *5* fc.propagateDirtyNodes 

1212 def propagateDirtyNodes(self): 

1213 c = self.c 

1214 aList = [z for z in c.all_positions() if z.isDirty()] 

1215 for p in aList: 

1216 p.setAllAncestorAtFileNodesDirty() 

1217 #@+node:ekr.20080805132422.3: *5* fc.resolveArchivedPosition 

1218 def resolveArchivedPosition(self, archivedPosition, root_v): 

1219 """ 

1220 Return a VNode corresponding to the archived position relative to root 

1221 node root_v. 

1222 """ 

1223 

1224 def oops(message): 

1225 """Give an error only if no file errors have been seen.""" 

1226 return None 

1227 

1228 try: 

1229 aList = [int(z) for z in archivedPosition.split('.')] 

1230 aList.reverse() 

1231 except Exception: 

1232 return oops(f'"{archivedPosition}"') 

1233 if not aList: 

1234 return oops('empty') 

1235 last_v = root_v 

1236 n = aList.pop() 

1237 if n != 0: 

1238 return oops(f'root index="{n}"') 

1239 while aList: 

1240 n = aList.pop() 

1241 children = last_v.children 

1242 if n < len(children): 

1243 last_v = children[n] 

1244 else: 

1245 return oops(f'bad index="{n}", len(children)="{len(children)}"') 

1246 return last_v 

1247 #@+node:EKR.20040627120120: *5* fc.restoreDescendentAttributes 

1248 def restoreDescendentAttributes(self): 

1249 """Called from fc.readExternalFiles.""" 

1250 c = self.c 

1251 for resultDict in self.descendentTnodeUaDictList: 

1252 for gnx in resultDict: 

1253 v = self.gnxDict.get(gnx) 

1254 if v: 

1255 v.unknownAttributes = resultDict[gnx] 

1256 v._p_changed = True 

1257 # New in Leo 4.5: keys are archivedPositions, values are attributes. 

1258 for root_v, resultDict in self.descendentVnodeUaDictList: 

1259 for key in resultDict: 

1260 v = self.resolveArchivedPosition(key, root_v) 

1261 if v: 

1262 v.unknownAttributes = resultDict[key] 

1263 v._p_changed = True 

1264 expanded, marks = {}, {} 

1265 for gnx in self.descendentExpandedList: 

1266 v = self.gnxDict.get(gnx) 

1267 if v: 

1268 expanded[v] = v 

1269 for gnx in self.descendentMarksList: 

1270 v = self.gnxDict.get(gnx) 

1271 if v: 

1272 marks[v] = v 

1273 if marks or expanded: 

1274 for p in c.all_unique_positions(): 

1275 if marks.get(p.v): 

1276 p.v.initMarkedBit() 

1277 # This was the problem: was p.setMark. 

1278 # There was a big performance bug in the mark hook in the Node Navigator plugin. 

1279 if expanded.get(p.v): 

1280 p.expand() 

1281 #@+node:ekr.20060919110638.13: *5* fc.setPositionsFromVnodes 

1282 def setPositionsFromVnodes(self): 

1283 

1284 c, root = self.c, self.c.rootPosition() 

1285 if c.sqlite_connection: 

1286 # position is already selected 

1287 return 

1288 current, str_pos = None, None 

1289 if c.mFileName: 

1290 str_pos = c.db.get('current_position') 

1291 if str_pos is None: 

1292 d = root.v.u 

1293 if d: 

1294 str_pos = d.get('str_leo_pos') 

1295 if str_pos is not None: 

1296 current = self.archivedPositionToPosition(str_pos) 

1297 c.setCurrentPosition(current or c.rootPosition()) 

1298 #@+node:ekr.20031218072017.3032: *3* fc: Writing 

1299 #@+node:ekr.20070413045221.2: *4* fc: Writing save* 

1300 #@+node:ekr.20031218072017.1720: *5* fc.save 

1301 def save(self, fileName, silent=False): 

1302 """fc.save: A helper for c.save.""" 

1303 c = self.c 

1304 p = c.p 

1305 # New in 4.2. Return ok flag so shutdown logic knows if all went well. 

1306 ok = g.doHook("save1", c=c, p=p, fileName=fileName) 

1307 if ok is None: 

1308 c.endEditing() # Set the current headline text. 

1309 self.setDefaultDirectoryForNewFiles(fileName) 

1310 g.app.commander_cacher.save(c, fileName) 

1311 ok = c.checkFileTimeStamp(fileName) 

1312 if ok: 

1313 if c.sqlite_connection: 

1314 c.sqlite_connection.close() 

1315 c.sqlite_connection = None 

1316 ok = self.write_Leo_file(fileName) 

1317 if ok: 

1318 if not silent: 

1319 self.putSavedMessage(fileName) 

1320 c.clearChanged() # Clears all dirty bits. 

1321 if c.config.save_clears_undo_buffer: 

1322 g.es("clearing undo") 

1323 c.undoer.clearUndoState() 

1324 c.redraw_after_icons_changed() 

1325 g.doHook("save2", c=c, p=p, fileName=fileName) 

1326 return ok 

1327 #@+node:vitalije.20170831135146.1: *5* fc.save_ref & helpers 

1328 def save_ref(self): 

1329 """Saves reference outline file""" 

1330 c = self.c 

1331 p = c.p 

1332 fc = self 

1333 #@+others 

1334 #@+node:vitalije.20170831135535.1: *6* function: put_v_elements 

1335 def put_v_elements(): 

1336 """ 

1337 Puts all <v> elements in the order in which they appear in the outline. 

1338  

1339 This is not the same as fc.put_v_elements! 

1340 """ 

1341 c.clearAllVisited() 

1342 fc.put("<vnodes>\n") 

1343 # Make only one copy for all calls. 

1344 fc.currentPosition = c.p 

1345 fc.rootPosition = c.rootPosition() 

1346 fc.vnodesDict = {} 

1347 ref_fname = None 

1348 for p in c.rootPosition().self_and_siblings(copy=False): 

1349 if p.h == PRIVAREA: 

1350 ref_fname = p.b.split('\n', 1)[0].strip() 

1351 break 

1352 # An optimization: Write the next top-level node. 

1353 fc.put_v_element(p, isIgnore=p.isAtIgnoreNode()) 

1354 fc.put("</vnodes>\n") 

1355 return ref_fname 

1356 #@+node:vitalije.20170831135447.1: *6* function: getPublicLeoFile 

1357 def getPublicLeoFile(): 

1358 fc.outputFile = io.StringIO() 

1359 fc.putProlog() 

1360 fc.putHeader() 

1361 fc.putGlobals() 

1362 fc.putPrefs() 

1363 fc.putFindSettings() 

1364 fname = put_v_elements() 

1365 put_t_elements() 

1366 fc.putPostlog() 

1367 return fname, fc.outputFile.getvalue() 

1368 

1369 #@+node:vitalije.20211218225014.1: *6* function: put_t_elements 

1370 def put_t_elements(): 

1371 """ 

1372 Write all <t> elements except those for vnodes appearing in @file, @edit or @auto nodes. 

1373 """ 

1374 

1375 def should_suppress(p): 

1376 return any(z.isAtFileNode() or z.isAtEditNode() or z.isAtAutoNode() 

1377 for z in p.self_and_parents()) 

1378 

1379 fc.put("<tnodes>\n") 

1380 suppress = {} 

1381 for p in c.all_positions(copy=False): 

1382 if should_suppress(p): 

1383 suppress[p.v] = True 

1384 

1385 toBeWritten = {} 

1386 for root in c.rootPosition().self_and_siblings(): 

1387 if root.h == PRIVAREA: 

1388 break 

1389 for p in root.self_and_subtree(): 

1390 if p.v not in suppress and p.v not in toBeWritten: 

1391 toBeWritten[p.v.fileIndex] = p.v 

1392 for gnx in sorted(toBeWritten): 

1393 v = toBeWritten[gnx] 

1394 fc.put_t_element(v) 

1395 fc.put("</tnodes>\n") 

1396 #@-others 

1397 c.endEditing() 

1398 for v in c.hiddenRootNode.children: 

1399 if v.h == PRIVAREA: 

1400 fileName = g.splitLines(v.b)[0].strip() 

1401 break 

1402 else: 

1403 fileName = c.mFileName 

1404 # New in 4.2. Return ok flag so shutdown logic knows if all went well. 

1405 ok = g.doHook("save1", c=c, p=p, fileName=fileName) 

1406 if ok is None: 

1407 fileName, content = getPublicLeoFile() 

1408 fileName = g.os_path_finalize_join(c.openDirectory, fileName) 

1409 with open(fileName, 'w', encoding="utf-8", newline='\n') as out: 

1410 out.write(content) 

1411 g.es('updated reference file:', 

1412 g.shortFileName(fileName)) 

1413 g.doHook("save2", c=c, p=p, fileName=fileName) 

1414 return ok 

1415 #@+node:ekr.20031218072017.3043: *5* fc.saveAs 

1416 def saveAs(self, fileName): 

1417 """fc.saveAs: A helper for c.saveAs.""" 

1418 c = self.c 

1419 p = c.p 

1420 if not g.doHook("save1", c=c, p=p, fileName=fileName): 

1421 c.endEditing() # Set the current headline text. 

1422 if c.sqlite_connection: 

1423 c.sqlite_connection.close() 

1424 c.sqlite_connection = None 

1425 self.setDefaultDirectoryForNewFiles(fileName) 

1426 g.app.commander_cacher.save(c, fileName) 

1427 # Disable path-changed messages in writeAllHelper. 

1428 c.ignoreChangedPaths = True 

1429 try: 

1430 if self.write_Leo_file(fileName): 

1431 c.clearChanged() # Clears all dirty bits. 

1432 self.putSavedMessage(fileName) 

1433 finally: 

1434 c.ignoreChangedPaths = False # #1367. 

1435 c.redraw_after_icons_changed() 

1436 g.doHook("save2", c=c, p=p, fileName=fileName) 

1437 #@+node:ekr.20031218072017.3044: *5* fc.saveTo 

1438 def saveTo(self, fileName, silent=False): 

1439 """fc.saveTo: A helper for c.saveTo.""" 

1440 c = self.c 

1441 p = c.p 

1442 if not g.doHook("save1", c=c, p=p, fileName=fileName): 

1443 c.endEditing() # Set the current headline text. 

1444 if c.sqlite_connection: 

1445 c.sqlite_connection.close() 

1446 c.sqlite_connection = None 

1447 self.setDefaultDirectoryForNewFiles(fileName) 

1448 g.app.commander_cacher.commit() # Commit, but don't save file name. 

1449 # Disable path-changed messages in writeAllHelper. 

1450 c.ignoreChangedPaths = True 

1451 try: 

1452 self.write_Leo_file(fileName) 

1453 finally: 

1454 c.ignoreChangedPaths = False 

1455 if not silent: 

1456 self.putSavedMessage(fileName) 

1457 c.redraw_after_icons_changed() 

1458 g.doHook("save2", c=c, p=p, fileName=fileName) 

1459 #@+node:ekr.20210316034237.1: *4* fc: Writing top-level 

1460 #@+node:vitalije.20170630172118.1: *5* fc.exportToSqlite & helpers 

1461 def exportToSqlite(self, fileName): 

1462 """Dump all vnodes to sqlite database. Returns True on success.""" 

1463 c, fc = self.c, self 

1464 if c.sqlite_connection is None: 

1465 c.sqlite_connection = sqlite3.connect(fileName, isolation_level='DEFERRED') 

1466 conn = c.sqlite_connection 

1467 

1468 def dump_u(v) -> bytes: 

1469 try: 

1470 s = pickle.dumps(v.u, protocol=1) 

1471 except pickle.PicklingError: 

1472 s = b'' # 2021/06/25: fixed via mypy complaint. 

1473 g.trace('unpickleable value', repr(v.u)) 

1474 return s 

1475 

1476 dbrow = lambda v: ( 

1477 v.gnx, 

1478 v.h, 

1479 v.b, 

1480 ' '.join(x.gnx for x in v.children), 

1481 ' '.join(x.gnx for x in v.parents), 

1482 v.iconVal, 

1483 v.statusBits, 

1484 dump_u(v) 

1485 ) 

1486 ok = False 

1487 try: 

1488 fc.prepareDbTables(conn) 

1489 fc.exportDbVersion(conn) 

1490 fc.exportVnodesToSqlite(conn, (dbrow(v) for v in c.all_unique_nodes())) 

1491 fc.exportGeomToSqlite(conn) 

1492 fc.exportHashesToSqlite(conn) 

1493 conn.commit() 

1494 ok = True 

1495 except sqlite3.Error as e: 

1496 g.internalError(e) 

1497 return ok 

1498 #@+node:vitalije.20170705075107.1: *6* fc.decodePosition 

1499 def decodePosition(self, s): 

1500 """Creates position from its string representation encoded by fc.encodePosition.""" 

1501 fc = self 

1502 if not s: 

1503 return fc.c.rootPosition() 

1504 sep = '<->' 

1505 comma = ',' 

1506 stack = [x.split(comma) for x in s.split(sep)] 

1507 stack = [(fc.gnxDict[x], int(y)) for x, y in stack] 

1508 v, ci = stack[-1] 

1509 p = leoNodes.Position(v, ci, stack[:-1]) 

1510 return p 

1511 #@+node:vitalije.20170705075117.1: *6* fc.encodePosition 

1512 def encodePosition(self, p): 

1513 """New schema for encoding current position hopefully simplier one.""" 

1514 jn = '<->' 

1515 mk = '%s,%s' 

1516 res = [mk % (x.gnx, y) for x, y in p.stack] 

1517 res.append(mk % (p.gnx, p._childIndex)) 

1518 return jn.join(res) 

1519 #@+node:vitalije.20170811130512.1: *6* fc.prepareDbTables 

1520 def prepareDbTables(self, conn): 

1521 conn.execute('''drop table if exists vnodes;''') 

1522 conn.execute( 

1523 ''' 

1524 create table if not exists vnodes( 

1525 gnx primary key, 

1526 head, 

1527 body, 

1528 children, 

1529 parents, 

1530 iconVal, 

1531 statusBits, 

1532 ua);''', 

1533 ) 

1534 conn.execute( 

1535 '''create table if not exists extra_infos(name primary key, value)''') 

1536 #@+node:vitalije.20170701161851.1: *6* fc.exportVnodesToSqlite 

1537 def exportVnodesToSqlite(self, conn, rows): 

1538 conn.executemany( 

1539 '''insert into vnodes 

1540 (gnx, head, body, children, parents, 

1541 iconVal, statusBits, ua) 

1542 values(?,?,?,?,?,?,?,?);''', 

1543 rows, 

1544 ) 

1545 #@+node:vitalije.20170701162052.1: *6* fc.exportGeomToSqlite 

1546 def exportGeomToSqlite(self, conn): 

1547 c = self.c 

1548 data = zip( 

1549 ( 

1550 'width', 'height', 'left', 'top', 

1551 'ratio', 'secondary_ratio', 

1552 'current_position' 

1553 ), 

1554 c.frame.get_window_info() + 

1555 ( 

1556 c.frame.ratio, c.frame.secondary_ratio, 

1557 self.encodePosition(c.p) 

1558 ) 

1559 ) 

1560 conn.executemany('replace into extra_infos(name, value) values(?, ?)', data) 

1561 #@+node:vitalije.20170811130559.1: *6* fc.exportDbVersion 

1562 def exportDbVersion(self, conn): 

1563 conn.execute( 

1564 "replace into extra_infos(name, value) values('dbversion', ?)", ('1.0',)) 

1565 #@+node:vitalije.20170701162204.1: *6* fc.exportHashesToSqlite 

1566 def exportHashesToSqlite(self, conn): 

1567 c = self.c 

1568 

1569 def md5(x): 

1570 try: 

1571 s = open(x, 'rb').read() 

1572 except Exception: 

1573 return '' 

1574 s = s.replace(b'\r\n', b'\n') 

1575 return hashlib.md5(s).hexdigest() 

1576 

1577 files = set() 

1578 

1579 p = c.rootPosition() 

1580 while p: 

1581 if p.isAtIgnoreNode(): 

1582 p.moveToNodeAfterTree() 

1583 elif p.isAtAutoNode() or p.isAtFileNode(): 

1584 fn = c.getNodeFileName(p) 

1585 files.add((fn, 'md5_' + p.gnx)) 

1586 p.moveToNodeAfterTree() 

1587 else: 

1588 p.moveToThreadNext() 

1589 conn.executemany( 

1590 'replace into extra_infos(name, value) values(?,?)', 

1591 map(lambda x: (x[1], md5(x[0])), files)) 

1592 #@+node:ekr.20031218072017.1573: *5* fc.outline_to_clipboard_string 

1593 def outline_to_clipboard_string(self, p=None): 

1594 """ 

1595 Return a string suitable for pasting to the clipboard. 

1596 """ 

1597 try: 

1598 # Save 

1599 tua = self.descendentTnodeUaDictList 

1600 vua = self.descendentVnodeUaDictList 

1601 gnxDict = self.gnxDict 

1602 vnodesDict = self.vnodesDict 

1603 # Paste. 

1604 self.outputFile = io.StringIO() 

1605 self.usingClipboard = True 

1606 self.putProlog() 

1607 self.putHeader() 

1608 self.put_v_elements(p or self.c.p) 

1609 self.put_t_elements() 

1610 self.putPostlog() 

1611 s = self.outputFile.getvalue() 

1612 self.outputFile = None 

1613 finally: 

1614 # Restore 

1615 self.descendentTnodeUaDictList = tua 

1616 self.descendentVnodeUaDictList = vua 

1617 self.gnxDict = gnxDict 

1618 self.vnodesDict = vnodesDict 

1619 self.usingClipboard = False 

1620 return s 

1621 #@+node:ekr.20040324080819.1: *5* fc.outline_to_xml_string 

1622 def outline_to_xml_string(self): 

1623 """Write the outline in .leo (XML) format to a string.""" 

1624 self.outputFile = io.StringIO() 

1625 self.putProlog() 

1626 self.putHeader() 

1627 self.putGlobals() 

1628 self.putPrefs() 

1629 self.putFindSettings() 

1630 self.put_v_elements() 

1631 self.put_t_elements() 

1632 self.putPostlog() 

1633 s = self.outputFile.getvalue() 

1634 self.outputFile = None 

1635 return s 

1636 #@+node:ekr.20031218072017.3046: *5* fc.write_Leo_file 

1637 def write_Leo_file(self, fileName): 

1638 """ 

1639 Write all external files and the .leo file itself.""" 

1640 c, fc = self.c, self 

1641 if c.checkOutline(): 

1642 g.error('Structural errors in outline! outline not written') 

1643 return False 

1644 g.app.recentFilesManager.writeRecentFilesFile(c) 

1645 fc.writeAllAtFileNodes() # Ignore any errors. 

1646 return fc.writeOutline(fileName) 

1647 

1648 write_LEO_file = write_Leo_file # For compatibility with old plugins. 

1649 #@+node:ekr.20210316050301.1: *5* fc.write_leojs & helpers 

1650 def write_leojs(self, fileName): 

1651 """Write the outline in .leojs (JSON) format.""" 

1652 c = self.c 

1653 ok, backupName = self.createBackupFile(fileName) 

1654 if not ok: 

1655 return False 

1656 f = self.openOutlineForWriting(fileName) 

1657 if not f: 

1658 return False 

1659 try: 

1660 # Create the dict corresponding to the JSON. 

1661 d = self.leojs_file() 

1662 # Convert the dict to JSON. 

1663 json_s = json.dumps(d, indent=2) 

1664 s = bytes(json_s, self.leo_file_encoding, 'replace') 

1665 f.write(s) 

1666 f.close() 

1667 g.app.commander_cacher.save(c, fileName) 

1668 c.setFileTimeStamp(fileName) 

1669 # Delete backup file. 

1670 if backupName and g.os_path_exists(backupName): 

1671 self.deleteBackupFile(backupName) 

1672 self.mFileName = fileName 

1673 return True 

1674 except Exception: 

1675 self.handleWriteLeoFileException(fileName, backupName, f) 

1676 return False 

1677 #@+node:ekr.20210316095706.1: *6* fc.leojs_file 

1678 def leojs_file(self): 

1679 """Return a dict representing the outline.""" 

1680 c = self.c 

1681 return { 

1682 'leoHeader': {'fileFormat': 2}, 

1683 'globals': self.leojs_globals(), 

1684 'tnodes': {v.gnx: v._bodyString for v in c.all_unique_nodes()}, 

1685 # 'tnodes': [ 

1686 # { 

1687 # 'tx': v.fileIndex, 

1688 # 'body': v._bodyString, 

1689 # } for v in c.all_unique_nodes() 

1690 # ], 

1691 'vnodes': [ 

1692 self.leojs_vnode(p.v) for p in c.rootPosition().self_and_siblings() 

1693 ], 

1694 } 

1695 #@+node:ekr.20210316092313.1: *6* fc.leojs_globals (sets window_position) 

1696 def leojs_globals(self): 

1697 """Put json representation of Leo's cached globals.""" 

1698 c = self.c 

1699 width, height, left, top = c.frame.get_window_info() 

1700 if 1: # Write to the cache, not the file. 

1701 d: Dict[str, str] = {} 

1702 c.db['body_outline_ratio'] = str(c.frame.ratio) 

1703 c.db['body_secondary_ratio'] = str(c.frame.secondary_ratio) 

1704 c.db['window_position'] = str(top), str(left), str(height), str(width) 

1705 if 'size' in g.app.debug: 

1706 g.trace('set window_position:', c.db['window_position'], c.shortFileName()) 

1707 else: 

1708 d = { 

1709 'body_outline_ratio': c.frame.ratio, 

1710 'body_secondary_ratio': c.frame.secondary_ratio, 

1711 'globalWindowPosition': { 

1712 'top': top, 

1713 'left': left, 

1714 'width': width, 

1715 'height': height, 

1716 }, 

1717 } 

1718 return d 

1719 #@+node:ekr.20210316085413.2: *6* fc.leojs_vnodes 

1720 def leojs_vnode(self, v): 

1721 """Return a jsonized vnode.""" 

1722 return { 

1723 'gnx': v.fileIndex, 

1724 'vh': v._headString, 

1725 'status': v.statusBits, 

1726 'children': [self.leojs_vnode(child) for child in v.children] 

1727 } 

1728 #@+node:ekr.20100119145629.6111: *5* fc.write_xml_file 

1729 def write_xml_file(self, fileName): 

1730 """Write the outline in .leo (XML) format.""" 

1731 c = self.c 

1732 ok, backupName = self.createBackupFile(fileName) 

1733 if not ok: 

1734 return False 

1735 f = self.openOutlineForWriting(fileName) 

1736 if not f: 

1737 return False 

1738 self.mFileName = fileName 

1739 try: 

1740 s = self.outline_to_xml_string() 

1741 s = bytes(s, self.leo_file_encoding, 'replace') 

1742 f.write(s) 

1743 f.close() 

1744 c.setFileTimeStamp(fileName) 

1745 # Delete backup file. 

1746 if backupName and g.os_path_exists(backupName): 

1747 self.deleteBackupFile(backupName) 

1748 return True 

1749 except Exception: 

1750 self.handleWriteLeoFileException(fileName, backupName, f) 

1751 return False 

1752 #@+node:ekr.20100119145629.6114: *5* fc.writeAllAtFileNodes 

1753 def writeAllAtFileNodes(self): 

1754 """Write all @<file> nodes and set orphan bits.""" 

1755 c = self.c 

1756 try: 

1757 # To allow Leo to quit properly, do *not* signal failure here. 

1758 c.atFileCommands.writeAll(all=False) 

1759 return True 

1760 except Exception: 

1761 # #1260415: https://bugs.launchpad.net/leo-editor/+bug/1260415 

1762 g.es_error("exception writing external files") 

1763 g.es_exception() 

1764 g.es('Internal error writing one or more external files.', color='red') 

1765 g.es('Please report this error to:', color='blue') 

1766 g.es('https://groups.google.com/forum/#!forum/leo-editor', color='blue') 

1767 g.es('All changes will be lost unless you', color='red') 

1768 g.es('can save each changed file.', color='red') 

1769 return False 

1770 #@+node:ekr.20210316041806.1: *5* fc.writeOutline (write switch) 

1771 def writeOutline(self, fileName): 

1772 

1773 c = self.c 

1774 if c.checkOutline(): 

1775 g.error('Structure errors in outline! outline not written') 

1776 return False 

1777 if self.isReadOnly(fileName): 

1778 return False 

1779 if fileName.endswith('.db'): 

1780 return self.exportToSqlite(fileName) 

1781 if fileName.endswith('.leojs'): 

1782 return self.write_leojs(fileName) 

1783 return self.write_xml_file(fileName) 

1784 #@+node:ekr.20070412095520: *5* fc.writeZipFile 

1785 def writeZipFile(self, s): 

1786 """Write string s as a .zip file.""" 

1787 # The name of the file in the archive. 

1788 contentsName = g.toEncodedString( 

1789 g.shortFileName(self.mFileName), 

1790 self.leo_file_encoding, reportErrors=True) 

1791 # The name of the archive itself. 

1792 fileName = g.toEncodedString( 

1793 self.mFileName, 

1794 self.leo_file_encoding, reportErrors=True) 

1795 # Write the archive. 

1796 # These mypy complaints look valid. 

1797 theFile = zipfile.ZipFile(fileName, 'w', zipfile.ZIP_DEFLATED) # type:ignore 

1798 theFile.writestr(contentsName, s) # type:ignore 

1799 theFile.close() 

1800 #@+node:ekr.20210316034532.1: *4* fc.Writing Utils 

1801 #@+node:ekr.20080805085257.2: *5* fc.pickle 

1802 def pickle(self, torv, val, tag): 

1803 """Pickle val and return the hexlified result.""" 

1804 try: 

1805 s = pickle.dumps(val, protocol=1) 

1806 s2 = binascii.hexlify(s) 

1807 s3 = g.toUnicode(s2, 'utf-8') 

1808 field = f' {tag}="{s3}"' 

1809 return field 

1810 except pickle.PicklingError: 

1811 if tag: # The caller will print the error if tag is None. 

1812 g.warning("ignoring non-pickleable value", val, "in", torv) 

1813 return '' 

1814 except Exception: 

1815 g.error("fc.pickle: unexpected exception in", torv) 

1816 g.es_exception() 

1817 return '' 

1818 #@+node:ekr.20031218072017.1470: *5* fc.put 

1819 def put(self, s): 

1820 """Put string s to self.outputFile. All output eventually comes here.""" 

1821 if s: 

1822 self.outputFile.write(s) 

1823 #@+node:ekr.20080805071954.2: *5* fc.putDescendentVnodeUas & helper 

1824 def putDescendentVnodeUas(self, p): 

1825 """ 

1826 Return the a uA field for descendent VNode attributes, 

1827 suitable for reconstituting uA's for anonymous vnodes. 

1828 """ 

1829 # 

1830 # Create aList of tuples (p,v) having a valid unknownAttributes dict. 

1831 # Create dictionary: keys are vnodes, values are corresonding archived positions. 

1832 aList = [] 

1833 pDict = {} 

1834 for p2 in p.self_and_subtree(copy=False): 

1835 if hasattr(p2.v, "unknownAttributes"): 

1836 aList.append((p2.copy(), p2.v),) 

1837 pDict[p2.v] = p2.archivedPosition(root_p=p) 

1838 # Create aList of pairs (v,d) where d contains only pickleable entries. 

1839 if aList: 

1840 aList = self.createUaList(aList) 

1841 if not aList: 

1842 return '' 

1843 # Create d, an enclosing dict to hold all the inner dicts. 

1844 d = {} 

1845 for v, d2 in aList: 

1846 aList2 = [str(z) for z in pDict.get(v)] 

1847 key = '.'.join(aList2) 

1848 d[key] = d2 

1849 # Pickle and hexlify d 

1850 # pylint: disable=consider-using-ternary 

1851 return d and self.pickle( 

1852 torv=p.v, val=d, tag='descendentVnodeUnknownAttributes') or '' 

1853 #@+node:ekr.20080805085257.1: *6* fc.createUaList 

1854 def createUaList(self, aList): 

1855 """ 

1856 Given aList of pairs (p,torv), return a list of pairs (torv,d) 

1857 where d contains all picklable items of torv.unknownAttributes. 

1858 """ 

1859 result = [] 

1860 for p, torv in aList: 

1861 if isinstance(torv.unknownAttributes, dict): 

1862 # Create a new dict containing only entries that can be pickled. 

1863 d = dict(torv.unknownAttributes) # Copy the dict. 

1864 for key in d: 

1865 # Just see if val can be pickled. Suppress any error. 

1866 ok = self.pickle(torv=torv, val=d.get(key), tag=None) 

1867 if not ok: 

1868 del d[key] 

1869 g.warning("ignoring bad unknownAttributes key", key, "in", p.h) 

1870 if d: 

1871 result.append((torv, d),) 

1872 else: 

1873 g.warning("ignoring non-dictionary uA for", p) 

1874 return result 

1875 #@+node:ekr.20031218072017.3035: *5* fc.putFindSettings 

1876 def putFindSettings(self): 

1877 # New in 4.3: These settings never get written to the .leo file. 

1878 self.put("<find_panel_settings/>\n") 

1879 #@+node:ekr.20031218072017.3037: *5* fc.putGlobals (sets window_position) 

1880 def putGlobals(self): 

1881 """Put a vestigial <globals> element, and write global data to the cache.""" 

1882 trace = 'cache' in g.app.debug 

1883 c = self.c 

1884 self.put("<globals/>\n") 

1885 if not c.mFileName: 

1886 return 

1887 c.db['body_outline_ratio'] = str(c.frame.ratio) 

1888 c.db['body_secondary_ratio'] = str(c.frame.secondary_ratio) 

1889 w, h, l, t = c.frame.get_window_info() 

1890 c.db['window_position'] = str(t), str(l), str(h), str(w) 

1891 if trace: 

1892 g.trace(f"\nset c.db for {c.shortFileName()}") 

1893 print('window_position:', c.db['window_position']) 

1894 #@+node:ekr.20031218072017.3041: *5* fc.putHeader 

1895 def putHeader(self): 

1896 self.put('<leo_header file_format="2"/>\n') 

1897 #@+node:ekr.20031218072017.3042: *5* fc.putPostlog 

1898 def putPostlog(self): 

1899 self.put("</leo_file>\n") 

1900 #@+node:ekr.20031218072017.2066: *5* fc.putPrefs 

1901 def putPrefs(self): 

1902 # New in 4.3: These settings never get written to the .leo file. 

1903 self.put("<preferences/>\n") 

1904 #@+node:ekr.20031218072017.1246: *5* fc.putProlog 

1905 def putProlog(self): 

1906 """Put the prolog of the xml file.""" 

1907 tag = 'http://leoeditor.com/namespaces/leo-python-editor/1.1' 

1908 self.putXMLLine() 

1909 # Put "created by Leo" line. 

1910 self.put('<!-- Created by Leo: http://leoeditor.com/leo_toc.html -->\n') 

1911 self.putStyleSheetLine() 

1912 # Put the namespace 

1913 self.put(f'<leo_file xmlns:leo="{tag}" >\n') 

1914 #@+node:ekr.20070413061552: *5* fc.putSavedMessage 

1915 def putSavedMessage(self, fileName): 

1916 c = self.c 

1917 # #531: Optionally report timestamp... 

1918 if c.config.getBool('log-show-save-time', default=False): 

1919 format = c.config.getString('log-timestamp-format') or "%H:%M:%S" 

1920 timestamp = time.strftime(format) + ' ' 

1921 else: 

1922 timestamp = '' 

1923 g.es(f"{timestamp}saved: {g.shortFileName(fileName)}") 

1924 #@+node:ekr.20031218072017.1248: *5* fc.putStyleSheetLine 

1925 def putStyleSheetLine(self): 

1926 """ 

1927 Put the xml stylesheet line. 

1928 

1929 Leo 5.3: 

1930 - Use only the stylesheet setting, ignoreing c.frame.stylesheet. 

1931 - Write no stylesheet element if there is no setting. 

1932 

1933 The old way made it almost impossible to delete stylesheet element. 

1934 """ 

1935 c = self.c 

1936 sheet = (c.config.getString('stylesheet') or '').strip() 

1937 # sheet2 = c.frame.stylesheet and c.frame.stylesheet.strip() or '' 

1938 # sheet = sheet or sheet2 

1939 if sheet: 

1940 self.put(f"<?xml-stylesheet {sheet} ?>\n") 

1941 

1942 #@+node:ekr.20031218072017.1577: *5* fc.put_t_element 

1943 def put_t_element(self, v): 

1944 b, gnx = v.b, v.fileIndex 

1945 ua = self.putUnknownAttributes(v) 

1946 body = xml.sax.saxutils.escape(b) if b else '' 

1947 self.put(f'<t tx="{gnx}"{ua}>{body}</t>\n') 

1948 #@+node:ekr.20031218072017.1575: *5* fc.put_t_elements 

1949 def put_t_elements(self): 

1950 """Put all <t> elements as required for copy or save commands""" 

1951 self.put("<tnodes>\n") 

1952 self.putReferencedTElements() 

1953 self.put("</tnodes>\n") 

1954 #@+node:ekr.20031218072017.1576: *6* fc.putReferencedTElements 

1955 def putReferencedTElements(self): 

1956 """Put <t> elements for all referenced vnodes.""" 

1957 c = self.c 

1958 if self.usingClipboard: # write the current tree. 

1959 theIter = self.currentPosition.self_and_subtree(copy=False) 

1960 else: # write everything 

1961 theIter = c.all_unique_positions(copy=False) 

1962 # Populate the vnodes dict. 

1963 vnodes = {} 

1964 for p in theIter: 

1965 # Make *sure* the file index has the proper form. 

1966 # pylint: disable=unbalanced-tuple-unpacking 

1967 index = p.v.fileIndex 

1968 vnodes[index] = p.v 

1969 # Put all vnodes in index order. 

1970 for index in sorted(vnodes): 

1971 v = vnodes.get(index) 

1972 if v: 

1973 # Write <t> elements only for vnodes that will be written. 

1974 # For example, vnodes in external files will be written 

1975 # only if the vnodes are cloned outside the file. 

1976 if v.isWriteBit(): 

1977 self.put_t_element(v) 

1978 else: 

1979 g.trace('can not happen: no VNode for', repr(index)) 

1980 # This prevents the file from being written. 

1981 raise BadLeoFile(f"no VNode for {repr(index)}") 

1982 #@+node:ekr.20050418161620.2: *5* fc.putUaHelper 

1983 def putUaHelper(self, torv, key, val): 

1984 """Put attribute whose name is key and value is val to the output stream.""" 

1985 # New in 4.3: leave string attributes starting with 'str_' alone. 

1986 if key.startswith('str_'): 

1987 if isinstance(val, (str, bytes)): 

1988 val = g.toUnicode(val) 

1989 attr = f' {key}="{xml.sax.saxutils.escape(val)}"' 

1990 return attr 

1991 g.trace(type(val), repr(val)) 

1992 g.warning("ignoring non-string attribute", key, "in", torv) 

1993 return '' 

1994 return self.pickle(torv=torv, val=val, tag=key) 

1995 #@+node:EKR.20040526202501: *5* fc.putUnknownAttributes 

1996 def putUnknownAttributes(self, v): 

1997 """Put pickleable values for all keys in v.unknownAttributes dictionary.""" 

1998 if not hasattr(v, 'unknownAttributes'): 

1999 return '' 

2000 attrDict = v.unknownAttributes 

2001 if isinstance(attrDict, dict): 

2002 val = ''.join( 

2003 [self.putUaHelper(v, key, val) 

2004 for key, val in attrDict.items()]) 

2005 return val 

2006 g.warning("ignoring non-dictionary unknownAttributes for", v) 

2007 return '' 

2008 #@+node:ekr.20031218072017.1863: *5* fc.put_v_element & helper 

2009 def put_v_element(self, p, isIgnore=False): 

2010 """Write a <v> element corresponding to a VNode.""" 

2011 fc = self 

2012 v = p.v 

2013 # 

2014 # Precompute constants. 

2015 isAuto = p.isAtAutoNode() and p.atAutoNodeName().strip() 

2016 isEdit = p.isAtEditNode() and p.atEditNodeName().strip() and not p.hasChildren() 

2017 # Write the entire @edit tree if it has children. 

2018 isFile = p.isAtFileNode() 

2019 isShadow = p.isAtShadowFileNode() 

2020 isThin = p.isAtThinFileNode() 

2021 # 

2022 # Set forcewrite. 

2023 if isIgnore or p.isAtIgnoreNode(): 

2024 forceWrite = True 

2025 elif isAuto or isEdit or isFile or isShadow or isThin: 

2026 forceWrite = False 

2027 else: 

2028 forceWrite = True 

2029 # 

2030 # Set the write bit if necessary. 

2031 gnx = v.fileIndex 

2032 if forceWrite or self.usingClipboard: 

2033 v.setWriteBit() # 4.2: Indicate we wrote the body text. 

2034 

2035 attrs = fc.compute_attribute_bits(forceWrite, p) 

2036 # 

2037 # Write the node. 

2038 v_head = f'<v t="{gnx}"{attrs}>' 

2039 if gnx in fc.vnodesDict: 

2040 fc.put(v_head + '</v>\n') 

2041 else: 

2042 fc.vnodesDict[gnx] = True 

2043 v_head += f"<vh>{xml.sax.saxutils.escape(p.v.headString() or '')}</vh>" 

2044 # New in 4.2: don't write child nodes of @file-thin trees 

2045 # (except when writing to clipboard) 

2046 if p.hasChildren() and (forceWrite or self.usingClipboard): 

2047 fc.put(f"{v_head}\n") 

2048 # This optimization eliminates all "recursive" copies. 

2049 p.moveToFirstChild() 

2050 while 1: 

2051 fc.put_v_element(p, isIgnore) 

2052 if p.hasNext(): 

2053 p.moveToNext() 

2054 else: 

2055 break 

2056 p.moveToParent() # Restore p in the caller. 

2057 fc.put('</v>\n') 

2058 else: 

2059 fc.put(f"{v_head}</v>\n") # Call put only once. 

2060 #@+node:ekr.20031218072017.1865: *6* fc.compute_attribute_bits 

2061 def compute_attribute_bits(self, forceWrite, p): 

2062 """Return the initial values of v's attributes.""" 

2063 attrs = [] 

2064 if p.hasChildren() and not forceWrite and not self.usingClipboard: 

2065 # Fix #526: do this for @auto nodes as well. 

2066 attrs.append(self.putDescendentVnodeUas(p)) 

2067 return ''.join(attrs) 

2068 #@+node:ekr.20031218072017.1579: *5* fc.put_v_elements & helper 

2069 def put_v_elements(self, p=None): 

2070 """Puts all <v> elements in the order in which they appear in the outline.""" 

2071 c = self.c 

2072 c.clearAllVisited() 

2073 self.put("<vnodes>\n") 

2074 # Make only one copy for all calls. 

2075 self.currentPosition = p or c.p 

2076 self.rootPosition = c.rootPosition() 

2077 self.vnodesDict = {} 

2078 if self.usingClipboard: 

2079 self.expanded_gnxs, self.marked_gnxs = set(), set() 

2080 # These will be ignored. 

2081 self.put_v_element(self.currentPosition) 

2082 # Write only current tree. 

2083 else: 

2084 for p in c.rootPosition().self_and_siblings(): 

2085 self.put_v_element(p, isIgnore=p.isAtIgnoreNode()) 

2086 # Fix #1018: scan *all* nodes. 

2087 self.setCachedBits() 

2088 self.put("</vnodes>\n") 

2089 #@+node:ekr.20190328160622.1: *6* fc.setCachedBits 

2090 def setCachedBits(self): 

2091 """ 

2092 Set the cached expanded and marked bits for *all* nodes. 

2093 Also cache the current position. 

2094 """ 

2095 trace = 'cache' in g.app.debug 

2096 c = self.c 

2097 if not c.mFileName: 

2098 return # New. 

2099 current = [str(z) for z in self.currentPosition.archivedPosition()] 

2100 expanded = [v.gnx for v in c.all_unique_nodes() if v.isExpanded()] 

2101 marked = [v.gnx for v in c.all_unique_nodes() if v.isMarked()] 

2102 c.db['expanded'] = ','.join(expanded) 

2103 c.db['marked'] = ','.join(marked) 

2104 c.db['current_position'] = ','.join(current) 

2105 if trace: 

2106 g.trace(f"\nset c.db for {c.shortFileName()}") 

2107 print('expanded:', expanded) 

2108 print('marked:', marked) 

2109 print('current_position:', current) 

2110 print('') 

2111 #@+node:ekr.20031218072017.1247: *5* fc.putXMLLine 

2112 def putXMLLine(self): 

2113 """Put the **properly encoded** <?xml> element.""" 

2114 # Use self.leo_file_encoding encoding. 

2115 self.put( 

2116 f"{g.app.prolog_prefix_string}" 

2117 f'"{self.leo_file_encoding}"' 

2118 f"{g.app.prolog_postfix_string}\n") 

2119 #@-others 

2120#@-others 

2121#@@language python 

2122#@@tabwidth -4 

2123#@@pagewidth 70 

2124#@-leo