Coverage for C:\Repos\leo-editor\leo\plugins\qt_frame.py: 37%

3115 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

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

2#@+leo-ver=5-thin 

3#@+node:ekr.20140907123524.18774: * @file ../plugins/qt_frame.py 

4#@@first 

5"""Leo's qt frame classes.""" 

6#@+<< imports qt_frame.py >> 

7#@+node:ekr.20110605121601.18003: ** << imports qt_frame.py >> 

8from collections import defaultdict 

9import os 

10import platform 

11import string 

12import sys 

13import time 

14from typing import Any, Callable, Dict, List, Optional, Tuple 

15from typing import TYPE_CHECKING 

16from leo.core import leoGlobals as g 

17from leo.core import leoColor 

18from leo.core import leoColorizer 

19from leo.core import leoFrame 

20from leo.core import leoGui 

21from leo.core import leoMenu 

22from leo.commands import gotoCommands 

23from leo.core.leoQt import isQt5, isQt6, QtCore, QtGui, QtWidgets 

24from leo.core.leoQt import QAction, Qsci 

25from leo.core.leoQt import Alignment, ContextMenuPolicy, DropAction, FocusReason, KeyboardModifier 

26from leo.core.leoQt import MoveOperation, Orientation, MouseButton 

27from leo.core.leoQt import Policy, ScrollBarPolicy, SelectionBehavior, SelectionMode, SizeAdjustPolicy 

28from leo.core.leoQt import Shadow, Shape, Style 

29from leo.core.leoQt import TextInteractionFlag, ToolBarArea, Type, Weight, WindowState, WrapMode 

30from leo.plugins import qt_events 

31from leo.plugins import qt_text 

32from leo.plugins import qt_tree 

33from leo.plugins.mod_scripting import build_rclick_tree 

34from leo.plugins.nested_splitter import NestedSplitter 

35#@-<< imports qt_frame.py >> 

36#@+<< type aliases qt_frame.py >> 

37#@+node:ekr.20220415080427.1: ** << type aliases qt_frame.py >> 

38if TYPE_CHECKING: # Always False at runtime. 

39 from leo.core.leoCommands import Commands as Cmdr 

40 from leo.core.leoNodes import Position as Pos 

41else: 

42 Cmdr = Any 

43 Pos = Any 

44 QComboBox = Any 

45Event = Any 

46Widget = Any 

47Wrapper = Any 

48#@-<< type aliases qt_frame.py >> 

49#@+others 

50#@+node:ekr.20200303082457.1: ** top-level commands (qt_frame.py) 

51#@+node:ekr.20200303082511.6: *3* 'contract-body-pane' & 'expand-outline-pane' 

52@g.command('contract-body-pane') 

53@g.command('expand-outline-pane') 

54def contractBodyPane(event: Event) -> None: 

55 """Contract the body pane. Expand the outline/log splitter.""" 

56 c = event.get('c') 

57 if not c: 

58 return 

59 f = c.frame 

60 r = min(1.0, f.ratio + 0.1) 

61 f.divideLeoSplitter1(r) 

62 

63expandOutlinePane = contractBodyPane 

64#@+node:ekr.20200303084048.1: *3* 'contract-log-pane' 

65@g.command('contract-log-pane') 

66def contractLogPane(event: Event) -> None: 

67 """Contract the log pane. Expand the outline pane.""" 

68 c = event.get('c') 

69 if not c: 

70 return 

71 f = c.frame 

72 r = min(1.0, f.secondary_ratio + 0.1) 

73 f.divideLeoSplitter2(r) 

74#@+node:ekr.20200303084225.1: *3* 'contract-outline-pane' & 'expand-body-pane' 

75@g.command('contract-outline-pane') 

76@g.command('expand-body-pane') 

77def contractOutlinePane(event: Event) -> None: 

78 """Contract the outline pane. Expand the body pane.""" 

79 c = event.get('c') 

80 if not c: 

81 return 

82 f = c.frame 

83 r = max(0.0, f.ratio - 0.1) 

84 f.divideLeoSplitter1(r) 

85 

86expandBodyPane = contractOutlinePane 

87#@+node:ekr.20200303084226.1: *3* 'expand-log-pane' 

88@g.command('expand-log-pane') 

89def expandLogPane(event: Event) -> None: 

90 """Expand the log pane. Contract the outline pane.""" 

91 c = event.get('c') 

92 if not c: 

93 return 

94 f = c.frame 

95 r = max(0.0, f.secondary_ratio - 0.1) 

96 f.divideLeoSplitter2(r) 

97#@+node:ekr.20200303084610.1: *3* 'hide-body-pane' 

98@g.command('hide-body-pane') 

99def hideBodyPane(event: Event) -> None: 

100 """Hide the body pane. Fully expand the outline/log splitter.""" 

101 c = event.get('c') 

102 if not c: 

103 return 

104 c.frame.divideLeoSplitter1(1.0) 

105#@+node:ekr.20200303084625.1: *3* 'hide-log-pane' 

106@g.command('hide-log-pane') 

107def hideLogPane(event: Event) -> None: 

108 """Hide the log pane. Fully expand the outline pane.""" 

109 c = event.get('c') 

110 if not c: 

111 return 

112 c.frame.divideLeoSplitter2(1.0) 

113#@+node:ekr.20200303082511.7: *3* 'hide-outline-pane' 

114@g.command('hide-outline-pane') 

115def hideOutlinePane(event: Event) -> None: 

116 """Hide the outline/log splitter. Fully expand the body pane.""" 

117 c = event.get('c') 

118 if not c: 

119 return 

120 c.frame.divideLeoSplitter1(0.0) 

121 

122#@+node:ekr.20210228142208.1: ** decorators (qt_frame.py) 

123def body_cmd(name: str) -> Callable: 

124 """Command decorator for the LeoQtBody class.""" 

125 return g.new_cmd_decorator(name, ['c', 'frame', 'body']) 

126 

127def frame_cmd(name: str) -> Callable: 

128 """Command decorator for the LeoQtFrame class.""" 

129 return g.new_cmd_decorator(name, ['c', 'frame',]) 

130 

131def log_cmd(name: str) -> Callable: 

132 """Command decorator for the LeoQtLog class.""" 

133 return g.new_cmd_decorator(name, ['c', 'frame', 'log']) 

134#@+node:ekr.20110605121601.18137: ** class DynamicWindow (QMainWindow) 

135class DynamicWindow(QtWidgets.QMainWindow): # type:ignore 

136 """ 

137 A class representing all parts of the main Qt window. 

138 

139 c.frame.top is a DynamicWindow. 

140 c.frame.top.leo_master is a LeoTabbedTopLevel. 

141 c.frame.top.parent() is a QStackedWidget() 

142 

143 All leoQtX classes use the ivars of this Window class to 

144 support operations requested by Leo's core. 

145 """ 

146 #@+others 

147 #@+node:ekr.20110605121601.18138: *3* dw.ctor & reloadSettings 

148 def __init__(self, c: Cmdr, parent: Widget=None) -> None: 

149 """Ctor for the DynamicWindow class. The main window is c.frame.top""" 

150 # Called from LeoQtFrame.finishCreate. 

151 # parent is a LeoTabbedTopLevel. 

152 super().__init__(parent) 

153 self.leo_c = c 

154 self.leo_master = None # Set in construct. 

155 self.leo_menubar = None # Set in createMenuBar. 

156 c._style_deltas = defaultdict(lambda: 0) # for adjusting styles dynamically 

157 self.reloadSettings() 

158 

159 def reloadSettings(self) -> None: 

160 c = self.leo_c 

161 c.registerReloadSettings(self) 

162 self.bigTree = c.config.getBool('big-outline-pane') 

163 self.show_iconbar = c.config.getBool('show-iconbar', default=True) 

164 self.toolbar_orientation = c.config.getString('qt-toolbar-location') or '' 

165 self.use_gutter = c.config.getBool('use-gutter', default=False) 

166 if getattr(self, 'iconBar', None): 

167 if self.show_iconbar: 

168 self.iconBar.show() 

169 else: 

170 self.iconBar.hide() 

171 #@+node:ekr.20110605121601.18172: *3* dw.do_leo_spell_btn_* 

172 def doSpellBtn(self, btn: Any) -> None: 

173 """Execute btn, a button handler.""" 

174 # Make *sure* this never crashes. 

175 try: 

176 tab = self.leo_c.spellCommands.handler.tab 

177 button = getattr(tab, btn) 

178 button() 

179 except Exception: 

180 g.es_exception() 

181 

182 def do_leo_spell_btn_Add(self) -> None: 

183 self.doSpellBtn('onAddButton') 

184 

185 def do_leo_spell_btn_Change(self) -> None: 

186 self.doSpellBtn('onChangeButton') 

187 

188 def do_leo_spell_btn_Find(self) -> None: 

189 self.doSpellBtn('onFindButton') 

190 

191 def do_leo_spell_btn_FindChange(self) -> None: 

192 self.doSpellBtn('onChangeThenFindButton') 

193 

194 def do_leo_spell_btn_Hide(self) -> None: 

195 self.doSpellBtn('onHideButton') 

196 

197 def do_leo_spell_btn_Ignore(self) -> None: 

198 self.doSpellBtn('onIgnoreButton') 

199 #@+node:ekr.20110605121601.18140: *3* dw.closeEvent 

200 def closeEvent(self, event: Event) -> None: 

201 """Handle a close event in the Leo window.""" 

202 c = self.leo_c 

203 if not c.exists: 

204 # Fixes double-prompt bug on Linux. 

205 event.accept() 

206 return 

207 if c.inCommand: 

208 c.requestCloseWindow = True 

209 return 

210 ok = g.app.closeLeoWindow(c.frame) 

211 if ok: 

212 event.accept() 

213 else: 

214 event.ignore() 

215 #@+node:ekr.20110605121601.18139: *3* dw.construct & helpers 

216 def construct(self, master: Any=None) -> None: 

217 """ Factor 'heavy duty' code out from the DynamicWindow ctor """ 

218 c = self.leo_c 

219 self.leo_master = master # A LeoTabbedTopLevel or None for non-tabbed windows. 

220 self.useScintilla = c.config.getBool('qt-use-scintilla') 

221 self.reloadSettings() 

222 main_splitter, secondary_splitter = self.createMainWindow() 

223 self.iconBar = self.addToolBar("IconBar") 

224 self.iconBar.setObjectName('icon-bar') # Required for QMainWindow.saveState(). 

225 self.set_icon_bar_orientation(c) 

226 # #266 A setting to hide the icon bar. 

227 # Calling reloadSettings again would also work. 

228 if not self.show_iconbar: 

229 self.iconBar.hide() 

230 self.leo_menubar = self.menuBar() 

231 self.statusBar = QtWidgets.QStatusBar() 

232 self.setStatusBar(self.statusBar) 

233 orientation = c.config.getString('initial-split-orientation') 

234 self.setSplitDirection(main_splitter, secondary_splitter, orientation) 

235 if hasattr(c, 'styleSheetManager'): 

236 c.styleSheetManager.set_style_sheets(top=self, all=True) 

237 #@+node:ekr.20140915062551.19519: *4* dw.set_icon_bar_orientation 

238 def set_icon_bar_orientation(self, c: Cmdr) -> None: 

239 """Set the orientation of the icon bar based on settings.""" 

240 d = { 

241 'bottom': ToolBarArea.BottomToolBarArea, 

242 'left': ToolBarArea.LeftToolBarArea, 

243 'right': ToolBarArea.RightToolBarArea, 

244 'top': ToolBarArea.TopToolBarArea, 

245 } 

246 where = self.toolbar_orientation 

247 if not where: 

248 where = 'top' 

249 where = d.get(where.lower()) 

250 if where: 

251 self.addToolBar(where, self.iconBar) 

252 #@+node:ekr.20110605121601.18141: *3* dw.createMainWindow & helpers 

253 def createMainWindow(self) -> Tuple[Widget, Widget]: 

254 """ 

255 Create the component ivars of the main window. 

256 Copied/adapted from qt_main.py. 

257 Called instead of uic.loadUi(ui_description_file, self) 

258 """ 

259 self.setMainWindowOptions() 

260 # Legacy code: will not go away. 

261 self.createCentralWidget() 

262 # Create .verticalLayout 

263 main_splitter, secondary_splitter = self.createMainLayout(self.centralwidget) 

264 if self.bigTree: 

265 # Top pane contains only outline. Bottom pane contains body and log panes. 

266 self.createBodyPane(secondary_splitter) 

267 self.createLogPane(secondary_splitter) 

268 treeFrame = self.createOutlinePane(main_splitter) 

269 main_splitter.addWidget(treeFrame) 

270 main_splitter.addWidget(secondary_splitter) 

271 else: 

272 # Top pane contains outline and log panes. 

273 self.createOutlinePane(secondary_splitter) 

274 self.createLogPane(secondary_splitter) 

275 self.createBodyPane(main_splitter) 

276 self.createMiniBuffer(self.centralwidget) 

277 self.createMenuBar() 

278 self.createStatusBar(self) 

279 # Signals... 

280 QtCore.QMetaObject.connectSlotsByName(self) 

281 return main_splitter, secondary_splitter 

282 #@+node:ekr.20110605121601.18142: *4* dw.top-level 

283 #@+node:ekr.20190118150859.10: *5* dw.addNewEditor 

284 def addNewEditor(self, name: str) -> Tuple[Widget, Wrapper]: 

285 """Create a new body editor.""" 

286 c, p = self.leo_c, self.leo_c.p 

287 body = c.frame.body 

288 assert isinstance(body, LeoQtBody), repr(body) 

289 # Step 1: create the editor. 

290 parent_frame = c.frame.top.leo_body_inner_frame 

291 widget = qt_text.LeoQTextBrowser(parent_frame, c, self) 

292 widget.setObjectName('richTextEdit') # Will be changed later. 

293 wrapper = qt_text.QTextEditWrapper(widget, name='body', c=c) 

294 self.packLabel(widget) 

295 # Step 2: inject ivars, set bindings, etc. 

296 inner_frame = c.frame.top.leo_body_inner_frame # Inject ivars *here*. 

297 body.injectIvars(inner_frame, name, p, wrapper) 

298 body.updateInjectedIvars(widget, p) 

299 wrapper.setAllText(p.b) 

300 wrapper.see(0) 

301 c.k.completeAllBindingsForWidget(wrapper) 

302 if isinstance(widget, QtWidgets.QTextEdit): 

303 colorizer = leoColorizer.make_colorizer(c, widget) 

304 colorizer.highlighter.setDocument(widget.document()) 

305 else: 

306 # Scintilla only. 

307 body.recolorWidget(p, wrapper) 

308 return parent_frame, wrapper 

309 #@+node:ekr.20110605121601.18143: *5* dw.createBodyPane 

310 def createBodyPane(self, parent: Widget) -> Widget: 

311 """ 

312 Create the *pane* for the body, not the actual QTextBrowser. 

313 """ 

314 c = self.leo_c 

315 # 

316 # Create widgets. 

317 bodyFrame = self.createFrame(parent, 'bodyFrame') 

318 innerFrame = self.createFrame(bodyFrame, 'innerBodyFrame') 

319 sw = self.createStackedWidget(innerFrame, 'bodyStackedWidget', 

320 hPolicy=Policy.Expanding, vPolicy=Policy.Expanding) 

321 page2 = QtWidgets.QWidget() 

322 self.setName(page2, 'bodyPage2') 

323 body = self.createText(page2, 'richTextEdit') # A LeoQTextBrowser 

324 # 

325 # Pack. 

326 vLayout = self.createVLayout(page2, 'bodyVLayout', spacing=0) 

327 grid = self.createGrid(bodyFrame, 'bodyGrid') 

328 innerGrid = self.createGrid(innerFrame, 'bodyInnerGrid') 

329 if self.use_gutter: 

330 lineWidget = qt_text.LeoLineTextWidget(c, body) 

331 vLayout.addWidget(lineWidget) 

332 else: 

333 vLayout.addWidget(body) 

334 sw.addWidget(page2) 

335 innerGrid.addWidget(sw, 0, 0, 1, 1) 

336 grid.addWidget(innerFrame, 0, 0, 1, 1) 

337 self.verticalLayout.addWidget(parent) 

338 # 

339 # Official ivars 

340 self.text_page = page2 

341 self.stackedWidget = sw # used by LeoQtBody 

342 self.richTextEdit = body 

343 self.leo_body_frame = bodyFrame 

344 self.leo_body_inner_frame = innerFrame 

345 return bodyFrame 

346 

347 #@+node:ekr.20110605121601.18144: *5* dw.createCentralWidget 

348 def createCentralWidget(self) -> Widget: 

349 """Create the central widget.""" 

350 dw = self 

351 w = QtWidgets.QWidget(dw) 

352 w.setObjectName("centralwidget") 

353 dw.setCentralWidget(w) 

354 # Official ivars. 

355 self.centralwidget = w 

356 return w 

357 #@+node:ekr.20110605121601.18145: *5* dw.createLogPane & helpers 

358 def createLogPane(self, parent: Any) -> None: 

359 """Create all parts of Leo's log pane.""" 

360 c = self.leo_c 

361 # 

362 # Create the log frame. 

363 logFrame = self.createFrame(parent, 'logFrame', vPolicy=Policy.Minimum) 

364 innerFrame = self.createFrame(logFrame, 'logInnerFrame', 

365 hPolicy=Policy.Preferred, vPolicy=Policy.Expanding) 

366 tabWidget = self.createTabWidget(innerFrame, 'logTabWidget') 

367 # 

368 # Pack. 

369 innerGrid = self.createGrid(innerFrame, 'logInnerGrid') 

370 innerGrid.addWidget(tabWidget, 0, 0, 1, 1) 

371 outerGrid = self.createGrid(logFrame, 'logGrid') 

372 outerGrid.addWidget(innerFrame, 0, 0, 1, 1) 

373 # 

374 # Create the Find tab, embedded in a QScrollArea. 

375 findScrollArea = QtWidgets.QScrollArea() 

376 findScrollArea.setObjectName('findScrollArea') 

377 # Find tab. 

378 findTab = QtWidgets.QWidget() 

379 findTab.setObjectName('findTab') 

380 # 

381 # #516 and #1507: Create a Find tab unless we are using a dialog. 

382 # 

383 # Careful: @bool minibuffer-ding-mode overrides @bool use-find-dialog. 

384 use_minibuffer = c.config.getBool('minibuffer-find-mode', default=False) 

385 use_dialog = c.config.getBool('use-find-dialog', default=False) 

386 if use_minibuffer or not use_dialog: 

387 tabWidget.addTab(findScrollArea, 'Find') 

388 # Complete the Find tab in LeoFind.finishCreate. 

389 self.findScrollArea = findScrollArea 

390 self.findTab = findTab 

391 # 

392 # Spell tab. 

393 spellTab = QtWidgets.QWidget() 

394 spellTab.setObjectName('spellTab') 

395 tabWidget.addTab(spellTab, 'Spell') 

396 self.createSpellTab(spellTab) 

397 tabWidget.setCurrentIndex(1) 

398 # 

399 # Official ivars 

400 self.tabWidget = tabWidget # Used by LeoQtLog. 

401 #@+node:ekr.20131118172620.16858: *6* dw.finishCreateLogPane 

402 def finishCreateLogPane(self) -> None: 

403 """It's useful to create this late, because c.config is now valid.""" 

404 assert self.findTab 

405 self.createFindTab(self.findTab, self.findScrollArea) 

406 self.findScrollArea.setWidget(self.findTab) 

407 #@+node:ekr.20110605121601.18146: *5* dw.createMainLayout 

408 def createMainLayout(self, parent: Widget) -> Tuple[Widget, Widget]: 

409 """Create the layout for Leo's main window.""" 

410 # c = self.leo_c 

411 vLayout = self.createVLayout(parent, 'mainVLayout', margin=3) 

412 main_splitter = NestedSplitter(parent) 

413 main_splitter.setObjectName('main_splitter') 

414 main_splitter.setOrientation(Orientation.Vertical) 

415 secondary_splitter = NestedSplitter(main_splitter) 

416 secondary_splitter.setObjectName('secondary_splitter') 

417 secondary_splitter.setOrientation(Orientation.Horizontal) 

418 # Official ivar: 

419 self.verticalLayout = vLayout 

420 self.setSizePolicy(secondary_splitter) 

421 self.verticalLayout.addWidget(main_splitter) 

422 return main_splitter, secondary_splitter 

423 #@+node:ekr.20110605121601.18147: *5* dw.createMenuBar 

424 def createMenuBar(self) -> None: 

425 """Create Leo's menu bar.""" 

426 dw = self 

427 w = QtWidgets.QMenuBar(dw) 

428 w.setNativeMenuBar(platform.system() == 'Darwin') 

429 w.setGeometry(QtCore.QRect(0, 0, 957, 22)) 

430 w.setObjectName("menubar") 

431 dw.setMenuBar(w) 

432 # Official ivars. 

433 self.leo_menubar = w 

434 #@+node:ekr.20110605121601.18148: *5* dw.createMiniBuffer (class VisLineEdit) 

435 def createMiniBuffer(self, parent: Widget) -> Widget: 

436 """Create the widgets for Leo's minibuffer area.""" 

437 # Create widgets. 

438 frame = self.createFrame(parent, 'minibufferFrame', 

439 hPolicy=Policy.MinimumExpanding, vPolicy=Policy.Fixed) 

440 frame.setMinimumSize(QtCore.QSize(100, 0)) 

441 label = self.createLabel(frame, 'minibufferLabel', 'Minibuffer:') 

442 

443 

444 class VisLineEdit(QtWidgets.QLineEdit): # type:ignore 

445 """In case user has hidden minibuffer with gui-minibuffer-hide""" 

446 

447 def focusInEvent(self, event: Event) -> None: 

448 self.parent().show() 

449 super().focusInEvent(event) # Call the base class method. 

450 

451 def focusOutEvent(self, event: Event) -> None: 

452 self.store_selection() 

453 super().focusOutEvent(event) 

454 

455 def restore_selection(self) -> None: 

456 w = self 

457 i, j, ins = self._sel_and_insert 

458 if i == j: 

459 w.setCursorPosition(i) 

460 else: 

461 length = j - i 

462 # Set selection is a QLineEditMethod 

463 if ins < j: 

464 w.setSelection(j, -length) 

465 else: 

466 w.setSelection(i, length) 

467 

468 def store_selection(self) -> None: 

469 w = self 

470 ins = w.cursorPosition() 

471 if w.hasSelectedText(): 

472 i = w.selectionStart() 

473 s = w.selectedText() 

474 j = i + len(s) 

475 else: 

476 i = j = ins 

477 w._sel_and_insert = (i, j, ins) 

478 

479 lineEdit = VisLineEdit(frame) 

480 lineEdit._sel_and_insert = (0, 0, 0) 

481 lineEdit.setObjectName('lineEdit') # name important. 

482 # Pack. 

483 hLayout = self.createHLayout(frame, 'minibufferHLayout', spacing=4) 

484 hLayout.setContentsMargins(3, 2, 2, 0) 

485 hLayout.addWidget(label) 

486 hLayout.addWidget(lineEdit) 

487 self.verticalLayout.addWidget(frame) 

488 # Transfers focus request from label to lineEdit. 

489 label.setBuddy(lineEdit) 

490 # 

491 # Official ivars. 

492 self.lineEdit = lineEdit 

493 # self.leo_minibuffer_frame = frame 

494 # self.leo_minibuffer_layout = layout 

495 return frame 

496 #@+node:ekr.20110605121601.18149: *5* dw.createOutlinePane 

497 def createOutlinePane(self, parent: Widget) -> Widget: 

498 """Create the widgets and ivars for Leo's outline.""" 

499 # Create widgets. 

500 treeFrame = self.createFrame(parent, 'outlineFrame', vPolicy=Policy.Expanding) 

501 innerFrame = self.createFrame(treeFrame, 'outlineInnerFrame', hPolicy=Policy.Preferred) 

502 treeWidget = self.createTreeWidget(innerFrame, 'treeWidget') 

503 grid = self.createGrid(treeFrame, 'outlineGrid') 

504 grid.addWidget(innerFrame, 0, 0, 1, 1) 

505 innerGrid = self.createGrid(innerFrame, 'outlineInnerGrid') 

506 innerGrid.addWidget(treeWidget, 0, 0, 1, 1) 

507 # Official ivars... 

508 self.treeWidget = treeWidget 

509 return treeFrame 

510 #@+node:ekr.20110605121601.18150: *5* dw.createStatusBar 

511 def createStatusBar(self, parent: Widget) -> None: 

512 """Create the widgets and ivars for Leo's status area.""" 

513 w = QtWidgets.QStatusBar(parent) 

514 w.setObjectName("statusbar") 

515 parent.setStatusBar(w) 

516 # Official ivars. 

517 self.statusBar = w 

518 #@+node:ekr.20110605121601.18212: *5* dw.packLabel 

519 def packLabel(self, w: Wrapper, n: int=None) -> None: 

520 """ 

521 Pack w into the body frame's QVGridLayout. 

522 

523 The type of w does not affect the following code. In fact, w is a 

524 QTextBrowser possibly packed inside a LeoLineTextWidget. 

525 """ 

526 c = self.leo_c 

527 # 

528 # Reuse the grid layout in the body frame. 

529 grid = self.leo_body_frame.layout() 

530 # Pack the label and the text widget. 

531 label = QtWidgets.QLineEdit(None) 

532 label.setObjectName('editorLabel') 

533 label.setText(c.p.h) 

534 if n is None: 

535 n = c.frame.body.numberOfEditors 

536 n = max(0, n - 1) 

537 # mypy error: grid is a QGridLayout, not a QLayout. 

538 grid.addWidget(label, 0, n) # type:ignore 

539 grid.addWidget(w, 1, n) # type:ignore 

540 grid.setRowStretch(0, 0) # Don't grow the label vertically. 

541 grid.setRowStretch(1, 1) # Give row 1 as much as vertical room as possible. 

542 # Inject the ivar. 

543 w.leo_label = label 

544 #@+node:ekr.20110605121601.18151: *5* dw.setMainWindowOptions 

545 def setMainWindowOptions(self) -> None: 

546 """Set default options for Leo's main window.""" 

547 dw = self 

548 dw.setObjectName("MainWindow") 

549 dw.resize(691, 635) 

550 #@+node:ekr.20110605121601.18152: *4* dw.widgets 

551 #@+node:ekr.20110605121601.18153: *5* dw.createButton 

552 def createButton(self, parent: Widget, name: str, label: str) -> Widget: 

553 w = QtWidgets.QPushButton(parent) 

554 w.setObjectName(name) 

555 w.setText(self.tr(label)) 

556 return w 

557 #@+node:ekr.20110605121601.18154: *5* dw.createCheckBox 

558 def createCheckBox(self, parent: Widget, name: str, label: str) -> Widget: 

559 w = QtWidgets.QCheckBox(parent) 

560 self.setName(w, name) 

561 w.setText(self.tr(label)) 

562 return w 

563 #@+node:ekr.20110605121601.18155: *5* dw.createFrame 

564 def createFrame( 

565 self, 

566 parent: Widget, 

567 name: str, 

568 hPolicy: Any=None, 

569 vPolicy: Any=None, 

570 lineWidth: int=1, 

571 shadow: Any=None, 

572 shape: Any=None, 

573 ) -> Widget: 

574 """Create a Qt Frame.""" 

575 if shadow is None: 

576 shadow = Shadow.Plain 

577 if shape is None: 

578 shape = Shape.NoFrame 

579 # 

580 w = QtWidgets.QFrame(parent) 

581 self.setSizePolicy(w, kind1=hPolicy, kind2=vPolicy) 

582 w.setFrameShape(shape) 

583 w.setFrameShadow(shadow) 

584 w.setLineWidth(lineWidth) 

585 self.setName(w, name) 

586 return w 

587 #@+node:ekr.20110605121601.18156: *5* dw.createGrid 

588 def createGrid(self, parent: Widget, name: str, margin: int=0, spacing: int=0) -> Widget: 

589 w = QtWidgets.QGridLayout(parent) 

590 w.setContentsMargins(QtCore.QMargins(margin, margin, margin, margin)) 

591 w.setSpacing(spacing) 

592 self.setName(w, name) 

593 return w 

594 #@+node:ekr.20110605121601.18157: *5* dw.createHLayout & createVLayout 

595 def createHLayout(self, parent: Widget, name: str, margin: int=0, spacing: int=0) -> Any: 

596 hLayout = QtWidgets.QHBoxLayout(parent) 

