Source code for leo.core.leoChapters

#@+leo-ver=5-thin
#@+node:ekr.20070317085508.1: * @file leoChapters.py
'''Classes that manage chapters in Leo's core.'''
import re
import string
import leo.core.leoGlobals as g
#@+others
#@+node:ekr.20070317085437: ** class ChapterController
[docs]class ChapterController(object): '''A per-commander controller that manages chapters and related nodes.''' #@+others #@+node:ekr.20070530075604: *3* Birth #@+node:ekr.20070317085437.2: *4* cc.ctor def __init__(self, c): '''Ctor for ChapterController class.''' self.c = c self.chaptersDict = {} # Keys are chapter names, values are chapters. # Important: chapter names never change, # even if their @chapter node changes. self.initing = True # Fix bug: https://github.com/leo-editor/leo-editor/issues/31 # True: suppress undo when creating chapters. self.re_chapter = None # Set where used. self.selectedChapter = None self.selectChapterLockout = False # True: cc.selectChapterForPosition does nothing. # Note: Used in qt_frame.py. self.tt = None # May be set in finishCreate. self.reloadSettings()
[docs] def reloadSettings(self): c = self.c self.use_tabs = c.config.getBool('use_chapter_tabs')
#@+node:ekr.20160402024827.1: *4* cc.createIcon
[docs] def createIcon(self): '''Create chapter-selection Qt ListBox in the icon area.''' cc = self c = cc.c if cc.use_tabs: if hasattr(c.frame.iconBar, 'createChaptersIcon'): if not cc.tt: cc.tt = c.frame.iconBar.createChaptersIcon()
#@+node:ekr.20070325104904: *4* cc.finishCreate # This must be called late in the init process, after the first redraw.
[docs] def finishCreate(self): '''Create the box in the icon area.''' cc = self cc.createIcon() cc.setAllChapterNames() # Create all chapters. # Fix bug: https://github.com/leo-editor/leo-editor/issues/31 cc.initing = False cc.selectChapterByName('main', collapse=False)
# Always select the main chapter. # It can be alarming to open a small chapter in a large .leo file. #@+node:ekr.20160411145155.1: *4* cc.makeCommand
[docs] def makeCommand(self, chapterName, binding=None): '''Make chapter-select-<chapterName> command.''' c, cc = self.c, self commandName = 'chapter-select-%s' % chapterName # # For tracing: # inverseBindingsDict = c.k.computeInverseBindingDict() if commandName in c.commandsDict: return def select_chapter_callback(event,cc=cc,name=chapterName): chapter = cc.chaptersDict.get(name) if chapter: try: cc.selectChapterLockout = True cc.selectChapterByNameHelper(chapter,collapse=True) c.redraw(chapter.p) # 2016/04/20. finally: cc.selectChapterLockout = False else: # Possible, but not likely. cc.note('no such chapter: %s' % name) # Always bind the command without a shortcut. # This will create the command bound to any existing settings. bindings = (None, binding) if binding else (None,) for shortcut in bindings: c.k.registerCommand(commandName, select_chapter_callback, shortcut=shortcut)
#@+node:ekr.20150509030349.1: *3* cc.cmd (decorator)
[docs] def cmd(name): '''Command decorator for the ChapterController class.''' # pylint: disable=no-self-argument return g.new_cmd_decorator(name, ['c', 'chapterController',])
#@+node:ekr.20070604165126: *3* cc.selectChapter
[docs] @cmd('chapter-select') def selectChapter(self, event=None): '''Use the minibuffer to get a chapter name, then create the chapter.''' cc, k = self, self.c.k names = cc.setAllChapterNames() g.es('Chapters:\n' + '\n'.join(names)) k.setLabelBlue('Select chapter: ') k.get1Arg(event, handler=self.selectChapter1, tabList=names)
[docs] def selectChapter1(self, event): cc, k = self, self.c.k k.clearState() k.resetLabel() if k.arg: cc.selectChapterByName(k.arg)
#@+node:ekr.20170202061705.1: *3* cc.selectNext/Back
[docs] @cmd('chapter-back') def backChapter(self, event=None): cc = self names = sorted(cc.setAllChapterNames()) sel_name = cc.selectedChapter.name if cc.selectedChapter else 'main' i = names.index(sel_name) new_name = names[i-1 if i > 0 else len(names)-1] cc.selectChapterByName(new_name)
[docs] @cmd('chapter-next') def nextChapter(self, event=None): cc = self names = sorted(cc.setAllChapterNames()) sel_name = cc.selectedChapter.name if cc.selectedChapter else 'main' i = names.index(sel_name) new_name = names[i+1 if i+1 < len(names) else 0] cc.selectChapterByName(new_name)
#@+node:ekr.20070317130250: *3* cc.selectChapterByName & helper
[docs] def selectChapterByName(self, name, collapse=True): '''Select a chapter. Return True if a redraw is needed.''' cc = self if self.selectChapterLockout: return if g.isInt(name): return cc.note('PyQt5 chapters not supported') chapter = cc.getChapter(name) if not chapter: g.es_print('no such @chapter node: %s' % name) return try: cc.selectChapterLockout = True cc.selectChapterByNameHelper(chapter, collapse=collapse) finally: cc.selectChapterLockout = False
#@+node:ekr.20090306060344.2: *4* cc.selectChapterByNameHelper
[docs] def selectChapterByNameHelper(self, chapter, collapse=True): '''Select the chapter, and redraw if necessary.''' cc, c = self, self.c if not cc.selectedChapter and chapter.name == 'main': chapter.p = c.p return if chapter == cc.selectedChapter: chapter.p = c.p return if cc.selectedChapter: cc.selectedChapter.unselect() else: main_chapter = cc.getChapter('main') if main_chapter: main_chapter.unselect() if chapter.p and c.positionExists(chapter.p): p = chapter.p elif chapter.name == 'main': p = chapter.p # Do *not* use c.p here! else: p = chapter.p = chapter.findRootNode() if not p: return chapter.select() c.setCurrentPosition(chapter.p) # Clean up, but not initially. if collapse and chapter.name == 'main': for p in c.all_positions(): # Compare vnodes, not positions. if p.v != c.p.v: p.contract() c.redraw(chapter.p)
# Fix part of #265. # Redraw only here, when we are sure it is needed. #@+node:ekr.20070317130648: *3* cc.Utils #@+node:ekr.20070320085610: *4* cc.error/note/warning
[docs] def error(self, s): g.error('Error: %s' % (s))
[docs] def note(self, s, killUnitTest=False): if g.unitTesting: if 0: # To trace cause of failed unit test. g.trace('=====',s, g.callers()) if killUnitTest: assert False, s else: g.note('Note: %s' % (s))
[docs] def warning(self, s): g.es_print('Warning: %s' % (s))
#@+node:ekr.20160402025448.1: *4* cc.findAnyChapterNode
[docs] def findAnyChapterNode(self): '''Return True if the outline contains any @chapter node.''' cc = self for p in cc.c.all_unique_positions(): if p.h.startswith('@chapter '): return True return False
#@+node:ekr.20071028091719: *4* cc.findChapterNameForPosition
[docs] def findChapterNameForPosition(self, p): '''Return the name of a chapter containing p or None if p does not exist.''' cc, c = self, self.c if not p or not c.positionExists(p): return None for name in cc.chaptersDict: if name != 'main': theChapter = cc.chaptersDict.get(name) if theChapter.positionIsInChapter(p): return name return 'main'
#@+node:ekr.20070325093617: *4* cc.findChapterNode
[docs] def findChapterNode(self, name): ''' Return the position of the first @chapter node with the given name anywhere in the entire outline. All @chapter nodes are created as children of the @chapters node, but users may move them anywhere. ''' cc = self name = g.toUnicode(name) for p in cc.c.all_positions(): chapterName, binding = self.parseHeadline(p) if chapterName == name: return p return None # Not an error.
#@+node:ekr.20070318124004: *4* cc.getChapter
[docs] def getChapter(self, name): cc = self return cc.chaptersDict.get(name)
#@+node:ekr.20070318122708: *4* cc.getSelectedChapter
[docs] def getSelectedChapter(self): cc = self return cc.selectedChapter
#@+node:ekr.20070605124356: *4* cc.inChapter
[docs] def inChapter(self): cc = self theChapter = cc.getSelectedChapter() return theChapter and theChapter.name != 'main'
#@+node:ekr.20160411152842.1: *4* cc.parseHeadline
[docs] def parseHeadline(self, p): '''Return the chapter name and key binding for p.h.''' if not self.re_chapter: self.re_chapter = re.compile( r'^@chapter\s+([^@]+)\s*(@key\s*=\s*(.+)\s*)?') # @chapter (all up to @) (@key=(binding))? # name=group(1), binding=group(3) m = self.re_chapter.search(p.h) if m: chapterName, binding = m.group(1), m.group(3) if chapterName: chapterName = self.sanitize(chapterName) if binding: binding = binding.strip() else: chapterName = binding = None return chapterName, binding
#@+node:ekr.20160414183716.1: *4* cc.sanitize
[docs] def sanitize(self, s): '''Convert s to a safe chapter name.''' # Similar to g.sanitize_filename, but simpler. result = [] for ch in s.strip(): if ch in (string.ascii_letters + string.digits): result.append(ch) elif ch in ' \t': result.append('-') s = ''.join(result) s = s.replace('--','-') return s[: 128]
#@+node:ekr.20070615075643: *4* cc.selectChapterForPosition
[docs] def selectChapterForPosition(self, p, chapter=None): ''' Select a chapter containing position p. New in Leo 4.11: prefer the given chapter if possible. Do nothing if p if p does not exist or is in the presently selected chapter. Note: this code calls c.redraw() if the chapter changes. ''' c, cc = self.c, self # New in Leo 4.11 if cc.selectChapterLockout: return selChapter = cc.getSelectedChapter() if not chapter and not selChapter: return if not p: return if not c.positionExists(p): return # New in Leo 4.11: prefer the given chapter if possible. theChapter = chapter or selChapter if not theChapter: return # First, try the presently selected chapter. firstName = theChapter.name if firstName == 'main': return if theChapter.positionIsInChapter(p): cc.selectChapterByName(theChapter.name) return for name in cc.chaptersDict: if name not in (firstName, 'main'): theChapter = cc.chaptersDict.get(name) if theChapter.positionIsInChapter(p): cc.selectChapterByName(name) break else: cc.selectChapterByName('main') # Fix bug 869385: Chapters make the nav_qt.py plugin useless assert not self.selectChapterLockout # New in Leo 5.6: don't call c.redraw immediately. c.redraw_later()
#@+node:ekr.20130915052002.11289: *4* cc.setAllChapterNames
[docs] def setAllChapterNames(self): '''Called early and often to discover all chapter names.''' c, cc = self.c, self # sel_name = cc.selectedChapter and cc.selectedChapter.name or 'main' if 'main' not in cc.chaptersDict: cc.chaptersDict['main'] = Chapter(c, cc, 'main') cc.makeCommand('main') # This binds any existing bindings to chapter-select-main. result, seen = ['main'], set() for p in c.all_unique_positions(): chapterName, binding = self.parseHeadline(p) if chapterName and p.v not in seen: seen.add(p.v) result.append(chapterName) if chapterName not in cc.chaptersDict: cc.chaptersDict[chapterName] = Chapter(c, cc, chapterName) cc.makeCommand(chapterName, binding) return result
#@-others #@+node:ekr.20070317085708: ** class Chapter
[docs]class Chapter(object): '''A class representing the non-gui data of a single chapter.''' #@+others #@+node:ekr.20070317085708.1: *3* chapter.__init__ def __init__(self, c, chapterController, name): self.c = c self.cc = cc = chapterController self.name = g.toUnicode(name) self.selectLockout = False # True: in chapter.select logic. # State variables: saved/restored when the chapter is unselected/selected. self.p = c.p self.root = self.findRootNode() if cc.tt: cc.tt.createTab(name) #@+node:ekr.20070317085708.2: *3* chapter.__str__ and __repr__ def __str__(self): '''Chapter.__str__''' return '<chapter: %s, p: %s>' % (self.name, repr(self.p and self.p.h)) __repr__ = __str__ #@+node:ekr.20110607182447.16464: *3* chapter.findRootNode
[docs] def findRootNode(self): '''Return the @chapter node for this chapter.''' if self.name == 'main': return None else: return self.cc.findChapterNode(self.name)
#@+node:ekr.20070317131205.1: *3* chapter.select & helpers
[docs] def select(self, w=None, selectEditor=True): '''Restore chapter information and redraw the tree when a chapter is selected.''' if self.selectLockout: return try: tt = self.cc.tt self.selectLockout = True self.chapterSelectHelper(w, selectEditor) if tt: # A bad kludge: update all the chapter names *after* the selection. tt.setTabLabel(self.name) finally: self.selectLockout = False
#@+node:ekr.20070423102603.1: *4* chapter.chapterSelectHelper
[docs] def chapterSelectHelper(self, w=None, selectEditor=True): c, cc = self.c, self.cc cc.selectedChapter = self if self.name == 'main': return # 2016/04/20 # Remember the root (it may have changed) for dehoist. self.root = root = self.findRootNode() if not root: # Might happen during unit testing or startup. return if self.p and not c.positionExists(self.p): self.p = p = root.copy() # Next, recompute p and possibly select a new editor. if w: assert w == c.frame.body.wrapper assert w.leo_p self.p = p = self.findPositionInChapter(w.leo_p) or root.copy() else: # This must be done *after* switching roots. self.p = p = self.findPositionInChapter(self.p) or root.copy() if selectEditor: # Careful: c.selectPosition would pop the hoist stack. w = self.findEditorInChapter(p) c.frame.body.selectEditor(w) # Switches text. self.p = p # 2016/04/20: Apparently essential. if g.match_word(p.h, 0, '@chapter'): if p.hasChildren(): self.p = p = p.firstChild() else: # 2016/04/20: Create a dummy first child. self.p = p = p.insertAsLastChild() p.h = 'New Headline' c.hoistStack.append(g.Bunch(p=root.copy(), expanded=True)) # Careful: c.selectPosition would pop the hoist stack. c.setCurrentPosition(p) g.doHook('hoist-changed', c=c)
#@+node:ekr.20070317131708: *4* chapter.findPositionInChapter
[docs] def findPositionInChapter(self, p1, strict=False): '''Return a valid position p such that p.v == v.''' c, name = self.c, self.name # Bug fix: 2012/05/24: Search without root arg in the main chapter. if name == 'main' and c.positionExists(p1): return p1 if not p1: return None root = self.findRootNode() if not root: return None if c.positionExists(p1, root=root.copy()): return p1 if strict: return None if name == 'main': theIter = c.all_unique_positions else: theIter = root.self_and_subtree for p in theIter(): if p.v == p1.v: return p.copy() return None
#@+node:ekr.20070425175522: *4* chapter.findEditorInChapter
[docs] def findEditorInChapter(self, p): '''return w, an editor displaying position p.''' chapter, c = self, self.c w = c.frame.body.findEditorForChapter(chapter, p) if w: w.leo_chapter = chapter w.leo_p = p and p.copy() return w
#@+node:ekr.20070615065222: *4* chapter.positionIsInChapter
[docs] def positionIsInChapter(self, p): p2 = self.findPositionInChapter(p, strict=True) return p2
#@+node:ekr.20070320091806.1: *3* chapter.unselect
[docs] def unselect(self): '''Remember chapter info when a chapter is about to be unselected.''' c = self.c # Always try to return to the same position. self.p = c.p if self.name == 'main': return root = None while c.hoistStack: bunch = c.hoistStack.pop() root = bunch.p if root == self.root: break # Re-institute the previous hoist. if c.hoistStack: p = c.hoistStack[-1].p # Careful: c.selectPosition would pop the hoist stack. c.setCurrentPosition(p) else: p = root or c.p c.setCurrentPosition(p)
#@-others #@-others #@@language python #@@tabwidth -4 #@@pagewidth 70 #@-leo