Coverage for C:\leo.repo\leo-editor\leo\core\leoChapters.py : 58%

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#@+leo-ver=5-thin
2#@+node:ekr.20070317085508.1: * @file leoChapters.py
3"""Classes that manage chapters in Leo's core."""
4import re
5import string
6from leo.core import leoGlobals as g
7#@+others
8#@+node:ekr.20150509030349.1: ** cc.cmd (decorator)
9def cmd(name):
10 """Command decorator for the ChapterController class."""
11 return g.new_cmd_decorator(name, ['c', 'chapterController',])
12#@+node:ekr.20070317085437: ** class ChapterController
13class ChapterController:
14 """A per-commander controller that manages chapters and related nodes."""
15 #@+others
16 #@+node:ekr.20070530075604: *3* Birth
17 #@+node:ekr.20070317085437.2: *4* cc.ctor
18 def __init__(self, c):
19 """Ctor for ChapterController class."""
20 self.c = c
21 self.chaptersDict = {}
22 # Keys are chapter names, values are chapters.
23 # Important: chapter names never change,
24 # even if their @chapter node changes.
25 self.initing = True
26 # #31
27 # True: suppress undo when creating chapters.
28 self.re_chapter = None
29 # Set where used.
30 self.selectedChapter = None
31 self.selectChapterLockout = False
32 # True: cc.selectChapterForPosition does nothing.
33 # Note: Used in qt_frame.py.
34 self.tt = None # May be set in finishCreate.
35 self.reloadSettings()
37 def reloadSettings(self):
38 c = self.c
39 self.use_tabs = c.config.getBool('use-chapter-tabs')
40 #@+node:ekr.20160402024827.1: *4* cc.createIcon
41 def createIcon(self):
42 """Create chapter-selection Qt ListBox in the icon area."""
43 cc = self
44 c = cc.c
45 if cc.use_tabs:
46 if hasattr(c.frame.iconBar, 'createChaptersIcon'):
47 if not cc.tt:
48 cc.tt = c.frame.iconBar.createChaptersIcon()
49 #@+node:ekr.20070325104904: *4* cc.finishCreate
50 # This must be called late in the init process, after the first redraw.
52 def finishCreate(self):
53 """Create the box in the icon area."""
54 c, cc = self.c, self
55 cc.createIcon()
56 cc.setAllChapterNames()
57 # Create all chapters.
58 # #31.
59 cc.initing = False
60 # Always select the main chapter.
61 # It can be alarming to open a small chapter in a large .leo file.
62 cc.selectChapterByName('main')
63 c.redraw()
64 #@+node:ekr.20160411145155.1: *4* cc.makeCommand
65 def makeCommand(self, chapterName, binding=None):
66 """Make chapter-select-<chapterName> command."""
67 c, cc = self.c, self
68 commandName = f"chapter-select-{chapterName}"
69 #
70 # For tracing:
71 # inverseBindingsDict = c.k.computeInverseBindingDict()
72 if commandName in c.commandsDict:
73 return
75 def select_chapter_callback(event, cc=cc, name=chapterName):
76 chapter = cc.chaptersDict.get(name)
77 if chapter:
78 try:
79 cc.selectChapterLockout = True
80 cc.selectChapterByNameHelper(chapter, collapse=True)
81 c.redraw(chapter.p) # 2016/04/20.
82 finally:
83 cc.selectChapterLockout = False
84 elif not g.unitTesting:
85 # Possible, but not likely.
86 cc.note(f"no such chapter: {name}")
88 # Always bind the command without a shortcut.
89 # This will create the command bound to any existing settings.
91 bindings = (None, binding) if binding else (None,)
92 for shortcut in bindings:
93 c.k.registerCommand(commandName, select_chapter_callback, shortcut=shortcut)
94 #@+node:ekr.20070604165126: *3* cc.selectChapter
95 @cmd('chapter-select')
96 def selectChapter(self, event=None):
97 """Use the minibuffer to get a chapter name, then create the chapter."""
98 cc, k = self, self.c.k
99 names = cc.setAllChapterNames()
100 g.es('Chapters:\n' + '\n'.join(names))
101 k.setLabelBlue('Select chapter: ')
102 k.get1Arg(event, handler=self.selectChapter1, tabList=names)
104 def selectChapter1(self, event):
105 cc, k = self, self.c.k
106 k.clearState()
107 k.resetLabel()
108 if k.arg:
109 cc.selectChapterByName(k.arg)
110 #@+node:ekr.20170202061705.1: *3* cc.selectNext/Back
111 @cmd('chapter-back')
112 def backChapter(self, event=None):
113 cc = self
114 names = sorted(cc.setAllChapterNames())
115 sel_name = cc.selectedChapter.name if cc.selectedChapter else 'main'
116 i = names.index(sel_name)
117 new_name = names[i - 1 if i > 0 else len(names) - 1]
118 cc.selectChapterByName(new_name)
120 @cmd('chapter-next')
121 def nextChapter(self, event=None):
122 cc = self
123 names = sorted(cc.setAllChapterNames())
124 sel_name = cc.selectedChapter.name if cc.selectedChapter else 'main'
125 i = names.index(sel_name)
126 new_name = names[i + 1 if i + 1 < len(names) else 0]
127 cc.selectChapterByName(new_name)
128 #@+node:ekr.20070317130250: *3* cc.selectChapterByName & helper
129 def selectChapterByName(self, name):
130 """Select a chapter without redrawing."""
131 cc = self
132 if self.selectChapterLockout:
133 return
134 if isinstance(name, int):
135 cc.note('PyQt5 chapters not supported')
136 return
137 chapter = cc.getChapter(name)
138 if not chapter:
139 if not g.unitTesting:
140 g.es_print(f"no such @chapter node: {name}")
141 return
142 try:
143 cc.selectChapterLockout = True
144 cc.selectChapterByNameHelper(chapter)
145 finally:
146 cc.selectChapterLockout = False
147 #@+node:ekr.20090306060344.2: *4* cc.selectChapterByNameHelper
148 def selectChapterByNameHelper(self, chapter, collapse=True):
149 """Select the chapter."""
150 cc, c = self, self.c
151 if not cc.selectedChapter and chapter.name == 'main':
152 chapter.p = c.p
153 return
154 if chapter == cc.selectedChapter:
155 chapter.p = c.p
156 return
157 if cc.selectedChapter:
158 cc.selectedChapter.unselect()
159 else:
160 main_chapter = cc.getChapter('main')
161 if main_chapter:
162 main_chapter.unselect()
163 if chapter.p and c.positionExists(chapter.p):
164 pass
165 elif chapter.name == 'main':
166 pass # Do not use c.p.
167 else:
168 chapter.p = chapter.findRootNode()
169 chapter.select()
170 c.contractAllHeadlines()
171 chapter.p.v.expand()
172 c.selectPosition(chapter.p)
173 #@+node:ekr.20070317130648: *3* cc.Utils
174 #@+node:ekr.20070320085610: *4* cc.error/note/warning
175 def error(self, s):
176 g.error(f"Error: {s}")
178 def note(self, s, killUnitTest=False):
179 if g.unitTesting:
180 if 0: # To trace cause of failed unit test.
181 g.trace('=====', s, g.callers())
182 if killUnitTest:
183 assert False, s
184 else:
185 g.note(f"Note: {s}")
187 def warning(self, s):
188 g.es_print(f"Warning: {s}")
189 #@+node:ekr.20160402025448.1: *4* cc.findAnyChapterNode
190 def findAnyChapterNode(self):
191 """Return True if the outline contains any @chapter node."""
192 cc = self
193 for p in cc.c.all_unique_positions():
194 if p.h.startswith('@chapter '):
195 return True
196 return False
197 #@+node:ekr.20071028091719: *4* cc.findChapterNameForPosition
198 def findChapterNameForPosition(self, p):
199 """Return the name of a chapter containing p or None if p does not exist."""
200 cc, c = self, self.c
201 if not p or not c.positionExists(p):
202 return None
203 for name in cc.chaptersDict:
204 if name != 'main':
205 theChapter = cc.chaptersDict.get(name)
206 if theChapter.positionIsInChapter(p):
207 return name
208 return 'main'
209 #@+node:ekr.20070325093617: *4* cc.findChapterNode
210 def findChapterNode(self, name):
211 """
212 Return the position of the first @chapter node with the given name
213 anywhere in the entire outline.
215 All @chapter nodes are created as children of the @chapters node,
216 but users may move them anywhere.
217 """
218 cc = self
219 name = g.checkUnicode(name)
220 for p in cc.c.all_positions():
221 chapterName, binding = self.parseHeadline(p)
222 if chapterName == name:
223 return p
224 return None # Not an error.
225 #@+node:ekr.20070318124004: *4* cc.getChapter
226 def getChapter(self, name):
227 cc = self
228 return cc.chaptersDict.get(name)
229 #@+node:ekr.20070318122708: *4* cc.getSelectedChapter
230 def getSelectedChapter(self):
231 cc = self
232 return cc.selectedChapter
233 #@+node:ekr.20070605124356: *4* cc.inChapter
234 def inChapter(self):
235 cc = self
236 theChapter = cc.getSelectedChapter()
237 return theChapter and theChapter.name != 'main'
238 #@+node:ekr.20160411152842.1: *4* cc.parseHeadline
239 def parseHeadline(self, p):
240 """Return the chapter name and key binding for p.h."""
241 if not self.re_chapter:
242 self.re_chapter = re.compile(
243 r'^@chapter\s+([^@]+)\s*(@key\s*=\s*(.+)\s*)?')
244 # @chapter (all up to @) (@key=(binding))?
245 # name=group(1), binding=group(3)
246 m = self.re_chapter.search(p.h)
247 if m:
248 chapterName, binding = m.group(1), m.group(3)
249 if chapterName:
250 chapterName = self.sanitize(chapterName)
251 if binding:
252 binding = binding.strip()
253 else:
254 chapterName = binding = None
255 return chapterName, binding
256 #@+node:ekr.20160414183716.1: *4* cc.sanitize
257 def sanitize(self, s):
258 """Convert s to a safe chapter name."""
259 # Similar to g.sanitize_filename, but simpler.
260 result = []
261 for ch in s.strip():
262 # pylint: disable=superfluous-parens
263 if ch in (string.ascii_letters + string.digits):
264 result.append(ch)
265 elif ch in ' \t':
266 result.append('-')
267 s = ''.join(result)
268 s = s.replace('--', '-')
269 return s[:128]
270 #@+node:ekr.20070615075643: *4* cc.selectChapterForPosition
271 def selectChapterForPosition(self, p, chapter=None):
272 """
273 Select a chapter containing position p.
274 New in Leo 4.11: prefer the given chapter if possible.
275 Do nothing if p if p does not exist or is in the presently selected chapter.
277 Note: this code calls c.redraw() if the chapter changes.
278 """
279 c, cc = self.c, self
280 # New in Leo 4.11
281 if cc.selectChapterLockout:
282 return
283 selChapter = cc.getSelectedChapter()
284 if not chapter and not selChapter:
285 return
286 if not p:
287 return
288 if not c.positionExists(p):
289 return
290 # New in Leo 4.11: prefer the given chapter if possible.
291 theChapter = chapter or selChapter
292 if not theChapter:
293 return
294 # First, try the presently selected chapter.
295 firstName = theChapter.name
296 if firstName == 'main':
297 return
298 if theChapter.positionIsInChapter(p):
299 cc.selectChapterByName(theChapter.name)
300 return
301 for name in cc.chaptersDict:
302 if name not in (firstName, 'main'):
303 theChapter = cc.chaptersDict.get(name)
304 if theChapter.positionIsInChapter(p):
305 cc.selectChapterByName(name)
306 break
307 else:
308 cc.selectChapterByName('main')
309 # Fix bug 869385: Chapters make the nav_qt.py plugin useless
310 assert not self.selectChapterLockout
311 # New in Leo 5.6: don't call c.redraw immediately.
312 c.redraw_later()
313 #@+node:ekr.20130915052002.11289: *4* cc.setAllChapterNames
314 def setAllChapterNames(self):
315 """Called early and often to discover all chapter names."""
316 c, cc = self.c, self
317 # sel_name = cc.selectedChapter and cc.selectedChapter.name or 'main'
318 if 'main' not in cc.chaptersDict:
319 cc.chaptersDict['main'] = Chapter(c, cc, 'main')
320 cc.makeCommand('main')
321 # This binds any existing bindings to chapter-select-main.
322 result, seen = ['main'], set()
323 for p in c.all_unique_positions():
324 chapterName, binding = self.parseHeadline(p)
325 if chapterName and p.v not in seen:
326 seen.add(p.v)
327 result.append(chapterName)
328 if chapterName not in cc.chaptersDict:
329 cc.chaptersDict[chapterName] = Chapter(c, cc, chapterName)
330 cc.makeCommand(chapterName, binding)
331 return result
332 #@-others
333#@+node:ekr.20070317085708: ** class Chapter
334class Chapter:
335 """A class representing the non-gui data of a single chapter."""
336 #@+others
337 #@+node:ekr.20070317085708.1: *3* chapter.__init__
338 def __init__(self, c, chapterController, name):
339 self.c = c
340 self.cc = cc = chapterController
341 self.name = g.checkUnicode(name)
342 self.selectLockout = False # True: in chapter.select logic.
343 # State variables: saved/restored when the chapter is unselected/selected.
344 self.p = c.p
345 self.root = self.findRootNode()
346 if cc.tt:
347 cc.tt.createTab(name)
348 #@+node:ekr.20070317085708.2: *3* chapter.__str__ and __repr__
349 def __str__(self):
350 """Chapter.__str__"""
351 return f"<chapter: {self.name}, p: {repr(self.p and self.p.h)}>"
353 __repr__ = __str__
354 #@+node:ekr.20110607182447.16464: *3* chapter.findRootNode
355 def findRootNode(self):
356 """Return the @chapter node for this chapter."""
357 if self.name == 'main':
358 return None
359 return self.cc.findChapterNode(self.name)
360 #@+node:ekr.20070317131205.1: *3* chapter.select & helpers
361 def select(self, w=None):
362 """Restore chapter information and redraw the tree when a chapter is selected."""
363 if self.selectLockout:
364 return
365 try:
366 tt = self.cc.tt
367 self.selectLockout = True
368 self.chapterSelectHelper(w)
369 if tt:
370 # A bad kludge: update all the chapter names *after* the selection.
371 tt.setTabLabel(self.name)
372 finally:
373 self.selectLockout = False
374 #@+node:ekr.20070423102603.1: *4* chapter.chapterSelectHelper
375 def chapterSelectHelper(self, w=None):
377 c, cc = self.c, self.cc
378 cc.selectedChapter = self
379 if self.name == 'main':
380 return # 2016/04/20
381 # Remember the root (it may have changed) for dehoist.
382 self.root = root = self.findRootNode()
383 if not root:
384 # Might happen during unit testing or startup.
385 return
386 if self.p and not c.positionExists(self.p):
387 self.p = p = root.copy()
388 # Next, recompute p and possibly select a new editor.
389 if w:
390 assert w == c.frame.body.wrapper
391 assert w.leo_p
392 self.p = p = self.findPositionInChapter(w.leo_p) or root.copy()
393 else:
394 # This must be done *after* switching roots.
395 self.p = p = self.findPositionInChapter(self.p) or root.copy()
396 # Careful: c.selectPosition would pop the hoist stack.
397 w = self.findEditorInChapter(p)
398 c.frame.body.selectEditor(w) # Switches text.
399 self.p = p # 2016/04/20: Apparently essential.
400 if g.match_word(p.h, 0, '@chapter'):
401 if p.hasChildren():
402 self.p = p = p.firstChild()
403 else:
404 # 2016/04/20: Create a dummy first child.
405 self.p = p = p.insertAsLastChild()
406 p.h = 'New Headline'
407 c.hoistStack.append(g.Bunch(p=root.copy(), expanded=True))
408 # Careful: c.selectPosition would pop the hoist stack.
409 c.setCurrentPosition(p)
410 g.doHook('hoist-changed', c=c)
411 #@+node:ekr.20070317131708: *4* chapter.findPositionInChapter
412 def findPositionInChapter(self, p1, strict=False):
413 """Return a valid position p such that p.v == v."""
414 c, name = self.c, self.name
415 # Bug fix: 2012/05/24: Search without root arg in the main chapter.
416 if name == 'main' and c.positionExists(p1):
417 return p1
418 if not p1:
419 return None
420 root = self.findRootNode()
421 if not root:
422 return None
423 if c.positionExists(p1, root=root.copy()):
424 return p1
425 if strict:
426 return None
427 if name == 'main':
428 theIter = c.all_unique_positions
429 else:
430 theIter = root.self_and_subtree
431 for p in theIter(copy=False):
432 if p.v == p1.v:
433 return p.copy()
434 return None
435 #@+node:ekr.20070425175522: *4* chapter.findEditorInChapter
436 def findEditorInChapter(self, p):
437 """return w, an editor displaying position p."""
438 chapter, c = self, self.c
439 w = c.frame.body.findEditorForChapter(chapter, p)
440 if w:
441 w.leo_chapter = chapter
442 w.leo_p = p and p.copy()
443 return w
444 #@+node:ekr.20070615065222: *4* chapter.positionIsInChapter
445 def positionIsInChapter(self, p):
446 p2 = self.findPositionInChapter(p, strict=True)
447 return p2
448 #@+node:ekr.20070320091806.1: *3* chapter.unselect
449 def unselect(self):
450 """Remember chapter info when a chapter is about to be unselected."""
451 c = self.c
452 # Always try to return to the same position.
453 self.p = c.p
454 if self.name == 'main':
455 return
456 root = None
457 while c.hoistStack:
458 bunch = c.hoistStack.pop()
459 root = bunch.p
460 if root == self.root:
461 break
462 # Re-institute the previous hoist.
463 if c.hoistStack:
464 p = c.hoistStack[-1].p
465 # Careful: c.selectPosition would pop the hoist stack.
466 c.setCurrentPosition(p)
467 else:
468 p = root or c.p
469 c.setCurrentPosition(p)
470 #@-others
471#@-others
472#@@language python
473#@@tabwidth -4
474#@@pagewidth 70
475#@-leo