597 hLayout.setSpacing(spacing) 

598 hLayout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0)) 

599 self.setName(hLayout, name) 

600 return hLayout 

601 

602 def createVLayout(self, parent: Widget, name: str, margin: int=0, spacing: int=0) -> Any: 

603 vLayout = QtWidgets.QVBoxLayout(parent) 

604 vLayout.setSpacing(spacing) 

605 vLayout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0)) 

606 self.setName(vLayout, name) 

607 return vLayout 

608 #@+node:ekr.20110605121601.18158: *5* dw.createLabel 

609 def createLabel(self, parent: Widget, name: str, label: str) -> Widget: 

610 w = QtWidgets.QLabel(parent) 

611 self.setName(w, name) 

612 w.setText(self.tr(label)) 

613 return w 

614 #@+node:ekr.20110605121601.18159: *5* dw.createLineEdit 

615 def createLineEdit(self, parent: Widget, name: str, disabled: bool=True) -> Widget: 

616 

617 w = QtWidgets.QLineEdit(parent) 

618 w.setObjectName(name) 

619 w.leo_disabled = disabled # Inject the ivar. 

620 return w 

621 #@+node:ekr.20110605121601.18160: *5* dw.createRadioButton 

622 def createRadioButton(self, parent: Widget, name: str, label: str) -> Widget: 

623 w = QtWidgets.QRadioButton(parent) 

624 self.setName(w, name) 

625 w.setText(self.tr(label)) 

626 return w 

627 #@+node:ekr.20110605121601.18161: *5* dw.createStackedWidget 

628 def createStackedWidget( 

629 self, 

630 parent: Widget, 

631 name: str, 

632 lineWidth: int=1, 

633 hPolicy: Any=None, 

634 vPolicy: Any=None, 

635 ) -> Widget: 

636 w = QtWidgets.QStackedWidget(parent) 

637 self.setSizePolicy(w, kind1=hPolicy, kind2=vPolicy) 

638 w.setAcceptDrops(True) 

639 w.setLineWidth(1) 

640 self.setName(w, name) 

641 return w 

642 #@+node:ekr.20110605121601.18162: *5* dw.createTabWidget 

643 def createTabWidget(self, parent: Widget, name: str, hPolicy: Any=None, vPolicy: Any=None) -> Widget: 

644 w = QtWidgets.QTabWidget(parent) 

645 # tb = w.tabBar() 

646 # tb.setTabsClosable(True) 

647 self.setSizePolicy(w, kind1=hPolicy, kind2=vPolicy) 

648 self.setName(w, name) 

649 return w 

650 #@+node:ekr.20110605121601.18163: *5* dw.createText (creates QTextBrowser) 

651 def createText( 

652 self, 

653 parent: Widget, 

654 name: str, 

655 lineWidth: int=0, 

656 shadow: Any=None, 

657 shape: Any=None, 

658 ) -> Widget: 

659 # Create a text widget. 

660 c = self.leo_c 

661 if name == 'richTextEdit' and self.useScintilla and Qsci: 

662 # Do this in finishCreate, when c.frame.body exists. 

663 w = Qsci.QsciScintilla(parent) 

664 self.scintilla_widget = w 

665 else: 

666 if shadow is None: 

667 shadow = Shadow.Plain 

668 if shape is None: 

669 shape = Shape.NoFrame 

670 # 

671 w = qt_text.LeoQTextBrowser(parent, c, None) 

672 w.setFrameShape(shape) 

673 w.setFrameShadow(shadow) 

674 w.setLineWidth(lineWidth) 

675 self.setName(w, name) 

676 return w 

677 #@+node:ekr.20110605121601.18164: *5* dw.createTreeWidget 

678 def createTreeWidget(self, parent: Widget, name: str) -> Widget: 

679 c = self.leo_c 

680 w = LeoQTreeWidget(c, parent) 

681 self.setSizePolicy(w) 

682 # 12/01/07: add new config setting. 

683 multiple_selection = c.config.getBool('qt-tree-multiple-selection', default=True) 

684 if multiple_selection: 

685 w.setSelectionMode(SelectionMode.ExtendedSelection) 

686 w.setSelectionBehavior(SelectionBehavior.SelectRows) 

687 else: 

688 w.setSelectionMode(SelectionMode.SingleSelection) 

689 w.setSelectionBehavior(SelectionBehavior.SelectItems) 

690 w.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu) 

691 w.setHeaderHidden(False) 

692 self.setName(w, name) 

693 return w 

694 #@+node:ekr.20110605121601.18165: *4* dw.log tabs 

695 #@+node:ekr.20110605121601.18167: *5* dw.createSpellTab 

696 def createSpellTab(self, parent: Widget) -> None: 

697 # dw = self 

698 vLayout = self.createVLayout(parent, 'spellVLayout', margin=2) 

699 spellFrame = self.createFrame(parent, 'spellFrame') 

700 vLayout2 = self.createVLayout(spellFrame, 'spellVLayout') 

701 grid = self.createGrid(None, 'spellGrid', spacing=2) 

702 table = ( 

703 ('Add', 'Add', 2, 1), 

704 ('Find', 'Find', 2, 0), 

705 ('Change', 'Change', 3, 0), 

706 ('FindChange', 'Change,Find', 3, 1), 

707 ('Ignore', 'Ignore', 4, 0), 

708 ('Hide', 'Hide', 4, 1), 

709 ) 

710 for (ivar, label, row, col) in table: 

711 name = f"spell_{label}_button" 

712 button = self.createButton(spellFrame, name, label) 

713 grid.addWidget(button, row, col) 

714 func = getattr(self, f"do_leo_spell_btn_{ivar}") 

715 button.clicked.connect(func) 

716 # This name is significant. 

717 setattr(self, f"leo_spell_btn_{ivar}", button) 

718 self.leo_spell_btn_Hide.setCheckable(False) 

719 spacerItem = QtWidgets.QSpacerItem(20, 40, Policy.Minimum, Policy.Expanding) 

720 grid.addItem(spacerItem, 5, 0, 1, 1) 

721 listBox = QtWidgets.QListWidget(spellFrame) 

722 self.setSizePolicy(listBox, kind1=Policy.MinimumExpanding, kind2=Policy.Expanding) 

723 listBox.setMinimumSize(QtCore.QSize(0, 0)) 

724 listBox.setMaximumSize(QtCore.QSize(150, 150)) 

725 listBox.setObjectName("leo_spell_listBox") 

726 grid.addWidget(listBox, 1, 0, 1, 2) 

727 spacerItem1 = QtWidgets.QSpacerItem(40, 20, Policy.Expanding, Policy.Minimum) 

728 grid.addItem(spacerItem1, 2, 2, 1, 1) 

729 lab = self.createLabel(spellFrame, 'spellLabel', 'spellLabel') 

730 grid.addWidget(lab, 0, 0, 1, 2) 

731 vLayout2.addLayout(grid) 

732 vLayout.addWidget(spellFrame) 

733 listBox.itemDoubleClicked.connect(self.do_leo_spell_btn_FindChange) 

734 # Official ivars. 

735 self.spellFrame = spellFrame 

736 self.spellGrid = grid 

737 self.leo_spell_widget = parent # 2013/09/20: To allow bindings to be set. 

738 self.leo_spell_listBox = listBox # Must exist 

739 self.leo_spell_label = lab # Must exist (!!) 

740 #@+node:ekr.20110605121601.18166: *5* dw.createFindTab & helpers 

741 def createFindTab(self, parent: Widget, tab_widget: Widget) -> None: 

742 """Create a Find Tab in the given parent.""" 

743 c, dw = self.leo_c, self 

744 fc = c.findCommands 

745 assert not fc.ftm 

746 fc.ftm = ftm = FindTabManager(c) 

747 grid = self.create_find_grid(parent) 

748 row = 0 # The index for the present row. 

749 row = dw.create_find_header(grid, parent, row) 

750 row = dw.create_find_findbox(grid, parent, row) 

751 row = dw.create_find_replacebox(grid, parent, row) 

752 max_row2 = 1 

753 max_row2 = dw.create_find_checkboxes(grid, parent, max_row2, row) 

754 row = dw.create_find_buttons(grid, parent, max_row2, row) 

755 row = dw.create_help_row(grid, parent, row) 

756 dw.override_events() 

757 # Last row: Widgets that take all additional vertical space. 

758 w = QtWidgets.QWidget() 

759 grid.addWidget(w, row, 0) 

760 grid.addWidget(w, row, 1) 

761 grid.addWidget(w, row, 2) 

762 grid.setRowStretch(row, 100) 

763 # Official ivars (in addition to checkbox ivars). 

764 self.leo_find_widget = tab_widget # A scrollArea. 

765 ftm.init_widgets() 

766 #@+node:ekr.20131118152731.16847: *6* dw.create_find_grid 

767 def create_find_grid(self, parent: Widget) -> Any: 

768 grid = self.createGrid(parent, 'findGrid', margin=10, spacing=10) 

769 grid.setColumnStretch(0, 100) 

770 grid.setColumnStretch(1, 100) 

771 grid.setColumnStretch(2, 10) 

772 grid.setColumnMinimumWidth(1, 75) 

773 grid.setColumnMinimumWidth(2, 175) 

774 return grid 

775 #@+node:ekr.20131118152731.16849: *6* dw.create_find_header 

776 def create_find_header(self, grid: Any, parent: Widget, row: int) -> int: 

777 if False: 

778 dw = self 

779 lab1 = dw.createLabel(parent, 'findHeading', 'Find/Change Settings...') 

780 grid.addWidget(lab1, row, 0, 1, 2, Alignment.AlignLeft) # AlignHCenter 

781 row += 1 

782 return row 

783 #@+node:ekr.20131118152731.16848: *6* dw.create_find_findbox 

784 def create_find_findbox(self, grid: Any, parent: Widget, row: int) -> int: 

785 """Create the Find: label and text area.""" 

786 c, dw = self.leo_c, self 

787 fc = c.findCommands 

788 ftm = fc.ftm 

789 assert ftm.find_findbox is None 

790 ftm.find_findbox = w = dw.createLineEdit( 

791 parent, 'findPattern', disabled=fc.expert_mode) 

792 lab2 = self.createLabel(parent, 'findLabel', 'Find:') 

793 grid.addWidget(lab2, row, 0) 

794 grid.addWidget(w, row, 1, 1, 2) 

795 row += 1 

796 return row 

797 #@+node:ekr.20131118152731.16850: *6* dw.create_find_replacebox 

798 def create_find_replacebox(self, grid: Any, parent: Widget, row: int) -> int: 

799 """Create the Replace: label and text area.""" 

800 c, dw = self.leo_c, self 

801 fc = c.findCommands 

802 ftm = fc.ftm 

803 assert ftm.find_replacebox is None 

804 ftm.find_replacebox = w = dw.createLineEdit( 

805 parent, 'findChange', disabled=fc.expert_mode) 

806 lab3 = dw.createLabel(parent, 'changeLabel', 'Replace:') # Leo 4.11.1. 

807 grid.addWidget(lab3, row, 0) 

808 grid.addWidget(w, row, 1, 1, 2) 

809 row += 1 

810 return row 

811 #@+node:ekr.20131118152731.16851: *6* dw.create_find_checkboxes 

812 def create_find_checkboxes(self, grid: Any, parent: Widget, max_row2: int, row: int) -> int: 

813 """Create check boxes and radio buttons.""" 

814 c, dw = self.leo_c, self 

815 fc = c.findCommands 

816 ftm = fc.ftm 

817 

818 def mungeName(kind: str, label: str) -> str: 

819 # The returned value is the namve of an ivar. 

820 kind = 'check_box_' if kind == 'box' else 'radio_button_' 

821 name = label.replace(' ', '_').replace('&', '').lower() 

822 return f"{kind}{name}" 

823 

824 # Rows for check boxes, radio buttons & execution buttons... 

825 

826 d = { 

827 'box': dw.createCheckBox, 

828 'rb': dw.createRadioButton, 

829 } 

830 table = ( 

831 # Note: the Ampersands create Alt bindings when the log pane is enable. 

832 # The QShortcut class is the workaround. 

833 # First row. 

834 ('box', 'whole &Word', 0, 0), 

835 ('rb', '&Entire outline', 0, 1), 

836 # Second row. 

837 ('box', '&Ignore case', 1, 0), 

838 ('rb', '&Suboutline only', 1, 1), 

839 # Third row. 

840 ('box', 'rege&Xp', 2, 0), 

841 ('rb', '&Node only', 2, 1), 

842 # Fourth row. 

843 ('box', 'mark &Finds', 3, 0), 

844 ('rb', 'file &only', 3, 1), 

845 # Fifth row. 

846 ('box', 'mark &Changes', 4, 0), 

847 ('box', 'search &Headline', 4, 1), 

848 # Sixth Row 

849 ('box', 'search &Body', 5, 1), 

850 

851 # ('box', 'rege&Xp', 2, 0), 

852 # ('rb', '&Node only', 2, 1), 

853 # # Fourth row. 

854 # ('box', 'mark &Finds', 3, 0), 

855 # ('box', 'search &Headline', 3, 1), 

856 # # Fifth row. 

857 # ('box', 'mark &Changes', 4, 0), 

858 # ('box', 'search &Body', 4, 1), 

859 # ('rb', 'File &Only', 5, 1), 

860 

861 # Sixth row. 

862 # ('box', 'wrap &Around', 5, 0), 

863 # a,b,c,e,f,h,i,n,rs,w 

864 ) 

865 for kind, label, row2, col in table: 

866 max_row2 = max(max_row2, row2) 

867 name = mungeName(kind, label) 

868 func = d.get(kind) 

869 assert func 

870 # Fix the greedy checkbox bug: 

871 label = label.replace('&', '') 

872 w = func(parent, name, label) 

873 grid.addWidget(w, row + row2, col) 

874 # The the checkbox ivars in dw and ftm classes. 

875 assert getattr(ftm, name) is None 

876 setattr(ftm, name, w) 

877 return max_row2 

878 #@+node:ekr.20131118152731.16853: *6* dw.create_help_row 

879 def create_help_row(self, grid: Any, parent: Widget, row: int) -> int: 

880 # Help row. 

881 if False: 

882 w = self.createLabel(parent, 

883 'findHelp', 'For help: <alt-x>help-for-find-commands<return>') 

884 grid.addWidget(w, row, 0, 1, 3) 

885 row += 1 

886 return row 

887 #@+node:ekr.20131118152731.16852: *6* dw.create_find_buttons 

888 def create_find_buttons(self, grid: Any, parent: Widget, max_row2: int, row: int) -> int: 

889 """ 

890 Per #1342, this method now creates labels, not real buttons. 

891 """ 

892 dw, k = self, self.leo_c.k 

893 

894 # Create Buttons in column 2 (Leo 4.11.1.) 

895 table = ( 

896 (0, 2, 'find-next'), # 'findButton', 

897 (1, 2, 'find-prev'), # 'findPreviousButton', 

898 (2, 2, 'find-all'), # 'findAllButton', 

899 (3, 2, 'replace'), # 'changeButton', 

900 (4, 2, 'replace-then-find'), # 'changeThenFindButton', 

901 (5, 2, 'replace-all'), # 'changeAllButton', 

902 ) 

903 for row2, col, cmd_name in table: 

904 stroke = k.getStrokeForCommandName(cmd_name) 

905 if stroke: 

906 label = f"{cmd_name}: {k.prettyPrintKey(stroke)}" 

907 else: 

908 label = cmd_name 

909 # #1342: Create a label, not a button. 

910 w = dw.createLabel(parent, cmd_name, label) 

911 w.setObjectName('find-label') 

912 grid.addWidget(w, row + row2, col) 

913 row += max_row2 

914 row += 2 

915 return row 

916 #@+node:ekr.20150618072619.1: *6* dw.create_find_status 

917 if 0: 

918 

919 def create_find_status(self, grid: Any, parent: Widget, row: int) -> None: 

920 """Create the status line.""" 

921 dw = self 

922 status_label = dw.createLabel(parent, 'status-label', 'Status') 

923 status_line = dw.createLineEdit(parent, 'find-status', disabled=True) 

924 grid.addWidget(status_label, row, 0) 

925 grid.addWidget(status_line, row, 1, 1, 2) 

926 # Official ivars. 

927 dw.find_status_label = status_label 

928 dw.find_status_edit = status_line 

929 #@+node:ekr.20131118172620.16891: *6* dw.override_events 

930 def override_events(self) -> None: 

931 # dw = self 

932 c = self.leo_c 

933 fc = c.findCommands 

934 ftm = fc.ftm 

935 # Define class EventWrapper. 

936 #@+others 

937 #@+node:ekr.20131118172620.16892: *7* class EventWrapper 

938 class EventWrapper: 

939 

940 def __init__(self, c: Cmdr, w: Wrapper, next_w: Wrapper, func: Callable) -> None: 

941 self.c = c 

942 self.d = self.create_d() # Keys: stroke.s; values: command-names. 

943 self.w = w 

944 self.next_w = next_w 

945 self.eventFilter = qt_events.LeoQtEventFilter(c, w, 'EventWrapper') 

946 self.func = func 

947 self.oldEvent = w.event 

948 w.event = self.wrapper 

949 

950 #@+others 

951 #@+node:ekr.20131120054058.16281: *8* EventWrapper.create_d 

952 def create_d(self) -> Dict[str, str]: 

953 """Create self.d dictionary.""" 

954 c = self.c 

955 d = {} 

956 table = ( 

957 'toggle-find-ignore-case-option', 

958 'toggle-find-in-body-option', 

959 'toggle-find-in-headline-option', 

960 'toggle-find-mark-changes-option', 

961 'toggle-find-mark-finds-option', 

962 'toggle-find-regex-option', 

963 'toggle-find-word-option', 

964 'toggle-find-wrap-around-option', 

965 # New in Leo 5.2: Support these in the Find Dialog. 

966 'find-all', 

967 'find-next', 

968 'find-prev', 

969 'hide-find-tab', 

970 'replace', 

971 'replace-all', 

972 'replace-then-find', 

973 'set-find-everywhere', 

974 'set-find-node-only', 

975 'set-find-suboutline-only', 

976 # #2041 & # 2094 (Leo 6.4): Support Alt-x. 

977 'full-command', 

978 'keyboard-quit', # Might as well :-) 

979 ) 

980 for cmd_name in table: 

981 stroke = c.k.getStrokeForCommandName(cmd_name) 

982 if stroke: 

983 d[stroke.s] = cmd_name 

984 return d 

985 #@+node:ekr.20131118172620.16893: *8* EventWrapper.wrapper 

986 def wrapper(self, event: Event) -> Any: 

987 

988 type_ = event.type() 

989 # Must intercept KeyPress for events that generate FocusOut! 

990 if type_ == Type.KeyPress: 

991 return self.keyPress(event) 

992 if type_ == Type.KeyRelease: 

993 return self.keyRelease(event) 

994 return self.oldEvent(event) 

995 #@+node:ekr.20131118172620.16894: *8* EventWrapper.keyPress 

996 def keyPress(self, event: Event) -> Any: 

997 

998 s = event.text() 

999 out = s and s in '\t\r\n' 

1000 if out: 

1001 # Move focus to next widget. 

1002 if s == '\t': 

1003 if self.next_w: 

1004 self.next_w.setFocus(FocusReason.TabFocusReason) 

1005 else: 

1006 # Do the normal processing. 

1007 return self.oldEvent(event) 

1008 elif self.func: 

1009 self.func() 

1010 return True 

1011 binding, ch, lossage = self.eventFilter.toBinding(event) 

1012 # #2094: Use code similar to the end of LeoQtEventFilter.eventFilter. 

1013 # The ctor converts <Alt-X> to <Atl-x> !! 

1014 # That is, we must use the stroke, not the binding. 

1015 key_event = leoGui.LeoKeyEvent( 

1016 c=self.c, char=ch, event=event, binding=binding, w=self.w) 

1017 if key_event.stroke: 

1018 cmd_name = self.d.get(key_event.stroke) 

1019 if cmd_name: 

1020 self.c.k.simulateCommand(cmd_name) 

1021 return True 

1022 # Do the normal processing. 

1023 return self.oldEvent(event) 

1024 #@+node:ekr.20131118172620.16895: *8* EventWrapper.keyRelease 

1025 def keyRelease(self, event: Event) -> None: 

1026 return self.oldEvent(event) 

1027 #@-others 

1028 #@-others 

1029 EventWrapper(c, w=ftm.find_findbox, next_w=ftm.find_replacebox, func=fc.find_next) 

1030 EventWrapper(c, w=ftm.find_replacebox, next_w=ftm.find_next_button, func=fc.find_next) 

1031 # Finally, checkBoxMarkChanges goes back to ftm.find_findBox. 

1032 EventWrapper(c, w=ftm.check_box_mark_changes, next_w=ftm.find_findbox, func=None) 

1033 #@+node:ekr.20110605121601.18168: *4* dw.utils 

1034 #@+node:ekr.20110605121601.18169: *5* dw.setName 

1035 def setName(self, widget: Widget, name: str) -> None: 

1036 if name: 

1037 # if not name.startswith('leo_'): 

1038 # name = 'leo_' + name 

1039 widget.setObjectName(name) 

1040 #@+node:ekr.20110605121601.18170: *5* dw.setSizePolicy 

1041 def setSizePolicy(self, widget: Widget, kind1: Any=None, kind2: Any=None) -> None: 

1042 if kind1 is None: 

1043 kind1 = Policy.Ignored 

1044 if kind2 is None: 

1045 kind2 = Policy.Ignored 

1046 sizePolicy = QtWidgets.QSizePolicy(kind1, kind2) 

1047 sizePolicy.setHorizontalStretch(0) 

1048 sizePolicy.setVerticalStretch(0) 

1049 sizePolicy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth()) 

1050 widget.setSizePolicy(sizePolicy) 

1051 #@+node:ekr.20110605121601.18171: *5* dw.tr 

1052 def tr(self, s: str) -> str: 

1053 # pylint: disable=no-member 

1054 if isQt5 or isQt6: 

1055 # QApplication.UnicodeUTF8 no longer exists. 

1056 return QtWidgets.QApplication.translate('MainWindow', s, None) 

1057 return QtWidgets.QApplication.translate( 

1058 'MainWindow', s, None, QtWidgets.QApplication.UnicodeUTF8) 

1059 #@+node:ekr.20110605121601.18173: *3* dw.select 

1060 def select(self, c: Cmdr) -> None: 

1061 """Select the window or tab for c.""" 

1062 # Called from the save commands. 

1063 self.leo_master.select(c) 

1064 #@+node:ekr.20110605121601.18178: *3* dw.setGeometry 

1065 def setGeometry(self, rect: Any) -> None: 

1066 """Set the window geometry, but only once when using the qt gui.""" 

1067 m = self.leo_master 

1068 assert self.leo_master 

1069 # Only set the geometry once, even for new files. 

1070 if not hasattr(m, 'leo_geom_inited'): 

1071 m.leo_geom_inited = True 

1072 self.leo_master.setGeometry(rect) 

1073 super().setGeometry(rect) 

1074 

1075 #@+node:ekr.20110605121601.18177: *3* dw.setLeoWindowIcon 

1076 def setLeoWindowIcon(self) -> None: 

1077 """ Set icon visible in title bar and task bar """ 

1078 # self.setWindowIcon(QtGui.QIcon(g.app.leoDir + "/Icons/leoapp32.png")) 

1079 g.app.gui.attachLeoIcon(self) 

1080 #@+node:ekr.20110605121601.18174: *3* dw.setSplitDirection 

1081 def setSplitDirection(self, main_splitter: Widget, secondary_splitter: Widget, orientation: Any) -> None: 

1082 """Set the orientations of the splitters in the Leo main window.""" 

1083 # c = self.leo_c 

1084 vert = orientation and orientation.lower().startswith('v') 

1085 h, v = Orientation.Horizontal, Orientation.Vertical 

1086 orientation1 = v if vert else h 

1087 orientation2 = h if vert else v 

1088 main_splitter.setOrientation(orientation1) 

1089 secondary_splitter.setOrientation(orientation2) 

1090 #@+node:ekr.20130804061744.12425: *3* dw.setWindowTitle 

1091 if 0: # Override for debugging only. 

1092 

1093 def setWindowTitle(self, s: str) -> None: 

1094 g.trace('***(DynamicWindow)', s, self.parent()) 

1095 # Call the base class method. 

1096 QtWidgets.QMainWindow.setWindowTitle(self, s) 

1097 #@-others 

1098#@+node:ekr.20131117054619.16698: ** class FindTabManager (qt_frame.py) 

1099class FindTabManager: 

1100 """A helper class for the LeoFind class.""" 

1101 #@+others 

1102 #@+node:ekr.20131117120458.16794: *3* ftm.ctor 

1103 def __init__(self, c: Cmdr) -> None: 

1104 """Ctor for the FindTabManager class.""" 

1105 self.c = c 

1106 self.entry_focus = None # The widget that had focus before find-pane entered. 

1107 # Find/change text boxes. 

1108 self.find_findbox = None 

1109 self.find_replacebox = None 

1110 # Check boxes. 

1111 self.check_box_ignore_case = None 

1112 self.check_box_mark_changes = None 

1113 self.check_box_mark_finds = None 

1114 self.check_box_regexp = None 

1115 self.check_box_search_body = None 

1116 self.check_box_search_headline = None 

1117 self.check_box_whole_word = None 

1118 # self.check_box_wrap_around = None 

1119 # Radio buttons 

1120 self.radio_button_entire_outline = None 

1121 self.radio_button_node_only = None 

1122 self.radio_button_suboutline_only = None 

1123 self.radio_button_file_only = None 

1124 # Push buttons 

1125 self.find_next_button = None 

1126 self.find_prev_button = None 

1127 self.find_all_button = None 

1128 self.help_for_find_commands_button = None 

1129 self.replace_button = None 

1130 self.replace_then_find_button = None 

1131 self.replace_all_button = None 

1132 #@+node:ekr.20131119185305.16478: *3* ftm.clear_focus & init_focus & set_entry_focus 

1133 def clear_focus(self) -> None: 

1134 self.entry_focus = None 

1135 self.find_findbox.clearFocus() 

1136 

1137 def init_focus(self) -> None: 

1138 self.set_entry_focus() 

1139 w = self.find_findbox 

1140 w.setFocus() 

1141 s = w.text() 

1142 w.setSelection(0, len(s)) 

1143 

1144 def set_entry_focus(self) -> None: 

1145 # Remember the widget that had focus, changing headline widgets 

1146 # to the tree pane widget. Headline widgets can disappear! 

1147 c = self.c 

1148 w = g.app.gui.get_focus(raw=True) 

1149 if w != c.frame.body.wrapper.widget: 

1150 w = c.frame.tree.treeWidget 

1151 self.entry_focus = w 

1152 #@+node:ekr.20210110143917.1: *3* ftm.get_settings 

1153 def get_settings(self) -> Any: 

1154 """ 

1155 Return a g.bunch representing all widget values. 

1156 

1157 Similar to LeoFind.default_settings, but only for find-tab values. 

1158 """ 

1159 return g.Bunch( 

1160 # Find/change strings... 

1161 find_text=self.find_findbox.text(), 

1162 change_text=self.find_replacebox.text(), 

1163 # Find options... 

1164 ignore_case=self.check_box_ignore_case.isChecked(), 

1165 mark_changes=self.check_box_mark_changes.isChecked(), 

1166 mark_finds=self.check_box_mark_finds.isChecked(), 

1167 node_only=self.radio_button_node_only.isChecked(), 

1168 pattern_match=self.check_box_regexp.isChecked(), 

1169 # reverse = False, 

1170 search_body=self.check_box_search_body.isChecked(), 

1171 search_headline=self.check_box_search_headline.isChecked(), 

1172 suboutline_only=self.radio_button_suboutline_only.isChecked(), 

1173 whole_word=self.check_box_whole_word.isChecked(), 

1174 # wrapping = self.check_box_wrap_around.isChecked(), 

1175 ) 

1176 #@+node:ekr.20131117120458.16789: *3* ftm.init_widgets (creates callbacks) 

1177 def init_widgets(self) -> None: 

1178 """ 

1179 Init widgets and ivars from c.config settings. 

1180 Create callbacks that always keep the LeoFind ivars up to date. 

1181 """ 

1182 c = self.c 

1183 find = c.findCommands 

1184 # Find/change text boxes. 

1185 table1 = ( 

1186 ('find_findbox', 'find_text', '<find pattern here>'), 

1187 ('find_replacebox', 'change_text', ''), 

1188 ) 

1189 for ivar, setting_name, default in table1: 

1190 s = c.config.getString(setting_name) or default 

1191 w = getattr(self, ivar) 

1192 w.insert(s) 

1193 if find.minibuffer_mode: 

1194 w.clearFocus() 

1195 else: 

1196 w.setSelection(0, len(s)) 

1197 # Check boxes. 

1198 table2 = ( 

1199 ('ignore_case', self.check_box_ignore_case), 

1200 ('mark_changes', self.check_box_mark_changes), 

1201 ('mark_finds', self.check_box_mark_finds), 

1202 ('pattern_match', self.check_box_regexp), 

1203 ('search_body', self.check_box_search_body), 

1204 ('search_headline', self.check_box_search_headline), 

1205 ('whole_word', self.check_box_whole_word), 

1206 # ('wrap', self.check_box_wrap_around), 

1207 ) 

1208 for setting_name, w in table2: 

1209 val = c.config.getBool(setting_name, default=False) 

1210 # The setting name is also the name of the LeoFind ivar. 

1211 assert hasattr(find, setting_name), setting_name 

1212 setattr(find, setting_name, val) 

1213 if val: 

1214 w.toggle() 

1215 

1216 def check_box_callback(n: int, setting_name: str=setting_name, w: str=w) -> None: 

1217 # The focus has already change when this gets called. 

1218 # focus_w = QtWidgets.QApplication.focusWidget() 

1219 val = w.isChecked() 

1220 assert hasattr(find, setting_name), setting_name 

1221 setattr(find, setting_name, val) 

1222 # Too kludgy: we must use an accurate setting. 

1223 # It would be good to have an "about to change" signal. 

1224 # Put focus in minibuffer if minibuffer find is in effect. 

1225 c.bodyWantsFocusNow() 

1226 

1227 w.stateChanged.connect(check_box_callback) 

1228 # Radio buttons 

1229 table3 = ( 

1230 ('node_only', 'node_only', self.radio_button_node_only), 

1231 ('entire_outline', None, self.radio_button_entire_outline), 

1232 ('suboutline_only', 'suboutline_only', self.radio_button_suboutline_only), 

1233 ('file_only', 'file_only', self.radio_button_file_only) 

1234 ) 

1235 for setting_name, ivar, w in table3: 

1236 val = c.config.getBool(setting_name, default=False) 

1237 # The setting name is also the name of the LeoFind ivar. 

1238 if ivar is not None: 

1239 assert hasattr(find, setting_name), setting_name 

1240 setattr(find, setting_name, val) 

1241 w.toggle() 

1242 

1243 def radio_button_callback(n: int, ivar: str=ivar, setting_name: str=setting_name, w: str=w) -> None: 

1244 val = w.isChecked() 

1245 if ivar: 

1246 assert hasattr(find, ivar), ivar 

1247 setattr(find, ivar, val) 

1248 

1249 w.toggled.connect(radio_button_callback) 

1250 # Ensure one radio button is set. 

1251 if not find.node_only and not find.suboutline_only and not find.file_only: 

1252 w = self.radio_button_entire_outline 

1253 w.toggle() 

1254 #@+node:ekr.20210923060904.1: *3* ftm.init_widgets_from_dict (new) 

1255 def set_widgets_from_dict(self, d: Dict[str, str]) -> None: 

1256 """Set all settings from d.""" 

1257 # Similar to ftm.init_widgets, which has already been called. 

1258 c = self.c 

1259 find = c.findCommands 

1260 # Set find text. 

1261 find_text = d.get('find_text') 

1262 self.set_find_text(find_text) 

1263 find.find_text = find_text 

1264 # Set change text. 

1265 change_text = d.get('change_text') 

1266 self.set_change_text(change_text) 

1267 find.change_text = change_text 

1268 # Check boxes... 

1269 table1 = ( 

1270 ('ignore_case', self.check_box_ignore_case), 

1271 ('mark_changes', self.check_box_mark_changes), 

1272 ('mark_finds', self.check_box_mark_finds), 

1273 ('pattern_match', self.check_box_regexp), 

1274 ('search_body', self.check_box_search_body), 

1275 ('search_headline', self.check_box_search_headline), 

1276 ('whole_word', self.check_box_whole_word), 

1277 ) 

1278 for setting_name, w in table1: 

1279 val = d.get(setting_name, False) 

1280 # The setting name is also the name of the LeoFind ivar. 

1281 assert hasattr(find, setting_name), setting_name 

1282 setattr(find, setting_name, val) 

1283 w.setChecked(val) 

1284 # Radio buttons... 

1285 table2 = ( 

1286 ('node_only', 'node_only', self.radio_button_node_only), 

1287 ('entire_outline', None, self.radio_button_entire_outline), 

1288 ('suboutline_only', 'suboutline_only', self.radio_button_suboutline_only), 

1289 ('file_only', 'file_only', self.radio_button_file_only), 

1290 ) 

1291 for setting_name, ivar, w in table2: 

1292 val = d.get(setting_name, False) 

1293 # The setting name is also the name of the LeoFind ivar. 

1294 if ivar is not None: 

1295 assert hasattr(find, setting_name), setting_name 

1296 setattr(find, setting_name, val) 

1297 w.setChecked(val) 

1298 # Ensure one radio button is set. 

1299 if not find.node_only and not find.suboutline_only and not find.file_only: 

1300 w = self.radio_button_entire_outline 

1301 w.setChecked(val) 

1302 #@+node:ekr.20210312120503.1: *3* ftm.set_body_and_headline_checkbox 

1303 def set_body_and_headline_checkbox(self) -> None: 

1304 """Return the search-body and search-headline checkboxes to their defaults.""" 

1305 # #1840: headline-only one-shot 

1306 c = self.c 

1307 find = c.findCommands 

1308 if not find: 

1309 return 

1310 table = ( 

1311 ('search_body', self.check_box_search_body), 

1312 ('search_headline', self.check_box_search_headline), 

1313 ) 

1314 for setting_name, w in table: 

1315 val = c.config.getBool(setting_name, default=False) 

1316 if val != w.isChecked(): 

1317 w.toggle() 

1318 if find.minibuffer_mode: 

1319 find.show_find_options_in_status_area() 

1320 #@+node:ekr.20150619082825.1: *3* ftm.set_ignore_case 

1321 def set_ignore_case(self, aBool: bool) -> None: 

1322 """Set the ignore-case checkbox to the given value.""" 

1323 c = self.c 

1324 c.findCommands.ignore_case = aBool 

1325 w = self.check_box_ignore_case 

1326 w.setChecked(aBool) 

1327 #@+node:ekr.20131117120458.16792: *3* ftm.set_radio_button 

1328 def set_radio_button(self, name: str) -> None: 

1329 """Set the value of the radio buttons""" 

1330 c = self.c 

1331 find = c.findCommands 

1332 d = { 

1333 # Name is not an ivar. Set by find.setFindScope... commands. 

1334 'node-only': self.radio_button_node_only, 

1335 'entire-outline': self.radio_button_entire_outline, 

1336 'suboutline-only': self.radio_button_suboutline_only, 

1337 'file-only': self.radio_button_file_only, 

1338 } 

1339 w = d.get(name) 

1340 # Most of the work will be done in the radio button callback. 

1341 if not w.isChecked(): 

1342 w.toggle() 

1343 if find.minibuffer_mode: 

1344 find.show_find_options_in_status_area() 

1345 #@+node:ekr.20131117164142.16853: *3* ftm.text getters/setters 

1346 def get_find_text(self) -> str: 

1347 s = self.find_findbox.text() 

1348 if s and s[-1] in ('\r', '\n'): 

1349 s = s[:-1] 

1350 return s 

1351 

1352 def get_change_text(self) -> str: 

1353 s = self.find_replacebox.text() 

1354 if s and s[-1] in ('\r', '\n'): 

1355 s = s[:-1] 

1356 return s 

1357 

1358 getChangeText = get_change_text 

1359 

1360 def set_find_text(self, s: str) -> None: 

1361 w = self.find_findbox 

1362 s = g.checkUnicode(s) 

1363 w.clear() 

1364 w.insert(s) 

1365 

1366 def set_change_text(self, s: str) -> None: 

1367 w = self.find_replacebox 

1368 s = g.checkUnicode(s) 

1369 w.clear() 

1370 w.insert(s) 

1371 #@+node:ekr.20131117120458.16791: *3* ftm.toggle_checkbox 

1372 #@@nobeautify 

1373 

1374 def toggle_checkbox(self, checkbox_name: str) -> None: 

1375 """Toggle the value of the checkbox whose name is given.""" 

1376 c = self.c 

1377 find = c.findCommands 

1378 if not find: 

1379 return 

1380 d = { 

1381 'ignore_case': self.check_box_ignore_case, 

1382 'mark_changes': self.check_box_mark_changes, 

1383 'mark_finds': self.check_box_mark_finds, 

1384 'pattern_match': self.check_box_regexp, 

1385 'search_body': self.check_box_search_body, 

1386 'search_headline': self.check_box_search_headline, 

1387 'whole_word': self.check_box_whole_word, 

1388 # 'wrap': self.check_box_wrap_around, 

1389 } 

1390 w = d.get(checkbox_name) 

1391 assert w 

1392 assert hasattr(find, checkbox_name), checkbox_name 

1393 w.toggle() # The checkbox callback toggles the ivar. 

1394 if find.minibuffer_mode: 

1395 find.show_find_options_in_status_area() 

1396 #@-others 

1397#@+node:ekr.20131115120119.17376: ** class LeoBaseTabWidget(QTabWidget) 

1398class LeoBaseTabWidget(QtWidgets.QTabWidget): # type:ignore 

1399 """Base class for all QTabWidgets in Leo.""" 

1400 #@+others 

1401 #@+node:ekr.20131115120119.17390: *3* qt_base_tab.__init__ 

1402 def __init__(self, *args: Any, **kwargs: Any) -> None: 

1403 # 

1404 # Called from frameFactory.createMaster. 

1405 # 

1406 self.factory = kwargs.get('factory') 

1407 if self.factory: 

1408 del kwargs['factory'] 

1409 super().__init__(*args, **kwargs) 

1410 self.detached: List[Any] = [] 

1411 self.setMovable(True) 

1412 

1413 def tabContextMenu(point: str) -> None: 

1414 index = self.tabBar().tabAt(point) 

1415 if index < 0: # or (self.count() < 1 and not self.detached): 

1416 return 

1417 menu = QtWidgets.QMenu() 

1418 # #310: Create new file on right-click in file tab in UI. 

1419 if True: 

1420 a = menu.addAction("New Outline") 

1421 a.triggered.connect(lambda checked: self.new_outline(index)) 

1422 if self.count() > 1: 

1423 a = menu.addAction("Detach") 

1424 a.triggered.connect(lambda checked: self.detach(index)) 

1425 a = menu.addAction("Horizontal tile") 

1426 a.triggered.connect( 

1427 lambda checked: self.tile(index, orientation='H')) 

1428 a = menu.addAction("Vertical tile") 

1429 a.triggered.connect( 

1430 lambda checked: self.tile(index, orientation='V')) 

1431 if self.detached: 

1432 a = menu.addAction("Re-attach All") 

1433 a.triggered.connect(lambda checked: self.reattach_all()) 

1434 

1435 global_point = self.mapToGlobal(point) 

1436 menu.exec_(global_point) 

1437 self.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu) 

1438 self.customContextMenuRequested.connect(tabContextMenu) 

1439 #@+node:ekr.20180123082452.1: *3* qt_base_tab.new_outline 

1440 def new_outline(self, index: int) -> None: 

1441 """Open a new outline tab.""" 

1442 w = self.widget(index) 

1443 c = w.leo_c 

1444 c.new() 

1445 #@+node:ekr.20131115120119.17391: *3* qt_base_tab.detach 

1446 def detach(self, index: int) -> Widget: 

1447 """detach tab (from tab's context menu)""" 

1448 w = self.widget(index) 

1449 name = self.tabText(index) 

1450 self.detached.append((name, w)) 

1451 self.factory.detachTab(w) 

1452 icon = g.app.gui.getImageFinder("application-x-leo-outline.png") 

1453 icon = QtGui.QIcon(icon) 

1454 if icon: 

1455 w.window().setWindowIcon(icon) 

1456 c = w.leo_c 

1457 if c.styleSheetManager: 

1458 c.styleSheetManager.set_style_sheets(w=w) 

1459 if platform.system() == 'Windows': 

1460 # Windows (XP and 7) put the windows title bar off screen. 

1461 w.move(20, 20) 

1462 return w 

1463 #@+node:ekr.20131115120119.17392: *3* qt_base_tab.tile 

1464 def tile(self, index: int, orientation: str='V') -> None: 

1465 """detach tab and tile with parent window""" 

1466 w = self.widget(index) 

1467 window = w.window() 

1468 # window.showMaximized() 

1469 # this doesn't happen until we've returned to main even loop 

1470 # user needs to do it before using this function 

1471 fg = window.frameGeometry() 

1472 geom = window.geometry() 

1473 x, y, fw, fh = fg.x(), fg.y(), fg.width(), fg.height() 

1474 ww, wh = geom.width(), geom.height() 

1475 w = self.detach(index) 

1476 if window.isMaximized(): 

1477 window.showNormal() 

1478 if orientation == 'V': 

1479 # follow MS Windows convention for which way is horizontal/vertical 

1480 window.resize(ww / 2, wh) 

1481 window.move(x, y) 

1482 w.resize(ww / 2, wh) 

1483 w.move(x + fw / 2, y) 

1484 else: 

1485 window.resize(ww, wh / 2) 

1486 window.move(x, y) 

1487 w.resize(ww, wh / 2) 

1488 w.move(x, y + fh / 2) 

1489 #@+node:ekr.20131115120119.17393: *3* qt_base_tab.reattach_all 

1490 def reattach_all(self) -> None: 

1491 """reattach all detached tabs""" 

1492 for name, w in self.detached: 

1493 self.addTab(w, name) 

1494 self.factory.leoFrames[w] = w.leo_c.frame 

1495 self.detached = [] 

1496 #@+node:ekr.20131115120119.17394: *3* qt_base_tab.delete 

1497 def delete(self, w: Wrapper) -> None: 

1498 """called by TabbedFrameFactory to tell us a detached tab 

1499 has been deleted""" 

1500 self.detached = [i for i in self.detached if i[1] != w] 

1501 #@+node:ekr.20131115120119.17395: *3* qt_base_tab.setChanged 

1502 def setChanged(self, c: Cmdr, changed: bool) -> None: 

1503 """Set the changed indicator in c's tab.""" 

1504 # Find the tab corresponding to c. 

1505 dw = c.frame.top # A DynamicWindow 

1506 i = self.indexOf(dw) 

1507 if i < 0: 

1508 return 

1509 s = self.tabText(i) 

1510 if len(s) > 2: 

1511 if changed: 

1512 if not s.startswith('* '): 

1513 title = "* " + s 

1514 self.setTabText(i, title) 

1515 else: 

1516 if s.startswith('* '): 

1517 title = s[2:] 

1518 self.setTabText(i, title) 

1519 #@+node:ekr.20131115120119.17396: *3* qt_base_tab.setTabName 

1520 def setTabName(self, c: Cmdr, fileName: str) -> None: 

1521 """Set the tab name for c's tab to fileName.""" 

1522 # Find the tab corresponding to c. 

1523 dw = c.frame.top # A DynamicWindow 

1524 i = self.indexOf(dw) 

1525 if i > -1: 

1526 self.setTabText(i, g.shortFileName(fileName)) 

1527 #@+node:ekr.20131115120119.17397: *3* qt_base_tab.closeEvent 

1528 def closeEvent(self, event: Event) -> None: 

1529 """Handle a close event.""" 

1530 g.app.gui.close_event(event) 

1531 #@+node:ekr.20131115120119.17398: *3* qt_base_tab.select (leoTabbedTopLevel) 

1532 def select(self, c: Cmdr) -> None: 

1533 """Select the tab for c.""" 

1534 dw = c.frame.top # A DynamicWindow 

1535 i = self.indexOf(dw) 

1536 self.setCurrentIndex(i) 

1537 # Fix bug 844953: tell Unity which menu to use. 

1538 # c.enableMenuBar() 

1539 #@-others 

1540#@+node:ekr.20110605121601.18180: ** class LeoQtBody(leoFrame.LeoBody) 

1541class LeoQtBody(leoFrame.LeoBody): 

1542 """A class that represents the body pane of a Qt window.""" 

1543 #@+others 

1544 #@+node:ekr.20150521061618.1: *3* LeoQtBody.body_cmd (decorator) 

1545 #@+node:ekr.20110605121601.18181: *3* LeoQtBody.Birth 

1546 #@+node:ekr.20110605121601.18182: *4* LeoQtBody.ctor 

1547 def __init__(self, frame: Wrapper, parentFrame: Widget) -> None: 

1548 """Ctor for LeoQtBody class.""" 

1549 # Call the base class constructor. 

1550 super().__init__(frame, parentFrame) 

1551 c = self.c 

1552 assert c.frame == frame and frame.c == c 

1553 self.colorizer: Any = None 

1554 self.wrapper: Wrapper = None 

1555 self.widget: Widget = None 

1556 self.reloadSettings() 

1557 self.set_widget() # Sets self.widget and self.wrapper. 

1558 self.setWrap(c.p) 

1559 # For multiple body editors. 

1560 self.editor_name = None 

1561 self.editor_v = None 

1562 self.numberOfEditors = 1 

1563 self.totalNumberOfEditors = 1 

1564 # For renderer panes. 

1565 self.canvasRenderer = None 

1566 self.canvasRendererLabel: Widget = None 

1567 self.canvasRendererVisible = False 

1568 self.textRenderer: Widget = None 

1569 self.textRendererLabel: Widget = None 

1570 self.textRendererVisible = False 

1571 self.textRendererWrapper: Wrapper = None 

1572 #@+node:ekr.20110605121601.18185: *5* LeoQtBody.get_name 

1573 def getName(self) -> str: 

1574 return 'body-widget' 

1575 #@+node:ekr.20140901062324.18562: *5* LeoQtBody.reloadSettings 

1576 def reloadSettings(self) -> None: 

1577 c = self.c 

1578 self.useScintilla = c.config.getBool('qt-use-scintilla') 

1579 self.use_chapters = c.config.getBool('use-chapters') 

1580 self.use_gutter = c.config.getBool('use-gutter', default=False) 

1581 #@+node:ekr.20160309074124.1: *5* LeoQtBody.set_invisibles 

1582 def set_invisibles(self, c: Cmdr) -> None: 

1583 """Set the show-invisibles bit in the document.""" 

1584 d = c.frame.body.wrapper.widget.document() 

1585 option = QtGui.QTextOption() 

1586 if c.frame.body.colorizer.showInvisibles: 

1587 # The following works with both Qt5 and Qt6. 

1588 # pylint: disable=no-member 

1589 option.setFlags(option.Flag.ShowTabsAndSpaces) 

1590 d.setDefaultTextOption(option) 

1591 #@+node:ekr.20140901062324.18563: *5* LeoQtBody.set_widget 

1592 def set_widget(self) -> None: 

1593 """Set the actual gui widget.""" 

1594 c = self.c 

1595 top = c.frame.top 

1596 sw = getattr(top, 'stackedWidget', None) 

1597 if sw: 

1598 sw.setCurrentIndex(1) 

1599 if self.useScintilla and not Qsci: 

1600 g.trace('Can not import Qsci: ignoring @bool qt-use-scintilla') 

1601 if self.useScintilla and Qsci: 

1602 # A Qsci.QsciSintilla object. 

1603 # dw.createText sets self.scintilla_widget 

1604 self.widget = c.frame.top.scintilla_widget 

1605 self.wrapper = qt_text.QScintillaWrapper(self.widget, name='body', c=c) 

1606 self.colorizer = leoColorizer.QScintillaColorizer(c, self.widget) 

1607 else: 

1608 self.widget = top.richTextEdit # A LeoQTextBrowser 

1609 self.wrapper = qt_text.QTextEditWrapper(self.widget, name='body', c=c) 

1610 self.widget.setAcceptRichText(False) 

1611 self.colorizer = leoColorizer.make_colorizer(c, self.widget) 

1612 #@+node:ekr.20110605121601.18183: *5* LeoQtBody.forceWrap and setWrap 

1613 def forceWrap(self, p: Pos) -> None: 

1614 """Set **only** the wrap bits in the body.""" 

1615 if not p or self.useScintilla: 

1616 return 

1617 c = self.c 

1618 w = c.frame.body.wrapper.widget 

1619 wrap = WrapMode.WrapAtWordBoundaryOrAnywhere 

1620 w.setWordWrapMode(wrap) 

1621 

1622 def setWrap(self, p: Pos) -> None: 

1623 """Set **only** the wrap bits in the body.""" 

1624 if not p or self.useScintilla: 

1625 return 

1626 c = self.c 

1627 w = c.frame.body.wrapper.widget 

1628 wrap = g.scanAllAtWrapDirectives(c, p) 

1629 policy = ScrollBarPolicy.ScrollBarAlwaysOff if wrap else ScrollBarPolicy.ScrollBarAsNeeded 

1630 w.setHorizontalScrollBarPolicy(policy) 

1631 wrap = WrapMode.WrapAtWordBoundaryOrAnywhere if wrap else WrapMode.NoWrap # type:ignore 

1632 w.setWordWrapMode(wrap) 

1633 #@+node:ekr.20110605121601.18193: *3* LeoQtBody.Editors 

1634 #@+node:ekr.20110605121601.18194: *4* LeoQtBody.entries 

1635 #@+node:ekr.20110605121601.18195: *5* LeoQtBody.add_editor_command 

1636 # An override of leoFrame.addEditor. 

1637 

1638 @body_cmd('editor-add') 

1639 @body_cmd('add-editor') 

1640 def add_editor_command(self, event: Event=None) -> None: 

1641 """Add another editor to the body pane.""" 

1642 c, p = self.c, self.c.p 

1643 d = self.editorWrappers 

1644 dw = c.frame.top 

1645 wrapper = c.frame.body.wrapper # A QTextEditWrapper 

1646 widget = wrapper.widget 

1647 self.totalNumberOfEditors += 1 

1648 self.numberOfEditors += 1 

1649 if self.totalNumberOfEditors == 2: 

1650 d['1'] = wrapper 

1651 # Pack the original body editor. 

1652 # Fix #1021: Pack differently depending on whether the gutter exists. 

1653 if self.use_gutter: 

1654 dw.packLabel(widget.parent(), n=1) 

1655 widget.leo_label = widget.parent().leo_label 

1656 else: 

1657 dw.packLabel(widget, n=1) 

1658 name = f"{self.totalNumberOfEditors}" 

1659 f, wrapper = dw.addNewEditor(name) 

1660 assert g.isTextWrapper(wrapper), wrapper 

1661 assert g.isTextWidget(widget), widget 

1662 assert isinstance(f, QtWidgets.QFrame), f 

1663 d[name] = wrapper 

1664 if self.numberOfEditors == 2: 

1665 # Inject the ivars into the first editor. 

1666 # The name of the last editor need not be '1' 

1667 keys = list(d.keys()) 

1668 old_name = keys[0] 

1669 old_wrapper = d.get(old_name) 

1670 old_w = old_wrapper.widget 

1671 self.injectIvars(f, old_name, p, old_wrapper) 

1672 self.updateInjectedIvars(old_w, p) 

1673 # Immediately create the label in the old editor. 

1674 self.selectLabel(old_wrapper) 

1675 # Switch editors. 

1676 c.frame.body.wrapper = wrapper 

1677 self.selectLabel(wrapper) 

1678 self.selectEditor(wrapper) 

1679 self.updateEditors() 

1680 c.bodyWantsFocus() 

1681 #@+node:ekr.20110605121601.18197: *5* LeoQtBody.assignPositionToEditor 

1682 def assignPositionToEditor(self, p: Pos) -> None: 

1683 """Called *only* from tree.select to select the present body editor.""" 

1684 c = self.c 

1685 wrapper = c.frame.body.wrapper 

1686 w = wrapper and wrapper.widget 

1687 if w: # Careful: w may not exist during unit testing. 

1688 self.updateInjectedIvars(w, p) 

1689 self.selectLabel(wrapper) 

1690 #@+node:ekr.20110605121601.18198: *5* LeoQtBody.cycleEditorFocus 

1691 # Use the base class method. 

1692 #@+node:ekr.20110605121601.18199: *5* LeoQtBody.delete_editor_command 

1693 @body_cmd('delete-editor') 

1694 @body_cmd('editor-delete') 

1695 def delete_editor_command(self, event: Event=None) -> None: 

1696 """Delete the presently selected body text editor.""" 

1697 c, d = self.c, self.editorWrappers 

1698 wrapper = c.frame.body.wrapper 

1699 w = wrapper.widget 

1700 assert g.isTextWrapper(wrapper), wrapper 

1701 assert g.isTextWidget(w), w 

1702 # Fix bug 228: make *sure* the old text is saved. 

1703 c.p.b = wrapper.getAllText() 

1704 name = getattr(w, 'leo_name', None) 

1705 if len(list(d.keys())) <= 1 or name == '1': 

1706 g.warning('can not delete main editor') 

1707 return 

1708 # 

1709 # Actually delete the widget. 

1710 del d[name] 

1711 f = c.frame.top.leo_body_frame 

1712 layout = f.layout() 

1713 for z in (w, w.leo_label): 

1714 if z: 

1715 self.unpackWidget(layout, z) 

1716 # 

1717 # Select another editor. 

1718 new_wrapper = list(d.values())[0] 

1719 self.numberOfEditors -= 1 

1720 if self.numberOfEditors == 1: 

1721 w = new_wrapper.widget 

1722 label = getattr(w, 'leo_label', None) 

1723 if label: 

1724 self.unpackWidget(layout, label) 

1725 w.leo_label = None 

1726 self.selectEditor(new_wrapper) 

1727 #@+node:ekr.20110605121601.18200: *5* LeoQtBody.findEditorForChapter 

1728 def findEditorForChapter(self, chapter: Any, p: Pos) -> None: 

1729 """Return an editor to be assigned to chapter.""" 

1730 c, d = self.c, self.editorWrappers 

1731 values = list(d.values()) 

1732 # First, try to match both the chapter and position. 

1733 if p: 

1734 for w in values: 

1735 if ( 

1736 hasattr(w, 'leo_chapter') and w.leo_chapter == chapter and 

1737 hasattr(w, 'leo_p') and w.leo_p and w.leo_p == p 

1738 ): 

1739 return w 

1740 # Next, try to match just the chapter. 

1741 for w in values: 

1742 if hasattr(w, 'leo_chapter') and w.leo_chapter == chapter: 

1743 return w 

1744 # As a last resort, return the present editor widget. 

1745 return c.frame.body.wrapper 

1746 #@+node:ekr.20110605121601.18201: *5* LeoQtBody.select/unselectLabel 

1747 def unselectLabel(self, wrapper: Wrapper) -> None: 

1748 # pylint: disable=arguments-differ 

1749 pass 

1750 # self.createChapterIvar(wrapper) 

1751 

1752 def selectLabel(self, wrapper: str) -> None: 

1753 # pylint: disable=arguments-differ 

1754 c = self.c 

1755 w = wrapper.widget 

1756 label = getattr(w, 'leo_label', None) 

1757 if label: 

1758 label.setEnabled(True) 

1759 label.setText(c.p.h) 

1760 label.setEnabled(False) 

1761 #@+node:ekr.20110605121601.18202: *5* LeoQtBody.selectEditor & helpers 

1762 selectEditorLockout = False 

1763 

1764 def selectEditor(self, wrapper: Wrapper) -> None: 

1765 """Select editor w and node w.leo_p.""" 

1766 # pylint: disable=arguments-differ 

1767 trace = 'select' in g.app.debug and not g.unitTesting 

1768 tag = 'qt_body.selectEditor' 

1769 c = self.c 

1770 if not wrapper: 

1771 return 

1772 if self.selectEditorLockout: 

1773 return 

1774 w = wrapper.widget 

1775 assert g.isTextWrapper(wrapper), wrapper 

1776 assert g.isTextWidget(w), w 

1777 if trace: 

1778 print(f"{tag:>30}: {wrapper} {c.p.h}") 

1779 if wrapper and wrapper == c.frame.body.wrapper: 

1780 self.deactivateEditors(wrapper) 

1781 if hasattr(w, 'leo_p') and w.leo_p and w.leo_p != c.p: 

1782 c.selectPosition(w.leo_p) 

1783 c.bodyWantsFocus() 

1784 return 

1785 try: 

1786 self.selectEditorLockout = True 

1787 self.selectEditorHelper(wrapper) 

1788 finally: 

1789 self.selectEditorLockout = False 

1790 #@+node:ekr.20110605121601.18203: *6* LeoQtBody.selectEditorHelper 

1791 def selectEditorHelper(self, wrapper: Wrapper) -> None: 

1792 c = self.c 

1793 w = wrapper.widget 

1794 assert g.isTextWrapper(wrapper), wrapper 

1795 assert g.isTextWidget(w), w 

1796 if not w.leo_p: 

1797 g.trace('no w.leo_p') 

1798 return 

1799 # The actual switch. 

1800 self.deactivateEditors(wrapper) 

1801 self.recolorWidget(w.leo_p, wrapper) # switches colorizers. 

1802 c.frame.body.wrapper = wrapper 

1803 # 2014/09/04: Must set both wrapper.widget and body.widget. 

1804 c.frame.body.wrapper.widget = w 

1805 c.frame.body.widget = w 

1806 w.leo_active = True 

1807 self.switchToChapter(wrapper) 

1808 self.selectLabel(wrapper) 

1809 if not self.ensurePositionExists(w): 

1810 g.trace('***** no position editor!') 

1811 return 

1812 if not (hasattr(w, 'leo_p') and w.leo_p): 

1813 g.trace('***** no w.leo_p', w) 

1814 return 

1815 p = w.leo_p 

1816 assert p, p 

1817 c.expandAllAncestors(p) 

1818 # Calls assignPositionToEditor. 

1819 # Calls p.v.restoreCursorAndScroll. 

1820 c.selectPosition(p) 

1821 c.redraw() 

1822 c.recolor() 

1823 c.bodyWantsFocus() 

1824 #@+node:ekr.20110605121601.18205: *5* LeoQtBody.updateEditors 

1825 # Called from addEditor and assignPositionToEditor 

1826 

1827 def updateEditors(self) -> None: 

1828 c, p = self.c, self.c.p 

1829 body = p.b 

1830 d = self.editorWrappers 

1831 if len(list(d.keys())) < 2: 

1832 return # There is only the main widget 

1833 w0 = c.frame.body.wrapper 

1834 i, j = w0.getSelectionRange() 

1835 ins = w0.getInsertPoint() 

1836 sb0 = w0.widget.verticalScrollBar() 

1837 pos0 = sb0.sliderPosition() 

1838 for key in d: 

1839 wrapper = d.get(key) 

1840 w = wrapper.widget 

1841 v = hasattr(w, 'leo_p') and w.leo_p.v 

1842 if v and v == p.v and w != w0: 

1843 sb = w.verticalScrollBar() 

1844 pos = sb.sliderPosition() 

1845 wrapper.setAllText(body) 

1846 self.recolorWidget(p, wrapper) 

1847 sb.setSliderPosition(pos) 

1848 c.bodyWantsFocus() 

1849 w0.setSelectionRange(i, j, insert=ins) 

1850 sb0.setSliderPosition(pos0) 

1851 #@+node:ekr.20110605121601.18206: *4* LeoQtBody.utils 

1852 #@+node:ekr.20110605121601.18207: *5* LeoQtBody.computeLabel 

1853 def computeLabel(self, w: Wrapper) -> str: 

1854 if hasattr(w, 'leo_label') and w.leo_label: # 2011/11/12 

1855 s = w.leo_label.text() 

1856 else: 

1857 s = '' 

1858 if hasattr(w, 'leo_chapter') and w.leo_chapter: 

1859 s = f"{w.leo_chapter}: {s}" 

1860 return s 

1861 #@+node:ekr.20110605121601.18208: *5* LeoQtBody.createChapterIvar 

1862 def createChapterIvar(self, w: Wrapper) -> None: 

1863 c = self.c 

1864 cc = c.chapterController 

1865 if hasattr(w, 'leo_chapter') and w.leo_chapter: 

1866 pass 

1867 elif cc and self.use_chapters: 

1868 w.leo_chapter = cc.getSelectedChapter() 

1869 else: 

1870 w.leo_chapter = None 

1871 #@+node:ekr.20110605121601.18209: *5* LeoQtBody.deactivateEditors 

1872 def deactivateEditors(self, wrapper: Wrapper) -> None: 

1873 """Deactivate all editors except wrapper's editor.""" 

1874 d = self.editorWrappers 

1875 # Don't capture ivars here! assignPositionToEditor keeps them up-to-date. (??) 

1876 for key in d: 

1877 wrapper2 = d.get(key) 

1878 w2 = wrapper2.widget 

1879 if hasattr(w2, 'leo_active'): 

1880 active = w2.leo_active 

1881 else: 

1882 active = True 

1883 if wrapper2 != wrapper and active: 

1884 w2.leo_active = False 

1885 self.unselectLabel(wrapper2) 

1886 self.onFocusOut(w2) 

1887 #@+node:ekr.20110605121601.18210: *5* LeoQtBody.ensurePositionExists 

1888 def ensurePositionExists(self, w: Wrapper) -> bool: 

1889 """Return True if w.leo_p exists or can be reconstituted.""" 

1890 c = self.c 

1891 if c.positionExists(w.leo_p): 

1892 return True 

1893 for p2 in c.all_unique_positions(): 

1894 if p2.v and p2.v == w.leo_p.v: 

1895 w.leo_p = p2.copy() 

1896 return True 

1897 # This *can* happen when selecting a deleted node. 

1898 w.leo_p = c.p.copy() 

1899 return False 

1900 #@+node:ekr.20110605121601.18211: *5* LeoQtBody.injectIvars 

1901 def injectIvars(self, parentFrame: Wrapper, name: str, p: Pos, wrapper: Wrapper) -> None: 

1902 

1903 trace = g.app.debug == 'select' and not g.unitTesting 

1904 tag = 'qt_body.injectIvars' 

1905 w = wrapper.widget 

1906 assert g.isTextWrapper(wrapper), wrapper 

1907 assert g.isTextWidget(w), w 

1908 if trace: 

1909 print(f"{tag:>30}: {wrapper!r} {g.callers(1)}") 

1910 # Inject ivars 

1911 if name == '1': 

1912 w.leo_p = None # Will be set when the second editor is created. 

1913 else: 

1914 w.leo_p = p and p.copy() 

1915 w.leo_active = True 

1916 w.leo_bodyBar = None 

1917 w.leo_bodyXBar = None 

1918 w.leo_chapter = None 

1919 # w.leo_colorizer injected by JEditColorizer ctor. 

1920 # w.leo_label injected by packLabel. 

1921 w.leo_frame = parentFrame 

1922 w.leo_name = name 

1923 w.leo_wrapper = wrapper 

1924 #@+node:ekr.20110605121601.18213: *5* LeoQtBody.recolorWidget (QScintilla only) 

1925 def recolorWidget(self, p: Pos, wrapper: Wrapper) -> None: 

1926 """Support QScintillaColorizer.colorize.""" 

1927 # pylint: disable=arguments-differ 

1928 c = self.c 

1929 colorizer = c.frame.body.colorizer 

1930 if p and colorizer and hasattr(colorizer, 'colorize'): 

1931 g.trace('=====', hasattr(colorizer, 'colorize'), p.h, g.callers()) 

1932 old_wrapper = c.frame.body.wrapper 

1933 c.frame.body.wrapper = wrapper 

1934 try: 

1935 colorizer.colorize(p) 

1936 finally: 

1937 # Restore. 

1938 c.frame.body.wrapper = old_wrapper 

1939 #@+node:ekr.20110605121601.18214: *5* LeoQtBody.switchToChapter 

1940 def switchToChapter(self, w: Wrapper) -> None: 

1941 """select w.leo_chapter.""" 

1942 c = self.c 

1943 cc = c.chapterController 

1944 if hasattr(w, 'leo_chapter') and w.leo_chapter: 

1945 chapter = w.leo_chapter 

1946 name = chapter and chapter.name 

1947 oldChapter = cc.getSelectedChapter() 

1948 if chapter != oldChapter: 

1949 cc.selectChapterByName(name) 

1950 c.bodyWantsFocus() 

1951 #@+node:ekr.20110605121601.18216: *5* LeoQtBody.unpackWidget 

1952 def unpackWidget(self, layout: Widget, w: Wrapper) -> None: 

1953 

1954 index = layout.indexOf(w) 

1955 if index == -1: 

1956 return 

1957 item = layout.itemAt(index) 

1958 if item: 

1959 item.setGeometry(QtCore.QRect(0, 0, 0, 0)) 

1960 layout.removeItem(item) 

1961 #@+node:ekr.20110605121601.18215: *5* LeoQtBody.updateInjectedIvars 

1962 def updateInjectedIvars(self, w: Wrapper, p: Pos) -> None: 

1963 

1964 c = self.c 

1965 cc = c.chapterController 

1966 assert g.isTextWidget(w), w 

1967 if cc and self.use_chapters: 

1968 w.leo_chapter = cc.getSelectedChapter() 

1969 else: 

1970 w.leo_chapter = None 

1971 w.leo_p = p.copy() 

1972 #@+node:ekr.20110605121601.18223: *3* LeoQtBody.Event handlers 

1973 #@+node:ekr.20110930174206.15472: *4* LeoQtBody.onFocusIn 

1974 def onFocusIn(self, obj: Any) -> None: 

1975 """Handle a focus-in event in the body pane.""" 

1976 trace = 'select' in g.app.debug and not g.unitTesting 

1977 tag = 'qt_body.onFocusIn' 

1978 if obj.objectName() == 'richTextEdit': 

1979 wrapper = getattr(obj, 'leo_wrapper', None) 

1980 if trace: 

1981 print(f"{tag:>30}: {wrapper}") 

1982 if wrapper and wrapper != self.wrapper: 

1983 self.selectEditor(wrapper) 

1984 self.onFocusColorHelper('focus-in', obj) 

1985 if hasattr(obj, 'leo_copy_button') and obj.leo_copy_button: 

1986 obj.setReadOnly(True) 

1987 else: 

1988 obj.setReadOnly(False) 

1989 obj.setFocus() # Weird, but apparently necessary. 

1990 #@+node:ekr.20110930174206.15473: *4* LeoQtBody.onFocusOut 

1991 def onFocusOut(self, obj: Any) -> None: 

1992 """Handle a focus-out event in the body pane.""" 

1993 # Apparently benign. 

1994 if obj.objectName() == 'richTextEdit': 

1995 self.onFocusColorHelper('focus-out', obj) 

1996 if hasattr(obj, 'setReadOnly'): 

1997 obj.setReadOnly(True) 

1998 #@+node:ekr.20110605121601.18224: *4* LeoQtBody.qtBody.onFocusColorHelper (revised) 

1999 def onFocusColorHelper(self, kind: str, obj: Any) -> None: 

2000 """Handle changes of style when focus changes.""" 

2001 c, vc = self.c, self.c.vimCommands 

2002 if vc and c.vim_mode: 

2003 try: 

2004 assert kind in ('focus-in', 'focus-out') 

2005 w = c.frame.body.wrapper.widget 

2006 vc.set_border(w=w, activeFlag=kind == 'focus-in') 

2007 except Exception: 

2008 # g.es_exception() 

2009 pass 

2010 #@+node:ekr.20110605121601.18217: *3* LeoQtBody.Renderer panes 

2011 #@+node:ekr.20110605121601.18218: *4* LeoQtBody.hideCanvasRenderer 

2012 def hideCanvasRenderer(self, event: Event=None) -> None: 

2013 """Hide canvas pane.""" 

2014 c, d = self.c, self.editorWrappers 

2015 wrapper = c.frame.body.wrapper 

2016 w = wrapper.widget 

2017 name = w.leo_name 

2018 assert name 

2019 assert wrapper == d.get(name), 'wrong wrapper' 

2020 assert g.isTextWrapper(wrapper), wrapper 

2021 assert g.isTextWidget(w), w 

2022 if len(list(d.keys())) <= 1: 

2023 return 

2024 # 

2025 # At present, can not delete the first column. 

2026 if name == '1': 

2027 g.warning('can not delete leftmost editor') 

2028 return 

2029 # 

2030 # Actually delete the widget. 

2031 del d[name] 

2032 f = c.frame.top.leo_body_inner_frame 

2033 layout = f.layout() 

2034 for z in (w, w.leo_label): 

2035 if z: 

2036 self.unpackWidget(layout, z) 

2037 # 

2038 # Select another editor. 

2039 w.leo_label = None 

2040 new_wrapper = list(d.values())[0] 

2041 self.numberOfEditors -= 1 

2042 if self.numberOfEditors == 1: 

2043 w = new_wrapper.widget 

2044 if w.leo_label: # 2011/11/12 

2045 self.unpackWidget(layout, w.leo_label) 

2046 w.leo_label = None # 2011/11/12 

2047 self.selectEditor(new_wrapper) 

2048 #@+node:ekr.20110605121601.18219: *4* LeoQtBody.hideTextRenderer 

2049 def hideCanvas(self, event: Event=None) -> None: 

2050 """Hide canvas pane.""" 

2051 c, d = self.c, self.editorWrappers 

2052 wrapper = c.frame.body.wrapper 

2053 w = wrapper.widget 

2054 name = w.leo_name 

2055 assert name 

2056 assert wrapper == d.get(name), 'wrong wrapper' 

2057 assert g.isTextWrapper(wrapper), wrapper 

2058 assert g.isTextWidget(w), w 

2059 if len(list(d.keys())) <= 1: 

2060 return 

2061 # At present, can not delete the first column. 

2062 if name == '1': 

2063 g.warning('can not delete leftmost editor') 

2064 return 

2065 # 

2066 # Actually delete the widget. 

2067 del d[name] 

2068 f = c.frame.top.leo_body_inner_frame 

2069 layout = f.layout() 

2070 for z in (w, w.leo_label): 

2071 if z: 

2072 self.unpackWidget(layout, z) 

2073 # 

2074 # Select another editor. 

2075 w.leo_label = None 

2076 new_wrapper = list(d.values())[0] 

2077 self.numberOfEditors -= 1 

2078 if self.numberOfEditors == 1: 

2079 w = new_wrapper.widget 

2080 if w.leo_label: 

2081 self.unpackWidget(layout, w.leo_label) 

2082 w.leo_label = None 

2083 self.selectEditor(new_wrapper) 

2084 #@+node:ekr.20110605121601.18220: *4* LeoQtBody.packRenderer 

2085 def packRenderer(self, f: str, name: str, w: Wrapper) -> Widget: 

2086 n = max(1, self.numberOfEditors) 

2087 assert isinstance(f, QtWidgets.QFrame), f 

2088 layout = f.layout() 

2089 f.setObjectName(f"{name} Frame") 

2090 # Create the text: to do: use stylesheet to set font, height. 

2091 lab = QtWidgets.QLineEdit(f) 

2092 lab.setObjectName(f"{name} Label") 

2093 lab.setText(name) 

2094 # Pack the label and the widget. 

2095 layout.addWidget(lab, 0, max(0, n - 1), Alignment.AlignVCenter) 

2096 layout.addWidget(w, 1, max(0, n - 1)) 

2097 layout.setRowStretch(0, 0) 

2098 layout.setRowStretch(1, 1) # Give row 1 as much as possible. 

2099 return lab 

2100 #@+node:ekr.20110605121601.18221: *4* LeoQtBody.showCanvasRenderer 

2101 # An override of leoFrame.addEditor. 

2102 

2103 def showCanvasRenderer(self, event: Event=None) -> None: 

2104 """Show the canvas area in the body pane, creating it if necessary.""" 

2105 c = self.c 

2106 f = c.frame.top.leo_body_inner_frame 

2107 assert isinstance(f, QtWidgets.QFrame), f 

2108 if not self.canvasRenderer: 

2109 name = 'Graphics Renderer' 

2110 self.canvasRenderer = w = QtWidgets.QGraphicsView(f) 

2111 w.setObjectName(name) 

2112 if not self.canvasRendererVisible: 

2113 self.canvasRendererLabel = self.packRenderer(f, name, w) 

2114 self.canvasRendererVisible = True 

2115 #@+node:ekr.20110605121601.18222: *4* LeoQtBody.showTextRenderer 

2116 # An override of leoFrame.addEditor. 

2117 

2118 def showTextRenderer(self, event: Event=None) -> None: 

2119 """Show the canvas area in the body pane, creating it if necessary.""" 

2120 c = self.c 

2121 f = c.frame.top.leo_body_inner_frame 

2122 name = 'Text Renderer' 

2123 w = self.textRenderer 

2124 assert isinstance(f, QtWidgets.QFrame), f 

2125 if w: 

2126 self.textRenderer = qt_text.LeoQTextBrowser(f, c, self) 

2127 w = self.textRenderer 

2128 w.setObjectName(name) 

2129 self.textRendererWrapper = qt_text.QTextEditWrapper(w, name='text-renderer', c=c) 

2130 if not self.textRendererVisible: 

2131 self.textRendererLabel = self.packRenderer(f, name, w) 

2132 self.textRendererVisible = True 

2133 #@-others 

2134#@+node:ekr.20110605121601.18245: ** class LeoQtFrame (leoFrame) 

2135class LeoQtFrame(leoFrame.LeoFrame): 

2136 """A class that represents a Leo window rendered in qt.""" 

2137 #@+others 

2138 #@+node:ekr.20110605121601.18246: *3* qtFrame.Birth & Death 

2139 #@+node:ekr.20110605121601.18247: *4* qtFrame.__init__ & reloadSettings 

2140 def __init__(self, c: Cmdr, title: str, gui: Any) -> None: 

2141 

2142 super().__init__(c, gui) 

2143 assert self.c == c 

2144 leoFrame.LeoFrame.instances += 1 # Increment the class var. 

2145 # Official ivars... 

2146 self.iconBar = None 

2147 self.iconBarClass = self.QtIconBarClass # type:ignore 

2148 self.initComplete = False # Set by initCompleteHint(). 

2149 self.minibufferVisible = True 

2150 self.statusLineClass = self.QtStatusLineClass # type:ignore 

2151 self.title = title 

2152 self.setIvars() 

2153 self.reloadSettings() 

2154 

2155 def reloadSettings(self) -> None: 

2156 c = self.c 

2157 self.cursorStay = c.config.getBool("cursor-stay-on-paste", default=True) 

2158 self.use_chapters = c.config.getBool('use-chapters') 

2159 self.use_chapter_tabs = c.config.getBool('use-chapter-tabs') 

2160 #@+node:ekr.20110605121601.18248: *5* qtFrame.setIvars 

2161 def setIvars(self) -> None: 

2162 # "Official ivars created in createLeoFrame and its allies. 

2163 self.bar1 = None 

2164 self.bar2 = None 

2165 self.body = None 

2166 self.f1 = self.f2 = None 

2167 self.findPanel = None # Inited when first opened. 

2168 self.iconBarComponentName = 'iconBar' 

2169 self.iconFrame = None 

2170 self.log = None 

2171 self.canvas = None 

2172 self.outerFrame = None 

2173 self.statusFrame = None 

2174 self.statusLineComponentName = 'statusLine' 

2175 self.statusText = None 

2176 self.statusLabel = None 

2177 self.top = None # This will be a class Window object. 

2178 self.tree = None 

2179 # Used by event handlers... 

2180 self.controlKeyIsDown = False # For control-drags 

2181 self.isActive = True 

2182 self.redrawCount = 0 

2183 self.wantedWidget = None 

2184 self.wantedCallbackScheduled = False 

2185 self.scrollWay = None 

2186 #@+node:ekr.20110605121601.18249: *4* qtFrame.__repr__ 

2187 def __repr__(self) -> str: 

2188 return f"<LeoQtFrame: {self.title}>" 

2189 #@+node:ekr.20110605121601.18250: *4* qtFrame.finishCreate & helpers 

2190 def finishCreate(self) -> None: 

2191 """Finish creating the outline's frame.""" 

2192 # Called from app.newCommander, Commands.__init__ 

2193 t1 = time.process_time() 

2194 c = self.c 

2195 assert c 

2196 frameFactory = g.app.gui.frameFactory 

2197 if not frameFactory.masterFrame: 

2198 frameFactory.createMaster() 

2199 self.top = frameFactory.createFrame(leoFrame=self) 

2200 self.createIconBar() # A base class method. 

2201 self.createSplitterComponents() 

2202 self.createStatusLine() # A base class method. 

2203 self.createFirstTreeNode() # Call the base-class method. 

2204 self.menu: Wrapper = LeoQtMenu(c, self, label='top-level-menu') 

2205 g.app.windowList.append(self) 

2206 t2 = time.process_time() 

2207 self.setQtStyle() # Slow, but only the first time it is called. 

2208 t3 = time.process_time() 

2209 self.miniBufferWidget: Wrapper = qt_text.QMinibufferWrapper(c) 

2210 c.bodyWantsFocus() 

2211 t4 = time.process_time() 

2212 if 'speed' in g.app.debug: 

2213 print('qtFrame.finishCreate') 

2214 print( 

2215 f" 1: {t2-t1:5.2f}\n" # 0.20 sec: before. 

2216 f" 2: {t3-t2:5.2f}\n" # 0.19 sec: setQtStyle (only once) 

2217 f" 3: {t4-t3:5.2f}\n" # 0.00 sec: after. 

2218 f"total: {t4-t1:5.2f}" 

2219 ) 

2220 #@+node:ekr.20110605121601.18251: *5* qtFrame.createSplitterComponents 

2221 def createSplitterComponents(self) -> None: 

2222 

2223 c = self.c 

2224 self.tree: Wrapper = qt_tree.LeoQtTree(c, self) 

2225 self.log: Wrapper = LeoQtLog(self, None) 

2226 self.body: Wrapper = LeoQtBody(self, None) 

2227 self.splitVerticalFlag, ratio, secondary_ratio = self.initialRatios() 

2228 self.resizePanesToRatio(ratio, secondary_ratio) 

2229 #@+node:ekr.20190412044556.1: *5* qtFrame.setQtStyle 

2230 def setQtStyle(self) -> None: 

2231 """ 

2232 Set the default Qt style. Based on pyzo code. 

2233 

2234 Copyright (C) 2013-2018, the Pyzo development team 

2235 

2236 Pyzo is distributed under the terms of the (new) BSD License. 

2237 The full license can be found in 'license.txt'. 

2238 """ 

2239 # Fix #1936: very slow new command. Only do this once! 

2240 if g.app.initStyleFlag: 

2241 return 

2242 g.app.initStyleFlag = True 

2243 c = self.c 

2244 trace = 'themes' in g.app.debug 

2245 # 

2246 # Get the requested style name. 

2247 stylename = c.config.getString('qt-style-name') or '' 

2248 if trace: 

2249 g.trace(repr(stylename)) 

2250 if not stylename: 

2251 return 

2252 # 

2253 # Return if the style does not exist. 

2254 styles = [z.lower() for z in QtWidgets.QStyleFactory.keys()] 

2255 if stylename.lower() not in styles: 

2256 g.es_print(f"ignoring unknown Qt style name: {stylename!r}") 

2257 g.printObj(styles) 

2258 return 

2259 # 

2260 # Change the style and palette. 

2261 app = g.app.gui.qtApp 

2262 if isQt5 or isQt6: 

2263 qstyle = app.setStyle(stylename) 

2264 if not qstyle: 

2265 g.es_print(f"failed to set Qt style name: {stylename!r}") 

2266 else: 

2267 QtWidgets.qApp.nativePalette = QtWidgets.qApp.palette() 

2268 qstyle = QtWidgets.qApp.setStyle(stylename) 

2269 if not qstyle: 

2270 g.es_print(f"failed to set Qt style name: {stylename!r}") 

2271 return 

2272 app.setPalette(QtWidgets.qApp.nativePalette) 

2273 #@+node:ekr.20110605121601.18252: *4* qtFrame.initCompleteHint 

2274 def initCompleteHint(self) -> None: 

2275 """A kludge: called to enable text changed events.""" 

2276 self.initComplete = True 

2277 #@+node:ekr.20110605121601.18253: *4* Destroying the qtFrame 

2278 #@+node:ekr.20110605121601.18254: *5* qtFrame.destroyAllObjects (not used) 

2279 def destroyAllObjects(self) -> None: 

2280 """Clear all links to objects in a Leo window.""" 

2281 c = self.c 

2282 # g.printGcAll() 

2283 # Do this first. 

2284 #@+<< clear all vnodes in the tree >> 

2285 #@+node:ekr.20110605121601.18255: *6* << clear all vnodes in the tree>> (qtFrame) 

2286 vList = [z for z in c.all_unique_nodes()] 

2287 for v in vList: 

2288 g.clearAllIvars(v) 

2289 vList = [] # Remove these references immediately. 

2290 #@-<< clear all vnodes in the tree >> 

2291 # Destroy all ivars in subcommanders. 

2292 g.clearAllIvars(c.atFileCommands) 

2293 if c.chapterController: # New in Leo 4.4.3 b1. 

2294 g.clearAllIvars(c.chapterController) 

2295 g.clearAllIvars(c.fileCommands) 

2296 g.clearAllIvars(c.keyHandler) # New in Leo 4.4.3 b1. 

2297 g.clearAllIvars(c.importCommands) 

2298 g.clearAllIvars(c.tangleCommands) 

2299 g.clearAllIvars(c.undoer) 

2300 g.clearAllIvars(c) 

2301 

2302 #@+node:ekr.20110605121601.18256: *5* qtFrame.destroySelf 

2303 def destroySelf(self) -> None: 

2304 # Remember these: we are about to destroy all of our ivars! 

2305 c, top = self.c, self.top 

2306 if hasattr(g.app.gui, 'frameFactory'): 

2307 g.app.gui.frameFactory.deleteFrame(top) 

2308 # Indicate that the commander is no longer valid. 

2309 c.exists = False 

2310 if 0: # We can't do this unless we unhook the event filter. 

2311 # Destroys all the objects of the commander. 

2312 self.destroyAllObjects() 

2313 c.exists = False # Make sure this one ivar has not been destroyed. 

2314 # print('destroySelf: qtFrame: %s' % c,g.callers(4)) 

2315 top.close() 

2316 #@+node:ekr.20110605121601.18257: *3* qtFrame.class QtStatusLineClass 

2317 class QtStatusLineClass: 

2318 """A class representing the status line.""" 

2319 #@+others 

2320 #@+node:ekr.20110605121601.18258: *4* QtStatusLineClass.ctor 

2321 def __init__(self, c: Cmdr, parentFrame: Widget) -> None: 

2322 """Ctor for LeoQtFrame class.""" 

2323 self.c = c 

2324 self.statusBar = c.frame.top.statusBar 

2325 self.lastFcol = 0 

2326 self.lastRow = 0 

2327 self.lastCol = 0 

2328 # Create the text widgets. 

2329 self.textWidget1 = w1 = QtWidgets.QLineEdit(self.statusBar) 

2330 self.textWidget2 = w2 = QtWidgets.QLineEdit(self.statusBar) 

2331 w1.setObjectName('status1') 

2332 w2.setObjectName('status2') 

2333 w1.setReadOnly(True) 

2334 w2.setReadOnly(True) 

2335 splitter = QtWidgets.QSplitter() 

2336 self.statusBar.addWidget(splitter, True) 

2337 sizes = c.config.getString('status-line-split-sizes') or '1 2' 

2338 sizes = [int(i) for i in sizes.replace(',', ' ').split()] 

2339 for n, i in enumerate(sizes): 

2340 w = [w1, w2][n] 

2341 policy = w.sizePolicy() 

2342 policy.setHorizontalStretch(i) 

2343 policy.setHorizontalPolicy(Policy.Minimum) 

2344 w.setSizePolicy(policy) 

2345 splitter.addWidget(w1) 

2346 splitter.addWidget(w2) 

2347 self.put('') 

2348 self.update() 

2349 #@+node:ekr.20110605121601.18260: *4* QtStatusLineClass.clear, get & put/1 

2350 def clear(self) -> None: 

2351 self.put('') 

2352 

2353 def get(self) -> str: 

2354 return self.textWidget2.text() 

2355 

2356 def put(self, s: str, bg: str=None, fg: str=None) -> None: 

2357 self.put_helper(s, self.textWidget2, bg, fg) 

2358 

2359 def put1(self, s: str, bg: str=None, fg: str=None) -> None: 

2360 self.put_helper(s, self.textWidget1, bg, fg) 

2361 

2362 # Keys are widgets, values are stylesheets. 

2363 styleSheetCache: Dict[Any, str] = {} 

2364 

2365 def put_helper(self, s: str, w: Wrapper, bg: str=None, fg: str=None) -> None: 

2366 """Put string s in the indicated widget, with proper colors.""" 

2367 c = self.c 

2368 bg = bg or c.config.getColor('status-bg') or 'white' 

2369 fg = fg or c.config.getColor('status-fg') or 'black' 

2370 if True: 

2371 # Work around #804. w is a QLineEdit. 

2372 w.setStyleSheet(f"background: {bg}; color: {fg};") 

2373 else: 

2374 # Rather than put(msg, explicit_color, explicit_color) we should use 

2375 # put(msg, status) where status is None, 'info', or 'fail'. 

2376 # Just as a quick hack to avoid dealing with propagating those changes 

2377 # back upstream, infer status like this: 

2378 if ( 

2379 fg == c.config.getColor('find-found-fg') and 

2380 bg == c.config.getColor('find-found-bg') 

2381 ): 

2382 status = 'info' 

2383 elif ( 

2384 fg == c.config.getColor('find-not-found-fg') and 

2385 bg == c.config.getColor('find-not-found-bg') 

2386 ): 

2387 status = 'fail' 

2388 else: 

2389 status = None 

2390 d = self.styleSheetCache 

2391 if status != d.get(w, '__undefined__'): 

2392 d[w] = status 

2393 c.styleSheetManager.mng.remove_sclass(w, ['info', 'fail']) 

2394 c.styleSheetManager.mng.add_sclass(w, status) 

2395 c.styleSheetManager.mng.update_view(w) # force appearance update 

2396 w.setText(s) 

2397 #@+node:chris.20180320072817.1: *4* QtStatusLineClass.update & helpers 

2398 def update(self) -> None: 

2399 if g.app.killed: 

2400 return 

2401 c, body = self.c, self.c.frame.body 

2402 if not c.p: 

2403 return 

2404 te = body.widget 

2405 if not isinstance(te, QtWidgets.QTextEdit): 

2406 return 

2407 cursor = te.textCursor() 

2408 block = cursor.block() 

2409 row = block.blockNumber() + 1 

2410 col, fcol = self.compute_columns(block, cursor) 

2411 words = len(c.p.b.split(None)) 

2412 self.put_status_line(col, fcol, row, words) 

2413 self.lastRow = row 

2414 self.lastCol = col 

2415 self.lastFcol = fcol 

2416 #@+node:ekr.20190118082646.1: *5* qstatus.compute_columns 

2417 def compute_columns(self, block: Any, cursor: Any) -> Tuple[int, int]: 

2418 

2419 c = self.c 

2420 line = block.text() 

2421 col = cursor.columnNumber() 

2422 offset = c.p.textOffset() 

2423 fcol_offset = 0 

2424 s2 = line[0:col] 

2425 col = g.computeWidth(s2, c.tab_width) 

2426 # 

2427 # #195: fcol when using @first directive is inaccurate 

2428 i = line.find('<<') 

2429 j = line.find('>>') 

2430 if -1 < i < j or g.match_word(line.strip(), 0, '@others'): 

2431 offset = None 

2432 else: 

2433 for tag in ('@first ', '@last '): 

2434 if line.startswith(tag): 

2435 fcol_offset = len(tag) 

2436 break 

2437 # 

2438 # fcol is '' if there is no ancestor @<file> node. 

2439 fcol = None if offset is None else max(0, col + offset - fcol_offset) 

2440 return col, fcol 

2441 #@+node:chris.20180320072817.2: *5* qstatus.file_line (not used) 

2442 def file_line(self) -> Optional[Pos]: 

2443 """ 

2444 Return the line of the first line of c.p in its external file. 

2445 Return None if c.p is not part of an external file. 

2446 """ 

2447 c, p = self.c, self.c.p 

2448 if p: 

2449 goto = gotoCommands.GoToCommands(c) 

2450 return goto.find_node_start(p) 

2451 return None 

2452 #@+node:ekr.20190118082047.1: *5* qstatus.put_status_line 

2453 def put_status_line(self, col: int, fcol: int, row: int, words: int) -> None: 

2454 

2455 if 1: 

2456 fcol_part = '' if fcol is None else f" fcol: {fcol}" 

2457 # For now, it seems to0 difficult to get alignment *exactly* right. 

2458 self.put1(f"line: {row:d} col: {col:d} {fcol_part} words: {words}") 

2459 else: 

2460 # #283 is not ready yet, and probably will never be. 

2461 fline = self.file_line() 

2462 fline = '' if fline is None else fline + row 

2463 self.put1( 

2464 f"fline: {fline:2} line: {row:2d} col: {col:2} fcol: {fcol:2}") 

2465 #@-others 

2466 #@+node:ekr.20110605121601.18262: *3* qtFrame.class QtIconBarClass 

2467 class QtIconBarClass: 

2468 """A class representing the singleton Icon bar""" 

2469 #@+others 

2470 #@+node:ekr.20110605121601.18263: *4* QtIconBar.ctor & reloadSettings 

2471 def __init__(self, c: Cmdr, parentFrame: Widget) -> None: 

2472 """Ctor for QtIconBarClass.""" 

2473 # Copy ivars 

2474 self.c = c 

2475 self.parentFrame = parentFrame 

2476 # Status ivars. 

2477 self.actions: List[Any] = [] 

2478 self.chapterController = None 

2479 self.toolbar = self 

2480 self.w = c.frame.top.iconBar # A QToolBar. 

2481 self.reloadSettings() 

2482 

2483 def reloadSettings(self) -> None: 

2484 c = self.c 

2485 c.registerReloadSettings(self) 

2486 self.buttonColor = c.config.getString('qt-button-color') 

2487 self.toolbar_orientation = c.config.getString('qt-toolbar-location') 

2488 #@+node:ekr.20110605121601.18264: *4* QtIconBar.do-nothings 

2489 # These *are* called from Leo's core. 

2490 

2491 def addRow(self, height: int=None) -> None: 

2492 pass # To do. 

2493 

2494 def getNewFrame(self) -> None: 

2495 return None # To do 

2496 #@+node:ekr.20110605121601.18265: *4* QtIconBar.add 

2497 def add(self, *args: Any, **keys: Any) -> Any: 

2498 """Add a button to the icon bar.""" 

2499 c = self.c 

2500 if not self.w: 

2501 return None 

2502 command: Callable = keys.get('command') 

2503 text: str = keys.get('text') 

2504 # able to specify low-level QAction directly (QPushButton not forced) 

2505 qaction: Any = keys.get('qaction') 

2506 if not text and not qaction: 

2507 g.es('bad toolbar item') 

2508 kind: str = keys.get('kind') or 'generic-button' 

2509 # imagefile = keys.get('imagefile') 

2510 # image = keys.get('image') 

2511 

2512 

2513 class leoIconBarButton(QtWidgets.QWidgetAction): # type:ignore 

2514 

2515 # toolbar is a QtIconBarClass object, not a QWidget. 

2516 def __init__(self, parent: Widget, text: str, toolbar: Any) -> None: 

2517 super().__init__(parent) 

2518 self.button: Widget = None # set below 

2519 self.text = text 

2520 self.toolbar = toolbar 

2521 

2522 def createWidget(self, parent: Widget) -> None: 

2523 self.button = b = QtWidgets.QPushButton(self.text, parent) 

2524 self.button.setProperty('button_kind', kind) # for styling 

2525 return b 

2526 

2527 action: Any 

2528 if qaction is None: 

2529 action = leoIconBarButton(parent=self.w, text=text, toolbar=self) 

2530 button_name = text 

2531 else: 

2532 action = qaction 

2533 button_name = action.text() 

2534 self.w.addAction(action) 

2535 self.actions.append(action) 

2536 b = self.w.widgetForAction(action) 

2537 # Set the button's object name so we can use the stylesheet to color it. 

2538 if not button_name: 

2539 button_name = 'unnamed' 

2540 button_name = button_name + '-button' 

2541 b.setObjectName(button_name) 

2542 b.setContextMenuPolicy(ContextMenuPolicy.ActionsContextMenu) 

2543 

2544 def delete_callback(checked: str, action: str=action) -> None: 

2545 self.w.removeAction(action) 

2546 

2547 b.leo_removeAction = rb = QAction('Remove Button', b) 

2548 b.addAction(rb) 

2549 rb.triggered.connect(delete_callback) 

2550 if command: 

2551 

2552 def button_callback(event: Event, c: Cmdr=c, command: Callable=command) -> None: 

2553 val = command() 

2554 if c.exists: 

2555 # c.bodyWantsFocus() 

2556 c.outerUpdate() 

2557 return val 

2558 

2559 b.clicked.connect(button_callback) 

2560 return action 

2561 #@+node:ekr.20110605121601.18266: *4* QtIconBar.addRowIfNeeded (not used) 

2562 def addRowIfNeeded(self) -> None: 

2563 """Add a new icon row if there are too many widgets.""" 

2564 # n = g.app.iconWidgetCount 

2565 # if n >= self.widgets_per_row: 

2566 # g.app.iconWidgetCount = 0 

2567 # self.addRow() 

2568 # g.app.iconWidgetCount += 1 

2569 #@+node:ekr.20110605121601.18267: *4* QtIconBar.addWidget 

2570 def addWidget(self, w: Wrapper) -> None: 

2571 self.w.addWidget(w) 

2572 #@+node:ekr.20110605121601.18268: *4* QtIconBar.clear 

2573 def clear(self) -> None: 

2574 """Destroy all the widgets in the icon bar""" 

2575 self.w.clear() 

2576 self.actions = [] 

2577 g.app.iconWidgetCount = 0 

2578 #@+node:ekr.20110605121601.18269: *4* QtIconBar.createChaptersIcon 

2579 def createChaptersIcon(self) -> Optional[Wrapper]: 

2580 

2581 c = self.c 

2582 f = c.frame 

2583 if f.use_chapters and f.use_chapter_tabs: 

2584 return LeoQtTreeTab(c, f.iconBar) 

2585 return None 

2586 #@+node:ekr.20110605121601.18270: *4* QtIconBar.deleteButton 

2587 def deleteButton(self, w: Wrapper) -> None: 

2588 """ w is button """ 

2589 self.w.removeAction(w) 

2590 self.c.bodyWantsFocus() 

2591 self.c.outerUpdate() 

2592 #@+node:ekr.20141031053508.14: *4* QtIconBar.goto_command 

2593 def goto_command(self, controller: Any, gnx: str) -> None: 

2594 """ 

2595 Select the node corresponding to the given gnx. 

2596 controller is a ScriptingController instance. 

2597 """ 

2598 # Fix bug 74: command_p may be in another outline. 

2599 c = self.c 

2600 c2, p = controller.open_gnx(c, gnx) 

2601 if p: 

2602 assert c2.positionExists(p) 

2603 if c == c2: 

2604 c2.selectPosition(p) 

2605 else: 

2606 g.app.selectLeoWindow(c2) 

2607 # Fix #367: Process events before selecting. 

2608 g.app.gui.qtApp.processEvents() 

2609 c2.selectPosition(p) 

2610 else: 

2611 g.trace('not found', gnx) 

2612 #@+node:ekr.20110605121601.18271: *4* QtIconBar.setCommandForButton (@rclick nodes) & helper 

2613 # qtFrame.QtIconBarClass.setCommandForButton 

2614 # Controller is a ScriptingController. 

2615 

2616 def setCommandForButton(self, 

2617 button: Any, command: Callable, command_p: Pos, controller: Any, gnx: str, script: str, 

2618 ) -> None: 

2619 """ 

2620 Set the "Goto Script" rlick item of an @button button. 

2621 Called from mod_scripting.py plugin. 

2622 

2623 button is a leoIconBarButton. 

2624 command is a callback, defined in mod_scripting.py. 

2625 command_p exists only if the @button node exists in the local .leo file. 

2626 gnx is the gnx of the @button node. 

2627 script is a static script for common @button nodes. 

2628 """ 

2629 if not command: 

2630 return 

2631 b = button.button 

2632 b.clicked.connect(command) 

2633 

2634 def goto_callback(checked: str, controller: Any=controller, gnx: str=gnx) -> None: 

2635 self.goto_command(controller, gnx) 

2636 

2637 b.goto_script = gts = QAction('Goto Script', b) 

2638 b.addAction(gts) 

2639 gts.triggered.connect(goto_callback) 

2640 rclicks = build_rclick_tree(command_p, top_level=True) 

2641 self.add_rclick_menu(b, rclicks, controller, script=script) 

2642 #@+node:ekr.20141031053508.15: *5* add_rclick_menu (QtIconBarClass) 

2643 def add_rclick_menu( 

2644 self, 

2645 action_container: Any, 

2646 rclicks: str, 

2647 controller: Cmdr, 

2648 top_level: bool=True, 

2649 button: str=None, 

2650 script: str=None, 

2651 ) -> None: 

2652 c = controller.c 

2653 top_offset = -2 # insert before the remove button and goto script items 

2654 if top_level: 

2655 button = action_container 

2656 for rc in rclicks: 

2657 # pylint: disable=cell-var-from-loop 

2658 headline = rc.position.h[8:].strip() 

2659 act = QAction(headline, action_container) 

2660 if '---' in headline and headline.strip().strip('-') == '': 

2661 act.setSeparator(True) 

2662 elif rc.position.b.strip(): 

2663 

2664 def cb(checked: str, p: str=rc.position, button: str=button) -> None: 

2665 controller.executeScriptFromButton( 

2666 b=button, 

2667 buttonText=p.h[8:].strip(), 

2668 p=p, 

2669 script=script, 

2670 ) 

2671 if c.exists: 

2672 c.outerUpdate() 

2673 

2674 act.triggered.connect(cb) 

2675 else: # recurse submenu 

2676 sub_menu = QtWidgets.QMenu(action_container) 

2677 act.setMenu(sub_menu) 

2678 self.add_rclick_menu(sub_menu, rc.children, controller, 

2679 top_level=False, button=button) 

2680 if top_level: 

2681 # insert act before Remove Button 

2682 action_container.insertAction( 

2683 action_container.actions()[top_offset], act) 

2684 else: 

2685 action_container.addAction(act) 

2686 if top_level and rclicks: 

2687 act = QAction('---', action_container) 

2688 act.setSeparator(True) 

2689 action_container.insertAction( 

2690 action_container.actions()[top_offset], act) 

2691 action_container.setText( 

2692 action_container.text() + 

2693 (c.config.getString('mod-scripting-subtext') or '') 

2694 ) 

2695 #@-others 

2696 #@+node:ekr.20110605121601.18274: *3* qtFrame.Configuration 

2697 #@+node:ekr.20110605121601.18275: *4* qtFrame.configureBar 

2698 def configureBar(self, bar: Wrapper, verticalFlag: bool) -> None: 

2699 c = self.c 

2700 # Get configuration settings. 

2701 w = c.config.getInt("split-bar-width") 

2702 if not w or w < 1: 

2703 w = 7 

2704 relief = c.config.get("split_bar_relief", "relief") 

2705 if not relief: 

2706 relief = "flat" 

2707 color = c.config.getColor("split-bar-color") 

2708 if not color: 

2709 color = "LightSteelBlue2" 

2710 try: 

2711 if verticalFlag: 

2712 # Panes arranged vertically; horizontal splitter bar 

2713 bar.configure( 

2714 relief=relief, height=w, bg=color, cursor="sb_v_double_arrow") 

2715 else: 

2716 # Panes arranged horizontally; vertical splitter bar 

2717 bar.configure( 

2718 relief=relief, width=w, bg=color, cursor="sb_h_double_arrow") 

2719 except Exception: 

2720 # Could be a user error. Use all defaults 

2721 g.es("exception in user configuration for splitbar") 

2722 g.es_exception() 

2723 if verticalFlag: 

2724 # Panes arranged vertically; horizontal splitter bar 

2725 bar.configure(height=7, cursor="sb_v_double_arrow") 

2726 else: 

2727 # Panes arranged horizontally; vertical splitter bar 

2728 bar.configure(width=7, cursor="sb_h_double_arrow") 

2729 #@+node:ekr.20110605121601.18276: *4* qtFrame.configureBarsFromConfig 

2730 def configureBarsFromConfig(self) -> None: 

2731 c = self.c 

2732 w = c.config.getInt("split-bar-width") 

2733 if not w or w < 1: 

2734 w = 7 

2735 relief = c.config.get("split_bar_relief", "relief") 

2736 if not relief or relief == "": 

2737 relief = "flat" 

2738 color = c.config.getColor("split-bar-color") 

2739 if not color or color == "": 

2740 color = "LightSteelBlue2" 

2741 if self.splitVerticalFlag: 

2742 bar1, bar2 = self.bar1, self.bar2 

2743 else: 

2744 bar1, bar2 = self.bar2, self.bar1 

2745 try: 

2746 bar1.configure(relief=relief, height=w, bg=color) 

2747 bar2.configure(relief=relief, width=w, bg=color) 

2748 except Exception: 

2749 # Could be a user error. 

2750 g.es("exception in user configuration for splitbar") 

2751 g.es_exception() 

2752 #@+node:ekr.20110605121601.18277: *4* qtFrame.reconfigureFromConfig 

2753 def reconfigureFromConfig(self) -> None: 

2754 """Init the configuration of the Qt frame from settings.""" 

2755 c, frame = self.c, self 

2756 frame.configureBarsFromConfig() 

2757 frame.setTabWidth(c.tab_width) 

2758 c.redraw() 

2759 #@+node:ekr.20110605121601.18278: *4* qtFrame.setInitialWindowGeometry 

2760 def setInitialWindowGeometry(self) -> None: 

2761 """Set the position and size of the frame to config params.""" 

2762 c = self.c 

2763 h = c.config.getInt("initial-window-height") or 500 

2764 w = c.config.getInt("initial-window-width") or 600 

2765 x = c.config.getInt("initial-window-left") or 50 # #1190: was 10 

2766 y = c.config.getInt("initial-window-top") or 50 # #1190: was 10 

2767 if h and w and x and y: 

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

2769 g.trace(w, h, x, y) 

2770 self.setTopGeometry(w, h, x, y) 

2771 #@+node:ekr.20110605121601.18279: *4* qtFrame.setTabWidth 

2772 def setTabWidth(self, w: Wrapper) -> None: 

2773 # A do-nothing because tab width is set automatically. 

2774 # It *is* called from Leo's core. 

2775 pass 

2776 #@+node:ekr.20110605121601.18280: *4* qtFrame.forceWrap & setWrap 

2777 def forceWrap(self, p: Pos=None) -> None: 

2778 self.c.frame.body.forceWrap(p) 

2779 

2780 def setWrap(self, p: Pos=None) -> None: 

2781 self.c.frame.body.setWrap(p) 

2782 #@+node:ekr.20110605121601.18281: *4* qtFrame.reconfigurePanes 

2783 def reconfigurePanes(self) -> None: 

2784 c, f = self.c, self 

2785 if f.splitVerticalFlag: 

2786 r = c.config.getRatio("initial-vertical-ratio") 

2787 if r is None or r < 0.0 or r > 1.0: 

2788 r = 0.5 

2789 r2 = c.config.getRatio("initial-vertical-secondary-ratio") 

2790 if r2 is None or r2 < 0.0 or r2 > 1.0: 

2791 r2 = 0.8 

2792 else: 

2793 r = c.config.getRatio("initial-horizontal-ratio") 

2794 if r is None or r < 0.0 or r > 1.0: 

2795 r = 0.3 

2796 r2 = c.config.getRatio("initial-horizontal-secondary-ratio") 

2797 if r2 is None or r2 < 0.0 or r2 > 1.0: 

2798 r2 = 0.8 

2799 f.resizePanesToRatio(r, r2) 

2800 #@+node:ekr.20110605121601.18282: *4* qtFrame.resizePanesToRatio 

2801 def resizePanesToRatio(self, ratio: float, ratio2: float) -> None: 

2802 """Resize splitter1 and splitter2 using the given ratios.""" 

2803 # pylint: disable=arguments-differ 

2804 self.divideLeoSplitter1(ratio) 

2805 self.divideLeoSplitter2(ratio2) 

2806 #@+node:ekr.20110605121601.18283: *4* qtFrame.divideLeoSplitter1/2 

2807 def divideLeoSplitter1(self, frac: float) -> None: 

2808 """Divide the main splitter.""" 

2809 layout = self.c and self.c.free_layout 

2810 if not layout: 

2811 return 

2812 w = layout.get_main_splitter() 

2813 if w: 

2814 self.divideAnySplitter(frac, w) 

2815 

2816 def divideLeoSplitter2(self, frac: float) -> None: 

2817 """Divide the secondary splitter.""" 

2818 layout = self.c and self.c.free_layout 

2819 if not layout: 

2820 return 

2821 w = layout.get_secondary_splitter() 

2822 if w: 

2823 self.divideAnySplitter(frac, w) 

2824 #@+node:ekr.20110605121601.18284: *4* qtFrame.divideAnySplitter 

2825 # This is the general-purpose placer for splitters. 

2826 # It is the only general-purpose splitter code in Leo. 

2827 

2828 def divideAnySplitter(self, frac: float, splitter: Wrapper) -> None: 

2829 """Set the splitter sizes.""" 

2830 sizes = splitter.sizes() 

2831 if len(sizes) != 2: 

2832 g.trace(f"{len(sizes)} widget(s) in {id(splitter)}") 

2833 return 

2834 if frac > 1 or frac < 0: 

2835 g.trace(f"split ratio [{frac}] out of range 0 <= frac <= 1") 

2836 return 

2837 s1, s2 = sizes 

2838 s = s1 + s2 

2839 s1 = int(s * frac + 0.5) 

2840 s2 = s - s1 

2841 splitter.setSizes([s1, s2]) 

2842 #@+node:ekr.20110605121601.18285: *3* qtFrame.Event handlers 

2843 #@+node:ekr.20110605121601.18286: *4* qtFrame.OnCloseLeoEvent 

2844 # Called from quit logic and when user closes the window. 

2845 # Returns True if the close happened. 

2846 

2847 def OnCloseLeoEvent(self) -> None: 

2848 f = self 

2849 c = f.c 

2850 if c.inCommand: 

2851 c.requestCloseWindow = True 

2852 else: 

2853 g.app.closeLeoWindow(self) 

2854 #@+node:ekr.20110605121601.18287: *4* qtFrame.OnControlKeyUp/Down 

2855 def OnControlKeyDown(self, event: Event=None) -> None: 

2856 self.controlKeyIsDown = True 

2857 

2858 def OnControlKeyUp(self, event: Event=None) -> None: 

2859 self.controlKeyIsDown = False 

2860 #@+node:ekr.20110605121601.18290: *4* qtFrame.OnActivateTree 

2861 def OnActivateTree(self, event: Event=None) -> None: 

2862 pass 

2863 #@+node:ekr.20110605121601.18293: *3* qtFrame.Gui-dependent commands 

2864 #@+node:ekr.20110605121601.18301: *4* qtFrame.Window Menu... 

2865 #@+node:ekr.20110605121601.18302: *5* qtFrame.toggleActivePane 

2866 @frame_cmd('toggle-active-pane') 

2867 def toggleActivePane(self, event: Event=None) -> None: 

2868 """Toggle the focus between the outline and body panes.""" 

2869 frame = self 

2870 c = frame.c 

2871 w = c.get_focus() 

2872 w_name = g.app.gui.widget_name(w) 

2873 if w_name in ('canvas', 'tree', 'treeWidget'): 

2874 c.endEditing() 

2875 c.bodyWantsFocus() 

2876 else: 

2877 c.treeWantsFocus() 

2878 #@+node:ekr.20110605121601.18303: *5* qtFrame.cascade 

2879 @frame_cmd('cascade-windows') 

2880 def cascade(self, event: Event=None) -> None: 

2881 """Cascade all Leo windows.""" 

2882 x, y, delta = 50, 50, 50 

2883 for frame in g.app.windowList: 

2884 w = frame and frame.top 

2885 if w: 

2886 r = w.geometry() # a Qt.Rect 

2887 # 2011/10/26: Fix bug 823601: cascade-windows fails. 

2888 w.setGeometry(QtCore.QRect(x, y, r.width(), r.height())) 

2889 # Compute the new offsets. 

2890 x += 30 

2891 y += 30 

2892 if x > 200: 

2893 x = 10 + delta 

2894 y = 40 + delta 

2895 delta += 10 

2896 #@+node:ekr.20110605121601.18304: *5* qtFrame.equalSizedPanes 

2897 @frame_cmd('equal-sized-panes') 

2898 def equalSizedPanes(self, event: Event=None) -> None: 

2899 """Make the outline and body panes have the same size.""" 

2900 self.resizePanesToRatio(0.5, self.secondary_ratio) 

2901 #@+node:ekr.20110605121601.18305: *5* qtFrame.hideLogWindow 

2902 def hideLogWindow(self, event: Event=None) -> None: 

2903 """Hide the log pane.""" 

2904 self.divideLeoSplitter2(0.99) 

2905 #@+node:ekr.20110605121601.18306: *5* qtFrame.minimizeAll 

2906 @frame_cmd('minimize-all') 

2907 def minimizeAll(self, event: Event=None) -> None: 

2908 """Minimize all Leo's windows.""" 

2909 for frame in g.app.windowList: 

2910 self.minimize(frame) 

2911 

2912 def minimize(self, frame: Widget) -> None: 

2913 # This unit test will fail when run externally. 

2914 if frame and frame.top: 

2915 w = frame.top.leo_master or frame.top 

2916 if g.unitTesting: 

2917 assert hasattr(w, 'setWindowState'), w 

2918 else: 

2919 w.setWindowState(WindowState.WindowMinimized) 

2920 #@+node:ekr.20110605121601.18307: *5* qtFrame.toggleSplitDirection 

2921 @frame_cmd('toggle-split-direction') 

2922 def toggleSplitDirection(self, event: Event=None) -> None: 

2923 """Toggle the split direction in the present Leo window.""" 

2924 if hasattr(self.c, 'free_layout'): 

2925 self.c.free_layout.get_top_splitter().rotate() 

2926 #@+node:ekr.20110605121601.18308: *5* qtFrame.resizeToScreen 

2927 @frame_cmd('resize-to-screen') 

2928 def resizeToScreen(self, event: Event=None) -> None: 

2929 """Resize the Leo window so it fill the entire screen.""" 

2930 frame = self 

2931 # This unit test will fail when run externally. 

2932 if frame and frame.top: 

2933 # frame.top.leo_master is a LeoTabbedTopLevel. 

2934 # frame.top is a DynamicWindow. 

2935 w = frame.top.leo_master or frame.top 

2936 if g.unitTesting: 

2937 assert hasattr(w, 'setWindowState'), w 

2938 else: 

2939 w.setWindowState(WindowState.WindowMaximized) 

2940 #@+node:ekr.20110605121601.18309: *4* qtFrame.Help Menu... 

2941 #@+node:ekr.20110605121601.18310: *5* qtFrame.leoHelp 

2942 @frame_cmd('open-offline-tutorial') 

2943 def leoHelp(self, event: Event=None) -> None: 

2944 """Open Leo's offline tutorial.""" 

2945 frame = self 

2946 c = frame.c 

2947 theFile = g.os_path_join(g.app.loadDir, "..", "doc", "sbooks.chm") 

2948 if g.os_path_exists(theFile) and sys.platform.startswith('win'): 

2949 # pylint: disable=no-member 

2950 os.startfile(theFile) 

2951 else: 

2952 answer = g.app.gui.runAskYesNoDialog(c, 

2953 "Download Tutorial?", 

2954 "Download tutorial (sbooks.chm) from SourceForge?") 

2955 if answer == "yes": 

2956 try: 

2957 url = "http://prdownloads.sourceforge.net/leo/sbooks.chm?download" 

2958 import webbrowser 

2959 os.chdir(g.app.loadDir) 

2960 webbrowser.open_new(url) 

2961 except Exception: 

2962 if 0: 

2963 g.es("exception downloading", "sbooks.chm") 

2964 g.es_exception() 

2965 #@+node:ekr.20160424080647.1: *3* qtFrame.Properties 

2966 # The ratio and secondary_ratio properties are read-only. 

2967 #@+node:ekr.20160424080815.2: *4* qtFrame.ratio property 

2968 def __get_ratio(self) -> float: 

2969 """Return splitter ratio of the main splitter.""" 

2970 c = self.c 

2971 free_layout = c.free_layout 

2972 if free_layout: 

2973 w = free_layout.get_main_splitter() 

2974 if w: 

2975 aList = w.sizes() 

2976 if len(aList) == 2: 

2977 n1, n2 = aList 

2978 # 2017/06/07: guard against division by zero. 

2979 ratio = 0.5 if n1 + n2 == 0 else float(n1) / float(n1 + n2) 

2980 return ratio 

2981 return 0.5 

2982 

2983 ratio = property( 

2984 __get_ratio, # No setter. 

2985 doc="qtFrame.ratio property") 

2986 #@+node:ekr.20160424080815.3: *4* qtFrame.secondary_ratio property 

2987 def __get_secondary_ratio(self) -> float: 

2988 """Return the splitter ratio of the secondary splitter.""" 

2989 c = self.c 

2990 free_layout = c.free_layout 

2991 if free_layout: 

2992 w = free_layout.get_secondary_splitter() 

2993 if w: 

2994 aList = w.sizes() 

2995 if len(aList) == 2: 

2996 n1, n2 = aList 

2997 ratio = float(n1) / float(n1 + n2) 

2998 return ratio 

2999 return 0.5 

3000 

3001 secondary_ratio = property( 

3002 __get_secondary_ratio, # no setter. 

3003 doc="qtFrame.secondary_ratio property") 

3004 #@+node:ekr.20110605121601.18311: *3* qtFrame.Qt bindings... 

3005 #@+node:ekr.20190611053431.1: *4* qtFrame.bringToFront 

3006 def bringToFront(self) -> None: 

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

3008 g.trace() 

3009 self.lift() 

3010 #@+node:ekr.20190611053431.2: *4* qtFrame.deiconify 

3011 def deiconify(self) -> None: 

3012 """Undo --minimized""" 

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

3014 g.trace( 

3015 'top:', bool(self.top), 

3016 'isMinimized:', self.top and self.top.isMinimized()) 

3017 if self.top and self.top.isMinimized(): # Bug fix: 400739. 

3018 self.lift() 

3019 #@+node:ekr.20190611053431.4: *4* qtFrame.get_window_info 

3020 def get_window_info(self) -> Tuple[int, int, int, int]: 

3021 """Return the geometry of the top window.""" 

3022 if getattr(self.top, 'leo_master', None): 

3023 f = self.top.leo_master 

3024 else: 

3025 f = self.top 

3026 rect = f.geometry() 

3027 topLeft = rect.topLeft() 

3028 x, y = topLeft.x(), topLeft.y() 

3029 w, h = rect.width(), rect.height() 

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

3031 g.trace('\n', w, h, x, y) 

3032 return w, h, x, y 

3033 #@+node:ekr.20190611053431.3: *4* qtFrame.getFocus 

3034 def getFocus(self) -> None: 

3035 return g.app.gui.get_focus(self.c) # Bug fix: 2009/6/30. 

3036 #@+node:ekr.20190611053431.7: *4* qtFrame.getTitle 

3037 def getTitle(self) -> None: 

3038 # Fix https://bugs.launchpad.net/leo-editor/+bug/1194209 

3039 # For qt, leo_master (a LeoTabbedTopLevel) contains the QMainWindow. 

3040 w = self.top.leo_master 

3041 return w.windowTitle() 

3042 #@+node:ekr.20190611053431.5: *4* qtFrame.iconify 

3043 def iconify(self) -> None: 

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

3045 g.trace(bool(self.top)) 

3046 if self.top: 

3047 self.top.showMinimized() 

3048 #@+node:ekr.20190611053431.6: *4* qtFrame.lift 

3049 def lift(self) -> None: 

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

3051 g.trace(bool(self.top), self.top and self.top.isMinimized()) 

3052 if not self.top: 

3053 return 

3054 if self.top.isMinimized(): # Bug 379141 

3055 self.top.showNormal() 

3056 self.top.activateWindow() 

3057 self.top.raise_() 

3058 #@+node:ekr.20190611053431.8: *4* qtFrame.setTitle 

3059 def setTitle(self, s: str) -> None: 

3060 # pylint: disable=arguments-differ 

3061 if self.top: 

3062 # Fix https://bugs.launchpad.net/leo-editor/+bug/1194209 

3063 # When using tabs, leo_master (a LeoTabbedTopLevel) contains the QMainWindow. 

3064 w = self.top.leo_master 

3065 w.setWindowTitle(s) 

3066 #@+node:ekr.20190611053431.9: *4* qtFrame.setTopGeometry 

3067 def setTopGeometry(self, w: int, h: int, x: int, y: int) -> None: 

3068 # self.top is a DynamicWindow. 

3069 if self.top: 

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

3071 g.trace(w, h, x, y, self.c.shortFileName(), g.callers()) 

3072 self.top.setGeometry(QtCore.QRect(x, y, w, h)) 

3073 #@+node:ekr.20190611053431.10: *4* qtFrame.update 

3074 def update(self, *args: Any, **keys: Any) -> None: 

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

3076 g.trace(bool(self.top)) 

3077 self.top.update() 

3078 #@-others 

3079#@+node:ekr.20110605121601.18312: ** class LeoQtLog (LeoLog) 

3080class LeoQtLog(leoFrame.LeoLog): 

3081 """A class that represents the log pane of a Qt window.""" 

3082 #@+others 

3083 #@+node:ekr.20110605121601.18313: *3* LeoQtLog.Birth 

3084 #@+node:ekr.20110605121601.18314: *4* LeoQtLog.__init__ & reloadSettings 

3085 def __init__(self, frame: Wrapper, parentFrame: Widget) -> None: 

3086 """Ctor for LeoQtLog class.""" 

3087 super().__init__(frame, parentFrame) # Calls createControl. 

3088 # Set in finishCreate. 

3089 # Important: depending on the log *tab*, 

3090 # logCtrl may be either a wrapper or a widget. 

3091 assert self.logCtrl is None, self.logCtrl # type:ignore 

3092 self.c = c = frame.c # Also set in the base constructor, but we need it here. 

3093 self.contentsDict: Dict[str, Widget] = {} # Keys are tab names. Values are widgets. 

3094 self.eventFilters: List = [] # Apparently needed to make filters work! 

3095 self.logCtrl: Any = None 

3096 self.logDict: Dict[str, Widget] = {} # Keys are tab names; values are the widgets. 

3097 self.logWidget: Widget = None # Set in finishCreate. 

3098 self.menu: Widget = None # A menu that pops up on right clicks in the hull or in tabs. 

3099 self.tabWidget: Widget = c.frame.top.tabWidget # The Qt.QTabWidget that holds all the tabs. 

3100 tw = self.tabWidget 

3101 

3102 # Bug 917814: Switching Log Pane tabs is done incompletely. 

3103 tw.currentChanged.connect(self.onCurrentChanged) 

3104 if 0: # Not needed to make onActivateEvent work. 

3105 # Works only for .tabWidget, *not* the individual tabs! 

3106 theFilter = qt_events.LeoQtEventFilter(c, w=tw, tag='tabWidget') 

3107 tw.installEventFilter(theFilter) 

3108 # Partial fix for bug 1251755: Log-pane refinements 

3109 tw.setMovable(True) 

3110 self.reloadSettings() 

3111 

3112 def reloadSettings(self) -> None: 

3113 c = self.c 

3114 self.wrap = bool(c.config.getBool('log-pane-wraps')) 

3115 #@+node:ekr.20110605121601.18315: *4* LeoQtLog.finishCreate 

3116 def finishCreate(self) -> None: 

3117 """Finish creating the LeoQtLog class.""" 

3118 c, log, w = self.c, self, self.tabWidget 

3119 # 

3120 # Create the log tab as the leftmost tab. 

3121 log.createTab('Log') 

3122 self.logWidget = self.contentsDict.get('Log') 

3123 logWidget = self.logWidget 

3124 logWidget.setWordWrapMode(WrapMode.WordWrap if self.wrap else WrapMode.NoWrap) 

3125 w.insertTab(0, logWidget, 'Log') # Required. 

3126 # 

3127 # set up links in log handling 

3128 logWidget.setTextInteractionFlags( 

3129 TextInteractionFlag.LinksAccessibleByMouse 

3130 | TextInteractionFlag.TextEditable 

3131 | TextInteractionFlag.TextSelectableByMouse 

3132 ) 

3133 logWidget.setOpenLinks(False) 

3134 logWidget.setOpenExternalLinks(False) 

3135 logWidget.anchorClicked.connect(self.linkClicked) 

3136 # 

3137 # Show the spell tab. 

3138 c.spellCommands.openSpellTab() 

3139 # 

3140 #794: Clicking Find Tab should do exactly what pushing Ctrl-F does 

3141 

3142 def tab_callback(index: str) -> None: 

3143 name = w.tabText(index) 

3144 if name == 'Find': 

3145 c.findCommands.startSearch(event=None) 

3146 

3147 w.currentChanged.connect(tab_callback) 

3148 # #1286. 

3149 w.customContextMenuRequested.connect(self.onContextMenu) 

3150 #@+node:ekr.20110605121601.18316: *4* LeoQtLog.getName 

3151 def getName(self) -> str: 

3152 return 'log' # Required for proper pane bindings. 

3153 #@+node:ekr.20150717102728.1: *3* LeoQtLog: clear-log & dump-log 

3154 @log_cmd('clear-log') 

3155 @log_cmd('log-clear') 

3156 def clearLog(self, event: Event=None) -> None: 

3157 """Clear the log pane.""" 

3158 # self.logCtrl may be either a wrapper or a widget. 

3159 w = self.logCtrl.widget # type:ignore 

3160 if w: 

3161 w.clear() 

3162 

3163 @log_cmd('dump-log') 

3164 @log_cmd('log-dump') 

3165 def dumpLog(self, event: Event=None) -> None: 

3166 """Clear the log pane.""" 

3167 # self.logCtrl may be either a wrapper or a widget. 

3168 w = self.logCtrl.widget # type:ignore 

3169 if not w: 

3170 return 

3171 

3172 fn = self.c.shortFileName() 

3173 printable = string.ascii_letters + string.digits + string.punctuation + ' ' 

3174 

3175 def dump(s: str) -> str: 

3176 return ''.join(c if c in printable else r'\x{0:02x}'.format(ord(c)) for c in s) 

3177 

3178 g.printObj([dump(z) for z in w.toPlainText().split('\n')], tag=f"{fn}: w.toPlainText") 

3179 g.printObj([f"{dump(z)}<br />" for z in w.toHtml().split('<br />')], tag=f"{fn}: w.toHtml") 

3180 

3181 

3182 #@+node:ekr.20110605121601.18333: *3* LeoQtLog.color tab stuff 

3183 def createColorPicker(self, tabName: str) -> None: 

3184 g.warning('color picker not ready for qt') 

3185 #@+node:ekr.20110605121601.18334: *3* LeoQtLog.font tab stuff 

3186 #@+node:ekr.20110605121601.18335: *4* LeoQtLog.createFontPicker 

3187 def createFontPicker(self, tabName: str) -> None: 

3188 # log = self 

3189 font, ok = QtWidgets.QFontDialog.getFont() 

3190 if not (font and ok): 

3191 return 

3192 style = font.style() 

3193 table1 = ( 

3194 (Style.StyleNormal, 'normal'), # #2330. 

3195 (Style.StyleItalic, 'italic'), 

3196 (Style.StyleOblique, 'oblique')) 

3197 for val, name in table1: 

3198 if style == val: 

3199 style = name 

3200 break 

3201 else: 

3202 style = '' 

3203 weight = font.weight() 

3204 table2 = ( 

3205 (Weight.Light, 'light'), # #2330. 

3206 (Weight.Normal, 'normal'), 

3207 (Weight.DemiBold, 'demibold'), 

3208 (Weight.Bold, 'bold'), 

3209 (Weight.Black, 'black')) 

3210 for val2, name2 in table2: 

3211 if weight == val2: 

3212 weight = name2 

3213 break 

3214 else: 

3215 weight = '' 

3216 table3 = ( 

3217 ('family', str(font.family())), 

3218 ('size ', font.pointSize()), 

3219 ('style ', style), 

3220 ('weight', weight), 

3221 ) 

3222 for key3, val3 in table3: 

3223 if val3: 

3224 g.es(key3, val3, tabName='Fonts') 

3225 #@+node:ekr.20110605121601.18339: *4* LeoQtLog.hideFontTab 

3226 def hideFontTab(self, event: Event=None) -> None: 

3227 c = self.c 

3228 c.frame.log.selectTab('Log') 

3229 c.bodyWantsFocus() 

3230 #@+node:ekr.20111120124732.10184: *3* LeoQtLog.isLogWidget 

3231 def isLogWidget(self, w: Wrapper) -> bool: 

3232 val = w == self or w in list(self.contentsDict.values()) 

3233 return val 

3234 #@+node:tbnorth.20171220123648.1: *3* LeoQtLog.linkClicked 

3235 def linkClicked(self, link: str) -> None: 

3236 """linkClicked - link clicked in log 

3237 

3238 :param QUrl link: link that was clicked 

3239 """ 

3240 # see addition of '/' in LeoQtLog.put() 

3241 url = s = g.toUnicode(link.toString()) 

3242 if platform.system() == 'Windows': 

3243 for scheme in 'file', 'unl': 

3244 if s.startswith(scheme + ':///') and s[len(scheme) + 5] == ':': 

3245 url = s.replace(':///', '://', 1) 

3246 break 

3247 g.handleUrl(url, c=self.c) 

3248 #@+node:ekr.20120304214900.9940: *3* LeoQtLog.onCurrentChanged 

3249 def onCurrentChanged(self, idx: int) -> None: 

3250 

3251 tabw = self.tabWidget 

3252 w = tabw.widget(idx) 

3253 # 

3254 # #917814: Switching Log Pane tabs is done incompletely 

3255 wrapper: Wrapper = getattr(w, 'leo_log_wrapper', None) 

3256 # 

3257 # #1161: Don't change logs unless the wrapper is correct. 

3258 if wrapper and isinstance(wrapper, qt_text.QTextEditWrapper): 

3259 self.logCtrl = wrapper 

3260 #@+node:ekr.20200304132424.1: *3* LeoQtLog.onContextMenu 

3261 def onContextMenu(self, point: Any) -> None: 

3262 """LeoQtLog: Callback for customContextMenuRequested events.""" 

3263 # #1286. 

3264 c, w = self.c, self 

3265 g.app.gui.onContextMenu(c, w, point) 

3266 #@+node:ekr.20110605121601.18321: *3* LeoQtLog.put and helpers 

3267 #@+node:ekr.20110605121601.18322: *4* LeoQtLog.put & helper 

3268 def put(self, s: str, color: str=None, tabName: str='Log', from_redirect: bool=False, nodeLink: str=None) -> None: 

3269 """ 

3270 Put s to the Qt Log widget, converting to html. 

3271 All output to the log stream eventually comes here. 

3272 

3273 The from_redirect keyword argument is no longer used. 

3274 """ 

3275 c = self.c 

3276 if g.app.quitting or not c or not c.exists: 

3277 return 

3278 # 

3279 # *Note*: For reasons that I don't fully understand, 

3280 # all lines sent to the log must now end in a newline. 

3281 # 

3282 s = s.rstrip() + '\n' 

3283 color = self.resolve_color(color) 

3284 self.selectTab(tabName or 'Log') 

3285 # Must be done after the call to selectTab. 

3286 wrapper = self.logCtrl 

3287 if not isinstance(wrapper, qt_text.QTextEditWrapper): 

3288 g.trace('BAD wrapper', wrapper.__class__.__name__) 

3289 return 

3290 w = wrapper.widget 

3291 if not isinstance(w, QtWidgets.QTextEdit): 

3292 g.trace('BAD widget', w.__class__.__name__) 

3293 return 

3294 sb = w.horizontalScrollBar() 

3295 s = self.to_html(color, s) 

3296 if nodeLink: 

3297 url = nodeLink 

3298 for scheme in 'file', 'unl': 

3299 # QUrl requires paths start with '/' 

3300 if ( 

3301 url.startswith(scheme + '://') and not 

3302 url.startswith(scheme + ':///') 

3303 ): 

3304 url = url.replace('://', ':///', 1) 

3305 s = f'<a href="{url}" title="{nodeLink}">{s}</a>' 

3306 w.insertHtml(s) 

3307 w.moveCursor(MoveOperation.End) 

3308 sb.setSliderPosition(0) # Force the slider to the initial position. 

3309 w.repaint() # Slow, but essential. 

3310 #@+node:ekr.20220411085334.1: *5* LeoQtLog.to_html 

3311 def to_html(self, color: str, s: str) -> str: 

3312 """Convert s to html.""" 

3313 s = s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;') 

3314 # #884: Always convert leading blanks and tabs to &nbsp. 

3315 n = len(s) - len(s.lstrip()) 

3316 if n > 0 and s.strip(): 

3317 s = '&nbsp;' * (n) + s[n:] 

3318 if not self.wrap: 

3319 # Convert all other blanks to &nbsp; 

3320 s = s.replace(' ', '&nbsp;') 

3321 s = s.replace('\n', '<br>') # The caller is responsible for newlines! 

3322 s = f'<font color="{color}">{s}</font>' 

3323 return s 

3324 #@+node:ekr.20110605121601.18323: *4* LeoQtLog.putnl 

3325 def putnl(self, tabName: str='Log') -> None: 

3326 """Put a newline to the Qt log.""" 

3327 # 

3328 # This is not called normally. 

3329 if g.app.quitting: 

3330 return 

3331 if tabName: 

3332 self.selectTab(tabName) 

3333 wrapper = self.logCtrl 

3334 if not isinstance(wrapper, qt_text.QTextEditWrapper): 

3335 g.trace('BAD wrapper', wrapper.__class__.__name__) 

3336 return 

3337 w = wrapper.widget 

3338 if not isinstance(w, QtWidgets.QTextEdit): 

3339 g.trace('BAD widget', w.__class__.__name__) 

3340 return 

3341 sb = w.horizontalScrollBar() 

3342 pos = sb.sliderPosition() 

3343 # Not needed! 

3344 # contents = w.toHtml() 

3345 # w.setHtml(contents + '\n') 

3346 w.moveCursor(MoveOperation.End) 

3347 sb.setSliderPosition(pos) 

3348 w.repaint() # Slow, but essential. 

3349 #@+node:ekr.20220411085427.1: *4* LeoQtLog.resolve_color 

3350 def resolve_color(self, color: str) -> str: 

3351 """Resolve the given color name to an actual color name.""" 

3352 c = self.c 

3353 # Note: g.actualColor does all color translation. 

3354 if color: 

3355 color = leoColor.getColor(color) 

3356 if not color: 

3357 # #788: First, fall back to 'log_black_color', not 'black. 

3358 color = c.config.getColor('log-black-color') 

3359 if not color: 

3360 # Should never be necessary. 

3361 color = 'black' 

3362 return color 

3363 #@+node:ekr.20150205181818.5: *4* LeoQtLog.scrollToEnd 

3364 def scrollToEnd(self, tabName: str='Log') -> None: 

3365 """Scroll the log to the end.""" 

3366 if g.app.quitting: 

3367 return 

3368 if tabName: 

3369 self.selectTab(tabName) 

3370 w = self.logCtrl.widget 

3371 if not w: 

3372 return 

3373 sb = w.horizontalScrollBar() 

3374 pos = sb.sliderPosition() 

3375 w.moveCursor(MoveOperation.End) 

3376 sb.setSliderPosition(pos) 

3377 w.repaint() # Slow, but essential. 

3378 #@+node:ekr.20110605121601.18324: *3* LeoQtLog.Tab 

3379 #@+node:ekr.20110605121601.18325: *4* LeoQtLog.clearTab 

3380 def clearTab(self, tabName: str, wrap: str='none') -> None: 

3381 w = self.logDict.get(tabName) 

3382 if w: 

3383 w.clear() # w is a QTextBrowser. 

3384 #@+node:ekr.20110605121601.18326: *4* LeoQtLog.createTab 

3385 def createTab(self, 

3386 tabName: str, createText: bool=True, widget: Widget=None, wrap: str='none', 

3387 ) -> Any: # Widget or LeoQTextBrowser. 

3388 """ 

3389 Create a new tab in tab widget 

3390 if widget is None, Create a QTextBrowser, 

3391 suitable for log functionality. 

3392 """ 

3393 c = self.c 

3394 contents: Any 

3395 if widget is None: 

3396 # widget is subclass of QTextBrowser. 

3397 widget = qt_text.LeoQTextBrowser(parent=None, c=c, wrapper=self) 

3398 # contents is a wrapper. 

3399 contents = qt_text.QTextEditWrapper(widget=widget, name='log', c=c) 

3400 # Inject an ivar into the QTextBrowser that points to the wrapper. 

3401 widget.leo_log_wrapper = contents 

3402 widget.setWordWrapMode(WrapMode.WordWrap if self.wrap else WrapMode.NoWrap) 

3403 widget.setReadOnly(False) # Allow edits. 

3404 self.logDict[tabName] = widget 

3405 if tabName == 'Log': 

3406 self.logCtrl = contents 

3407 widget.setObjectName('log-widget') 

3408 # Set binding on all log pane widgets. 

3409 g.app.gui.setFilter(c, widget, self, tag='log') 

3410 self.contentsDict[tabName] = widget 

3411 self.tabWidget.addTab(widget, tabName) 

3412 else: 

3413 # #1161: Don't set the wrapper unless it has the correct type. 

3414 contents = widget # Unlike text widgets, contents is the actual widget. 

3415 if isinstance(contents, qt_text.QTextEditWrapper): 

3416 widget.leo_log_wrapper = widget # The leo_log_wrapper is the widget itself. 

3417 else: 

3418 widget.leo_log_wrapper = None # Tell the truth. 

3419 g.app.gui.setFilter(c, widget, contents, 'tabWidget') 

3420 self.contentsDict[tabName] = contents 

3421 self.tabWidget.addTab(contents, tabName) 

3422 return contents 

3423 #@+node:ekr.20110605121601.18328: *4* LeoQtLog.deleteTab 

3424 def deleteTab(self, tabName: str) -> None: 

3425 """ 

3426 Delete the tab if it exists. Otherwise do *nothing*. 

3427 """ 

3428 c = self.c 

3429 w = self.tabWidget 

3430 i = self.findTabIndex(tabName) 

3431 if i is None: 

3432 return 

3433 w.removeTab(i) 

3434 self.selectTab('Log') 

3435 c.invalidateFocus() 

3436 c.bodyWantsFocus() 

3437 #@+node:ekr.20190603062456.1: *4* LeoQtLog.findTabIndex 

3438 def findTabIndex(self, tabName: str) -> Optional[int]: 

3439 """Return the tab index for tabName, or None.""" 

3440 w = self.tabWidget 

3441 for i in range(w.count()): 

3442 if tabName == w.tabText(i): 

3443 return i 

3444 return None 

3445 #@+node:ekr.20110605121601.18329: *4* LeoQtLog.hideTab 

3446 def hideTab(self, tabName: str) -> None: 

3447 self.selectTab('Log') 

3448 #@+node:ekr.20111122080923.10185: *4* LeoQtLog.orderedTabNames 

3449 def orderedTabNames(self, LeoLog: str=None) -> List[str]: # Unused: LeoLog 

3450 """Return a list of tab names in the order in which they appear in the QTabbedWidget.""" 

3451 w = self.tabWidget 

3452 return [w.tabText(i) for i in range(w.count())] 

3453 #@+node:ekr.20110605121601.18330: *4* LeoQtLog.numberOfVisibleTabs 

3454 def numberOfVisibleTabs(self) -> int: 

3455 # **Note**: the base-class version of this uses frameDict. 

3456 return len([val for val in self.contentsDict.values() if val is not None]) 

3457 #@+node:ekr.20110605121601.18331: *4* LeoQtLog.selectTab & helpers 

3458 def selectTab(self, tabName: str, wrap: str='none') -> None: 

3459 """Create the tab if necessary and make it active.""" 

3460 i = self.findTabIndex(tabName) 

3461 if i is None: 

3462 self.createTab(tabName, wrap=wrap) 

3463 self.finishCreateTab(tabName) 

3464 self.finishSelectTab(tabName) 

3465 #@+node:ekr.20190603064815.1: *5* LeoQtLog.finishCreateTab 

3466 def finishCreateTab(self, tabName: str) -> None: 

3467 """Finish creating the given tab. Do not set focus!""" 

3468 c = self.c 

3469 i = self.findTabIndex(tabName) 

3470 if i is None: 

3471 g.trace('Can not happen', tabName) 

3472 self.tabName = None 

3473 return 

3474 # #1161. 

3475 if tabName == 'Log': 

3476 wrapper: Wrapper = None 

3477 widget = self.contentsDict.get('Log') # a qt_text.QTextEditWrapper 

3478 if widget: 

3479 wrapper = getattr(widget, 'leo_log_wrapper', None) 

3480 if wrapper and isinstance(wrapper, qt_text.QTextEditWrapper): 

3481 self.logCtrl = wrapper 

3482 if not wrapper: 

3483 g.trace('NO LOG WRAPPER') 

3484 if tabName == 'Find': 

3485 # Do *not* set focus here! 

3486 # #1254861: Ctrl-f doesn't ensure find input field visible. 

3487 if c.config.getBool('auto-scroll-find-tab', default=True): 

3488 # This is the cause of unwanted scrolling. 

3489 findbox = c.findCommands.ftm.find_findbox 

3490 if hasattr(widget, 'ensureWidgetVisible'): 

3491 widget.ensureWidgetVisible(findbox) 

3492 else: 

3493 findbox.setFocus() 

3494 if tabName == 'Spell': 

3495 # Set a flag for the spell system. 

3496 widget = self.tabWidget.widget(i) 

3497 self.frameDict['Spell'] = widget 

3498 #@+node:ekr.20190603064816.1: *5* LeoQtLog.finishSelectTab 

3499 def finishSelectTab(self, tabName: str) -> None: 

3500 """Select the proper tab.""" 

3501 w = self.tabWidget 

3502 # Special case for Spell tab. 

3503 if tabName == 'Spell': 

3504 return 

3505 i = self.findTabIndex(tabName) 

3506 if i is None: 

3507 g.trace('can not happen', tabName) 

3508 self.tabName = None 

3509 return 

3510 w.setCurrentIndex(i) 

3511 self.tabName = tabName 

3512 #@-others 

3513#@+node:ekr.20110605121601.18340: ** class LeoQtMenu (LeoMenu) 

3514class LeoQtMenu(leoMenu.LeoMenu): 

3515 

3516 #@+others 

3517 #@+node:ekr.20110605121601.18341: *3* LeoQtMenu.__init__ 

3518 def __init__(self, c: Cmdr, frame: Wrapper, label: str) -> None: 

3519 """ctor for LeoQtMenu class.""" 

3520 assert frame 

3521 assert frame.c 

3522 super().__init__(frame) 

3523 self.leo_menu_label = label.replace('&', '').lower() 

3524 self.frame = frame 

3525 self.c = c 

3526 self.menuBar: Wrapper = c.frame.top.menuBar() 

3527 assert self.menuBar is not None 

3528 # Inject this dict into the commander. 

3529 if not hasattr(c, 'menuAccels'): 

3530 setattr(c, 'menuAccels', {}) 

3531 if 0: 

3532 self.font = c.config.getFontFromParams( 

3533 'menu_text_font_family', 'menu_text_font_size', 

3534 'menu_text_font_slant', 'menu_text_font_weight', 

3535 c.config.defaultMenuFontSize) 

3536 #@+node:ekr.20120306130648.9848: *3* LeoQtMenu.__repr__ 

3537 def __repr__(self) -> str: 

3538 return f"<LeoQtMenu: {self.leo_menu_label}>" 

3539 

3540 __str__ = __repr__ 

3541 #@+node:ekr.20110605121601.18342: *3* LeoQtMenu.Tkinter menu bindings 

3542 # See the Tk docs for what these routines are to do 

3543 #@+node:ekr.20110605121601.18343: *4* LeoQtMenu.Methods with Tk spellings 

3544 #@+node:ekr.20110605121601.18344: *5* LeoQtMenu.add_cascade 

3545 def add_cascade(self, parent: Widget, label: str, menu: Wrapper, underline: int) -> Wrapper: 

3546 """Wrapper for the Tkinter add_cascade menu method. 

3547 

3548 Adds a submenu to the parent menu, or the menubar.""" 

3549 # menu and parent are a QtMenuWrappers, subclasses of QMenu. 

3550 n = underline 

3551 if -1 < n < len(label): 

3552 label = label[:n] + '&' + label[n:] 

3553 menu.setTitle(label) 

3554 if parent: 

3555 parent.addMenu(menu) # QMenu.addMenu. 

3556 else: 

3557 self.menuBar.addMenu(menu) 

3558 label = label.replace('&', '').lower() 

3559 menu.leo_menu_label = label 

3560 return menu 

3561 #@+node:ekr.20110605121601.18345: *5* LeoQtMenu.add_command (Called by createMenuEntries) 

3562 def add_command(self, menu: Widget, 

3563 accelerator: str='', command: Callable=None, commandName: str=None, label: str=None, underline: int=0, 

3564 ) -> None: 

3565 """Wrapper for the Tkinter add_command menu method.""" 

3566 if not label: 

3567 return 

3568 if -1 < underline < len(label): 

3569 label = label[:underline] + '&' + label[underline:] 

3570 if accelerator: 

3571 label = f"{label}\t{accelerator}" 

3572 action = menu.addAction(label) # type:ignore 

3573 # Inject the command name into the action so that it can be enabled/disabled dynamically. 

3574 action.leo_command_name = commandName or '' 

3575 if command: 

3576 

3577 def qt_add_command_callback(checked: int, label: str=label, command: Callable=command) -> None: 

3578 return command() 

3579 

3580 action.triggered.connect(qt_add_command_callback) 

3581 #@+node:ekr.20110605121601.18346: *5* LeoQtMenu.add_separator 

3582 def add_separator(self, menu: Wrapper) -> None: 

3583 """Wrapper for the Tkinter add_separator menu method.""" 

3584 if menu: 

3585 action = menu.addSeparator() 

3586 action.leo_menu_label = '*seperator*' 

3587 #@+node:ekr.20110605121601.18347: *5* LeoQtMenu.delete 

3588 def delete(self, menu: Wrapper, realItemName: str='<no name>') -> None: 

3589 """Wrapper for the Tkinter delete menu method.""" 

3590 # if menu: 

3591 # return menu.delete(realItemName) 

3592 #@+node:ekr.20110605121601.18348: *5* LeoQtMenu.delete_range 

3593 def delete_range(self, menu: Wrapper, n1: int, n2: int) -> None: 

3594 """Wrapper for the Tkinter delete menu method.""" 

3595 # Menu is a subclass of QMenu and LeoQtMenu. 

3596 for z in menu.actions()[n1:n2]: 

3597 menu.removeAction(z) 

3598 #@+node:ekr.20110605121601.18349: *5* LeoQtMenu.destroy 

3599 def destroy(self, menu: Wrapper) -> None: 

3600 """Wrapper for the Tkinter destroy menu method.""" 

3601 # Fixed bug https://bugs.launchpad.net/leo-editor/+bug/1193870 

3602 if menu: 

3603 menu.menuBar.removeAction(menu.menuAction()) 

3604 #@+node:ekr.20110605121601.18350: *5* LeoQtMenu.index 

3605 def index(self, label: str) -> int: 

3606 """Return the index of the menu with the given label.""" 

3607 return 0 

3608 #@+node:ekr.20110605121601.18351: *5* LeoQtMenu.insert 

3609 def insert(self, 

3610 menuName: str, position: int, label: str, command: Callable, underline: int=None, 

3611 ) -> None: 

3612 

3613 menu = self.getMenu(menuName) 

3614 if menu and label: 

3615 n = underline or 0 

3616 if -1 > n > len(label): 

3617 label = label[:n] + '&' + label[n:] 

3618 action = menu.addAction(label) 

3619 if command: 

3620 

3621 def insert_callback(checked: str, label: str=label, command: Callable=command) -> None: 

3622 command() 

3623 

3624 action.triggered.connect(insert_callback) 

3625 #@+node:ekr.20110605121601.18352: *5* LeoQtMenu.insert_cascade 

3626 def insert_cascade(self, 

3627 parent: Widget, 

3628 index: int, 

3629 label: str, 

3630 menu: Widget, 

3631 underline: int, # Not used 

3632 ) -> Widget: 

3633 """Wrapper for the Tkinter insert_cascade menu method.""" 

3634 menu.setTitle(label) 

3635 label.replace('&', '').lower() 

3636 menu.leo_menu_label = label # was leo_label 

3637 if parent: 

3638 parent.addMenu(menu) 

3639 else: 

3640 self.menuBar.addMenu(menu) 

3641 action = menu.menuAction() 

3642 if action: 

3643 action.leo_menu_label = label 

3644 else: 

3645 g.trace('no action for menu', label) 

3646 return menu 

3647 #@+node:ekr.20110605121601.18353: *5* LeoQtMenu.new_menu 

3648 def new_menu(self, parent: Widget, tearoff: int=0, label: str='') -> Any: # label is for debugging. 

3649 """Wrapper for the Tkinter new_menu menu method.""" 

3650 c, leoFrame = self.c, self.frame 

3651 # Parent can be None, in which case it will be added to the menuBar. 

3652 menu = QtMenuWrapper(c, leoFrame, parent, label) 

3653 return menu 

3654 #@+node:ekr.20110605121601.18354: *4* LeoQtMenu.Methods with other spellings 

3655 #@+node:ekr.20110605121601.18355: *5* LeoQtMenu.clearAccel 

3656 def clearAccel(self, menu: Wrapper, name: str) -> None: 

3657 pass 

3658 # if not menu: 

3659 # return 

3660 # realName = self.getRealMenuName(name) 

3661 # realName = realName.replace("&","") 

3662 # menu.entryconfig(realName,accelerator='') 

3663 #@+node:ekr.20110605121601.18356: *5* LeoQtMenu.createMenuBar 

3664 def createMenuBar(self, frame: Widget) -> None: 

3665 """ 

3666 (LeoQtMenu) Create all top-level menus. 

3667 The menuBar itself has already been created. 

3668 """ 

3669 self.createMenusFromTables() # This is LeoMenu.createMenusFromTables. 

3670 #@+node:ekr.20110605121601.18357: *5* LeoQtMenu.createOpenWithMenu 

3671 def createOpenWithMenu(self, parent: Any, label: str, index: int, amp_index: int) -> Any: 

3672 """ 

3673 Create the File:Open With submenu. 

3674 

3675 This is called from LeoMenu.createOpenWithMenuFromTable. 

3676 """ 

3677 # Use the existing Open With menu if possible. 

3678 menu = self.getMenu('openwith') 

3679 if not menu: 

3680 menu = self.new_menu(parent, tearoff=False, label=label) 

3681 menu.insert_cascade(parent, index, label, menu, underline=amp_index) 

3682 return menu 

3683 #@+node:ekr.20110605121601.18358: *5* LeoQtMenu.disable/enableMenu (not used) 

3684 def disableMenu(self, menu: Wrapper, name: str) -> None: 

3685 self.enableMenu(menu, name, False) 

3686 

3687 def enableMenu(self, menu: Wrapper, name: str, val: bool) -> None: 

3688 """Enable or disable the item in the menu with the given name.""" 

3689 if menu and name: 

3690 for action in menu.actions(): 

3691 s = g.checkUnicode(action.text()).replace('&', '') 

3692 if s.startswith(name): 

3693 action.setEnabled(val) 

3694 break 

3695 #@+node:ekr.20110605121601.18359: *5* LeoQtMenu.getMenuLabel 

3696 def getMenuLabel(self, menu: Wrapper, name: str) -> None: 

3697 """Return the index of the menu item whose name (or offset) is given. 

3698 Return None if there is no such menu item.""" 

3699 # At present, it is valid to always return None. 

3700 #@+node:ekr.20110605121601.18360: *5* LeoQtMenu.setMenuLabel 

3701 def setMenuLabel(self, menu: Wrapper, name: str, label: str, underline: int=-1) -> None: 

3702 

3703 def munge(s: str) -> str: 

3704 return (s or '').replace('&', '') 

3705 

3706 # menu is a QtMenuWrapper. 

3707 if not menu: 

3708 return 

3709 

3710 realName = munge(self.getRealMenuName(name)) 

3711 realLabel = self.getRealMenuName(label) 

3712 for action in menu.actions(): 

3713 s = munge(action.text()) 

3714 s = s.split('\t')[0] 

3715 if s == realName: 

3716 action.setText(realLabel) 

3717 break 

3718 #@+node:ekr.20110605121601.18361: *3* LeoQtMenu.activateMenu & helper 

3719 def activateMenu(self, menuName: str) -> None: 

3720 """Activate the menu with the given name""" 

3721 # Menu is a QtMenuWrapper, a subclass of both QMenu and LeoQtMenu. 

3722 menu = self.getMenu(menuName) 

3723 if menu: 

3724 self.activateAllParentMenus(menu) 

3725 else: 

3726 g.trace(f"No such menu: {menuName}") 

3727 #@+node:ekr.20120922041923.10607: *4* LeoQtMenu.activateAllParentMenus 

3728 def activateAllParentMenus(self, menu: Wrapper) -> None: 

3729 """menu is a QtMenuWrapper. Activate it and all parent menus.""" 

3730 parent = menu.parent() 

3731 action = menu.menuAction() 

3732 if action: 

3733 if parent and isinstance(parent, QtWidgets.QMenuBar): 

3734 parent.setActiveAction(action) 

3735 elif parent: 

3736 self.activateAllParentMenus(parent) 

3737 parent.setActiveAction(action) 

3738 else: 

3739 g.trace(f"can not happen: no parent for {menu}") 

3740 else: 

3741 g.trace(f"can not happen: no action for {menu}") 

3742 #@+node:ekr.20120922041923.10613: *3* LeoQtMenu.deactivateMenuBar 

3743 # def deactivateMenuBar (self): 

3744 # """Activate the menu with the given name""" 

3745 # menubar = self.c.frame.top.leo_menubar 

3746 # menubar.setActiveAction(None) 

3747 # menubar.repaint() 

3748 #@+node:ekr.20110605121601.18362: *3* LeoQtMenu.getMacHelpMenu 

3749 def getMacHelpMenu(self, table: List) -> None: 

3750 return None 

3751 #@-others 

3752#@+node:ekr.20110605121601.18363: ** class LeoQTreeWidget (QTreeWidget) 

3753class LeoQTreeWidget(QtWidgets.QTreeWidget): # type:ignore 

3754 

3755 def __init__(self, c: Cmdr, parent: Widget) -> None: 

3756 super().__init__(parent) 

3757 self.setAcceptDrops(True) 

3758 enable_drag = c.config.getBool('enable-tree-dragging') 

3759 self.setDragEnabled(bool(enable_drag)) 

3760 self.c = c 

3761 self.was_alt_drag = False 

3762 self.was_control_drag = False 

3763 

3764 def __repr__(self) -> str: 

3765 return f"LeoQTreeWidget: {id(self)}" 

3766 

3767 __str__ = __repr__ 

3768 

3769 

3770 def dragMoveEvent(self, ev: Event) -> None: # Called during drags. 

3771 pass 

3772 

3773 #@+others 

3774 #@+node:ekr.20111022222228.16980: *3* LeoQTreeWidget: Event handlers 

3775 #@+node:ekr.20110605121601.18364: *4* LeoQTreeWidget.dragEnterEvent & helper 

3776 def dragEnterEvent(self, ev: Event) -> None: 

3777 """Export c.p's tree as a Leo mime-data.""" 

3778 c = self.c 

3779 if not ev: 

3780 g.trace('no event!') 

3781 return 

3782 md = ev.mimeData() 

3783 if not md: 

3784 g.trace('No mimeData!') 

3785 return 

3786 c.endEditing() 

3787 # Fix bug 135: cross-file drag and drop is broken. 

3788 # This handler may be called several times for the same drag. 

3789 # Only the first should should set g.app.drag_source. 

3790 if g.app.dragging: 

3791 pass 

3792 else: 

3793 g.app.dragging = True 

3794 g.app.drag_source = c, c.p 

3795 self.setText(md) 

3796 # Always accept the drag, even if we are already dragging. 

3797 ev.accept() 

3798 #@+node:ekr.20110605121601.18384: *5* LeoQTreeWidget.setText 

3799 def setText(self, md: Any) -> None: 

3800 c = self.c 

3801 fn = self.fileName() 

3802 s = c.fileCommands.outline_to_clipboard_string() 

3803 md.setText(f"{fn},{s}") 

3804 #@+node:ekr.20110605121601.18365: *4* LeoQTreeWidget.dropEvent & helpers 

3805 def dropEvent(self, ev: Event) -> None: 

3806 """Handle a drop event in the QTreeWidget.""" 

3807 if not ev: 

3808 return 

3809 md = ev.mimeData() 

3810 if not md: 

3811 g.trace('no mimeData!') 

3812 return 

3813 try: 

3814 mods = ev.modifiers() if isQt6 else int(ev.keyboardModifiers()) 

3815 self.was_alt_drag = bool(mods & KeyboardModifier.AltModifier) 

3816 self.was_control_drag = bool(mods & KeyboardModifier.ControlModifier) 

3817 except Exception: # Defensive. 

3818 g.es_exception() 

3819 g.app.dragging = False 

3820 return 

3821 c, tree = self.c, self.c.frame.tree 

3822 p = None 

3823 point = ev.position().toPoint() if isQt6 else ev.pos() 

3824 item = self.itemAt(point) 

3825 if item: 

3826 itemHash = tree.itemHash(item) 

3827 p = tree.item2positionDict.get(itemHash) 

3828 if not p: 

3829 # #59: Drop at last node. 

3830 p = c.rootPosition() 

3831 while p.hasNext(): 

3832 p.moveToNext() 

3833 formats = set(str(f) for f in md.formats()) 

3834 ev.setDropAction(DropAction.IgnoreAction) 

3835 ev.accept() 

3836 hookres = g.doHook("outlinedrop", c=c, p=p, dropevent=ev, formats=formats) 

3837 if hookres: 

3838 # A plugin handled the drop. 

3839 pass 

3840 else: 

3841 if md.hasUrls(): 

3842 self.urlDrop(md, p) 

3843 else: 

3844 self.nodeDrop(md, p) 

3845 g.app.dragging = False 

3846 #@+node:ekr.20110605121601.18366: *5* LeoQTreeWidget.nodeDrop & helpers 

3847 def nodeDrop(self, md: Any, p: Pos) -> None: 

3848 """ 

3849 Handle a drop event when not md.urls(). 

3850 This will happen when we drop an outline node. 

3851 We get the copied text from md.text(). 

3852 """ 

3853 c = self.c 

3854 fn, s = self.parseText(md) 

3855 if not s or not fn: 

3856 return 

3857 if fn == self.fileName(): 

3858 if p and p == c.p: 

3859 pass 

3860 elif g.os_path_exists(fn): 

3861 self.intraFileDrop(fn, c.p, p) 

3862 else: 

3863 self.interFileDrop(fn, p, s) 

3864 #@+node:ekr.20110605121601.18367: *6* LeoQTreeWidget.interFileDrop 

3865 def interFileDrop(self, fn: str, p: Pos, s: str) -> None: 

3866 """Paste the mime data after (or as the first child of) p.""" 

3867 c = self.c 

3868 u = c.undoer 

3869 undoType = 'Drag Outline' 

3870 isLeo = g.match(s, 0, g.app.prolog_prefix_string) 

3871 if not isLeo: 

3872 return 

3873 c.selectPosition(p) 

3874 # Paste the node after the presently selected node. 

3875 pasted = c.fileCommands.getLeoOutlineFromClipboard(s) 

3876 if not pasted: 

3877 return 

3878 if c.config.getBool('inter-outline-drag-moves'): 

3879 src_c, src_p = g.app.drag_source 

3880 if src_p.hasVisNext(src_c): 

3881 nxt = src_p.getVisNext(src_c).v 

3882 elif src_p.hasVisBack(src_c): 

3883 nxt = src_p.getVisBack(src_c).v 

3884 else: 

3885 nxt = None 

3886 if nxt is not None: 

3887 src_p.doDelete() 

3888 src_c.selectPosition(src_c.vnode2position(nxt)) 

3889 src_c.setChanged() 

3890 src_c.redraw() 

3891 else: 

3892 g.es("Can't move last node out of outline") 

3893 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

3894 c.validateOutline() 

3895 c.selectPosition(pasted) 

3896 pasted.setDirty() # 2011/02/27: Fix bug 690467. 

3897 c.setChanged() 

3898 back = pasted.back() 

3899 if back and back.isExpanded(): 

3900 pasted.moveToNthChildOf(back, 0) 

3901 # c.setRootPosition(c.findRootPosition(pasted)) 

3902 u.afterInsertNode(pasted, undoType, undoData) 

3903 c.redraw(pasted) 

3904 c.recolor() 

3905 #@+node:ekr.20110605121601.18368: *6* LeoQTreeWidget.intraFileDrop 

3906 def intraFileDrop(self, fn: str, p1: Pos, p2: Pos) -> None: 

3907 """Move p1 after (or as the first child of) p2.""" 

3908 as_child = self.was_alt_drag 

3909 cloneDrag = self.was_control_drag 

3910 c = self.c 

3911 u = c.undoer 

3912 c.selectPosition(p1) 

3913 if as_child or p2.hasChildren() and p2.isExpanded(): 

3914 # Attempt to move p1 to the first child of p2. 

3915 # parent = p2 

3916 

3917 def move(p1: Pos, p2: Pos) -> Pos: 

3918 if cloneDrag: 

3919 p1 = p1.clone() 

3920 p1.moveToNthChildOf(p2, 0) 

3921 p1.setDirty() 

3922 return p1 

3923 

3924 else: 

3925 # Attempt to move p1 after p2. 

3926 # parent = p2.parent() 

3927 

3928 def move(p1: Pos, p2: Pos) -> Pos: 

3929 if cloneDrag: 

3930 p1 = p1.clone() 

3931 p1.moveAfter(p2) 

3932 p1.setDirty() 

3933 return p1 

3934 

3935 ok = ( 

3936 # 2011/10/03: Major bug fix. 

3937 c.checkDrag(p1, p2) and 

3938 c.checkMoveWithParentWithWarning(p1, p2, True)) 

3939 if ok: 

3940 undoData = u.beforeMoveNode(p1) 

3941 p1.setDirty() 

3942 p1 = move(p1, p2) 

3943 if cloneDrag: 

3944 # Set dirty bits for ancestors of *all* cloned nodes. 

3945 for z in p1.self_and_subtree(): 

3946 z.setDirty() 

3947 c.setChanged() 

3948 u.afterMoveNode(p1, 'Drag', undoData) 

3949 if (not as_child or 

3950 p2.isExpanded() or 

3951 c.config.getBool("drag-alt-drag-expands") is not False 

3952 ): 

3953 c.redraw(p1) 

3954 else: 

3955 c.redraw(p2) 

3956 #@+node:ekr.20110605121601.18383: *6* LeoQTreeWidget.parseText 

3957 def parseText(self, md: Any) -> Tuple[str, str]: 

3958 """Parse md.text() into (fn,s)""" 

3959 fn = '' 

3960 s = md.text() 

3961 if s: 

3962 i = s.find(',') 

3963 if i == -1: 

3964 pass 

3965 else: 

3966 fn = s[:i] 

3967 s = s[i + 1 :] 

3968 return fn, s 

3969 #@+node:ekr.20110605121601.18369: *5* LeoQTreeWidget.urlDrop & helpers 

3970 def urlDrop(self, md: Any, p: Pos) -> None: 

3971 """Handle a drop when md.urls().""" 

3972 c, u, undoType = self.c, self.c.undoer, 'Drag Urls' 

3973 urls = md.urls() 

3974 if not urls: 

3975 return 

3976 c.undoer.beforeChangeGroup(c.p, undoType) 

3977 changed = False 

3978 for z in urls: 

3979 url = QtCore.QUrl(z) 

3980 scheme = url.scheme() 

3981 if scheme == 'file': 

3982 changed |= self.doFileUrl(p, url) 

3983 elif scheme in ('http',): # 'ftp','mailto', 

3984 changed |= self.doHttpUrl(p, url) 

3985 if changed: 

3986 c.setChanged() 

3987 u.afterChangeGroup(c.p, undoType, reportFlag=False) 

3988 c.redraw() 

3989 #@+node:ekr.20110605121601.18370: *6* LeoQTreeWidget.doFileUrl & helper 

3990 def doFileUrl(self, p: Pos, url: str) -> bool: 

3991 """Read the file given by the url and put it in the outline.""" 

3992 # 2014/06/06: Work around a possible bug in QUrl. 

3993 # fn = str(url.path()) # Fails. 

3994 e = sys.getfilesystemencoding() 

3995 fn = g.toUnicode(url.path(), encoding=e) 

3996 if sys.platform.lower().startswith('win'): 

3997 if fn.startswith('/'): 

3998 fn = fn[1:] 

3999 if os.path.isdir(fn): 

4000 # Just insert an @path directory. 

4001 self.doPathUrlHelper(fn, p) 

4002 return True 

4003 if g.os_path_exists(fn): 

4004 try: 

4005 f = open(fn, 'rb') # 2012/03/09: use 'rb' 

4006 except IOError: 

4007 f = None 

4008 if f: 

4009 b = f.read() 

4010 s = g.toUnicode(b) 

4011 f.close() 

4012 return self.doFileUrlHelper(fn, p, s) 

4013 nodeLink = p.get_UNL() 

4014 g.es_print(f"not found: {fn}", nodeLink=nodeLink) 

4015 return False 

4016 #@+node:ekr.20110605121601.18371: *7* LeoQTreeWidget.doFileUrlHelper & helper 

4017 def doFileUrlHelper(self, fn: str, p: Pos, s: str) -> bool: 

4018 """ 

4019 Insert s in an @file, @auto or @edit node after p. 

4020 If fn is a .leo file, insert a node containing its top-level nodes as children. 

4021 """ 

4022 c = self.c 

4023 if self.isLeoFile(fn, s) and not self.was_control_drag: 

4024 g.openWithFileName(fn, old_c=c) 

4025 return False # Don't set the changed marker in the original file. 

4026 u, undoType = c.undoer, 'Drag File' 

4027 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

4028 if p.hasChildren() and p.isExpanded(): 

4029 p2 = p.insertAsNthChild(0) 

4030 parent = p 

4031 elif p.h.startswith('@path '): 

4032 # #60: create relative paths & urls when dragging files. 

4033 p2 = p.insertAsNthChild(0) 

4034 p.expand() 

4035 parent = p 

4036 else: 

4037 p2 = p.insertAfter() 

4038 parent = p.parent() 

4039 # #60: create relative paths & urls when dragging files. 

4040 aList = g.get_directives_dict_list(parent) 

4041 path = g.scanAtPathDirectives(c, aList) 

4042 if path: 

4043 fn = os.path.relpath(fn, path) 

4044 self.createAtFileNode(fn, p2, s) 

4045 u.afterInsertNode(p2, undoType, undoData) 

4046 c.selectPosition(p2) 

4047 return True # The original .leo file has changed. 

4048 #@+node:ekr.20110605121601.18372: *8* LeoQTreeWidget.createAtFileNode & helpers (QTreeWidget) 

4049 def createAtFileNode(self, fn: str, p: Pos, s: str) -> None: 

4050 """ 

4051 Set p's headline, body text and possibly descendants 

4052 based on the file's name fn and contents s. 

4053 

4054 If the file is an thin file, create an @file tree. 

4055 Othewise, create an @auto tree. 

4056 If all else fails, create an @edit node. 

4057 

4058 Give a warning if a node with the same headline already exists. 

4059 """ 

4060 c = self.c 

4061 c.init_error_dialogs() 

4062 if self.isLeoFile(fn, s): 

4063 self.createLeoFileTree(fn, p) 

4064 elif self.isThinFile(fn, s): 

4065 self.createAtFileTree(fn, p, s) 

4066 elif self.isAutoFile(fn): 

4067 self.createAtAutoTree(fn, p) 

4068 elif self.isBinaryFile(fn): 

4069 self.createUrlForBinaryFile(fn, p) 

4070 else: 

4071 self.createAtEditNode(fn, p) 

4072 self.warnIfNodeExists(p) 

4073 c.raise_error_dialogs(kind='read') 

4074 #@+node:ekr.20110605121601.18373: *9* LeoQTreeWidget.createAtAutoTree (QTreeWidget) 

4075 def createAtAutoTree(self, fn: str, p: Pos) -> None: 

4076 """ 

4077 Make p an @auto node and create the tree using s, the file's contents. 

4078 """ 

4079 c = self.c 

4080 at = c.atFileCommands 

4081 p.h = f"@auto {fn}" 

4082 at.readOneAtAutoNode(p) 

4083 # No error recovery should be needed here. 

4084 p.clearDirty() # Don't automatically rewrite this node. 

4085 #@+node:ekr.20110605121601.18374: *9* LeoQTreeWidget.createAtEditNode 

4086 def createAtEditNode(self, fn: str, p: Pos) -> None: 

4087 c = self.c 

4088 at = c.atFileCommands 

4089 # Use the full @edit logic, so dragging will be 

4090 # exactly the same as reading. 

4091 at.readOneAtEditNode(fn, p) 

4092 p.h = f"@edit {fn}" 

4093 p.clearDirty() # Don't automatically rewrite this node. 

4094 #@+node:ekr.20110605121601.18375: *9* LeoQTreeWidget.createAtFileTree 

4095 def createAtFileTree(self, fn: str, p: Pos, s: str) -> None: 

4096 """Make p an @file node and create the tree using s, the file's contents.""" 

4097 c = self.c 

4098 at = c.atFileCommands 

4099 p.h = f"@file {fn}" 

4100 # Read the file into p. 

4101 ok = at.read(root=p.copy(), fromString=s) 

4102 if not ok: 

4103 g.error('Error reading', fn) 

4104 p.b = '' # Safe: will not cause a write later. 

4105 p.clearDirty() # Don't automatically rewrite this node. 

4106 #@+node:ekr.20141007223054.18004: *9* LeoQTreeWidget.createLeoFileTree 

4107 def createLeoFileTree(self, fn: str, p: Pos) -> None: 

4108 """Copy all nodes from fn, a .leo file, to the children of p.""" 

4109 c = self.c 

4110 p.h = f"From {g.shortFileName(fn)}" 

4111 c.selectPosition(p) 

4112 # Create a dummy first child of p. 

4113 dummy_p = p.insertAsNthChild(0) 

4114 c.selectPosition(dummy_p) 

4115 c2 = g.openWithFileName(fn, old_c=c, gui=g.app.nullGui) 

4116 for p2 in c2.rootPosition().self_and_siblings(): 

4117 c2.selectPosition(p2) 

4118 s = c2.fileCommands.outline_to_clipboard_string() 

4119 # Paste the outline after the selected node. 

4120 c.fileCommands.getLeoOutlineFromClipboard(s) 

4121 dummy_p.doDelete() 

4122 c.selectPosition(p) 

4123 p.v.contract() 

4124 c2.close() 

4125 g.app.forgetOpenFile(c2.fileName()) # Necessary. 

4126 #@+node:ekr.20120309075544.9882: *9* LeoQTreeWidget.createUrlForBinaryFile 

4127 def createUrlForBinaryFile(self, fn: str, p: Pos) -> None: 

4128 # Fix bug 1028986: create relative urls when dragging binary files to Leo. 

4129 c = self.c 

4130 base_fn = g.os_path_normcase(g.os_path_abspath(c.mFileName)) 

4131 abs_fn = g.os_path_normcase(g.os_path_abspath(fn)) 

4132 prefix = os.path.commonprefix([abs_fn, base_fn]) 

4133 if prefix and len(prefix) > 3: # Don't just strip off c:\. 

4134 p.h = abs_fn[len(prefix) :].strip() 

4135 else: 

4136 p.h = f"@url file://{fn}" 

4137 #@+node:ekr.20110605121601.18377: *9* LeoQTreeWidget.isAutoFile (LeoQTreeWidget) 

4138 def isAutoFile(self, fn: str) -> bool: 

4139 """Return true if fn (a file name) can be parsed with an @auto parser.""" 

4140 d = g.app.classDispatchDict 

4141 junk, ext = g.os_path_splitext(fn) 

4142 return bool(d.get(ext)) 

4143 #@+node:ekr.20120309075544.9881: *9* LeoQTreeWidget.isBinaryFile 

4144 def isBinaryFile(self, fn: str) -> bool: 

4145 # The default for unknown files is True. Not great, but safe. 

4146 junk, ext = g.os_path_splitext(fn) 

4147 ext = ext.lower() 

4148 if not ext: 

4149 val = False 

4150 elif ext.startswith('~'): 

4151 val = False 

4152 elif ext in ('.css', '.htm', '.html', '.leo', '.txt'): 

4153 val = False 

4154 # elif ext in ('.bmp','gif','ico',): 

4155 # val = True 

4156 else: 

4157 keys = (z.lower() for z in g.app.extension_dict) 

4158 val = ext not in keys 

4159 return val 

4160 #@+node:ekr.20141007223054.18003: *9* LeoQTreeWidget.isLeoFile 

4161 def isLeoFile(self, fn: str, s: str) -> bool: 

4162 """Return true if fn (a file name) represents an entire .leo file.""" 

4163 return fn.endswith('.leo') and s.startswith(g.app.prolog_prefix_string) 

4164 #@+node:ekr.20110605121601.18376: *9* LeoQTreeWidget.isThinFile 

4165 def isThinFile(self, fn: str, s: str) -> bool: 

4166 """ 

4167 Return true if the file whose contents is s 

4168 was created from an @thin or @file tree. 

4169 """ 

4170 c = self.c 

4171 at = c.atFileCommands 

4172 # Skip lines before the @+leo line. 

4173 i = s.find('@+leo') 

4174 if i == -1: 

4175 return False 

4176 # Like at.isFileLike. 

4177 j, k = g.getLine(s, i) 

4178 line = s[j:k] 

4179 valid, new_df, start, end, isThin = at.parseLeoSentinel(line) 

4180 return valid and new_df and isThin 

4181 #@+node:ekr.20110605121601.18378: *9* LeoQTreeWidget.warnIfNodeExists 

4182 def warnIfNodeExists(self, p: Pos) -> None: 

4183 c = self.c 

4184 h = p.h 

4185 for p2 in c.all_unique_positions(): 

4186 if p2.h == h and p2 != p: 

4187 g.warning('Warning: duplicate node:', h) 

4188 break 

4189 #@+node:ekr.20110605121601.18379: *7* LeoQTreeWidget.doPathUrlHelper 

4190 def doPathUrlHelper(self, fn: str, p: Pos) -> None: 

4191 """Insert fn as an @path node after p.""" 

4192 c = self.c 

4193 u, undoType = c.undoer, 'Drag Directory' 

4194 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

4195 if p.hasChildren() and p.isExpanded(): 

4196 p2 = p.insertAsNthChild(0) 

4197 else: 

4198 p2 = p.insertAfter() 

4199 p2.h = '@path ' + fn 

4200 u.afterInsertNode(p2, undoType, undoData) 

4201 c.selectPosition(p2) 

4202 #@+node:ekr.20110605121601.18380: *6* LeoQTreeWidget.doHttpUrl 

4203 def doHttpUrl(self, p: Pos, url: str) -> bool: 

4204 """Insert the url in an @url node after p.""" 

4205 c = self.c 

4206 u = c.undoer 

4207 undoType = 'Drag Url' 

4208 s = str(url.toString()).strip() 

4209 if not s: 

4210 return False 

4211 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

4212 if p.hasChildren() and p.isExpanded(): 

4213 p2 = p.insertAsNthChild(0) 

4214 else: 

4215 p2 = p.insertAfter() 

4216 p2.h = '@url' 

4217 p2.b = s 

4218 p2.clearDirty() # Don't automatically rewrite this node. 

4219 u.afterInsertNode(p2, undoType, undoData) 

4220 return True 

4221 #@+node:ekr.20110605121601.18381: *3* LeoQTreeWidget: utils 

4222 #@+node:ekr.20110605121601.18382: *4* LeoQTreeWidget.dump 

4223 def dump(self, ev: Event, p: Pos, tag: str) -> None: 

4224 if ev: 

4225 md = ev.mimeData() 

4226 s = g.checkUnicode(md.text(), encoding='utf-8') 

4227 g.trace('md.text:', repr(s) if len(s) < 100 else len(s)) 

4228 for url in md.urls() or []: 

4229 g.trace(' url:', url) 

4230 g.trace(' url.fn:', url.toLocalFile()) 

4231 g.trace('url.text:', url.toString()) 

4232 else: 

4233 g.trace('', tag, '** no event!') 

4234 #@+node:ekr.20141007223054.18002: *4* LeoQTreeWidget.fileName 

4235 def fileName(self) -> str: 

4236 """Return the commander's filename.""" 

4237 return self.c.fileName() or '<unsaved file>' 

4238 #@-others 

4239#@+node:ekr.20110605121601.18385: ** class LeoQtSpellTab 

4240class LeoQtSpellTab: 

4241 #@+others 

4242 #@+node:ekr.20110605121601.18386: *3* LeoQtSpellTab.__init__ 

4243 def __init__(self, c: Cmdr, handler: Callable, tabName: str) -> None: 

4244 """Ctor for LeoQtSpellTab class.""" 

4245 self.c = c 

4246 top = c.frame.top 

4247 self.handler = handler 

4248 # hack: 

4249 handler.workCtrl = leoFrame.StringTextWrapper(c, 'spell-workctrl') 

4250 self.tabName = tabName 

4251 if hasattr(top, 'leo_spell_label'): 

4252 self.wordLabel = top.leo_spell_label 

4253 self.listBox = top.leo_spell_listBox 

4254 self.fillbox([]) 

4255 else: 

4256 self.handler.loaded = False 

4257 #@+node:ekr.20110605121601.18389: *3* Event handlers 

4258 #@+node:ekr.20110605121601.18390: *4* onAddButton 

4259 def onAddButton(self) -> None: 

4260 """Handle a click in the Add button in the Check Spelling dialog.""" 

4261 self.handler.add() 

4262 #@+node:ekr.20110605121601.18391: *4* onChangeButton & onChangeThenFindButton 

4263 def onChangeButton(self, event: Event=None) -> None: 

4264 """Handle a click in the Change button in the Spell tab.""" 

4265 state = self.updateButtons() 

4266 if state: 

4267 self.handler.change() 

4268 self.updateButtons() 

4269 

4270 def onChangeThenFindButton(self, event: Event=None) -> None: 

4271 """Handle a click in the "Change, Find" button in the Spell tab.""" 

4272 state = self.updateButtons() 

4273 if state: 

4274 self.handler.change() 

4275 if self.handler.change(): 

4276 self.handler.find() 

4277 self.updateButtons() 

4278 #@+node:ekr.20110605121601.18392: *4* onFindButton 

4279 def onFindButton(self) -> None: 

4280 """Handle a click in the Find button in the Spell tab.""" 

4281 c = self.c 

4282 self.handler.find() 

4283 self.updateButtons() 

4284 c.invalidateFocus() 

4285 c.bodyWantsFocus() 

4286 #@+node:ekr.20110605121601.18393: *4* onHideButton 

4287 def onHideButton(self) -> None: 

4288 """Handle a click in the Hide button in the Spell tab.""" 

4289 self.handler.hide() 

4290 #@+node:ekr.20110605121601.18394: *4* onIgnoreButton 

4291 def onIgnoreButton(self, event: Event=None) -> None: 

4292 """Handle a click in the Ignore button in the Check Spelling dialog.""" 

4293 self.handler.ignore() 

4294 #@+node:ekr.20110605121601.18395: *4* onMap 

4295 def onMap(self, event: Event=None) -> None: 

4296 """Respond to a Tk <Map> event.""" 

4297 self.update(show=False, fill=False) 

4298 #@+node:ekr.20110605121601.18396: *4* onSelectListBox 

4299 def onSelectListBox(self, event: Event=None) -> None: 

4300 """Respond to a click in the selection listBox.""" 

4301 c = self.c 

4302 self.updateButtons() 

4303 c.bodyWantsFocus() 

4304 #@+node:ekr.20110605121601.18397: *3* Helpers 

4305 #@+node:ekr.20110605121601.18398: *4* bringToFront (LeoQtSpellTab) 

4306 def bringToFront(self) -> None: 

4307 self.c.frame.log.selectTab('Spell') 

4308 #@+node:ekr.20110605121601.18399: *4* fillbox (LeoQtSpellTab) 

4309 def fillbox(self, alts: List[str], word: str=None) -> None: 

4310 """Update the suggestions listBox in the Check Spelling dialog.""" 

4311 self.suggestions = alts 

4312 if not word: 

4313 word = "" 

4314 self.wordLabel.setText("Suggestions for: " + word) 

4315 self.listBox.clear() 

4316 if self.suggestions: 

4317 self.listBox.addItems(self.suggestions) 

4318 self.listBox.setCurrentRow(0) 

4319 #@+node:ekr.20110605121601.18400: *4* getSuggestion (LeoQtSpellTab) 

4320 def getSuggestion(self) -> str: 

4321 """Return the selected suggestion from the listBox.""" 

4322 idx = self.listBox.currentRow() 

4323 value = self.suggestions[idx] 

4324 return value 

4325 #@+node:ekr.20141113094129.13: *4* setFocus (LeoQtSpellTab) 

4326 def setFocus(self) -> None: 

4327 """Actually put focus in the tab.""" 

4328 # Not a great idea: there is no indication of focus. 

4329 c = self.c 

4330 if c.frame and c.frame.top and hasattr(c.frame.top, 'spellFrame'): 

4331 w = self.c.frame.top.spellFrame 

4332 c.widgetWantsFocus(w) 

4333 #@+node:ekr.20110605121601.18401: *4* update (LeoQtSpellTab) 

4334 def update(self, show: bool=True, fill: bool=False) -> None: 

4335 """Update the Spell Check dialog.""" 

4336 c = self.c 

4337 if fill: 

4338 self.fillbox([]) 

4339 self.updateButtons() 

4340 if show: 

4341 self.bringToFront() 

4342 c.bodyWantsFocus() 

4343 #@+node:ekr.20110605121601.18402: *4* updateButtons (spellTab) 

4344 def updateButtons(self) -> bool: 

4345 """Enable or disable buttons in the Check Spelling dialog.""" 

4346 c = self.c 

4347 top, w = c.frame.top, c.frame.body.wrapper 

4348 state = bool(self.suggestions and w.hasSelection()) 

4349 top.leo_spell_btn_Change.setDisabled(not state) 

4350 top.leo_spell_btn_FindChange.setDisabled(not state) 

4351 return state 

4352 #@-others 

4353#@+node:ekr.20110605121601.18438: ** class LeoQtTreeTab 

4354class LeoQtTreeTab: 

4355 """ 

4356 A class representing a so-called tree-tab. 

4357 

4358 Actually, it represents a combo box 

4359 """ 

4360 #@+others 

4361 #@+node:ekr.20110605121601.18439: *3* Birth & death 

4362 #@+node:ekr.20110605121601.18440: *4* ctor (LeoQtTreeTab) 

4363 def __init__(self, c: Cmdr, iconBar: Widget) -> None: 

4364 """Ctor for LeoQtTreeTab class.""" 

4365 

4366 self.c = c 

4367 self.cc = c.chapterController 

4368 assert self.cc 

4369 self.iconBar = iconBar 

4370 self.lockout = False # True: do not redraw. 

4371 self.tabNames: List[str] = [] # The list of tab names. Changes when tabs are renamed. 

4372 self.w: Any = None # The QComboBox, not a QWidget. 

4373 # self.reloadSettings() 

4374 self.createControl() 

4375 #@+node:ekr.20110605121601.18441: *4* tt.createControl (defines class LeoQComboBox) 

4376 def createControl(self) -> None: 

4377 

4378 

4379 class LeoQComboBox(QtWidgets.QComboBox): # type:ignore 

4380 """Create a subclass in order to handle focusInEvents.""" 

4381 

4382 def __init__(self, tt: Wrapper) -> None: 

4383 self.leo_tt = tt 

4384 super().__init__() 

4385 # Fix #458: Chapters drop-down list is not automatically resized. 

4386 self.setSizeAdjustPolicy(SizeAdjustPolicy.AdjustToContents) 

4387 

4388 def focusInEvent(self, event: Event) -> None: 

4389 self.leo_tt.setNames() 

4390 QtWidgets.QComboBox.focusInEvent(self, event) # Call the base class 

4391 

4392 tt = self 

4393 frame = QtWidgets.QLabel('Chapters: ') 

4394 tt.iconBar.addWidget(frame) 

4395 tt.w = w = LeoQComboBox(tt) 

4396 tt.setNames() 

4397 tt.iconBar.addWidget(w) 

4398 

4399 def onIndexChanged(s: Any, tt: Any=tt) -> None: 

4400 if isinstance(s, int): 

4401 s = '' if s == -1 else tt.w.currentText() 

4402 else: # s is the tab name. 

4403 pass 

4404 if s and not tt.cc.selectChapterLockout: 

4405 tt.selectTab(s) 

4406 

4407 # A change: the argument could now be an int instead of a string. 

4408 w.currentIndexChanged.connect(onIndexChanged) 

4409 #@+node:ekr.20110605121601.18443: *3* tt.createTab 

4410 def createTab(self, tabName: str, select: bool=True) -> None: 

4411 """LeoQtTreeTab.""" 

4412 tt = self 

4413 # Avoid a glitch during initing. 

4414 if tabName != 'main' and tabName not in tt.tabNames: 

4415 tt.tabNames.append(tabName) 

4416 tt.setNames() 

4417 #@+node:ekr.20110605121601.18444: *3* tt.destroyTab 

4418 def destroyTab(self, tabName: str) -> None: 

4419 """LeoQtTreeTab.""" 

4420 tt = self 

4421 if tabName in tt.tabNames: 

4422 tt.tabNames.remove(tabName) 

4423 tt.setNames() 

4424 #@+node:ekr.20110605121601.18445: *3* tt.selectTab 

4425 def selectTab(self, tabName: str) -> None: 

4426 """LeoQtTreeTab.""" 

4427 tt, c, cc = self, self.c, self.cc 

4428 exists = tabName in self.tabNames 

4429 c.treeWantsFocusNow() # Fix #969. Somehow this is important. 

4430 if not exists: 

4431 tt.createTab(tabName) # Calls tt.setNames() 

4432 if tt.lockout: 

4433 return 

4434 cc.selectChapterByName(tabName) 

4435 c.redraw() 

4436 c.outerUpdate() 

4437 #@+node:ekr.20110605121601.18446: *3* tt.setTabLabel 

4438 def setTabLabel(self, tabName: str) -> None: 

4439 """LeoQtTreeTab.""" 

4440 w = self.w 

4441 i = w.findText(tabName) 

4442 if i > -1: 

4443 w.setCurrentIndex(i) 

4444 #@+node:ekr.20110605121601.18447: *3* tt.setNames 

4445 def setNames(self) -> None: 

4446 """LeoQtTreeTab: Recreate the list of items.""" 

4447 w = self.w 

4448 names = self.cc.setAllChapterNames() 

4449 w.clear() 

4450 w.insertItems(0, names) 

4451 #@-others 

4452#@+node:ekr.20110605121601.18448: ** class LeoTabbedTopLevel (LeoBaseTabWidget) 

4453class LeoTabbedTopLevel(LeoBaseTabWidget): 

4454 """ Toplevel frame for tabbed ui """ 

4455 

4456 def __init__(self, *args: Any, **kwargs: Any) -> None: 

4457 super().__init__(*args, **kwargs) 

4458 ## middle click close on tabs -- JMP 20140505 

4459 self.setMovable(False) 

4460 tb = QtTabBarWrapper(self) 

4461 self.setTabBar(tb) 

4462#@+node:peckj.20140505102552.10377: ** class QtTabBarWrapper (QTabBar) 

4463class QtTabBarWrapper(QtWidgets.QTabBar): # type:ignore 

4464 #@+others 

4465 #@+node:peckj.20140516114832.10108: *3* __init__ 

4466 def __init__(self, parent: Widget=None) -> None: 

4467 super().__init__(parent) 

4468 self.setMovable(True) 

4469 #@+node:peckj.20140516114832.10109: *3* mouseReleaseEvent (QtTabBarWrapper) 

4470 def mouseReleaseEvent(self, event: Event) -> None: 

4471 # middle click close on tabs -- JMP 20140505 

4472 # closes Launchpad bug: https://bugs.launchpad.net/leo-editor/+bug/1183528 

4473 if event.button() == MouseButton.MiddleButton: 

4474 self.tabCloseRequested.emit(self.tabAt(event.pos())) 

4475 QtWidgets.QTabBar.mouseReleaseEvent(self, event) 

4476 #@-others 

4477#@+node:ekr.20110605121601.18458: ** class QtMenuWrapper (LeoQtMenu,QMenu) 

4478class QtMenuWrapper(LeoQtMenu, QtWidgets.QMenu): # type:ignore 

4479 #@+others 

4480 #@+node:ekr.20110605121601.18459: *3* ctor and __repr__(QtMenuWrapper) 

4481 def __init__(self, c: Cmdr, frame: Widget, parent: Widget, label: str) -> None: 

4482 """ctor for QtMenuWrapper class.""" 

4483 assert c 

4484 assert frame 

4485 if parent is None: 

4486 parent = c.frame.top.menuBar() 

4487 # 

4488 # For reasons unknown, the calls must be in this order. 

4489 # Presumably, the order of base classes also matters(!) 

4490 LeoQtMenu.__init__(self, c, frame, label) 

4491 QtWidgets.QMenu.__init__(self, parent) 

4492 label = label.replace('&', '').lower() 

4493 self.leo_menu_label = label 

4494 action = self.menuAction() 

4495 if action: 

4496 action.leo_menu_label = label 

4497 self.aboutToShow.connect(self.onAboutToShow) 

4498 

4499 def __repr__(self) -> str: 

4500 return f"<QtMenuWrapper {self.leo_menu_label}>" 

4501 #@+node:ekr.20110605121601.18460: *3* onAboutToShow & helpers (QtMenuWrapper) 

4502 def onAboutToShow(self, *args: Any, **keys: Any) -> None: 

4503 

4504 name = self.leo_menu_label 

4505 if not name: 

4506 return 

4507 for action in self.actions(): 

4508 commandName = hasattr(action, 'leo_command_name') and action.leo_command_name 

4509 if commandName: 

4510 self.leo_update_shortcut(action, commandName) 

4511 self.leo_enable_menu_item(action, commandName) 

4512 self.leo_update_menu_label(action, commandName) 

4513 #@+node:ekr.20120120095156.10261: *4* leo_enable_menu_item 

4514 def leo_enable_menu_item(self, action: Any, commandName: str) -> None: 

4515 func = self.c.frame.menu.enable_dict.get(commandName) 

4516 if action and func: 

4517 val = func() 

4518 action.setEnabled(bool(val)) 

4519 #@+node:ekr.20120124115444.10190: *4* leo_update_menu_label 

4520 def leo_update_menu_label(self, action: Any, commandName: str) -> None: 

4521 c = self.c 

4522 if action and commandName == 'mark': 

4523 action.setText('UnMark' if c.p.isMarked() else 'Mark') 

4524 # Set the proper shortcut. 

4525 self.leo_update_shortcut(action, commandName) 

4526 #@+node:ekr.20120120095156.10260: *4* leo_update_shortcut 

4527 def leo_update_shortcut(self, action: Any, commandName: str) -> None: 

4528 

4529 c, k = self.c, self.c.k 

4530 if action: 

4531 s = action.text() 

4532 parts = s.split('\t') 

4533 if len(parts) >= 2: 

4534 s = parts[0] 

4535 key, aList = c.config.getShortcut(commandName) 

4536 if aList: 

4537 result = [] 

4538 for bi in aList: 

4539 # Don't show mode-related bindings. 

4540 if not bi.isModeBinding(): 

4541 accel = k.prettyPrintKey(bi.stroke) 

4542 result.append(accel) 

4543 # Break here if we want to show only one accerator. 

4544 action.setText(f"{s}\t{', '.join(result)}") 

4545 else: 

4546 action.setText(s) 

4547 else: 

4548 g.trace(f"can not happen: no action for {commandName}") 

4549 #@-others 

4550#@+node:ekr.20110605121601.18461: ** class QtSearchWidget 

4551class QtSearchWidget: 

4552 """A dummy widget class to pass to Leo's core find code.""" 

4553 

4554 def __init__(self) -> None: 

4555 self.insertPoint = 0 

4556 self.selection = 0, 0 

4557 self.wrapper = self 

4558 self.body = self 

4559 self.text = None 

4560#@+node:ekr.20110605121601.18464: ** class TabbedFrameFactory 

4561class TabbedFrameFactory: 

4562 """ 

4563 'Toplevel' frame builder for tabbed toplevel interface 

4564 

4565 This causes Leo to maintain only one toplevel window, 

4566 with multiple tabs for documents 

4567 """ 

4568 #@+others 

4569 #@+node:ekr.20110605121601.18465: *3* frameFactory.__init__ & __repr__ 

4570 def __init__(self) -> None: 

4571 # Will be created when first frame appears. 

4572 # Workaround a problem setting the window title when tabs are shown. 

4573 self.alwaysShowTabs = True 

4574 self.leoFrames: Dict[Any, Widget] = {} # Keys are DynamicWindows, values are frames. 

4575 self.masterFrame: Widget = None 

4576 self.createTabCommands() 

4577 #@+node:ekr.20110605121601.18466: *3* frameFactory.createFrame (changed, makes dw) 

4578 def createFrame(self, leoFrame: Widget) -> Widget: 

4579 

4580 c = leoFrame.c 

4581 tabw = self.masterFrame 

4582 dw = DynamicWindow(c, tabw) 

4583 self.leoFrames[dw] = leoFrame 

4584 # Shorten the title. 

4585 title = os.path.basename(c.mFileName) if c.mFileName else leoFrame.title 

4586 tip = leoFrame.title 

4587 dw.setWindowTitle(tip) 

4588 idx = tabw.addTab(dw, title) 

4589 if tip: 

4590 tabw.setTabToolTip(idx, tip) 

4591 dw.construct(master=tabw) 

4592 tabw.setCurrentIndex(idx) 

4593 g.app.gui.setFilter(c, dw, dw, tag='tabbed-frame') 

4594 # 

4595 # Work around the problem with missing dirty indicator 

4596 # by always showing the tab. 

4597 tabw.tabBar().setVisible(self.alwaysShowTabs or tabw.count() > 1) 

4598 tabw.setTabsClosable(c.config.getBool('outline-tabs-show-close', True)) 

4599 if not g.unitTesting: 

4600 # #1327: Must always do this. 

4601 # 2021/09/12: but not for new unit tests! 

4602 dw.show() 

4603 tabw.show() 

4604 return dw 

4605 #@+node:ekr.20110605121601.18468: *3* frameFactory.createMaster 

4606 def createMaster(self) -> None: 

4607 

4608 window = self.masterFrame = LeoTabbedTopLevel(factory=self) 

4609 tabbar = window.tabBar() 

4610 g.app.gui.attachLeoIcon(window) 

4611 try: 

4612 tabbar.setTabsClosable(True) 

4613 tabbar.tabCloseRequested.connect(self.slotCloseRequest) 

4614 except AttributeError: 

4615 pass # Qt 4.4 does not support setTabsClosable 

4616 window.currentChanged.connect(self.slotCurrentChanged) 

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

4618 g.trace( 

4619 f"minimized: {g.app.start_minimized}, " 

4620 f"maximized: {g.app.start_maximized}, " 

4621 f"fullscreen: {g.app.start_fullscreen}") 

4622 # 

4623 # #1189: We *can* (and should) minimize here, to eliminate flash. 

4624 if g.app.start_minimized: 

4625 window.showMinimized() 

4626 #@+node:ekr.20110605121601.18472: *3* frameFactory.createTabCommands 

4627 def detachTab(self, wdg: Widget) -> None: 

4628 """ Detach specified tab as individual toplevel window """ 

4629 del self.leoFrames[wdg] 

4630 wdg.setParent(None) 

4631 wdg.show() 

4632 

4633 def createTabCommands(self) -> None: 

4634 #@+<< Commands for tabs >> 

4635 #@+node:ekr.20110605121601.18473: *4* << Commands for tabs >> 

4636 @g.command('tab-detach') 

4637 def tab_detach(event: Event) -> None: 

4638 """ Detach current tab from tab bar """ 

4639 if len(self.leoFrames) < 2: 

4640 g.es_print_error("Can't detach last tab") 

4641 return 

4642 c = event['c'] 

4643 f = c.frame 

4644 tabwidget = g.app.gui.frameFactory.masterFrame 

4645 tabwidget.detach(tabwidget.indexOf(f.top)) 

4646 f.top.setWindowTitle(f.title + ' [D]') 

4647 

4648 # this is actually not tab-specific, move elsewhere? 

4649 

4650 @g.command('close-others') 

4651 def close_others(event: Event) -> None: 

4652 """Close all windows except the present window.""" 

4653 myc = event['c'] 

4654 for c in g.app.commanders(): 

4655 if c is not myc: 

4656 c.close() 

4657 

4658 def tab_cycle(offset: int) -> None: 

4659 

4660 tabw = self.masterFrame 

4661 cur = tabw.currentIndex() 

4662 count = tabw.count() 

4663 # g.es("cur: %s, count: %s, offset: %s" % (cur,count,offset)) 

4664 cur += offset 

4665 if cur < 0: 

4666 cur = count - 1 

4667 elif cur >= count: 

4668 cur = 0 

4669 tabw.setCurrentIndex(cur) 

4670 self.focusCurrentBody() 

4671 

4672 @g.command('tab-cycle-next') 

4673 def tab_cycle_next(event: Event) -> None: 

4674 """ Cycle to next tab """ 

4675 tab_cycle(1) 

4676 

4677 @g.command('tab-cycle-previous') 

4678 def tab_cycle_previous(event: Event) -> None: 

4679 """ Cycle to next tab """ 

4680 tab_cycle(-1) 

4681 #@-<< Commands for tabs >> 

4682 #@+node:ekr.20110605121601.18467: *3* frameFactory.deleteFrame 

4683 def deleteFrame(self, wdg: Widget) -> None: 

4684 

4685 if not wdg: 

4686 return 

4687 if wdg not in self.leoFrames: 

4688 # probably detached tab 

4689 self.masterFrame.delete(wdg) 

4690 return 

4691 tabw = self.masterFrame 

4692 idx = tabw.indexOf(wdg) 

4693 tabw.removeTab(idx) 

4694 del self.leoFrames[wdg] 

4695 wdg2 = tabw.currentWidget() 

4696 if wdg2: 

4697 g.app.selectLeoWindow(wdg2.leo_c) 

4698 tabw.tabBar().setVisible(self.alwaysShowTabs or tabw.count() > 1) 

4699 #@+node:ekr.20110605121601.18471: *3* frameFactory.focusCurrentBody 

4700 def focusCurrentBody(self) -> None: 

4701 """ Focus body control of current tab """ 

4702 tabw = self.masterFrame 

4703 w = tabw.currentWidget() 

4704 w.setFocus() 

4705 f = self.leoFrames[w] 

4706 c = f.c 

4707 c.bodyWantsFocusNow() 

4708 # Fix bug 690260: correct the log. 

4709 g.app.log = f.log 

4710 #@+node:ekr.20110605121601.18469: *3* frameFactory.setTabForCommander 

4711 def setTabForCommander(self, c: Cmdr) -> None: 

4712 tabw = self.masterFrame # a QTabWidget 

4713 for dw in self.leoFrames: # A dict whose keys are DynamicWindows. 

4714 if dw.leo_c == c: 

4715 for i in range(tabw.count()): 

4716 if tabw.widget(i) == dw: 

4717 tabw.setCurrentIndex(i) 

4718 break 

4719 break 

4720 #@+node:ekr.20110605121601.18470: *3* frameFactory.signal handlers 

4721 def slotCloseRequest(self, idx: int) -> None: 

4722 

4723 tabw = self.masterFrame 

4724 w = tabw.widget(idx) 

4725 f = self.leoFrames[w] 

4726 c = f.c 

4727 # 2012/03/04: Don't set the frame here. 

4728 # Wait until the next slotCurrentChanged event. 

4729 # This keeps the log and the QTabbedWidget in sync. 

4730 c.close(new_c=None) 

4731 

4732 def slotCurrentChanged(self, idx: str) -> None: 

4733 # Two events are generated, one for the tab losing focus, 

4734 # and another event for the tab gaining focus. 

4735 tabw = self.masterFrame 

4736 w = tabw.widget(idx) 

4737 f = self.leoFrames.get(w) 

4738 if not f: 

4739 return 

4740 tabw.setWindowTitle(f.title) 

4741 # Don't do this: it would break --minimize. 

4742 # g.app.selectLeoWindow(f.c) 

4743 # Fix bug 690260: correct the log. 

4744 g.app.log = f.log 

4745 # Redraw the tab. 

4746 c = f.c 

4747 if c: 

4748 c.redraw() 

4749 #@-others 

4750#@-others 

4751#@@language python 

4752#@@tabwidth -4 

4753#@@pagewidth 70 

4754#@-leo