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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

3116 statements  

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 >> 

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

8from collections import defaultdict 

9import os 

10import platform 

11import sys 

12import time 

13from typing import Any, Dict, List 

14from leo.core import leoGlobals as g 

15from leo.core import leoColor 

16from leo.core import leoColorizer 

17from leo.core import leoFrame 

18from leo.core import leoGui 

19from leo.core import leoMenu 

20from leo.commands import gotoCommands 

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

22from leo.core.leoQt import QAction, Qsci 

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

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

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

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

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

28from leo.plugins import qt_events 

29from leo.plugins import qt_text 

30from leo.plugins import qt_tree 

31from leo.plugins.mod_scripting import build_rclick_tree 

32from leo.plugins.nested_splitter import NestedSplitter 

33#@-<< imports >> 

34#@+others 

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

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

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

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

39def contractBodyPane(event): 

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

41 c = event.get('c') 

42 if not c: 

43 return 

44 f = c.frame 

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

46 f.divideLeoSplitter1(r) 

47 

48expandOutlinePane = contractBodyPane 

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

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

51def contractLogPane(event): 

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

53 c = event.get('c') 

54 if not c: 

55 return 

56 f = c.frame 

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

58 f.divideLeoSplitter2(r) 

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

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

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

62def contractOutlinePane(event): 

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

64 c = event.get('c') 

65 if not c: 

66 return 

67 f = c.frame 

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

69 f.divideLeoSplitter1(r) 

70 

71expandBodyPane = contractOutlinePane 

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

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

74def expandLogPane(event): # type:ignore 

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

76 c = event.get('c') 

77 if not c: 

78 return 

79 f = c.frame 

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

81 f.divideLeoSplitter2(r) 

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

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

84def hideBodyPane(event): 

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

86 c = event.get('c') 

87 if not c: 

88 return 

89 c.frame.divideLeoSplitter1(1.0) 

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

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

92def hideLogPane(event): 

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

94 c = event.get('c') 

95 if not c: 

96 return 

97 c.frame.divideLeoSplitter2(1.0) 

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

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

100def hideOutlinePane(event): 

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

102 c = event.get('c') 

103 if not c: 

104 return 

105 c.frame.divideLeoSplitter1(0.0) 

106 

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

108def body_cmd(name): 

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

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

111 

112def frame_cmd(name): 

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

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

115 

116def log_cmd(name): 

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

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

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

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

121 """ 

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

123 

124 c.frame.top is a DynamicWindow. 

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

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

127 

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

129 support operations requested by Leo's core. 

130 """ 

131 #@+others 

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

133 def __init__(self, c, parent=None): 

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

135 # Called from LeoQtFrame.finishCreate. 

136 # parent is a LeoTabbedTopLevel. 

137 super().__init__(parent) 

138 self.leo_c = c 

139 self.leo_master = None # Set in construct. 

140 self.leo_menubar = None # Set in createMenuBar. 

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

142 self.reloadSettings() 

143 

144 def reloadSettings(self): 

145 c = self.leo_c 

146 c.registerReloadSettings(self) 

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

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

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

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

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

152 if self.show_iconbar: 

153 self.iconBar.show() 

154 else: 

155 self.iconBar.hide() 

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

157 def doSpellBtn(self, btn): 

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

159 # Make *sure* this never crashes. 

160 try: 

161 tab = self.leo_c.spellCommands.handler.tab 

162 button = getattr(tab, btn) 

163 button() 

164 except Exception: 

165 g.es_exception() 

166 

167 def do_leo_spell_btn_Add(self): 

168 self.doSpellBtn('onAddButton') 

169 

170 def do_leo_spell_btn_Change(self): 

171 self.doSpellBtn('onChangeButton') 

172 

173 def do_leo_spell_btn_Find(self): 

174 self.doSpellBtn('onFindButton') 

175 

176 def do_leo_spell_btn_FindChange(self): 

177 self.doSpellBtn('onChangeThenFindButton') 

178 

179 def do_leo_spell_btn_Hide(self): 

180 self.doSpellBtn('onHideButton') 

181 

182 def do_leo_spell_btn_Ignore(self): 

183 self.doSpellBtn('onIgnoreButton') 

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

185 def closeEvent(self, event): 

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

187 c = self.leo_c 

188 if not c.exists: 

189 # Fixes double-prompt bug on Linux. 

190 event.accept() 

191 return 

192 if c.inCommand: 

193 c.requestCloseWindow = True 

194 return 

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

196 if ok: 

197 event.accept() 

198 else: 

199 event.ignore() 

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

201 def construct(self, master=None): 

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

203 c = self.leo_c 

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

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

206 self.reloadSettings() 

207 main_splitter, secondary_splitter = self.createMainWindow() 

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

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

210 self.set_icon_bar_orientation(c) 

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

212 # Calling reloadSettings again would also work. 

213 if not self.show_iconbar: 

214 self.iconBar.hide() 

215 self.leo_menubar = self.menuBar() 

216 self.statusBar = QtWidgets.QStatusBar() 

217 self.setStatusBar(self.statusBar) 

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

219 self.setSplitDirection(main_splitter, secondary_splitter, orientation) 

220 if hasattr(c, 'styleSheetManager'): 

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

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

223 def set_icon_bar_orientation(self, c): 

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

225 d = { 

226 'bottom': ToolBarArea.BottomToolBarArea, 

227 'left': ToolBarArea.LeftToolBarArea, 

228 'right': ToolBarArea.RightToolBarArea, 

229 'top': ToolBarArea.TopToolBarArea, 

230 } 

231 where = self.toolbar_orientation 

232 if not where: 

233 where = 'top' 

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

235 if where: 

236 self.addToolBar(where, self.iconBar) 

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

238 def createMainWindow(self): 

239 """ 

240 Create the component ivars of the main window. 

241 Copied/adapted from qt_main.py. 

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

243 """ 

244 self.setMainWindowOptions() 

245 # Legacy code: will not go away. 

246 self.createCentralWidget() 

247 # Create .verticalLayout 

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

249 if self.bigTree: 

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

251 self.createBodyPane(secondary_splitter) 

252 self.createLogPane(secondary_splitter) 

253 treeFrame = self.createOutlinePane(main_splitter) 

254 main_splitter.addWidget(treeFrame) 

255 main_splitter.addWidget(secondary_splitter) 

256 else: 

257 # Top pane contains outline and log panes. 

258 self.createOutlinePane(secondary_splitter) 

259 self.createLogPane(secondary_splitter) 

260 self.createBodyPane(main_splitter) 

261 self.createMiniBuffer(self.centralwidget) 

262 self.createMenuBar() 

263 self.createStatusBar(self) 

264 # Signals... 

265 QtCore.QMetaObject.connectSlotsByName(self) 

266 return main_splitter, secondary_splitter 

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

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

269 def addNewEditor(self, name): 

270 """Create a new body editor.""" 

271 c, p = self.leo_c, self.leo_c.p 

272 body = c.frame.body 

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

274 # Step 1: create the editor. 

275 parent_frame = c.frame.top.leo_body_inner_frame 

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

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

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

279 self.packLabel(widget) 

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

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

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

283 body.updateInjectedIvars(widget, p) 

284 wrapper.setAllText(p.b) 

285 wrapper.see(0) 

286 c.k.completeAllBindingsForWidget(wrapper) 

287 if isinstance(widget, QtWidgets.QTextEdit): 

288 colorizer = leoColorizer.make_colorizer(c, widget, wrapper) 

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

290 else: 

291 # Scintilla only. 

292 body.recolorWidget(p, wrapper) 

293 return parent_frame, wrapper 

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

295 def createBodyPane(self, parent): 

296 """ 

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

298 """ 

299 c = self.leo_c 

300 # 

301 # Create widgets. 

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

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

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

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

306 page2 = QtWidgets.QWidget() 

307 self.setName(page2, 'bodyPage2') 

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

309 # 

310 # Pack. 

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

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

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

314 if self.use_gutter: 

315 lineWidget = qt_text.LeoLineTextWidget(c, body) 

316 vLayout.addWidget(lineWidget) 

317 else: 

318 vLayout.addWidget(body) 

319 sw.addWidget(page2) 

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

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

322 self.verticalLayout.addWidget(parent) 

323 # 

324 # Official ivars 

325 self.text_page = page2 

326 self.stackedWidget = sw # used by LeoQtBody 

327 self.richTextEdit = body 

328 self.leo_body_frame = bodyFrame 

329 self.leo_body_inner_frame = innerFrame 

330 return bodyFrame 

331 

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

333 def createCentralWidget(self): 

334 """Create the central widget.""" 

335 dw = self 

336 w = QtWidgets.QWidget(dw) 

337 w.setObjectName("centralwidget") 

338 dw.setCentralWidget(w) 

339 # Official ivars. 

340 self.centralwidget = w 

341 return w 

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

343 def createLogPane(self, parent): 

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

345 c = self.leo_c 

346 # 

347 # Create the log frame. 

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

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

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

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

352 # 

353 # Pack. 

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

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

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

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

358 # 

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

360 findScrollArea = QtWidgets.QScrollArea() 

361 findScrollArea.setObjectName('findScrollArea') 

362 # Find tab. 

363 findTab = QtWidgets.QWidget() 

364 findTab.setObjectName('findTab') 

365 # 

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

367 # 

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

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

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

371 if use_minibuffer or not use_dialog: 

372 tabWidget.addTab(findScrollArea, 'Find') 

373 # Complete the Find tab in LeoFind.finishCreate. 

374 self.findScrollArea = findScrollArea 

375 self.findTab = findTab 

376 # 

377 # Spell tab. 

378 spellTab = QtWidgets.QWidget() 

379 spellTab.setObjectName('spellTab') 

380 tabWidget.addTab(spellTab, 'Spell') 

381 self.createSpellTab(spellTab) 

382 tabWidget.setCurrentIndex(1) 

383 # 

384 # Official ivars 

385 self.tabWidget = tabWidget # Used by LeoQtLog. 

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

387 def finishCreateLogPane(self): 

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

389 assert self.findTab 

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

391 self.findScrollArea.setWidget(self.findTab) 

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

393 def createMainLayout(self, parent): 

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

395 # c = self.leo_c 

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

397 main_splitter = NestedSplitter(parent) 

398 main_splitter.setObjectName('main_splitter') 

399 main_splitter.setOrientation(Orientation.Vertical) 

400 secondary_splitter = NestedSplitter(main_splitter) 

401 secondary_splitter.setObjectName('secondary_splitter') 

402 secondary_splitter.setOrientation(Orientation.Horizontal) 

403 # Official ivar: 

404 self.verticalLayout = vLayout 

405 self.setSizePolicy(secondary_splitter) 

406 self.verticalLayout.addWidget(main_splitter) 

407 return main_splitter, secondary_splitter 

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

409 def createMenuBar(self): 

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

411 dw = self 

412 w = QtWidgets.QMenuBar(dw) 

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

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

415 w.setObjectName("menubar") 

416 dw.setMenuBar(w) 

417 # Official ivars. 

418 self.leo_menubar = w 

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

420 def createMiniBuffer(self, parent): 

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

422 # Create widgets. 

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

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

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

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

427 

428 

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

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

431 

432 def focusInEvent(self, event): 

433 self.parent().show() 

434 super().focusInEvent(event) 

435 # Call the base class method. 

436 

437 def focusOutEvent(self, event): 

438 self.store_selection() 

439 super().focusOutEvent(event) 

440 

441 def restore_selection(self): 

442 w = self 

443 i, j, ins = self._sel_and_insert 

444 if i == j: 

445 w.setCursorPosition(i) 

446 else: 

447 length = j - i 

448 # Set selection is a QLineEditMethod 

449 if ins < j: 

450 w.setSelection(j, -length) 

451 else: 

452 w.setSelection(i, length) 

453 

454 def store_selection(self): 

455 w = self 

456 ins = w.cursorPosition() 

457 if w.hasSelectedText(): 

458 i = w.selectionStart() 

459 s = w.selectedText() 

460 j = i + len(s) 

461 else: 

462 i = j = ins 

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

464 

465 lineEdit = VisLineEdit(frame) 

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

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

468 # Pack. 

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

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

471 hLayout.addWidget(label) 

472 hLayout.addWidget(lineEdit) 

473 self.verticalLayout.addWidget(frame) 

474 label.setBuddy(lineEdit) 

475 # Transfers focus request from label to lineEdit. 

476 # 

477 # Official ivars. 

478 self.lineEdit = lineEdit 

479 # self.leo_minibuffer_frame = frame 

480 # self.leo_minibuffer_layout = layout 

481 return frame 

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

483 def createOutlinePane(self, parent): 

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

485 # Create widgets. 

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

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

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

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

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

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

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

493 # Official ivars... 

494 self.treeWidget = treeWidget 

495 return treeFrame 

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

497 def createStatusBar(self, parent): 

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

499 w = QtWidgets.QStatusBar(parent) 

500 w.setObjectName("statusbar") 

501 parent.setStatusBar(w) 

502 # Official ivars. 

503 self.statusBar = w 

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

505 def packLabel(self, w, n=None): 

506 """ 

507 Pack w into the body frame's QVGridLayout. 

508 

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

510 QTextBrowser possibly packed inside a LeoLineTextWidget. 

511 """ 

512 c = self.leo_c 

513 # 

514 # Reuse the grid layout in the body frame. 

515 grid = self.leo_body_frame.layout() 

516 # Pack the label and the text widget. 

517 label = QtWidgets.QLineEdit(None) 

518 label.setObjectName('editorLabel') 

519 label.setText(c.p.h) 

520 if n is None: 

521 n = c.frame.body.numberOfEditors 

522 n = max(0, n - 1) 

523 grid.addWidget(label, 0, n) 

524 grid.addWidget(w, 1, n) 

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

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

527 # Inject the ivar. 

528 w.leo_label = label 

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

530 def setMainWindowOptions(self): 

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

532 dw = self 

533 dw.setObjectName("MainWindow") 

534 dw.resize(691, 635) 

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

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

537 def createButton(self, parent, name, label): 

538 w = QtWidgets.QPushButton(parent) 

539 w.setObjectName(name) 

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

541 return w 

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

543 def createCheckBox(self, parent, name, label): 

544 w = QtWidgets.QCheckBox(parent) 

545 self.setName(w, name) 

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

547 return w 

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

549 def createFrame(self, parent, name, 

550 hPolicy=None, vPolicy=None, 

551 lineWidth=1, 

552 shadow=None, 

553 shape=None, 

554 ): 

555 """Create a Qt Frame.""" 

556 if shadow is None: 

557 shadow = Shadow.Plain 

558 if shape is None: 

559 shape = Shape.NoFrame 

560 # 

561 w = QtWidgets.QFrame(parent) 

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

563 w.setFrameShape(shape) 

564 w.setFrameShadow(shadow) 

565 w.setLineWidth(lineWidth) 

566 self.setName(w, name) 

567 return w 

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

569 def createGrid(self, parent, name, margin=0, spacing=0): 

570 w = QtWidgets.QGridLayout(parent) 

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

572 w.setSpacing(spacing) 

573 self.setName(w, name) 

574 return w 

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

576 def createHLayout(self, parent, name, margin=0, spacing=0): 

577 hLayout = QtWidgets.QHBoxLayout(parent) 

578 hLayout.setSpacing(spacing) 

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

580 self.setName(hLayout, name) 

581 return hLayout 

582 

583 def createVLayout(self, parent, name, margin=0, spacing=0): 

584 vLayout = QtWidgets.QVBoxLayout(parent) 

585 vLayout.setSpacing(spacing) 

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

587 self.setName(vLayout, name) 

588 return vLayout 

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

590 def createLabel(self, parent, name, label): 

591 w = QtWidgets.QLabel(parent) 

592 self.setName(w, name) 

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

594 return w 

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

596 def createLineEdit(self, parent, name, disabled=True): 

597 

598 w = QtWidgets.QLineEdit(parent) 

599 w.setObjectName(name) 

600 w.leo_disabled = disabled # Inject the ivar. 

601 return w 

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

603 def createRadioButton(self, parent, name, label): 

604 w = QtWidgets.QRadioButton(parent) 

605 self.setName(w, name) 

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

607 return w 

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

609 def createStackedWidget(self, parent, name, 

610 lineWidth=1, 

611 hPolicy=None, vPolicy=None, 

612 ): 

613 w = QtWidgets.QStackedWidget(parent) 

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

615 w.setAcceptDrops(True) 

616 w.setLineWidth(1) 

617 self.setName(w, name) 

618 return w 

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

620 def createTabWidget(self, parent, name, hPolicy=None, vPolicy=None): 

621 w = QtWidgets.QTabWidget(parent) 

622 # tb = w.tabBar() 

623 # tb.setTabsClosable(True) 

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

625 self.setName(w, name) 

626 return w 

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

628 def createText(self, parent, name, 

629 lineWidth=0, 

630 shadow=None, 

631 shape=None, 

632 ): 

633 # Create a text widget. 

634 c = self.leo_c 

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

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

637 w = Qsci.QsciScintilla(parent) 

638 self.scintilla_widget = w 

639 else: 

640 if shadow is None: 

641 shadow = Shadow.Plain 

642 if shape is None: 

643 shape = Shape.NoFrame 

644 # 

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

646 w.setFrameShape(shape) 

647 w.setFrameShadow(shadow) 

648 w.setLineWidth(lineWidth) 

649 self.setName(w, name) 

650 return w 

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

652 def createTreeWidget(self, parent, name): 

653 c = self.leo_c 

654 w = LeoQTreeWidget(c, parent) 

655 self.setSizePolicy(w) 

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

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

658 if multiple_selection: 

659 w.setSelectionMode(SelectionMode.ExtendedSelection) 

660 w.setSelectionBehavior(SelectionBehavior.SelectRows) 

661 else: 

662 w.setSelectionMode(SelectionMode.SingleSelection) 

663 w.setSelectionBehavior(SelectionBehavior.SelectItems) 

664 w.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu) 

665 w.setHeaderHidden(False) 

666 self.setName(w, name) 

667 return w 

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

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

670 def createSpellTab(self, parent): 

671 # dw = self 

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

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

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

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

676 table = ( 

677 ('Add', 'Add', 2, 1), 

678 ('Find', 'Find', 2, 0), 

679 ('Change', 'Change', 3, 0), 

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

681 ('Ignore', 'Ignore', 4, 0), 

682 ('Hide', 'Hide', 4, 1), 

683 ) 

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

685 name = f"spell_{label}_button" 

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

687 grid.addWidget(button, row, col) 

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

689 button.clicked.connect(func) 

690 # This name is significant. 

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

692 self.leo_spell_btn_Hide.setCheckable(False) 

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

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

695 listBox = QtWidgets.QListWidget(spellFrame) 

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

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

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

699 listBox.setObjectName("leo_spell_listBox") 

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

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

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

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

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

705 vLayout2.addLayout(grid) 

706 vLayout.addWidget(spellFrame) 

707 listBox.itemDoubleClicked.connect(self.do_leo_spell_btn_FindChange) 

708 # Official ivars. 

709 self.spellFrame = spellFrame 

710 self.spellGrid = grid 

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

712 self.leo_spell_listBox = listBox # Must exist 

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

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

715 def createFindTab(self, parent, tab_widget): 

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

717 c, dw = self.leo_c, self 

718 fc = c.findCommands 

719 assert not fc.ftm 

720 fc.ftm = ftm = FindTabManager(c) 

721 grid = self.create_find_grid(parent) 

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

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

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

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

726 max_row2 = 1 

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

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

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

730 dw.override_events() 

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

732 w = QtWidgets.QWidget() 

733 grid.addWidget(w, row, 0) 

734 grid.addWidget(w, row, 1) 

735 grid.addWidget(w, row, 2) 

736 grid.setRowStretch(row, 100) 

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

738 self.leo_find_widget = tab_widget # A scrollArea. 

739 ftm.init_widgets() 

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

741 def create_find_grid(self, parent): 

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

743 grid.setColumnStretch(0, 100) 

744 grid.setColumnStretch(1, 100) 

745 grid.setColumnStretch(2, 10) 

746 grid.setColumnMinimumWidth(1, 75) 

747 grid.setColumnMinimumWidth(2, 175) 

748 return grid 

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

750 def create_find_header(self, grid, parent, row): 

751 if False: 

752 dw = self 

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

754 grid.addWidget(lab1, row, 0, 1, 2, Alignment.AlignLeft) 

755 # AlignHCenter 

756 row += 1 

757 return row 

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

759 def create_find_findbox(self, grid, parent, row): 

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

761 c, dw = self.leo_c, self 

762 fc = c.findCommands 

763 ftm = fc.ftm 

764 assert ftm.find_findbox is None 

765 ftm.find_findbox = w = dw.createLineEdit( 

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

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

768 grid.addWidget(lab2, row, 0) 

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

770 row += 1 

771 return row 

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

773 def create_find_replacebox(self, grid, parent, row): 

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

775 c, dw = self.leo_c, self 

776 fc = c.findCommands 

777 ftm = fc.ftm 

778 assert ftm.find_replacebox is None 

779 ftm.find_replacebox = w = dw.createLineEdit( 

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

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

782 grid.addWidget(lab3, row, 0) 

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

784 row += 1 

785 return row 

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

787 def create_find_checkboxes(self, grid, parent, max_row2, row): 

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

789 c, dw = self.leo_c, self 

790 fc = c.findCommands 

791 ftm = fc.ftm 

792 

793 def mungeName(kind, label): 

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

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

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

797 return f"{kind}{name}" 

798 

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

800 

801 d = { 

802 'box': dw.createCheckBox, 

803 'rb': dw.createRadioButton, 

804 } 

805 table = ( 

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

807 # The QShortcut class is the workaround. 

808 # First row. 

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

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

811 # Second row. 

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

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

814 # Third row. 

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

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

817 # Fourth row. 

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

819 ('box', 'search &Headline', 3, 1), 

820 # Fifth row. 

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

822 ('box', 'search &Body', 4, 1), 

823 # Sixth row. 

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

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

826 ) 

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

828 max_row2 = max(max_row2, row2) 

829 name = mungeName(kind, label) 

830 func = d.get(kind) 

831 assert func 

832 # Fix the greedy checkbox bug: 

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

834 w = func(parent, name, label) 

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

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

837 assert getattr(ftm, name) is None 

838 setattr(ftm, name, w) 

839 return max_row2 

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

841 def create_find_buttons(self, grid, parent, max_row2, row): 

842 """ 

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

844 """ 

845 dw, k = self, self.leo_c.k 

846 

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

848 table = ( 

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

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

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

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

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

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

855 ) 

856 for row2, col, cmd_name in table: 

857 stroke = k.getStrokeForCommandName(cmd_name) 

858 if stroke: 

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

860 else: 

861 label = cmd_name 

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

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

864 w.setObjectName('find-label') 

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

866 row += max_row2 

867 row += 2 

868 return row 

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

870 def create_help_row(self, grid, parent, row): 

871 # Help row. 

872 if False: 

873 w = self.createLabel(parent, 

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

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

876 row += 1 

877 return row 

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

879 if 0: 

880 

881 def create_find_status(self, grid, parent, row): 

882 """Create the status line.""" 

883 dw = self 

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

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

886 grid.addWidget(status_label, row, 0) 

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

888 # Official ivars. 

889 dw.find_status_label = status_label 

890 dw.find_status_edit = status_line 

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

892 def override_events(self): 

893 # dw = self 

894 c = self.leo_c 

895 fc = c.findCommands 

896 ftm = fc.ftm 

897 # Define class EventWrapper. 

898 #@+others 

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

900 class EventWrapper: 

901 

902 def __init__(self, c, w, next_w, func): 

903 self.c = c 

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

905 self.w = w 

906 self.next_w = next_w 

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

908 self.func = func 

909 self.oldEvent = w.event 

910 w.event = self.wrapper 

911 #@+others 

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

913 def create_d(self): 

914 """Create self.d dictionary.""" 

915 c = self.c 

916 d = {} 

917 table = ( 

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

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

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

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

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

923 'toggle-find-regex-option', 

924 'toggle-find-word-option', 

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

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

927 'find-all', 

928 'find-next', 

929 'find-prev', 

930 'hide-find-tab', 

931 'replace', 

932 'replace-all', 

933 'replace-then-find', 

934 'set-find-everywhere', 

935 'set-find-node-only', 

936 'set-find-suboutline-only', 

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

938 'full-command', 

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

940 ) 

941 for cmd_name in table: 

942 stroke = c.k.getStrokeForCommandName(cmd_name) 

943 if stroke: 

944 d[stroke.s] = cmd_name 

945 return d 

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

947 def wrapper(self, event): 

948 

949 type_ = event.type() 

950 # Must intercept KeyPress for events that generate FocusOut! 

951 if type_ == Type.KeyPress: 

952 return self.keyPress(event) 

953 if type_ == Type.KeyRelease: 

954 return self.keyRelease(event) 

955 return self.oldEvent(event) 

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

957 def keyPress(self, event): 

958 

959 s = event.text() 

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

961 if out: 

962 # Move focus to next widget. 

963 if s == '\t': 

964 if self.next_w: 

965 self.next_w.setFocus(FocusReason.TabFocusReason) 

966 else: 

967 # Do the normal processing. 

968 return self.oldEvent(event) 

969 elif self.func: 

970 self.func() 

971 return True 

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

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

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

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

976 key_event = leoGui.LeoKeyEvent( 

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

978 if key_event.stroke: 

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

980 if cmd_name: 

981 self.c.k.simulateCommand(cmd_name) 

982 return True 

983 # Do the normal processing. 

984 return self.oldEvent(event) 

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

986 def keyRelease(self, event): 

987 return self.oldEvent(event) 

988 #@-others 

989 #@-others 

990 EventWrapper( 

991 c, w=ftm.find_findbox, next_w=ftm.find_replacebox, func=fc.find_next) 

992 EventWrapper( 

993 c, w=ftm.find_replacebox, next_w=ftm.find_next_button, func=fc.find_next) 

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

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

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

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

998 def setName(self, widget, name): 

999 if name: 

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

1001 # name = 'leo_' + name 

1002 widget.setObjectName(name) 

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

1004 def setSizePolicy(self, widget, kind1=None, kind2=None): 

1005 if kind1 is None: 

1006 kind1 = Policy.Ignored 

1007 if kind2 is None: 

1008 kind2 = Policy.Ignored 

1009 sizePolicy = QtWidgets.QSizePolicy(kind1, kind2) 

1010 sizePolicy.setHorizontalStretch(0) 

1011 sizePolicy.setVerticalStretch(0) 

1012 sizePolicy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth()) 

1013 widget.setSizePolicy(sizePolicy) 

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

1015 def tr(self, s): 

1016 # pylint: disable=no-member 

1017 if isQt5 or isQt6: 

1018 # QApplication.UnicodeUTF8 no longer exists. 

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

1020 return QtWidgets.QApplication.translate( 

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

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

1023 def select(self, c): 

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

1025 # Called from the save commands. 

1026 self.leo_master.select(c) 

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

1028 def setGeometry(self, rect): 

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

1030 m = self.leo_master 

1031 assert self.leo_master 

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

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

1034 m.leo_geom_inited = True 

1035 self.leo_master.setGeometry(rect) 

1036 super().setGeometry(rect) 

1037 

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

1039 def setLeoWindowIcon(self): 

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

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

1042 g.app.gui.attachLeoIcon(self) 

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

1044 def setSplitDirection(self, main_splitter, secondary_splitter, orientation): 

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

1046 # c = self.leo_c 

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

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

1049 orientation1 = v if vert else h 

1050 orientation2 = h if vert else v 

1051 main_splitter.setOrientation(orientation1) 

1052 secondary_splitter.setOrientation(orientation2) 

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

1054 if 0: # Override for debugging only. 

1055 

1056 def setWindowTitle(self, s): 

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

1058 # Call the base class method. 

1059 QtWidgets.QMainWindow.setWindowTitle(self, s) 

1060 #@-others 

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

1062class FindTabManager: 

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

1064 #@+others 

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

1066 def __init__(self, c): 

1067 """Ctor for the FindTabManager class.""" 

1068 self.c = c 

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

1070 # Find/change text boxes. 

1071 self.find_findbox = None 

1072 self.find_replacebox = None 

1073 # Check boxes. 

1074 self.check_box_ignore_case = None 

1075 self.check_box_mark_changes = None 

1076 self.check_box_mark_finds = None 

1077 self.check_box_regexp = None 

1078 self.check_box_search_body = None 

1079 self.check_box_search_headline = None 

1080 self.check_box_whole_word = None 

1081 # self.check_box_wrap_around = None 

1082 # Radio buttons 

1083 self.radio_button_entire_outline = None 

1084 self.radio_button_node_only = None 

1085 self.radio_button_suboutline_only = None 

1086 # Push buttons 

1087 self.find_next_button = None 

1088 self.find_prev_button = None 

1089 self.find_all_button = None 

1090 self.help_for_find_commands_button = None 

1091 self.replace_button = None 

1092 self.replace_then_find_button = None 

1093 self.replace_all_button = None 

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

1095 def clear_focus(self): 

1096 self.entry_focus = None 

1097 self.find_findbox.clearFocus() 

1098 

1099 def init_focus(self): 

1100 self.set_entry_focus() 

1101 w = self.find_findbox 

1102 w.setFocus() 

1103 s = w.text() 

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

1105 

1106 def set_entry_focus(self): 

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

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

1109 c = self.c 

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

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

1112 w = c.frame.tree.treeWidget 

1113 self.entry_focus = w 

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

1115 def get_settings(self): 

1116 """ 

1117 Return a g.bunch representing all widget values. 

1118 

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

1120 """ 

1121 return g.Bunch( 

1122 # Find/change strings... 

1123 find_text=self.find_findbox.text(), 

1124 change_text=self.find_replacebox.text(), 

1125 # Find options... 

1126 ignore_case=self.check_box_ignore_case.isChecked(), 

1127 mark_changes=self.check_box_mark_changes.isChecked(), 

1128 mark_finds=self.check_box_mark_finds.isChecked(), 

1129 node_only=self.radio_button_node_only.isChecked(), 

1130 pattern_match=self.check_box_regexp.isChecked(), 

1131 # reverse = False, 

1132 search_body=self.check_box_search_body.isChecked(), 

1133 search_headline=self.check_box_search_headline.isChecked(), 

1134 suboutline_only=self.radio_button_suboutline_only.isChecked(), 

1135 whole_word=self.check_box_whole_word.isChecked(), 

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

1137 ) 

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

1139 def init_widgets(self): 

1140 """ 

1141 Init widgets and ivars from c.config settings. 

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

1143 """ 

1144 c = self.c 

1145 find = c.findCommands 

1146 # Find/change text boxes. 

1147 table1 = ( 

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

1149 ('find_replacebox', 'change_text', ''), 

1150 ) 

1151 for ivar, setting_name, default in table1: 

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

1153 w = getattr(self, ivar) 

1154 w.insert(s) 

1155 if find.minibuffer_mode: 

1156 w.clearFocus() 

1157 else: 

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

1159 # Check boxes. 

1160 table2 = ( 

1161 ('ignore_case', self.check_box_ignore_case), 

1162 ('mark_changes', self.check_box_mark_changes), 

1163 ('mark_finds', self.check_box_mark_finds), 

1164 ('pattern_match', self.check_box_regexp), 

1165 ('search_body', self.check_box_search_body), 

1166 ('search_headline', self.check_box_search_headline), 

1167 ('whole_word', self.check_box_whole_word), 

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

1169 ) 

1170 for setting_name, w in table2: 

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

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

1173 assert hasattr(find, setting_name), setting_name 

1174 setattr(find, setting_name, val) 

1175 if val: 

1176 w.toggle() 

1177 

1178 def check_box_callback(n, setting_name=setting_name, w=w): 

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

1180 # focus_w = QtWidgets.QApplication.focusWidget() 

1181 val = w.isChecked() 

1182 assert hasattr(find, setting_name), setting_name 

1183 setattr(find, setting_name, val) 

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

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

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

1187 c.bodyWantsFocusNow() 

1188 

1189 w.stateChanged.connect(check_box_callback) 

1190 # Radio buttons 

1191 table3 = ( 

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

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

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

1195 ) 

1196 for setting_name, ivar, w in table3: 

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

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

1199 if ivar is not None: 

1200 assert hasattr(find, setting_name), setting_name 

1201 setattr(find, setting_name, val) 

1202 w.toggle() 

1203 

1204 def radio_button_callback(n, ivar=ivar, setting_name=setting_name, w=w): 

1205 val = w.isChecked() 

1206 if ivar: 

1207 assert hasattr(find, ivar), ivar 

1208 setattr(find, ivar, val) 

1209 

1210 w.toggled.connect(radio_button_callback) 

1211 # Ensure one radio button is set. 

1212 if not find.node_only and not find.suboutline_only: 

1213 w = self.radio_button_entire_outline 

1214 w.toggle() 

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

1216 def set_widgets_from_dict(self, d): 

1217 """Set all settings from d.""" 

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

1219 c = self.c 

1220 find = c.findCommands 

1221 # Set find text. 

1222 find_text = d.get('find_text') 

1223 self.set_find_text(find_text) 

1224 find.find_text = find_text 

1225 # Set change text. 

1226 change_text = d.get('change_text') 

1227 self.set_change_text(change_text) 

1228 find.change_text = change_text 

1229 # Check boxes... 

1230 table1 = ( 

1231 ('ignore_case', self.check_box_ignore_case), 

1232 ('mark_changes', self.check_box_mark_changes), 

1233 ('mark_finds', self.check_box_mark_finds), 

1234 ('pattern_match', self.check_box_regexp), 

1235 ('search_body', self.check_box_search_body), 

1236 ('search_headline', self.check_box_search_headline), 

1237 ('whole_word', self.check_box_whole_word), 

1238 ) 

1239 for setting_name, w in table1: 

1240 val = d.get(setting_name, False) 

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

1242 assert hasattr(find, setting_name), setting_name 

1243 setattr(find, setting_name, val) 

1244 w.setChecked(val) 

1245 # Radio buttons... 

1246 table2 = ( 

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

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

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

1250 ) 

1251 for setting_name, ivar, w in table2: 

1252 val = d.get(setting_name, False) 

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

1254 if ivar is not None: 

1255 assert hasattr(find, setting_name), setting_name 

1256 setattr(find, setting_name, val) 

1257 w.setChecked(val) 

1258 # Ensure one radio button is set. 

1259 if not find.node_only and not find.suboutline_only: 

1260 w = self.radio_button_entire_outline 

1261 w.setChecked(val) 

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

1263 def set_body_and_headline_checkbox(self): 

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

1265 # #1840: headline-only one-shot 

1266 c = self.c 

1267 find = c.findCommands 

1268 if not find: 

1269 return 

1270 table = ( 

1271 ('search_body', self.check_box_search_body), 

1272 ('search_headline', self.check_box_search_headline), 

1273 ) 

1274 for setting_name, w in table: 

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

1276 if val != w.isChecked(): 

1277 w.toggle() 

1278 if find.minibuffer_mode: 

1279 find.show_find_options_in_status_area() 

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

1281 def set_ignore_case(self, aBool): 

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

1283 c = self.c 

1284 c.findCommands.ignore_case = aBool 

1285 w = self.check_box_ignore_case 

1286 w.setChecked(aBool) 

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

1288 def set_radio_button(self, name): 

1289 """Set the value of the radio buttons""" 

1290 c = self.c 

1291 find = c.findCommands 

1292 d = { 

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

1294 'node-only': self.radio_button_node_only, 

1295 'entire-outline': self.radio_button_entire_outline, 

1296 'suboutline-only': self.radio_button_suboutline_only, 

1297 } 

1298 w = d.get(name) 

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

1300 if not w.isChecked(): 

1301 w.toggle() 

1302 if find.minibuffer_mode: 

1303 find.show_find_options_in_status_area() 

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

1305 def get_find_text(self): 

1306 s = self.find_findbox.text() 

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

1308 s = s[:-1] 

1309 return s 

1310 

1311 def get_change_text(self): 

1312 s = self.find_replacebox.text() 

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

1314 s = s[:-1] 

1315 return s 

1316 

1317 getChangeText = get_change_text 

1318 

1319 def set_find_text(self, s): 

1320 w = self.find_findbox 

1321 s = g.checkUnicode(s) 

1322 w.clear() 

1323 w.insert(s) 

1324 

1325 def set_change_text(self, s): 

1326 w = self.find_replacebox 

1327 s = g.checkUnicode(s) 

1328 w.clear() 

1329 w.insert(s) 

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

1331 #@@nobeautify 

1332 

1333 def toggle_checkbox(self, checkbox_name): 

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

1335 c = self.c 

1336 find = c.findCommands 

1337 if not find: 

1338 return 

1339 d = { 

1340 'ignore_case': self.check_box_ignore_case, 

1341 'mark_changes': self.check_box_mark_changes, 

1342 'mark_finds': self.check_box_mark_finds, 

1343 'pattern_match': self.check_box_regexp, 

1344 'search_body': self.check_box_search_body, 

1345 'search_headline': self.check_box_search_headline, 

1346 'whole_word': self.check_box_whole_word, 

1347 # 'wrap': self.check_box_wrap_around, 

1348 } 

1349 w = d.get(checkbox_name) 

1350 assert w 

1351 assert hasattr(find, checkbox_name), checkbox_name 

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

1353 if find.minibuffer_mode: 

1354 find.show_find_options_in_status_area() 

1355 #@-others 

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

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

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

1359 #@+others 

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

1361 def __init__(self, *args, **kwargs): 

1362 

1363 # 

1364 # Called from frameFactory.createMaster. 

1365 # 

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

1367 if self.factory: 

1368 del kwargs['factory'] 

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

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

1371 self.setMovable(True) 

1372 

1373 def tabContextMenu(point): 

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

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

1376 return 

1377 menu = QtWidgets.QMenu() 

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

1379 if True: 

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

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

1382 if self.count() > 1: 

1383 a = menu.addAction("Detach") 

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

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

1386 a.triggered.connect( 

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

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

1389 a.triggered.connect( 

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

1391 if self.detached: 

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

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

1394 

1395 global_point = self.mapToGlobal(point) 

1396 menu.exec_(global_point) 

1397 self.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu) 

1398 self.customContextMenuRequested.connect(tabContextMenu) 

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

1400 def new_outline(self, index): 

1401 """Open a new outline tab.""" 

1402 w = self.widget(index) 

1403 c = w.leo_c 

1404 c.new() 

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

1406 def detach(self, index): 

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

1408 w = self.widget(index) 

1409 name = self.tabText(index) 

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

1411 self.factory.detachTab(w) 

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

1413 icon = QtGui.QIcon(icon) 

1414 if icon: 

1415 w.window().setWindowIcon(icon) 

1416 c = w.leo_c 

1417 if c.styleSheetManager: 

1418 c.styleSheetManager.set_style_sheets(w=w) 

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

1420 w.move(20, 20) 

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

1422 return w 

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

1424 def tile(self, index, orientation='V'): 

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

1426 w = self.widget(index) 

1427 window = w.window() 

1428 # window.showMaximized() 

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

1430 # user needs to do it before using this function 

1431 fg = window.frameGeometry() 

1432 geom = window.geometry() 

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

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

1435 w = self.detach(index) 

1436 if window.isMaximized(): 

1437 window.showNormal() 

1438 if orientation == 'V': 

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

1440 window.resize(ww / 2, wh) 

1441 window.move(x, y) 

1442 w.resize(ww / 2, wh) 

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

1444 else: 

1445 window.resize(ww, wh / 2) 

1446 window.move(x, y) 

1447 w.resize(ww, wh / 2) 

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

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

1450 def reattach_all(self): 

1451 """reattach all detached tabs""" 

1452 for name, w in self.detached: 

1453 self.addTab(w, name) 

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

1455 self.detached = [] 

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

1457 def delete(self, w): 

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

1459 has been deleted""" 

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

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

1462 def setChanged(self, c, changed): 

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

1464 # Find the tab corresponding to c. 

1465 dw = c.frame.top # A DynamicWindow 

1466 i = self.indexOf(dw) 

1467 if i < 0: 

1468 return 

1469 s = self.tabText(i) 

1470 if len(s) > 2: 

1471 if changed: 

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

1473 title = "* " + s 

1474 self.setTabText(i, title) 

1475 else: 

1476 if s.startswith('* '): 

1477 title = s[2:] 

1478 self.setTabText(i, title) 

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

1480 def setTabName(self, c, fileName): 

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

1482 # Find the tab corresponding to c. 

1483 dw = c.frame.top # A DynamicWindow 

1484 i = self.indexOf(dw) 

1485 if i > -1: 

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

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

1488 def closeEvent(self, event): 

1489 """Handle a close event.""" 

1490 g.app.gui.close_event(event) 

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

1492 def select(self, c): 

1493 """Select the tab for c.""" 

1494 dw = c.frame.top # A DynamicWindow 

1495 i = self.indexOf(dw) 

1496 self.setCurrentIndex(i) 

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

1498 # c.enableMenuBar() 

1499 #@-others 

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

1501class LeoQtBody(leoFrame.LeoBody): 

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

1503 #@+others 

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

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

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

1507 def __init__(self, frame, parentFrame): 

1508 """Ctor for LeoQtBody class.""" 

1509 # Call the base class constructor. 

1510 super().__init__(frame, parentFrame) 

1511 c = self.c 

1512 assert c.frame == frame and frame.c == c 

1513 self.reloadSettings() 

1514 self.set_widget() 

1515 # Sets self.widget and self.wrapper. 

1516 self.setWrap(c.p) 

1517 # For multiple body editors. 

1518 self.editor_name = None 

1519 self.editor_v = None 

1520 self.numberOfEditors = 1 

1521 self.totalNumberOfEditors = 1 

1522 # For renderer panes. 

1523 self.canvasRenderer = None 

1524 self.canvasRendererLabel = None 

1525 self.canvasRendererVisible = False 

1526 self.textRenderer = None 

1527 self.textRendererLabel = None 

1528 self.textRendererVisible = False 

1529 self.textRendererWrapper = None 

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

1531 def getName(self): 

1532 return 'body-widget' 

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

1534 def reloadSettings(self): 

1535 c = self.c 

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

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

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

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

1540 def set_invisibles(self, c): 

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

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

1543 option = QtGui.QTextOption() 

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

1545 # The following works with both Qt5 and Qt6. 

1546 # pylint: disable=no-member 

1547 option.setFlags(option.Flag.ShowTabsAndSpaces) 

1548 d.setDefaultTextOption(option) 

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

1550 def set_widget(self): 

1551 """Set the actual gui widget.""" 

1552 c = self.c 

1553 top = c.frame.top 

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

1555 if sw: 

1556 sw.setCurrentIndex(1) 

1557 if self.useScintilla and not Qsci: 

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

1559 if self.useScintilla and Qsci: 

1560 # A Qsci.QsciSintilla object. 

1561 # dw.createText sets self.scintilla_widget 

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

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

1564 self.colorizer = leoColorizer.QScintillaColorizer( 

1565 c, self.widget, self.wrapper) 

1566 else: 

1567 self.widget = top.richTextEdit # A LeoQTextBrowser 

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

1569 self.widget.setAcceptRichText(False) 

1570 self.colorizer = leoColorizer.make_colorizer(c, self.widget, self.wrapper) 

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

1572 def forceWrap(self, p): 

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

1574 if not p or self.useScintilla: 

1575 return 

1576 c = self.c 

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

1578 wrap = WrapMode.WrapAtWordBoundaryOrAnywhere 

1579 w.setWordWrapMode(wrap) 

1580 

1581 def setWrap(self, p): 

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

1583 if not p or self.useScintilla: 

1584 return 

1585 c = self.c 

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

1587 wrap = g.scanAllAtWrapDirectives(c, p) 

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

1589 w.setHorizontalScrollBarPolicy(policy) 

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

1591 w.setWordWrapMode(wrap) 

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

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

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

1595 # An override of leoFrame.addEditor. 

1596 

1597 @body_cmd('editor-add') 

1598 @body_cmd('add-editor') 

1599 def add_editor_command(self, event=None): 

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

1601 c, p = self.c, self.c.p 

1602 d = self.editorWrappers 

1603 dw = c.frame.top 

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

1605 widget = wrapper.widget 

1606 self.totalNumberOfEditors += 1 

1607 self.numberOfEditors += 1 

1608 if self.totalNumberOfEditors == 2: 

1609 d['1'] = wrapper 

1610 # Pack the original body editor. 

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

1612 if self.use_gutter: 

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

1614 widget.leo_label = widget.parent().leo_label 

1615 else: 

1616 dw.packLabel(widget, n=1) 

1617 name = f"{self.totalNumberOfEditors}" 

1618 f, wrapper = dw.addNewEditor(name) 

1619 assert g.isTextWrapper(wrapper), wrapper 

1620 assert g.isTextWidget(widget), widget 

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

1622 d[name] = wrapper 

1623 if self.numberOfEditors == 2: 

1624 # Inject the ivars into the first editor. 

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

1626 keys = list(d.keys()) 

1627 old_name = keys[0] 

1628 old_wrapper = d.get(old_name) 

1629 old_w = old_wrapper.widget 

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

1631 self.updateInjectedIvars(old_w, p) 

1632 self.selectLabel(old_wrapper) 

1633 # Immediately create the label in the old editor. 

1634 # Switch editors. 

1635 c.frame.body.wrapper = wrapper 

1636 self.selectLabel(wrapper) 

1637 self.selectEditor(wrapper) 

1638 self.updateEditors() 

1639 c.bodyWantsFocus() 

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

1641 def assignPositionToEditor(self, p): 

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

1643 c = self.c 

1644 wrapper = c.frame.body.wrapper 

1645 w = wrapper and wrapper.widget 

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

1647 self.updateInjectedIvars(w, p) 

1648 self.selectLabel(wrapper) 

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

1650 # Use the base class method. 

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

1652 @body_cmd('delete-editor') 

1653 @body_cmd('editor-delete') 

1654 def delete_editor_command(self, event=None): 

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

1656 c, d = self.c, self.editorWrappers 

1657 wrapper = c.frame.body.wrapper 

1658 w = wrapper.widget 

1659 assert g.isTextWrapper(wrapper), wrapper 

1660 assert g.isTextWidget(w), w 

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

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

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

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

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

1666 return 

1667 # 

1668 # Actually delete the widget. 

1669 del d[name] 

1670 f = c.frame.top.leo_body_frame 

1671 layout = f.layout() 

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

1673 if z: 

1674 self.unpackWidget(layout, z) 

1675 # 

1676 # Select another editor. 

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

1678 self.numberOfEditors -= 1 

1679 if self.numberOfEditors == 1: 

1680 w = new_wrapper.widget 

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

1682 if label: 

1683 self.unpackWidget(layout, label) 

1684 w.leo_label = None 

1685 self.selectEditor(new_wrapper) 

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

1687 def findEditorForChapter(self, chapter, p): 

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

1689 c, d = self.c, self.editorWrappers 

1690 values = list(d.values()) 

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

1692 if p: 

1693 for w in values: 

1694 if ( 

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

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

1697 ): 

1698 return w 

1699 # Next, try to match just the chapter. 

1700 for w in values: 

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

1702 return w 

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

1704 return c.frame.body.wrapper 

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

1706 def unselectLabel(self, wrapper): 

1707 # pylint: disable=arguments-differ 

1708 pass 

1709 # self.createChapterIvar(wrapper) 

1710 

1711 def selectLabel(self, wrapper): 

1712 # pylint: disable=arguments-differ 

1713 c = self.c 

1714 w = wrapper.widget 

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

1716 if label: 

1717 label.setEnabled(True) 

1718 label.setText(c.p.h) 

1719 label.setEnabled(False) 

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

1721 selectEditorLockout = False 

1722 

1723 def selectEditor(self, wrapper): 

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

1725 # pylint: disable=arguments-differ 

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

1727 tag = 'qt_body.selectEditor' 

1728 c = self.c 

1729 if not wrapper: 

1730 return c.frame.body.wrapper 

1731 if self.selectEditorLockout: 

1732 return None 

1733 w = wrapper.widget 

1734 assert g.isTextWrapper(wrapper), wrapper 

1735 assert g.isTextWidget(w), w 

1736 if trace: 

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

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

1739 self.deactivateEditors(wrapper) 

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

1741 c.selectPosition(w.leo_p) 

1742 c.bodyWantsFocus() 

1743 return None 

1744 try: 

1745 val = None 

1746 self.selectEditorLockout = True 

1747 val = self.selectEditorHelper(wrapper) 

1748 finally: 

1749 self.selectEditorLockout = False 

1750 return val # Don't put a return in a finally clause. 

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

1752 def selectEditorHelper(self, wrapper): 

1753 

1754 c = self.c 

1755 w = wrapper.widget 

1756 assert g.isTextWrapper(wrapper), wrapper 

1757 assert g.isTextWidget(w), w 

1758 if not w.leo_p: 

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

1760 return 'break' 

1761 # The actual switch. 

1762 self.deactivateEditors(wrapper) 

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

1764 c.frame.body.wrapper = wrapper 

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

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

1767 c.frame.body.widget = w 

1768 w.leo_active = True 

1769 self.switchToChapter(wrapper) 

1770 self.selectLabel(wrapper) 

1771 if not self.ensurePositionExists(w): 

1772 return g.trace('***** no position editor!') 

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

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

1775 return None 

1776 p = w.leo_p 

1777 assert p, p 

1778 c.expandAllAncestors(p) 

1779 c.selectPosition(p) 

1780 # Calls assignPositionToEditor. 

1781 # Calls p.v.restoreCursorAndScroll. 

1782 c.redraw() 

1783 c.recolor() 

1784 c.bodyWantsFocus() 

1785 return None 

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

1787 # Called from addEditor and assignPositionToEditor 

1788 

1789 def updateEditors(self): 

1790 c, p = self.c, self.c.p 

1791 body = p.b 

1792 d = self.editorWrappers 

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

1794 return # There is only the main widget 

1795 w0 = c.frame.body.wrapper 

1796 i, j = w0.getSelectionRange() 

1797 ins = w0.getInsertPoint() 

1798 sb0 = w0.widget.verticalScrollBar() 

1799 pos0 = sb0.sliderPosition() 

1800 for key in d: 

1801 wrapper = d.get(key) 

1802 w = wrapper.widget 

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

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

1805 sb = w.verticalScrollBar() 

1806 pos = sb.sliderPosition() 

1807 wrapper.setAllText(body) 

1808 self.recolorWidget(p, wrapper) 

1809 sb.setSliderPosition(pos) 

1810 c.bodyWantsFocus() 

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

1812 sb0.setSliderPosition(pos0) 

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

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

1815 def computeLabel(self, w): 

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

1817 s = w.leo_label.text() 

1818 else: 

1819 s = '' 

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

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

1822 return s 

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

1824 def createChapterIvar(self, w): 

1825 c = self.c 

1826 cc = c.chapterController 

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

1828 pass 

1829 elif cc and self.use_chapters: 

1830 w.leo_chapter = cc.getSelectedChapter() 

1831 else: 

1832 w.leo_chapter = None 

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

1834 def deactivateEditors(self, wrapper): 

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

1836 d = self.editorWrappers 

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

1838 for key in d: 

1839 wrapper2 = d.get(key) 

1840 w2 = wrapper2.widget 

1841 if hasattr(w2, 'leo_active'): 

1842 active = w2.leo_active 

1843 else: 

1844 active = True 

1845 if wrapper2 != wrapper and active: 

1846 w2.leo_active = False 

1847 self.unselectLabel(wrapper2) 

1848 self.onFocusOut(w2) 

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

1850 def ensurePositionExists(self, w): 

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

1852 c = self.c 

1853 if c.positionExists(w.leo_p): 

1854 return True 

1855 for p2 in c.all_unique_positions(): 

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

1857 w.leo_p = p2.copy() 

1858 return True 

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

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

1861 return False 

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

1863 def injectIvars(self, parentFrame, name, p, wrapper): 

1864 

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

1866 tag = 'qt_body.injectIvars' 

1867 w = wrapper.widget 

1868 assert g.isTextWrapper(wrapper), wrapper 

1869 assert g.isTextWidget(w), w 

1870 if trace: 

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

1872 # Inject ivars 

1873 if name == '1': 

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

1875 else: 

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

1877 w.leo_active = True 

1878 w.leo_bodyBar = None 

1879 w.leo_bodyXBar = None 

1880 w.leo_chapter = None 

1881 # w.leo_colorizer = None # Set in JEditColorizer ctor. 

1882 w.leo_frame = parentFrame 

1883 # w.leo_label = None # Injected by packLabel. 

1884 w.leo_name = name 

1885 w.leo_wrapper = wrapper 

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

1887 def recolorWidget(self, p, wrapper): 

1888 """Support QScintillaColorizer.colorize.""" 

1889 # pylint: disable=arguments-differ 

1890 c = self.c 

1891 colorizer = c.frame.body.colorizer 

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

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

1894 old_wrapper = c.frame.body.wrapper 

1895 c.frame.body.wrapper = wrapper 

1896 try: 

1897 colorizer.colorize(p) 

1898 finally: 

1899 # Restore. 

1900 c.frame.body.wrapper = old_wrapper 

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

1902 def switchToChapter(self, w): 

1903 """select w.leo_chapter.""" 

1904 c = self.c 

1905 cc = c.chapterController 

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

1907 chapter = w.leo_chapter 

1908 name = chapter and chapter.name 

1909 oldChapter = cc.getSelectedChapter() 

1910 if chapter != oldChapter: 

1911 cc.selectChapterByName(name) 

1912 c.bodyWantsFocus() 

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

1914 def unpackWidget(self, layout, w): 

1915 

1916 index = layout.indexOf(w) 

1917 if index == -1: 

1918 return 

1919 item = layout.itemAt(index) 

1920 if item: 

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

1922 layout.removeItem(item) 

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

1924 def updateInjectedIvars(self, w, p): 

1925 

1926 c = self.c 

1927 cc = c.chapterController 

1928 assert g.isTextWidget(w), w 

1929 if cc and self.use_chapters: 

1930 w.leo_chapter = cc.getSelectedChapter() 

1931 else: 

1932 w.leo_chapter = None 

1933 w.leo_p = p.copy() 

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

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

1936 def onFocusIn(self, obj): 

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

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

1939 tag = 'qt_body.onFocusIn' 

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

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

1942 if trace: 

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

1944 if wrapper and wrapper != self.wrapper: 

1945 self.selectEditor(wrapper) 

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

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

1948 obj.setReadOnly(True) 

1949 else: 

1950 obj.setReadOnly(False) 

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

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

1953 def onFocusOut(self, obj): 

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

1955 # Apparently benign. 

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

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

1958 if hasattr(obj, 'setReadOnly'): 

1959 obj.setReadOnly(True) 

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

1961 def onFocusColorHelper(self, kind, obj): 

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

1963 c, vc = self.c, self.c.vimCommands 

1964 if vc and c.vim_mode: 

1965 try: 

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

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

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

1969 except Exception: 

1970 # g.es_exception() 

1971 pass 

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

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

1974 def hideCanvasRenderer(self, event=None): 

1975 """Hide canvas pane.""" 

1976 c, d = self.c, self.editorWrappers 

1977 wrapper = c.frame.body.wrapper 

1978 w = wrapper.widget 

1979 name = w.leo_name 

1980 assert name 

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

1982 assert g.isTextWrapper(wrapper), wrapper 

1983 assert g.isTextWidget(w), w 

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

1985 return 

1986 # 

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

1988 if name == '1': 

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

1990 return 

1991 # 

1992 # Actually delete the widget. 

1993 del d[name] 

1994 f = c.frame.top.leo_body_inner_frame 

1995 layout = f.layout() 

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

1997 if z: 

1998 self.unpackWidget(layout, z) 

1999 # 

2000 # Select another editor. 

2001 w.leo_label = None 

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

2003 self.numberOfEditors -= 1 

2004 if self.numberOfEditors == 1: 

2005 w = new_wrapper.widget 

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

2007 self.unpackWidget(layout, w.leo_label) 

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

2009 self.selectEditor(new_wrapper) 

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

2011 def hideCanvas(self, event=None): 

2012 """Hide canvas pane.""" 

2013 c, d = self.c, self.editorWrappers 

2014 wrapper = c.frame.body.wrapper 

2015 w = wrapper.widget 

2016 name = w.leo_name 

2017 assert name 

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

2019 assert g.isTextWrapper(wrapper), wrapper 

2020 assert g.isTextWidget(w), w 

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

2022 return 

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

2024 if name == '1': 

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

2026 return 

2027 # 

2028 # Actually delete the widget. 

2029 del d[name] 

2030 f = c.frame.top.leo_body_inner_frame 

2031 layout = f.layout() 

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

2033 if z: 

2034 self.unpackWidget(layout, z) 

2035 # 

2036 # Select another editor. 

2037 w.leo_label = None 

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

2039 self.numberOfEditors -= 1 

2040 if self.numberOfEditors == 1: 

2041 w = new_wrapper.widget 

2042 if w.leo_label: 

2043 self.unpackWidget(layout, w.leo_label) 

2044 w.leo_label = None 

2045 self.selectEditor(new_wrapper) 

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

2047 def packRenderer(self, f, name, w): 

2048 n = max(1, self.numberOfEditors) 

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

2050 layout = f.layout() 

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

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

2053 lab = QtWidgets.QLineEdit(f) 

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

2055 lab.setText(name) 

2056 # Pack the label and the widget. 

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

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

2059 layout.setRowStretch(0, 0) 

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

2061 return lab 

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

2063 # An override of leoFrame.addEditor. 

2064 

2065 def showCanvasRenderer(self, event=None): 

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

2067 c = self.c 

2068 f = c.frame.top.leo_body_inner_frame 

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

2070 if not self.canvasRenderer: 

2071 name = 'Graphics Renderer' 

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

2073 w.setObjectName(name) 

2074 if not self.canvasRendererVisible: 

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

2076 self.canvasRendererVisible = True 

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

2078 # An override of leoFrame.addEditor. 

2079 

2080 def showTextRenderer(self, event=None): 

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

2082 c = self.c 

2083 f = c.frame.top.leo_body_inner_frame 

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

2085 if not self.textRenderer: 

2086 name = 'Text Renderer' 

2087 self.textRenderer = w = qt_text.LeoQTextBrowser(f, c, self) 

2088 w.setObjectName(name) 

2089 self.textRendererWrapper = qt_text.QTextEditWrapper( 

2090 w, name='text-renderer', c=c) 

2091 if not self.textRendererVisible: 

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

2093 self.textRendererVisible = True 

2094 #@-others 

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

2096class LeoQtFrame(leoFrame.LeoFrame): 

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

2098 #@+others 

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

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

2101 def __init__(self, c, title, gui): 

2102 

2103 super().__init__(c, gui) 

2104 assert self.c == c 

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

2106 # Official ivars... 

2107 self.iconBar = None 

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

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

2110 self.minibufferVisible = True 

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

2112 self.title = title 

2113 self.setIvars() 

2114 self.reloadSettings() 

2115 

2116 def reloadSettings(self): 

2117 c = self.c 

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

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

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

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

2122 def setIvars(self): 

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

2124 self.bar1 = None 

2125 self.bar2 = None 

2126 self.body = None 

2127 self.f1 = self.f2 = None 

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

2129 self.iconBarComponentName = 'iconBar' 

2130 self.iconFrame = None 

2131 self.log = None 

2132 self.canvas = None 

2133 self.outerFrame = None 

2134 self.statusFrame = None 

2135 self.statusLineComponentName = 'statusLine' 

2136 self.statusText = None 

2137 self.statusLabel = None 

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

2139 self.tree = None 

2140 # Used by event handlers... 

2141 self.controlKeyIsDown = False # For control-drags 

2142 self.isActive = True 

2143 self.redrawCount = 0 

2144 self.wantedWidget = None 

2145 self.wantedCallbackScheduled = False 

2146 self.scrollWay = None 

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

2148 def __repr__(self): 

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

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

2151 def finishCreate(self): 

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

2153 # Called from app.newCommander, Commands.__init__ 

2154 t1 = time.process_time() 

2155 c = self.c 

2156 assert c 

2157 frameFactory = g.app.gui.frameFactory 

2158 if not frameFactory.masterFrame: 

2159 frameFactory.createMaster() 

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

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

2162 self.createSplitterComponents() 

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

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

2165 self.menu = LeoQtMenu(c, self, label='top-level-menu') 

2166 g.app.windowList.append(self) 

2167 t2 = time.process_time() 

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

2169 t3 = time.process_time() 

2170 self.miniBufferWidget = qt_text.QMinibufferWrapper(c) 

2171 c.bodyWantsFocus() 

2172 t4 = time.process_time() 

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

2174 print('qtFrame.finishCreate') 

2175 print( 

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

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

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

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

2180 ) 

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

2182 def createSplitterComponents(self): 

2183 

2184 c = self.c 

2185 self.tree = qt_tree.LeoQtTree(c, self) 

2186 self.log = LeoQtLog(self, None) 

2187 self.body = LeoQtBody(self, None) 

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

2189 self.resizePanesToRatio(ratio, secondary_ratio) 

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

2191 def setQtStyle(self): 

2192 """ 

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

2194 

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

2196 

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

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

2199 """ 

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

2201 if g.app.initStyleFlag: 

2202 return 

2203 g.app.initStyleFlag = True 

2204 c = self.c 

2205 trace = 'themes' in g.app.debug 

2206 # 

2207 # Get the requested style name. 

2208 stylename = c.config.getString('qt-style-name') or '' 

2209 if trace: 

2210 g.trace(repr(stylename)) 

2211 if not stylename: 

2212 return 

2213 # 

2214 # Return if the style does not exist. 

2215 styles = [z.lower() for z in QtWidgets.QStyleFactory.keys()] 

2216 if stylename.lower() not in styles: 

2217 g.es_print(f"ignoring unknown Qt style name: {stylename!r}") 

2218 g.printObj(styles) 

2219 return 

2220 # 

2221 # Change the style and palette. 

2222 app = g.app.gui.qtApp 

2223 if isQt5 or isQt6: 

2224 qstyle = app.setStyle(stylename) 

2225 if not qstyle: 

2226 g.es_print(f"failed to set Qt style name: {stylename!r}") 

2227 else: 

2228 QtWidgets.qApp.nativePalette = QtWidgets.qApp.palette() 

2229 qstyle = QtWidgets.qApp.setStyle(stylename) 

2230 if not qstyle: 

2231 g.es_print(f"failed to set Qt style name: {stylename!r}") 

2232 return 

2233 app.setPalette(QtWidgets.qApp.nativePalette) 

2234 #@+node:ekr.20110605121601.18252: *4* qtFrame.initCompleteHint 

2235 def initCompleteHint(self): 

2236 """A kludge: called to enable text changed events.""" 

2237 self.initComplete = True 

2238 #@+node:ekr.20110605121601.18253: *4* Destroying the qtFrame 

2239 #@+node:ekr.20110605121601.18254: *5* qtFrame.destroyAllObjects (not used) 

2240 def destroyAllObjects(self): 

2241 """Clear all links to objects in a Leo window.""" 

2242 c = self.c 

2243 # g.printGcAll() 

2244 # Do this first. 

2245 #@+<< clear all vnodes in the tree >> 

2246 #@+node:ekr.20110605121601.18255: *6* << clear all vnodes in the tree>> (qtFrame) 

2247 vList = [z for z in c.all_unique_nodes()] 

2248 for v in vList: 

2249 g.clearAllIvars(v) 

2250 vList = [] # Remove these references immediately. 

2251 #@-<< clear all vnodes in the tree >> 

2252 # Destroy all ivars in subcommanders. 

2253 g.clearAllIvars(c.atFileCommands) 

2254 if c.chapterController: # New in Leo 4.4.3 b1. 

2255 g.clearAllIvars(c.chapterController) 

2256 g.clearAllIvars(c.fileCommands) 

2257 g.clearAllIvars(c.keyHandler) # New in Leo 4.4.3 b1. 

2258 g.clearAllIvars(c.importCommands) 

2259 g.clearAllIvars(c.tangleCommands) 

2260 g.clearAllIvars(c.undoer) 

2261 g.clearAllIvars(c) 

2262 

2263 #@+node:ekr.20110605121601.18256: *5* qtFrame.destroySelf 

2264 def destroySelf(self): 

2265 # Remember these: we are about to destroy all of our ivars! 

2266 c, top = self.c, self.top 

2267 if hasattr(g.app.gui, 'frameFactory'): 

2268 g.app.gui.frameFactory.deleteFrame(top) 

2269 # Indicate that the commander is no longer valid. 

2270 c.exists = False 

2271 if 0: # We can't do this unless we unhook the event filter. 

2272 # Destroys all the objects of the commander. 

2273 self.destroyAllObjects() 

2274 c.exists = False # Make sure this one ivar has not been destroyed. 

2275 # print('destroySelf: qtFrame: %s' % c,g.callers(4)) 

2276 top.close() 

2277 #@+node:ekr.20110605121601.18257: *3* qtFrame.class QtStatusLineClass 

2278 class QtStatusLineClass: 

2279 """A class representing the status line.""" 

2280 #@+others 

2281 #@+node:ekr.20110605121601.18258: *4* QtStatusLineClass.ctor 

2282 def __init__(self, c, parentFrame): 

2283 """Ctor for LeoQtFrame class.""" 

2284 self.c = c 

2285 self.statusBar = c.frame.top.statusBar 

2286 self.lastFcol = 0 

2287 self.lastRow = 0 

2288 self.lastCol = 0 

2289 # Create the text widgets. 

2290 self.textWidget1 = w1 = QtWidgets.QLineEdit(self.statusBar) 

2291 self.textWidget2 = w2 = QtWidgets.QLineEdit(self.statusBar) 

2292 w1.setObjectName('status1') 

2293 w2.setObjectName('status2') 

2294 w1.setReadOnly(True) 

2295 w2.setReadOnly(True) 

2296 splitter = QtWidgets.QSplitter() 

2297 self.statusBar.addWidget(splitter, True) 

2298 sizes = c.config.getString('status-line-split-sizes') or '1 2' 

2299 sizes = [int(i) for i in sizes.replace(',', ' ').split()] 

2300 for n, i in enumerate(sizes): 

2301 w = [w1, w2][n] 

2302 policy = w.sizePolicy() 

2303 policy.setHorizontalStretch(i) 

2304 policy.setHorizontalPolicy(Policy.Minimum) 

2305 w.setSizePolicy(policy) 

2306 splitter.addWidget(w1) 

2307 splitter.addWidget(w2) 

2308 self.put('') 

2309 self.update() 

2310 #@+node:ekr.20110605121601.18260: *4* QtStatusLineClass.clear, get & put/1 

2311 def clear(self): 

2312 self.put('') 

2313 

2314 def get(self): 

2315 return self.textWidget2.text() 

2316 

2317 def put(self, s, bg=None, fg=None): 

2318 self.put_helper(s, self.textWidget2, bg, fg) 

2319 

2320 def put1(self, s, bg=None, fg=None): 

2321 self.put_helper(s, self.textWidget1, bg, fg) 

2322 

2323 styleSheetCache: Dict[Any, str] = {} 

2324 # Keys are widgets, values are stylesheets. 

2325 

2326 def put_helper(self, s, w, bg=None, fg=None): 

2327 """Put string s in the indicated widget, with proper colors.""" 

2328 c = self.c 

2329 bg = bg or c.config.getColor('status-bg') or 'white' 

2330 fg = fg or c.config.getColor('status-fg') or 'black' 

2331 if True: 

2332 # Work around #804. w is a QLineEdit. 

2333 w.setStyleSheet(f"background: {bg}; color: {fg};") 

2334 else: 

2335 # Rather than put(msg, explicit_color, explicit_color) we should use 

2336 # put(msg, status) where status is None, 'info', or 'fail'. 

2337 # Just as a quick hack to avoid dealing with propagating those changes 

2338 # back upstream, infer status like this: 

2339 if ( 

2340 fg == c.config.getColor('find-found-fg') and 

2341 bg == c.config.getColor('find-found-bg') 

2342 ): 

2343 status = 'info' 

2344 elif ( 

2345 fg == c.config.getColor('find-not-found-fg') and 

2346 bg == c.config.getColor('find-not-found-bg') 

2347 ): 

2348 status = 'fail' 

2349 else: 

2350 status = None 

2351 d = self.styleSheetCache 

2352 if status != d.get(w, '__undefined__'): 

2353 d[w] = status 

2354 c.styleSheetManager.mng.remove_sclass(w, ['info', 'fail']) 

2355 c.styleSheetManager.mng.add_sclass(w, status) 

2356 c.styleSheetManager.mng.update_view(w) # force appearance update 

2357 w.setText(s) 

2358 #@+node:chris.20180320072817.1: *4* QtStatusLineClass.update & helpers 

2359 def update(self): 

2360 if g.app.killed: 

2361 return 

2362 c, body = self.c, self.c.frame.body 

2363 if not c.p: 

2364 return 

2365 te = body.widget 

2366 if not isinstance(te, QtWidgets.QTextEdit): 

2367 return 

2368 cursor = te.textCursor() 

2369 block = cursor.block() 

2370 row = block.blockNumber() + 1 

2371 col, fcol = self.compute_columns(block, cursor) 

2372 words = len(c.p.b.split(None)) 

2373 self.put_status_line(col, fcol, row, words) 

2374 self.lastRow = row 

2375 self.lastCol = col 

2376 self.lastFcol = fcol 

2377 #@+node:ekr.20190118082646.1: *5* qstatus.compute_columns 

2378 def compute_columns(self, block, cursor): 

2379 

2380 c = self.c 

2381 line = block.text() 

2382 col = cursor.columnNumber() 

2383 offset = c.p.textOffset() 

2384 fcol_offset = 0 

2385 s2 = line[0:col] 

2386 col = g.computeWidth(s2, c.tab_width) 

2387 # 

2388 # #195: fcol when using @first directive is inaccurate 

2389 i = line.find('<<') 

2390 j = line.find('>>') 

2391 if -1 < i < j or g.match_word(line.strip(), 0, '@others'): 

2392 offset = None 

2393 else: 

2394 for tag in ('@first ', '@last '): 

2395 if line.startswith(tag): 

2396 fcol_offset = len(tag) 

2397 break 

2398 # 

2399 # fcol is '' if there is no ancestor @<file> node. 

2400 fcol = None if offset is None else max(0, col + offset - fcol_offset) 

2401 return col, fcol 

2402 #@+node:chris.20180320072817.2: *5* qstatus.file_line (not used) 

2403 def file_line(self): 

2404 """ 

2405 Return the line of the first line of c.p in its external file. 

2406 Return None if c.p is not part of an external file. 

2407 """ 

2408 c, p = self.c, self.c.p 

2409 if p: 

2410 goto = gotoCommands.GoToCommands(c) 

2411 return goto.find_node_start(p) 

2412 return None 

2413 #@+node:ekr.20190118082047.1: *5* qstatus.put_status_line 

2414 def put_status_line(self, col, fcol, row, words): 

2415 

2416 if 1: 

2417 fcol_part = '' if fcol is None else f" fcol: {fcol}" 

2418 # For now, it seems to0 difficult to get alignment *exactly* right. 

2419 self.put1(f"line: {row:d} col: {col:d} {fcol_part} words: {words}") 

2420 else: 

2421 # #283 is not ready yet, and probably will never be. 

2422 fline = self.file_line() 

2423 fline = '' if fline is None else fline + row 

2424 self.put1( 

2425 f"fline: {fline:2} line: {row:2d} col: {col:2} fcol: {fcol:2}") 

2426 #@-others 

2427 #@+node:ekr.20110605121601.18262: *3* qtFrame.class QtIconBarClass 

2428 class QtIconBarClass: 

2429 """A class representing the singleton Icon bar""" 

2430 #@+others 

2431 #@+node:ekr.20110605121601.18263: *4* QtIconBar.ctor & reloadSettings 

2432 def __init__(self, c, parentFrame): 

2433 """Ctor for QtIconBarClass.""" 

2434 # Copy ivars 

2435 self.c = c 

2436 self.parentFrame = parentFrame 

2437 # Status ivars. 

2438 self.actions = [] 

2439 self.chapterController = None 

2440 self.toolbar = self 

2441 self.w = c.frame.top.iconBar # A QToolBar. 

2442 self.reloadSettings() 

2443 

2444 def reloadSettings(self): 

2445 c = self.c 

2446 c.registerReloadSettings(self) 

2447 self.buttonColor = c.config.getString('qt-button-color') 

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

2449 #@+node:ekr.20110605121601.18264: *4* QtIconBar.do-nothings 

2450 # These *are* called from Leo's core. 

2451 

2452 def addRow(self, height=None): 

2453 pass # To do. 

2454 

2455 def getNewFrame(self): 

2456 return None # To do 

2457 #@+node:ekr.20110605121601.18265: *4* QtIconBar.add 

2458 def add(self, *args, **keys): 

2459 """Add a button to the icon bar.""" 

2460 c = self.c 

2461 if not self.w: 

2462 return None 

2463 command = keys.get('command') 

2464 text = keys.get('text') 

2465 # able to specify low-level QAction directly (QPushButton not forced) 

2466 qaction = keys.get('qaction') 

2467 if not text and not qaction: 

2468 g.es('bad toolbar item') 

2469 kind = keys.get('kind') or 'generic-button' 

2470 # imagefile = keys.get('imagefile') 

2471 # image = keys.get('image') 

2472 

2473 

2474 class leoIconBarButton(QtWidgets.QWidgetAction): # type:ignore 

2475 

2476 def __init__(self, parent, text, toolbar): 

2477 super().__init__(parent) 

2478 self.button = None # set below 

2479 self.text = text 

2480 self.toolbar = toolbar 

2481 

2482 def createWidget(self, parent): 

2483 self.button = b = QtWidgets.QPushButton(self.text, parent) 

2484 self.button.setProperty('button_kind', kind) # for styling 

2485 return b 

2486 

2487 if qaction is None: 

2488 action = leoIconBarButton(parent=self.w, text=text, toolbar=self) 

2489 button_name = text 

2490 else: 

2491 action = qaction 

2492 button_name = action.text() 

2493 self.w.addAction(action) 

2494 self.actions.append(action) 

2495 b = self.w.widgetForAction(action) 

2496 # Set the button's object name so we can use the stylesheet to color it. 

2497 if not button_name: 

2498 button_name = 'unnamed' 

2499 button_name = button_name + '-button' 

2500 b.setObjectName(button_name) 

2501 b.setContextMenuPolicy(ContextMenuPolicy.ActionsContextMenu) 

2502 

2503 def delete_callback(checked, action=action,): 

2504 self.w.removeAction(action) 

2505 

2506 b.leo_removeAction = rb = QAction('Remove Button', b) 

2507 b.addAction(rb) 

2508 rb.triggered.connect(delete_callback) 

2509 if command: 

2510 

2511 def button_callback(event, c=c, command=command): 

2512 val = command() 

2513 if c.exists: 

2514 # c.bodyWantsFocus() 

2515 c.outerUpdate() 

2516 return val 

2517 

2518 b.clicked.connect(button_callback) 

2519 return action 

2520 #@+node:ekr.20110605121601.18266: *4* QtIconBar.addRowIfNeeded (not used) 

2521 def addRowIfNeeded(self): 

2522 """Add a new icon row if there are too many widgets.""" 

2523 # n = g.app.iconWidgetCount 

2524 # if n >= self.widgets_per_row: 

2525 # g.app.iconWidgetCount = 0 

2526 # self.addRow() 

2527 # g.app.iconWidgetCount += 1 

2528 #@+node:ekr.20110605121601.18267: *4* QtIconBar.addWidget 

2529 def addWidget(self, w): 

2530 self.w.addWidget(w) 

2531 #@+node:ekr.20110605121601.18268: *4* QtIconBar.clear 

2532 def clear(self): 

2533 """Destroy all the widgets in the icon bar""" 

2534 self.w.clear() 

2535 self.actions = [] 

2536 g.app.iconWidgetCount = 0 

2537 #@+node:ekr.20110605121601.18269: *4* QtIconBar.createChaptersIcon 

2538 def createChaptersIcon(self): 

2539 

2540 c = self.c 

2541 f = c.frame 

2542 if f.use_chapters and f.use_chapter_tabs: 

2543 return LeoQtTreeTab(c, f.iconBar) 

2544 return None 

2545 #@+node:ekr.20110605121601.18270: *4* QtIconBar.deleteButton 

2546 def deleteButton(self, w): 

2547 """ w is button """ 

2548 self.w.removeAction(w) 

2549 self.c.bodyWantsFocus() 

2550 self.c.outerUpdate() 

2551 #@+node:ekr.20141031053508.14: *4* QtIconBar.goto_command 

2552 def goto_command(self, controller, gnx): 

2553 """ 

2554 Select the node corresponding to the given gnx. 

2555 controller is a ScriptingController instance. 

2556 """ 

2557 # Fix bug 74: command_p may be in another outline. 

2558 c = self.c 

2559 c2, p = controller.open_gnx(c, gnx) 

2560 if p: 

2561 assert c2.positionExists(p) 

2562 if c == c2: 

2563 c2.selectPosition(p) 

2564 else: 

2565 g.app.selectLeoWindow(c2) 

2566 # Fix #367: Process events before selecting. 

2567 g.app.gui.qtApp.processEvents() 

2568 c2.selectPosition(p) 

2569 else: 

2570 g.trace('not found', gnx) 

2571 #@+node:ekr.20110605121601.18271: *4* QtIconBar.setCommandForButton (@rclick nodes) & helper 

2572 # qtFrame.QtIconBarClass.setCommandForButton 

2573 

2574 def setCommandForButton(self, button, command, command_p, controller, gnx, script): 

2575 """ 

2576 Set the "Goto Script" rlick item of an @button button. 

2577 Called from mod_scripting.py plugin. 

2578 

2579 button is a leoIconBarButton. 

2580 command is a callback, defined in mod_scripting.py. 

2581 command_p exists only if the @button node exists in the local .leo file. 

2582 gnx is the gnx of the @button node. 

2583 script is a static script for common @button nodes. 

2584 """ 

2585 if not command: 

2586 return 

2587 b = button.button 

2588 b.clicked.connect(command) 

2589 

2590 # Fix bug 74: use the controller and gnx arguments. 

2591 

2592 def goto_callback(checked, controller=controller, gnx=gnx): 

2593 self.goto_command(controller, gnx) 

2594 b.goto_script = gts = QAction('Goto Script', b) 

2595 b.addAction(gts) 

2596 gts.triggered.connect(goto_callback) 

2597 rclicks = build_rclick_tree(command_p, top_level=True) 

2598 self.add_rclick_menu(b, rclicks, controller, script=script) 

2599 #@+node:ekr.20141031053508.15: *5* add_rclick_menu (QtIconBarClass) 

2600 def add_rclick_menu(self, action_container, rclicks, controller, 

2601 top_level=True, 

2602 button=None, 

2603 script=None 

2604 ): 

2605 c = controller.c 

2606 top_offset = -2 # insert before the remove button and goto script items 

2607 if top_level: 

2608 button = action_container 

2609 for rc in rclicks: 

2610 # pylint: disable=cell-var-from-loop 

2611 headline = rc.position.h[8:].strip() 

2612 act = QAction(headline, action_container) 

2613 if '---' in headline and headline.strip().strip('-') == '': 

2614 act.setSeparator(True) 

2615 elif rc.position.b.strip(): 

2616 

2617 def cb(checked, p=rc.position, button=button): 

2618 controller.executeScriptFromButton( 

2619 b=button, 

2620 buttonText=p.h[8:].strip(), 

2621 p=p, 

2622 script=script, 

2623 ) 

2624 if c.exists: 

2625 c.outerUpdate() 

2626 

2627 act.triggered.connect(cb) 

2628 else: # recurse submenu 

2629 sub_menu = QtWidgets.QMenu(action_container) 

2630 act.setMenu(sub_menu) 

2631 self.add_rclick_menu(sub_menu, rc.children, controller, 

2632 top_level=False, button=button) 

2633 if top_level: 

2634 # insert act before Remove Button 

2635 action_container.insertAction( 

2636 action_container.actions()[top_offset], act) 

2637 else: 

2638 action_container.addAction(act) 

2639 if top_level and rclicks: 

2640 act = QAction('---', action_container) 

2641 act.setSeparator(True) 

2642 action_container.insertAction( 

2643 action_container.actions()[top_offset], act) 

2644 action_container.setText( 

2645 action_container.text() + 

2646 (c.config.getString('mod-scripting-subtext') or '') 

2647 ) 

2648 #@-others 

2649 #@+node:ekr.20110605121601.18274: *3* qtFrame.Configuration 

2650 #@+node:ekr.20110605121601.18275: *4* qtFrame.configureBar 

2651 def configureBar(self, bar, verticalFlag): 

2652 c = self.c 

2653 # Get configuration settings. 

2654 w = c.config.getInt("split-bar-width") 

2655 if not w or w < 1: 

2656 w = 7 

2657 relief = c.config.get("split_bar_relief", "relief") 

2658 if not relief: 

2659 relief = "flat" 

2660 color = c.config.getColor("split-bar-color") 

2661 if not color: 

2662 color = "LightSteelBlue2" 

2663 try: 

2664 if verticalFlag: 

2665 # Panes arranged vertically; horizontal splitter bar 

2666 bar.configure( 

2667 relief=relief, height=w, bg=color, cursor="sb_v_double_arrow") 

2668 else: 

2669 # Panes arranged horizontally; vertical splitter bar 

2670 bar.configure( 

2671 relief=relief, width=w, bg=color, cursor="sb_h_double_arrow") 

2672 except Exception: 

2673 # Could be a user error. Use all defaults 

2674 g.es("exception in user configuration for splitbar") 

2675 g.es_exception() 

2676 if verticalFlag: 

2677 # Panes arranged vertically; horizontal splitter bar 

2678 bar.configure(height=7, cursor="sb_v_double_arrow") 

2679 else: 

2680 # Panes arranged horizontally; vertical splitter bar 

2681 bar.configure(width=7, cursor="sb_h_double_arrow") 

2682 #@+node:ekr.20110605121601.18276: *4* qtFrame.configureBarsFromConfig 

2683 def configureBarsFromConfig(self): 

2684 c = self.c 

2685 w = c.config.getInt("split-bar-width") 

2686 if not w or w < 1: 

2687 w = 7 

2688 relief = c.config.get("split_bar_relief", "relief") 

2689 if not relief or relief == "": 

2690 relief = "flat" 

2691 color = c.config.getColor("split-bar-color") 

2692 if not color or color == "": 

2693 color = "LightSteelBlue2" 

2694 if self.splitVerticalFlag: 

2695 bar1, bar2 = self.bar1, self.bar2 

2696 else: 

2697 bar1, bar2 = self.bar2, self.bar1 

2698 try: 

2699 bar1.configure(relief=relief, height=w, bg=color) 

2700 bar2.configure(relief=relief, width=w, bg=color) 

2701 except Exception: 

2702 # Could be a user error. 

2703 g.es("exception in user configuration for splitbar") 

2704 g.es_exception() 

2705 #@+node:ekr.20110605121601.18277: *4* qtFrame.reconfigureFromConfig 

2706 def reconfigureFromConfig(self): 

2707 """Init the configuration of the Qt frame from settings.""" 

2708 c, frame = self.c, self 

2709 frame.configureBarsFromConfig() 

2710 frame.setTabWidth(c.tab_width) 

2711 c.redraw() 

2712 #@+node:ekr.20110605121601.18278: *4* qtFrame.setInitialWindowGeometry 

2713 def setInitialWindowGeometry(self): 

2714 """Set the position and size of the frame to config params.""" 

2715 c = self.c 

2716 h = c.config.getInt("initial-window-height") or 500 

2717 w = c.config.getInt("initial-window-width") or 600 

2718 x = c.config.getInt("initial-window-left") or 50 # #1190: was 10 

2719 y = c.config.getInt("initial-window-top") or 50 # #1190: was 10 

2720 if h and w and x and y: 

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

2722 g.trace(w, h, x, y) 

2723 self.setTopGeometry(w, h, x, y) 

2724 #@+node:ekr.20110605121601.18279: *4* qtFrame.setTabWidth 

2725 def setTabWidth(self, w): 

2726 # A do-nothing because tab width is set automatically. 

2727 # It *is* called from Leo's core. 

2728 pass 

2729 #@+node:ekr.20110605121601.18280: *4* qtFrame.forceWrap & setWrap 

2730 def forceWrap(self, p=None): 

2731 return self.c.frame.body.forceWrap(p) 

2732 

2733 def setWrap(self, p=None): 

2734 return self.c.frame.body.setWrap(p) 

2735 

2736 #@+node:ekr.20110605121601.18281: *4* qtFrame.reconfigurePanes 

2737 def reconfigurePanes(self): 

2738 c, f = self.c, self 

2739 if f.splitVerticalFlag: 

2740 r = c.config.getRatio("initial-vertical-ratio") 

2741 if r is None or r < 0.0 or r > 1.0: 

2742 r = 0.5 

2743 r2 = c.config.getRatio("initial-vertical-secondary-ratio") 

2744 if r2 is None or r2 < 0.0 or r2 > 1.0: 

2745 r2 = 0.8 

2746 else: 

2747 r = c.config.getRatio("initial-horizontal-ratio") 

2748 if r is None or r < 0.0 or r > 1.0: 

2749 r = 0.3 

2750 r2 = c.config.getRatio("initial-horizontal-secondary-ratio") 

2751 if r2 is None or r2 < 0.0 or r2 > 1.0: 

2752 r2 = 0.8 

2753 f.resizePanesToRatio(r, r2) 

2754 #@+node:ekr.20110605121601.18282: *4* qtFrame.resizePanesToRatio 

2755 def resizePanesToRatio(self, ratio, ratio2): 

2756 """Resize splitter1 and splitter2 using the given ratios.""" 

2757 # pylint: disable=arguments-differ 

2758 self.divideLeoSplitter1(ratio) 

2759 self.divideLeoSplitter2(ratio2) 

2760 #@+node:ekr.20110605121601.18283: *4* qtFrame.divideLeoSplitter1/2 

2761 def divideLeoSplitter1(self, frac): 

2762 """Divide the main splitter.""" 

2763 layout = self.c and self.c.free_layout 

2764 if not layout: 

2765 return 

2766 w = layout.get_main_splitter() 

2767 if w: 

2768 self.divideAnySplitter(frac, w) 

2769 

2770 def divideLeoSplitter2(self, frac): 

2771 """Divide the secondary splitter.""" 

2772 layout = self.c and self.c.free_layout 

2773 if not layout: 

2774 return 

2775 w = layout.get_secondary_splitter() 

2776 if w: 

2777 self.divideAnySplitter(frac, w) 

2778 #@+node:ekr.20110605121601.18284: *4* qtFrame.divideAnySplitter 

2779 # This is the general-purpose placer for splitters. 

2780 # It is the only general-purpose splitter code in Leo. 

2781 

2782 def divideAnySplitter(self, frac, splitter): 

2783 """Set the splitter sizes.""" 

2784 sizes = splitter.sizes() 

2785 if len(sizes) != 2: 

2786 g.trace(f"{len(sizes)} widget(s) in {id(splitter)}") 

2787 return 

2788 if frac > 1 or frac < 0: 

2789 g.trace(f"split ratio [{frac}] out of range 0 <= frac <= 1") 

2790 return 

2791 s1, s2 = sizes 

2792 s = s1 + s2 

2793 s1 = int(s * frac + 0.5) 

2794 s2 = s - s1 

2795 splitter.setSizes([s1, s2]) 

2796 #@+node:ekr.20110605121601.18285: *3* qtFrame.Event handlers 

2797 #@+node:ekr.20110605121601.18286: *4* qtFrame.OnCloseLeoEvent 

2798 # Called from quit logic and when user closes the window. 

2799 # Returns True if the close happened. 

2800 

2801 def OnCloseLeoEvent(self): 

2802 f = self 

2803 c = f.c 

2804 if c.inCommand: 

2805 c.requestCloseWindow = True 

2806 else: 

2807 g.app.closeLeoWindow(self) 

2808 #@+node:ekr.20110605121601.18287: *4* qtFrame.OnControlKeyUp/Down 

2809 def OnControlKeyDown(self, event=None): 

2810 self.controlKeyIsDown = True 

2811 

2812 def OnControlKeyUp(self, event=None): 

2813 self.controlKeyIsDown = False 

2814 #@+node:ekr.20110605121601.18290: *4* qtFrame.OnActivateTree 

2815 def OnActivateTree(self, event=None): 

2816 pass 

2817 #@+node:ekr.20110605121601.18291: *4* qtFrame.OnBodyClick, OnBodyRClick (not used) 

2818 # At present, these are not called, 

2819 # but they could be called by LeoQTextBrowser. 

2820 

2821 def OnBodyClick(self, event=None): 

2822 g.trace() 

2823 try: 

2824 c, p = self.c, self.c.p 

2825 if g.doHook("bodyclick1", c=c, p=p, event=event): 

2826 g.doHook("bodyclick2", c=c, p=p, event=event) 

2827 return 

2828 c.k.showStateAndMode(w=c.frame.body.wrapper) 

2829 g.doHook("bodyclick2", c=c, p=p, event=event) 

2830 except Exception: 

2831 g.es_event_exception("bodyclick") 

2832 

2833 def OnBodyRClick(self, event=None): 

2834 try: 

2835 c, p = self.c, self.c.p 

2836 if g.doHook("bodyrclick1", c=c, p=p, event=event): 

2837 g.doHook("bodyrclick2", c=c, p=p, event=event) 

2838 return 

2839 c.k.showStateAndMode(w=c.frame.body.wrapper) 

2840 g.doHook("bodyrclick2", c=c, p=p, event=event) 

2841 except Exception: 

2842 g.es_event_exception("iconrclick") 

2843 #@+node:ekr.20110605121601.18292: *4* qtFrame.OnBodyDoubleClick (Events) (not used) 

2844 # Not called 

2845 

2846 def OnBodyDoubleClick(self, event=None): 

2847 try: 

2848 c, p = self.c, self.c.p 

2849 if event and not g.doHook("bodydclick1", c=c, p=p, event=event): 

2850 c.editCommands.extendToWord(event) # Handles unicode properly. 

2851 c.k.showStateAndMode(w=c.frame.body.wrapper) 

2852 g.doHook("bodydclick2", c=c, p=p, event=event) 

2853 except Exception: 

2854 g.es_event_exception("bodydclick") 

2855 return "break" # Restore this to handle proper double-click logic. 

2856 #@+node:ekr.20110605121601.18293: *3* qtFrame.Gui-dependent commands 

2857 #@+node:ekr.20110605121601.18301: *4* qtFrame.Window Menu... 

2858 #@+node:ekr.20110605121601.18302: *5* qtFrame.toggleActivePane 

2859 @frame_cmd('toggle-active-pane') 

2860 def toggleActivePane(self, event=None): 

2861 """Toggle the focus between the outline and body panes.""" 

2862 frame = self 

2863 c = frame.c 

2864 w = c.get_focus() 

2865 w_name = g.app.gui.widget_name(w) 

2866 if w_name in ('canvas', 'tree', 'treeWidget'): 

2867 c.endEditing() 

2868 c.bodyWantsFocus() 

2869 else: 

2870 c.treeWantsFocus() 

2871 #@+node:ekr.20110605121601.18303: *5* qtFrame.cascade 

2872 @frame_cmd('cascade-windows') 

2873 def cascade(self, event=None): 

2874 """Cascade all Leo windows.""" 

2875 x, y, delta = 50, 50, 50 

2876 for frame in g.app.windowList: 

2877 w = frame and frame.top 

2878 if w: 

2879 r = w.geometry() # a Qt.Rect 

2880 # 2011/10/26: Fix bug 823601: cascade-windows fails. 

2881 w.setGeometry(QtCore.QRect(x, y, r.width(), r.height())) 

2882 # Compute the new offsets. 

2883 x += 30 

2884 y += 30 

2885 if x > 200: 

2886 x = 10 + delta 

2887 y = 40 + delta 

2888 delta += 10 

2889 #@+node:ekr.20110605121601.18304: *5* qtFrame.equalSizedPanes 

2890 @frame_cmd('equal-sized-panes') 

2891 def equalSizedPanes(self, event=None): 

2892 """Make the outline and body panes have the same size.""" 

2893 self.resizePanesToRatio(0.5, self.secondary_ratio) 

2894 #@+node:ekr.20110605121601.18305: *5* qtFrame.hideLogWindow 

2895 def hideLogWindow(self, event=None): 

2896 """Hide the log pane.""" 

2897 self.divideLeoSplitter2(0.99) 

2898 #@+node:ekr.20110605121601.18306: *5* qtFrame.minimizeAll 

2899 @frame_cmd('minimize-all') 

2900 def minimizeAll(self, event=None): 

2901 """Minimize all Leo's windows.""" 

2902 for frame in g.app.windowList: 

2903 self.minimize(frame) 

2904 

2905 def minimize(self, frame): 

2906 # This unit test will fail when run externally. 

2907 if frame and frame.top: 

2908 w = frame.top.leo_master or frame.top 

2909 if g.unitTesting: 

2910 assert hasattr(w, 'setWindowState'), w 

2911 else: 

2912 w.setWindowState(WindowState.WindowMinimized) 

2913 #@+node:ekr.20110605121601.18307: *5* qtFrame.toggleSplitDirection 

2914 @frame_cmd('toggle-split-direction') 

2915 def toggleSplitDirection(self, event=None): 

2916 """Toggle the split direction in the present Leo window.""" 

2917 if hasattr(self.c, 'free_layout'): 

2918 self.c.free_layout.get_top_splitter().rotate() 

2919 #@+node:ekr.20110605121601.18308: *5* qtFrame.resizeToScreen 

2920 @frame_cmd('resize-to-screen') 

2921 def resizeToScreen(self, event=None): 

2922 """Resize the Leo window so it fill the entire screen.""" 

2923 frame = self 

2924 # This unit test will fail when run externally. 

2925 if frame and frame.top: 

2926 # frame.top.leo_master is a LeoTabbedTopLevel. 

2927 # frame.top is a DynamicWindow. 

2928 w = frame.top.leo_master or frame.top 

2929 if g.unitTesting: 

2930 assert hasattr(w, 'setWindowState'), w 

2931 else: 

2932 w.setWindowState(WindowState.WindowMaximized) 

2933 #@+node:ekr.20110605121601.18309: *4* qtFrame.Help Menu... 

2934 #@+node:ekr.20110605121601.18310: *5* qtFrame.leoHelp 

2935 @frame_cmd('open-offline-tutorial') 

2936 def leoHelp(self, event=None): 

2937 """Open Leo's offline tutorial.""" 

2938 frame = self 

2939 c = frame.c 

2940 theFile = g.os_path_join(g.app.loadDir, "..", "doc", "sbooks.chm") 

2941 if g.os_path_exists(theFile) and sys.platform.startswith('win'): 

2942 # pylint: disable=no-member 

2943 os.startfile(theFile) 

2944 else: 

2945 answer = g.app.gui.runAskYesNoDialog(c, 

2946 "Download Tutorial?", 

2947 "Download tutorial (sbooks.chm) from SourceForge?") 

2948 if answer == "yes": 

2949 try: 

2950 url = "http://prdownloads.sourceforge.net/leo/sbooks.chm?download" 

2951 import webbrowser 

2952 os.chdir(g.app.loadDir) 

2953 webbrowser.open_new(url) 

2954 except Exception: 

2955 if 0: 

2956 g.es("exception downloading", "sbooks.chm") 

2957 g.es_exception() 

2958 #@+node:ekr.20160424080647.1: *3* qtFrame.Properties 

2959 # The ratio and secondary_ratio properties are read-only. 

2960 #@+node:ekr.20160424080815.2: *4* qtFrame.ratio property 

2961 def __get_ratio(self): 

2962 """Return splitter ratio of the main splitter.""" 

2963 c = self.c 

2964 free_layout = c.free_layout 

2965 if free_layout: 

2966 w = free_layout.get_main_splitter() 

2967 if w: 

2968 aList = w.sizes() 

2969 if len(aList) == 2: 

2970 n1, n2 = aList 

2971 # 2017/06/07: guard against division by zero. 

2972 ratio = 0.5 if n1 + n2 == 0 else float(n1) / float(n1 + n2) 

2973 return ratio 

2974 return 0.5 

2975 

2976 ratio = property( 

2977 __get_ratio, # No setter. 

2978 doc="qtFrame.ratio property") 

2979 #@+node:ekr.20160424080815.3: *4* qtFrame.secondary_ratio property 

2980 def __get_secondary_ratio(self): 

2981 """Return the splitter ratio of the secondary splitter.""" 

2982 c = self.c 

2983 free_layout = c.free_layout 

2984 if free_layout: 

2985 w = free_layout.get_secondary_splitter() 

2986 if w: 

2987 aList = w.sizes() 

2988 if len(aList) == 2: 

2989 n1, n2 = aList 

2990 ratio = float(n1) / float(n1 + n2) 

2991 return ratio 

2992 return 0.5 

2993 

2994 secondary_ratio = property( 

2995 __get_secondary_ratio, # no setter. 

2996 doc="qtFrame.secondary_ratio property") 

2997 #@+node:ekr.20110605121601.18311: *3* qtFrame.Qt bindings... 

2998 #@+node:ekr.20190611053431.1: *4* qtFrame.bringToFront 

2999 def bringToFront(self): 

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

3001 g.trace() 

3002 self.lift() 

3003 #@+node:ekr.20190611053431.2: *4* qtFrame.deiconify 

3004 def deiconify(self): 

3005 """Undo --minimized""" 

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

3007 g.trace( 

3008 'top:', bool(self.top), 

3009 'isMinimized:', self.top and self.top.isMinimized()) 

3010 if self.top and self.top.isMinimized(): # Bug fix: 400739. 

3011 self.lift() 

3012 #@+node:ekr.20190611053431.4: *4* qtFrame.get_window_info 

3013 def get_window_info(self): 

3014 """Return the geometry of the top window.""" 

3015 if getattr(self.top, 'leo_master', None): 

3016 f = self.top.leo_master 

3017 else: 

3018 f = self.top 

3019 rect = f.geometry() 

3020 topLeft = rect.topLeft() 

3021 x, y = topLeft.x(), topLeft.y() 

3022 w, h = rect.width(), rect.height() 

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

3024 g.trace('\n', w, h, x, y) 

3025 return w, h, x, y 

3026 #@+node:ekr.20190611053431.3: *4* qtFrame.getFocus 

3027 def getFocus(self): 

3028 return g.app.gui.get_focus(self.c) # Bug fix: 2009/6/30. 

3029 #@+node:ekr.20190611053431.7: *4* qtFrame.getTitle 

3030 def getTitle(self): 

3031 # Fix https://bugs.launchpad.net/leo-editor/+bug/1194209 

3032 # For qt, leo_master (a LeoTabbedTopLevel) contains the QMainWindow. 

3033 w = self.top.leo_master 

3034 return w.windowTitle() 

3035 #@+node:ekr.20190611053431.5: *4* qtFrame.iconify 

3036 def iconify(self): 

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

3038 g.trace(bool(self.top)) 

3039 if self.top: 

3040 self.top.showMinimized() 

3041 #@+node:ekr.20190611053431.6: *4* qtFrame.lift 

3042 def lift(self): 

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

3044 g.trace(bool(self.top), self.top and self.top.isMinimized()) 

3045 if not self.top: 

3046 return 

3047 if self.top.isMinimized(): # Bug 379141 

3048 self.top.showNormal() 

3049 self.top.activateWindow() 

3050 self.top.raise_() 

3051 #@+node:ekr.20190611053431.8: *4* qtFrame.setTitle 

3052 def setTitle(self, s): 

3053 # pylint: disable=arguments-differ 

3054 if self.top: 

3055 # Fix https://bugs.launchpad.net/leo-editor/+bug/1194209 

3056 # When using tabs, leo_master (a LeoTabbedTopLevel) contains the QMainWindow. 

3057 w = self.top.leo_master 

3058 w.setWindowTitle(s) 

3059 #@+node:ekr.20190611053431.9: *4* qtFrame.setTopGeometry 

3060 def setTopGeometry(self, w, h, x, y): 

3061 # self.top is a DynamicWindow. 

3062 if self.top: 

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

3064 g.trace(w, h, x, y, self.c.shortFileName(), g.callers()) 

3065 self.top.setGeometry(QtCore.QRect(x, y, w, h)) 

3066 #@+node:ekr.20190611053431.10: *4* qtFrame.update 

3067 def update(self, *args, **keys): 

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

3069 g.trace(bool(self.top)) 

3070 self.top.update() 

3071 #@-others 

3072#@+node:ekr.20110605121601.18312: ** class LeoQtLog (LeoLog) 

3073class LeoQtLog(leoFrame.LeoLog): 

3074 """A class that represents the log pane of a Qt window.""" 

3075 #@+others 

3076 #@+node:ekr.20110605121601.18313: *3* LeoQtLog.Birth 

3077 #@+node:ekr.20110605121601.18314: *4* LeoQtLog.__init__ & reloadSettings 

3078 def __init__(self, frame, parentFrame): 

3079 """Ctor for LeoQtLog class.""" 

3080 super().__init__(frame, parentFrame) # Calls createControl. 

3081 # Set in finishCreate. 

3082 # Important: depending on the log *tab*, 

3083 # logCtrl may be either a wrapper or a widget. 

3084 assert self.logCtrl is None, self.logCtrl # type:ignore 

3085 self.c = c = frame.c # Also set in the base constructor, but we need it here. 

3086 self.contentsDict = {} # Keys are tab names. Values are widgets. 

3087 self.eventFilters = [] # Apparently needed to make filters work! 

3088 self.logDict = {} # Keys are tab names text widgets; values are the widgets. 

3089 self.logWidget = None # Set in finishCreate. 

3090 self.menu = None # A menu that pops up on right clicks in the hull or in tabs. 

3091 self.tabWidget = tw = c.frame.top.tabWidget # The Qt.QTabWidget that holds all the tabs. 

3092 

3093 # Bug 917814: Switching Log Pane tabs is done incompletely. 

3094 tw.currentChanged.connect(self.onCurrentChanged) 

3095 if 0: # Not needed to make onActivateEvent work. 

3096 # Works only for .tabWidget, *not* the individual tabs! 

3097 theFilter = qt_events.LeoQtEventFilter(c, w=tw, tag='tabWidget') 

3098 tw.installEventFilter(theFilter) 

3099 # Partial fix for bug 1251755: Log-pane refinements 

3100 tw.setMovable(True) 

3101 self.reloadSettings() 

3102 

3103 def reloadSettings(self): 

3104 c = self.c 

3105 self.wrap = bool(c.config.getBool('log-pane-wraps')) 

3106 #@+node:ekr.20110605121601.18315: *4* LeoQtLog.finishCreate 

3107 def finishCreate(self): 

3108 """Finish creating the LeoQtLog class.""" 

3109 c, log, w = self.c, self, self.tabWidget 

3110 # 

3111 # Create the log tab as the leftmost tab. 

3112 log.createTab('Log') 

3113 self.logWidget = logWidget = self.contentsDict.get('Log') 

3114 logWidget.setWordWrapMode(WrapMode.WordWrap if self.wrap else WrapMode.NoWrap) 

3115 w.insertTab(0, logWidget, 'Log') 

3116 # Required. 

3117 # 

3118 # set up links in log handling 

3119 logWidget.setTextInteractionFlags( 

3120 TextInteractionFlag.LinksAccessibleByMouse 

3121 | TextInteractionFlag.TextEditable 

3122 | TextInteractionFlag.TextSelectableByMouse 

3123 ) 

3124 logWidget.setOpenLinks(False) 

3125 logWidget.setOpenExternalLinks(False) 

3126 logWidget.anchorClicked.connect(self.linkClicked) 

3127 # 

3128 # Show the spell tab. 

3129 c.spellCommands.openSpellTab() 

3130 # 

3131 #794: Clicking Find Tab should do exactly what pushing Ctrl-F does 

3132 

3133 def tab_callback(index): 

3134 name = w.tabText(index) 

3135 if name == 'Find': 

3136 c.findCommands.startSearch(event=None) 

3137 

3138 w.currentChanged.connect(tab_callback) 

3139 # #1286. 

3140 w.customContextMenuRequested.connect(self.onContextMenu) 

3141 #@+node:ekr.20110605121601.18316: *4* LeoQtLog.getName 

3142 def getName(self): 

3143 return 'log' # Required for proper pane bindings. 

3144 #@+node:ekr.20150717102728.1: *3* LeoQtLog.Commands 

3145 @log_cmd('clear-log') 

3146 def clearLog(self, event=None): 

3147 """Clear the log pane.""" 

3148 w = self.logCtrl.widget # type:ignore # w is a QTextBrowser 

3149 if w: 

3150 w.clear() 

3151 #@+node:ekr.20110605121601.18333: *3* LeoQtLog.color tab stuff 

3152 def createColorPicker(self, tabName): 

3153 g.warning('color picker not ready for qt') 

3154 #@+node:ekr.20110605121601.18334: *3* LeoQtLog.font tab stuff 

3155 #@+node:ekr.20110605121601.18335: *4* LeoQtLog.createFontPicker 

3156 def createFontPicker(self, tabName): 

3157 # log = self 

3158 font, ok = QtWidgets.QFontDialog.getFont() 

3159 if not (font and ok): 

3160 return 

3161 style = font.style() 

3162 table1 = ( 

3163 (Style.StyleNormal, 'normal'), # #2330. 

3164 (Style.StyleItalic, 'italic'), 

3165 (Style.StyleOblique, 'oblique')) 

3166 for val, name in table1: 

3167 if style == val: 

3168 style = name 

3169 break 

3170 else: 

3171 style = '' 

3172 weight = font.weight() 

3173 table2 = ( 

3174 (Weight.Light, 'light'), # #2330. 

3175 (Weight.Normal, 'normal'), 

3176 (Weight.DemiBold, 'demibold'), 

3177 (Weight.Bold, 'bold'), 

3178 (Weight.Black, 'black')) 

3179 for val2, name2 in table2: 

3180 if weight == val2: 

3181 weight = name2 

3182 break 

3183 else: 

3184 weight = '' 

3185 table3 = ( 

3186 ('family', str(font.family())), 

3187 ('size ', font.pointSize()), 

3188 ('style ', style), 

3189 ('weight', weight), 

3190 ) 

3191 for key3, val3 in table3: 

3192 if val3: 

3193 g.es(key3, val3, tabName='Fonts') 

3194 #@+node:ekr.20110605121601.18339: *4* LeoQtLog.hideFontTab 

3195 def hideFontTab(self, event=None): 

3196 c = self.c 

3197 c.frame.log.selectTab('Log') 

3198 c.bodyWantsFocus() 

3199 #@+node:ekr.20111120124732.10184: *3* LeoQtLog.isLogWidget 

3200 def isLogWidget(self, w): 

3201 val = w == self or w in list(self.contentsDict.values()) 

3202 return val 

3203 #@+node:tbnorth.20171220123648.1: *3* LeoQtLog.linkClicked 

3204 def linkClicked(self, link): 

3205 """linkClicked - link clicked in log 

3206 

3207 :param QUrl link: link that was clicked 

3208 """ 

3209 # see addition of '/' in LeoQtLog.put() 

3210 url = s = g.toUnicode(link.toString()) 

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

3212 for scheme in 'file', 'unl': 

3213 if s.startswith(scheme + ':///') and s[len(scheme) + 5] == ':': 

3214 url = s.replace(':///', '://', 1) 

3215 break 

3216 g.handleUrl(url, c=self.c) 

3217 #@+node:ekr.20120304214900.9940: *3* LeoQtLog.onCurrentChanged 

3218 def onCurrentChanged(self, idx): 

3219 

3220 tabw = self.tabWidget 

3221 w = tabw.widget(idx) 

3222 # 

3223 # #917814: Switching Log Pane tabs is done incompletely 

3224 wrapper = getattr(w, 'leo_log_wrapper', None) 

3225 # 

3226 # #1161: Don't change logs unless the wrapper is correct. 

3227 if wrapper and isinstance(wrapper, qt_text.QTextEditWrapper): 

3228 self.logCtrl = wrapper 

3229 #@+node:ekr.20200304132424.1: *3* LeoQtLog.onContextMenu 

3230 def onContextMenu(self, point): 

3231 """LeoQtLog: Callback for customContextMenuRequested events.""" 

3232 # #1286. 

3233 c, w = self.c, self 

3234 g.app.gui.onContextMenu(c, w, point) 

3235 #@+node:ekr.20110605121601.18321: *3* LeoQtLog.put & putnl 

3236 #@+node:ekr.20110605121601.18322: *4* LeoQtLog.put 

3237 def put(self, s, color=None, tabName='Log', from_redirect=False, nodeLink=None): 

3238 """ 

3239 Put s to the Qt Log widget, converting to html. 

3240 All output to the log stream eventually comes here. 

3241 

3242 The from_redirect keyword argument is no longer used. 

3243 """ 

3244 c = self.c 

3245 if g.app.quitting or not c or not c.exists: 

3246 return 

3247 # Note: g.actualColor does all color translation. 

3248 if color: 

3249 color = leoColor.getColor(color) 

3250 if not color: 

3251 # #788: First, fall back to 'log_black_color', not 'black. 

3252 color = c.config.getColor('log-black-color') 

3253 if not color: 

3254 # Should never be necessary. 

3255 color = 'black' 

3256 self.selectTab(tabName or 'Log') 

3257 # Must be done after the call to selectTab. 

3258 wrapper = self.logCtrl 

3259 if not isinstance(wrapper, qt_text.QTextEditWrapper): 

3260 g.trace('BAD wrapper', wrapper.__class__.__name__) 

3261 return 

3262 w = wrapper.widget 

3263 if not isinstance(w, QtWidgets.QTextEdit): 

3264 g.trace('BAD widget', w.__class__.__name__) 

3265 return 

3266 sb = w.horizontalScrollBar() 

3267 s = s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;') 

3268 # #884: Always convert leading blanks and tabs to &nbsp. 

3269 n = len(s) - len(s.lstrip()) 

3270 if n > 0 and s.strip(): 

3271 s = '&nbsp;' * (n) + s[n:] 

3272 if not self.wrap: 

3273 # Convert all other blanks to &nbsp; 

3274 s = s.replace(' ', '&nbsp;') 

3275 s = s.replace('\n', '<br>') # The caller is responsible for newlines! 

3276 s = f'<font color="{color}">{s}</font>' 

3277 if nodeLink: 

3278 url = nodeLink 

3279 for scheme in 'file', 'unl': 

3280 # QUrl requires paths start with '/' 

3281 if ( 

3282 url.startswith(scheme + '://') and not 

3283 url.startswith(scheme + ':///') 

3284 ): 

3285 url = url.replace('://', ':///', 1) 

3286 s = f'<a href="{url}" title="{nodeLink}">{s}</a>' 

3287 w.insertHtml(s) 

3288 w.moveCursor(MoveOperation.End) 

3289 sb.setSliderPosition(0) # Force the slider to the initial position. 

3290 w.repaint() # Slow, but essential. 

3291 #@+node:ekr.20110605121601.18323: *4* LeoQtLog.putnl 

3292 def putnl(self, tabName='Log'): 

3293 """Put a newline to the Qt log.""" 

3294 # 

3295 # This is not called normally. 

3296 if g.app.quitting: 

3297 return 

3298 if tabName: 

3299 self.selectTab(tabName) 

3300 wrapper = self.logCtrl 

3301 if not isinstance(wrapper, qt_text.QTextEditWrapper): 

3302 g.trace('BAD wrapper', wrapper.__class__.__name__) 

3303 return 

3304 w = wrapper.widget 

3305 if not isinstance(w, QtWidgets.QTextEdit): 

3306 g.trace('BAD widget', w.__class__.__name__) 

3307 return 

3308 sb = w.horizontalScrollBar() 

3309 pos = sb.sliderPosition() 

3310 # Not needed! 

3311 # contents = w.toHtml() 

3312 # w.setHtml(contents + '\n') 

3313 w.moveCursor(MoveOperation.End) 

3314 sb.setSliderPosition(pos) 

3315 w.repaint() # Slow, but essential. 

3316 #@+node:ekr.20150205181818.5: *4* LeoQtLog.scrollToEnd 

3317 def scrollToEnd(self, tabName='Log'): 

3318 """Scroll the log to the end.""" 

3319 if g.app.quitting: 

3320 return 

3321 if tabName: 

3322 self.selectTab(tabName) 

3323 w = self.logCtrl.widget 

3324 if not w: 

3325 return 

3326 sb = w.horizontalScrollBar() 

3327 pos = sb.sliderPosition() 

3328 w.moveCursor(MoveOperation.End) 

3329 sb.setSliderPosition(pos) 

3330 w.repaint() # Slow, but essential. 

3331 #@+node:ekr.20120913110135.10613: *3* LeoQtLog.putImage 

3332 #@+node:ekr.20110605121601.18324: *3* LeoQtLog.Tab 

3333 #@+node:ekr.20110605121601.18325: *4* LeoQtLog.clearTab 

3334 def clearTab(self, tabName, wrap='none'): 

3335 w = self.logDict.get(tabName) 

3336 if w: 

3337 w.clear() # w is a QTextBrowser. 

3338 #@+node:ekr.20110605121601.18326: *4* LeoQtLog.createTab 

3339 def createTab(self, tabName, createText=True, widget=None, wrap='none'): 

3340 """ 

3341 Create a new tab in tab widget 

3342 if widget is None, Create a QTextBrowser, 

3343 suitable for log functionality. 

3344 """ 

3345 c = self.c 

3346 if widget is None: 

3347 # widget is subclass of QTextBrowser. 

3348 widget = qt_text.LeoQTextBrowser(parent=None, c=c, wrapper=self) 

3349 # contents is a wrapper. 

3350 contents = qt_text.QTextEditWrapper(widget=widget, name='log', c=c) 

3351 # Inject an ivar into the QTextBrowser that points to the wrapper. 

3352 widget.leo_log_wrapper = contents 

3353 widget.setWordWrapMode(WrapMode.WordWrap if self.wrap else WrapMode.NoWrap) 

3354 widget.setReadOnly(False) # Allow edits. 

3355 self.logDict[tabName] = widget 

3356 if tabName == 'Log': 

3357 self.logCtrl = contents 

3358 widget.setObjectName('log-widget') 

3359 # Set binding on all log pane widgets. 

3360 g.app.gui.setFilter(c, widget, self, tag='log') 

3361 self.contentsDict[tabName] = widget 

3362 self.tabWidget.addTab(widget, tabName) 

3363 else: 

3364 # #1161: Don't set the wrapper unless it has the correct type. 

3365 contents = widget # Unlike text widgets, contents is the actual widget. 

3366 if isinstance(contents, qt_text.QTextEditWrapper): 

3367 widget.leo_log_wrapper = widget # The leo_log_wrapper is the widget itself. 

3368 else: 

3369 widget.leo_log_wrapper = None # Tell the truth. 

3370 g.app.gui.setFilter(c, widget, contents, 'tabWidget') 

3371 self.contentsDict[tabName] = contents 

3372 self.tabWidget.addTab(contents, tabName) 

3373 return contents 

3374 #@+node:ekr.20110605121601.18328: *4* LeoQtLog.deleteTab 

3375 def deleteTab(self, tabName): 

3376 """ 

3377 Delete the tab if it exists. Otherwise do *nothing*. 

3378 """ 

3379 c = self.c 

3380 w = self.tabWidget 

3381 i = self.findTabIndex(tabName) 

3382 if i is None: 

3383 return 

3384 w.removeTab(i) 

3385 self.selectTab('Log') 

3386 c.invalidateFocus() 

3387 c.bodyWantsFocus() 

3388 #@+node:ekr.20190603062456.1: *4* LeoQtLog.findTabIndex 

3389 def findTabIndex(self, tabName): 

3390 """Return the tab index for tabName, or None.""" 

3391 w = self.tabWidget 

3392 for i in range(w.count()): 

3393 if tabName == w.tabText(i): 

3394 return i 

3395 return None 

3396 #@+node:ekr.20110605121601.18329: *4* LeoQtLog.hideTab 

3397 def hideTab(self, tabName): 

3398 self.selectTab('Log') 

3399 #@+node:ekr.20111122080923.10185: *4* LeoQtLog.orderedTabNames 

3400 def orderedTabNames(self, LeoLog=None): # Unused: LeoLog 

3401 """Return a list of tab names in the order in which they appear in the QTabbedWidget.""" 

3402 w = self.tabWidget 

3403 return [w.tabText(i) for i in range(w.count())] 

3404 #@+node:ekr.20110605121601.18330: *4* LeoQtLog.numberOfVisibleTabs 

3405 def numberOfVisibleTabs(self): 

3406 return len([val for val in self.contentsDict.values() if val is not None]) 

3407 # **Note**: the base-class version of this uses frameDict. 

3408 #@+node:ekr.20110605121601.18331: *4* LeoQtLog.selectTab & helpers 

3409 def selectTab(self, tabName, createText=True, widget=None, wrap='none'): 

3410 """Create the tab if necessary and make it active.""" 

3411 i = self.findTabIndex(tabName) 

3412 if i is None: 

3413 self.createTab(tabName, widget=widget, wrap=wrap) 

3414 self.finishCreateTab(tabName) 

3415 self.finishSelectTab(tabName) 

3416 #@+node:ekr.20190603064815.1: *5* LeoQtLog.finishCreateTab 

3417 def finishCreateTab(self, tabName): 

3418 """Finish creating the given tab. Do not set focus!""" 

3419 c = self.c 

3420 i = self.findTabIndex(tabName) 

3421 if i is None: 

3422 g.trace('Can not happen', tabName) 

3423 self.tabName = None 

3424 return 

3425 # #1161. 

3426 if tabName == 'Log': 

3427 wrapper = None 

3428 widget = self.contentsDict.get('Log') # a qt_text.QTextEditWrapper 

3429 if widget: 

3430 wrapper = getattr(widget, 'leo_log_wrapper', None) 

3431 if wrapper and isinstance(wrapper, qt_text.QTextEditWrapper): 

3432 self.logCtrl = wrapper 

3433 if not wrapper: 

3434 g.trace('NO LOG WRAPPER') 

3435 if tabName == 'Find': 

3436 # Do *not* set focus here! 

3437 # #1254861: Ctrl-f doesn't ensure find input field visible. 

3438 if c.config.getBool('auto-scroll-find-tab', default=True): 

3439 # This is the cause of unwanted scrolling. 

3440 findbox = c.findCommands.ftm.find_findbox 

3441 if hasattr(widget, 'ensureWidgetVisible'): 

3442 widget.ensureWidgetVisible(findbox) 

3443 else: 

3444 findbox.setFocus() 

3445 if tabName == 'Spell': 

3446 # Set a flag for the spell system. 

3447 widget = self.tabWidget.widget(i) 

3448 self.frameDict['Spell'] = widget 

3449 #@+node:ekr.20190603064816.1: *5* LeoQtLog.finishSelectTab 

3450 def finishSelectTab(self, tabName): 

3451 """Select the proper tab.""" 

3452 w = self.tabWidget 

3453 # Special case for Spell tab. 

3454 if tabName == 'Spell': 

3455 return 

3456 i = self.findTabIndex(tabName) 

3457 if i is None: 

3458 g.trace('can not happen', tabName) 

3459 self.tabName = None 

3460 return 

3461 w.setCurrentIndex(i) 

3462 self.tabName = tabName 

3463 #@-others 

3464#@+node:ekr.20110605121601.18340: ** class LeoQtMenu (LeoMenu) 

3465class LeoQtMenu(leoMenu.LeoMenu): 

3466 

3467 #@+others 

3468 #@+node:ekr.20110605121601.18341: *3* LeoQtMenu.__init__ 

3469 def __init__(self, c, frame, label): 

3470 """ctor for LeoQtMenu class.""" 

3471 assert frame 

3472 assert frame.c 

3473 super().__init__(frame) 

3474 self.leo_menu_label = label.replace('&', '').lower() 

3475 self.frame = frame 

3476 self.c = c 

3477 self.menuBar = c.frame.top.menuBar() 

3478 assert self.menuBar is not None 

3479 # Inject this dict into the commander. 

3480 if not hasattr(c, 'menuAccels'): 

3481 setattr(c, 'menuAccels', {}) 

3482 if 0: 

3483 self.font = c.config.getFontFromParams( 

3484 'menu_text_font_family', 'menu_text_font_size', 

3485 'menu_text_font_slant', 'menu_text_font_weight', 

3486 c.config.defaultMenuFontSize) 

3487 #@+node:ekr.20120306130648.9848: *3* LeoQtMenu.__repr__ 

3488 def __repr__(self): 

3489 return f"<LeoQtMenu: {self.leo_menu_label}>" 

3490 

3491 __str__ = __repr__ 

3492 #@+node:ekr.20110605121601.18342: *3* LeoQtMenu.Tkinter menu bindings 

3493 # See the Tk docs for what these routines are to do 

3494 #@+node:ekr.20110605121601.18343: *4* LeoQtMenu.Methods with Tk spellings 

3495 #@+node:ekr.20110605121601.18344: *5* LeoQtMenu.add_cascade 

3496 def add_cascade(self, parent, label, menu, underline): 

3497 """Wrapper for the Tkinter add_cascade menu method. 

3498 

3499 Adds a submenu to the parent menu, or the menubar.""" 

3500 # menu and parent are a QtMenuWrappers, subclasses of QMenu. 

3501 n = underline 

3502 if -1 < n < len(label): 

3503 label = label[:n] + '&' + label[n:] 

3504 menu.setTitle(label) 

3505 if parent: 

3506 parent.addMenu(menu) # QMenu.addMenu. 

3507 else: 

3508 self.menuBar.addMenu(menu) 

3509 label = label.replace('&', '').lower() 

3510 menu.leo_menu_label = label 

3511 return menu 

3512 #@+node:ekr.20110605121601.18345: *5* LeoQtMenu.add_command (Called by createMenuEntries) 

3513 def add_command(self, **keys): 

3514 """Wrapper for the Tkinter add_command menu method.""" 

3515 # pylint: disable=arguments-differ 

3516 accel = keys.get('accelerator') or '' 

3517 command = keys.get('command') or '' 

3518 commandName = keys.get('commandName') 

3519 label = keys.get('label') 

3520 n = keys.get('underline') 

3521 if n is None: 

3522 n = -1 

3523 menu = keys.get('menu') or self 

3524 if not label: 

3525 return 

3526 if -1 < n < len(label): 

3527 label = label[:n] + '&' + label[n:] 

3528 if accel: 

3529 label = f"{label}\t{accel}" 

3530 action = menu.addAction(label) # type:ignore 

3531 # 2012/01/20: Inject the command name into the action 

3532 # so that it can be enabled/disabled dynamically. 

3533 action.leo_command_name = commandName 

3534 if command: 

3535 

3536 def qt_add_command_callback(checked, label=label, command=command): 

3537 return command() 

3538 

3539 action.triggered.connect(qt_add_command_callback) 

3540 #@+node:ekr.20110605121601.18346: *5* LeoQtMenu.add_separator 

3541 def add_separator(self, menu): 

3542 """Wrapper for the Tkinter add_separator menu method.""" 

3543 if menu: 

3544 action = menu.addSeparator() 

3545 action.leo_menu_label = '*seperator*' 

3546 #@+node:ekr.20110605121601.18347: *5* LeoQtMenu.delete 

3547 def delete(self, menu, realItemName='<no name>'): 

3548 """Wrapper for the Tkinter delete menu method.""" 

3549 # if menu: 

3550 # return menu.delete(realItemName) 

3551 #@+node:ekr.20110605121601.18348: *5* LeoQtMenu.delete_range 

3552 def delete_range(self, menu, n1, n2): 

3553 """Wrapper for the Tkinter delete menu method.""" 

3554 # Menu is a subclass of QMenu and LeoQtMenu. 

3555 for z in menu.actions()[n1:n2]: 

3556 menu.removeAction(z) 

3557 #@+node:ekr.20110605121601.18349: *5* LeoQtMenu.destroy 

3558 def destroy(self, menu): 

3559 """Wrapper for the Tkinter destroy menu method.""" 

3560 # Fixed bug https://bugs.launchpad.net/leo-editor/+bug/1193870 

3561 if menu: 

3562 menu.menuBar.removeAction(menu.menuAction()) 

3563 #@+node:ekr.20110605121601.18350: *5* LeoQtMenu.index 

3564 def index(self, label): 

3565 """Return the index of the menu with the given label.""" 

3566 return 0 

3567 #@+node:ekr.20110605121601.18351: *5* LeoQtMenu.insert 

3568 def insert(self, menuName, position, label, command, underline=None): 

3569 

3570 menu = self.getMenu(menuName) 

3571 if menu and label: 

3572 n = underline or 0 

3573 if -1 > n > len(label): 

3574 label = label[:n] + '&' + label[n:] 

3575 action = menu.addAction(label) 

3576 if command: 

3577 

3578 def insert_callback(checked, label=label, command=command): 

3579 command() 

3580 

3581 action.triggered.connect(insert_callback) 

3582 #@+node:ekr.20110605121601.18352: *5* LeoQtMenu.insert_cascade 

3583 def insert_cascade(self, parent, index, label, menu, underline): 

3584 """Wrapper for the Tkinter insert_cascade menu method.""" 

3585 menu.setTitle(label) 

3586 label.replace('&', '').lower() 

3587 menu.leo_menu_label = label # was leo_label 

3588 if parent: 

3589 parent.addMenu(menu) 

3590 else: 

3591 self.menuBar.addMenu(menu) 

3592 action = menu.menuAction() 

3593 if action: 

3594 action.leo_menu_label = label 

3595 else: 

3596 g.trace('no action for menu', label) 

3597 return menu 

3598 #@+node:ekr.20110605121601.18353: *5* LeoQtMenu.new_menu 

3599 def new_menu(self, parent, tearoff=False, label=''): # label is for debugging. 

3600 """Wrapper for the Tkinter new_menu menu method.""" 

3601 c, leoFrame = self.c, self.frame 

3602 # Parent can be None, in which case it will be added to the menuBar. 

3603 menu = QtMenuWrapper(c, leoFrame, parent, label) 

3604 return menu 

3605 #@+node:ekr.20110605121601.18354: *4* LeoQtMenu.Methods with other spellings 

3606 #@+node:ekr.20110605121601.18355: *5* LeoQtMenu.clearAccel 

3607 def clearAccel(self, menu, name): 

3608 pass 

3609 # if not menu: 

3610 # return 

3611 # realName = self.getRealMenuName(name) 

3612 # realName = realName.replace("&","") 

3613 # menu.entryconfig(realName,accelerator='') 

3614 #@+node:ekr.20110605121601.18356: *5* LeoQtMenu.createMenuBar 

3615 def createMenuBar(self, frame): 

3616 """ 

3617 (LeoQtMenu) Create all top-level menus. 

3618 The menuBar itself has already been created. 

3619 """ 

3620 self.createMenusFromTables() 

3621 # This is LeoMenu.createMenusFromTables. 

3622 #@+node:ekr.20110605121601.18357: *5* LeoQtMenu.createOpenWithMenu 

3623 def createOpenWithMenu(self, parent, label, index, amp_index): 

3624 """ 

3625 Create the File:Open With submenu. 

3626 

3627 This is called from LeoMenu.createOpenWithMenuFromTable. 

3628 """ 

3629 # Use the existing Open With menu if possible. 

3630 menu = self.getMenu('openwith') 

3631 if not menu: 

3632 menu = self.new_menu(parent, tearoff=False, label=label) 

3633 menu.insert_cascade(parent, index, label, menu, underline=amp_index) 

3634 return menu 

3635 #@+node:ekr.20110605121601.18358: *5* LeoQtMenu.disable/enableMenu (not used) 

3636 def disableMenu(self, menu, name): 

3637 self.enableMenu(menu, name, False) 

3638 

3639 def enableMenu(self, menu, name, val): 

3640 """Enable or disable the item in the menu with the given name.""" 

3641 if menu and name: 

3642 val = bool(val) 

3643 for action in menu.actions(): 

3644 s = g.checkUnicode(action.text()).replace('&', '') 

3645 if s.startswith(name): 

3646 action.setEnabled(val) 

3647 break 

3648 #@+node:ekr.20110605121601.18359: *5* LeoQtMenu.getMenuLabel 

3649 def getMenuLabel(self, menu, name): 

3650 """Return the index of the menu item whose name (or offset) is given. 

3651 Return None if there is no such menu item.""" 

3652 # At present, it is valid to always return None. 

3653 #@+node:ekr.20110605121601.18360: *5* LeoQtMenu.setMenuLabel 

3654 def setMenuLabel(self, menu, name, label, underline=-1): 

3655 

3656 def munge(s): 

3657 return (s or '').replace('&', '') 

3658 

3659 # menu is a QtMenuWrapper. 

3660 

3661 if not menu: 

3662 

3663 return 

3664 realName = munge(self.getRealMenuName(name)) 

3665 realLabel = self.getRealMenuName(label) 

3666 for action in menu.actions(): 

3667 s = munge(action.text()) 

3668 s = s.split('\t')[0] 

3669 if s == realName: 

3670 action.setText(realLabel) 

3671 break 

3672 #@+node:ekr.20110605121601.18361: *3* LeoQtMenu.activateMenu & helper 

3673 def activateMenu(self, menuName): 

3674 """Activate the menu with the given name""" 

3675 # Menu is a QtMenuWrapper, a subclass of both QMenu and LeoQtMenu. 

3676 menu = self.getMenu(menuName) 

3677 if menu: 

3678 self.activateAllParentMenus(menu) 

3679 else: 

3680 g.trace(f"No such menu: {menuName}") 

3681 #@+node:ekr.20120922041923.10607: *4* LeoQtMenu.activateAllParentMenus 

3682 def activateAllParentMenus(self, menu): 

3683 """menu is a QtMenuWrapper. Activate it and all parent menus.""" 

3684 parent = menu.parent() 

3685 action = menu.menuAction() 

3686 if action: 

3687 if parent and isinstance(parent, QtWidgets.QMenuBar): 

3688 parent.setActiveAction(action) 

3689 elif parent: 

3690 self.activateAllParentMenus(parent) 

3691 parent.setActiveAction(action) 

3692 else: 

3693 g.trace(f"can not happen: no parent for {menu}") 

3694 else: 

3695 g.trace(f"can not happen: no action for {menu}") 

3696 #@+node:ekr.20120922041923.10613: *3* LeoQtMenu.deactivateMenuBar 

3697 # def deactivateMenuBar (self): 

3698 # """Activate the menu with the given name""" 

3699 # menubar = self.c.frame.top.leo_menubar 

3700 # menubar.setActiveAction(None) 

3701 # menubar.repaint() 

3702 #@+node:ekr.20110605121601.18362: *3* LeoQtMenu.getMacHelpMenu 

3703 def getMacHelpMenu(self, table): 

3704 return None 

3705 #@-others 

3706#@+node:ekr.20110605121601.18363: ** class LeoQTreeWidget (QTreeWidget) 

3707class LeoQTreeWidget(QtWidgets.QTreeWidget): # type:ignore 

3708 

3709 # To do: Generate @auto or @file nodes when appropriate. 

3710 

3711 def __init__(self, c, parent): 

3712 super().__init__(parent) 

3713 self.setAcceptDrops(True) 

3714 enable_drag = c.config.getBool('enable-tree-dragging') 

3715 self.setDragEnabled(bool(enable_drag)) 

3716 self.c = c 

3717 self.was_alt_drag = False 

3718 self.was_control_drag = False 

3719 

3720 def __repr__(self): 

3721 return f"LeoQTreeWidget: {id(self)}" 

3722 

3723 __str__ = __repr__ 

3724 

3725 

3726 def dragMoveEvent(self, ev): # Called during drags. 

3727 pass 

3728 

3729 #@+others 

3730 #@+node:ekr.20111022222228.16980: *3* LeoQTreeWidget: Event handlers 

3731 #@+node:ekr.20110605121601.18364: *4* LeoQTreeWidget.dragEnterEvent & helper 

3732 def dragEnterEvent(self, ev): 

3733 """Export c.p's tree as a Leo mime-data.""" 

3734 c = self.c 

3735 if not ev: 

3736 g.trace('no event!') 

3737 return 

3738 md = ev.mimeData() 

3739 if not md: 

3740 g.trace('No mimeData!') 

3741 return 

3742 c.endEditing() 

3743 # Fix bug 135: cross-file drag and drop is broken. 

3744 # This handler may be called several times for the same drag. 

3745 # Only the first should should set g.app.drag_source. 

3746 if g.app.dragging: 

3747 pass 

3748 else: 

3749 g.app.dragging = True 

3750 g.app.drag_source = c, c.p 

3751 self.setText(md) 

3752 # Always accept the drag, even if we are already dragging. 

3753 ev.accept() 

3754 #@+node:ekr.20110605121601.18384: *5* LeoQTreeWidget.setText 

3755 def setText(self, md): 

3756 c = self.c 

3757 fn = self.fileName() 

3758 s = c.fileCommands.outline_to_clipboard_string() 

3759 md.setText(f"{fn},{s}") 

3760 #@+node:ekr.20110605121601.18365: *4* LeoQTreeWidget.dropEvent & helpers 

3761 def dropEvent(self, ev): 

3762 """Handle a drop event in the QTreeWidget.""" 

3763 if not ev: 

3764 return 

3765 md = ev.mimeData() 

3766 if not md: 

3767 g.trace('no mimeData!') 

3768 return 

3769 try: 

3770 mods = ev.modifiers() if isQt6 else int(ev.keyboardModifiers()) 

3771 self.was_alt_drag = bool(mods & KeyboardModifier.AltModifier) 

3772 self.was_control_drag = bool(mods & KeyboardModifier.ControlModifier) 

3773 except Exception: # Defensive. 

3774 g.es_exception() 

3775 g.app.dragging = False 

3776 return 

3777 c, tree = self.c, self.c.frame.tree 

3778 p = None 

3779 point = ev.position().toPoint() if isQt6 else ev.pos() 

3780 item = self.itemAt(point) 

3781 if item: 

3782 itemHash = tree.itemHash(item) 

3783 p = tree.item2positionDict.get(itemHash) 

3784 if not p: 

3785 # #59: Drop at last node. 

3786 p = c.rootPosition() 

3787 while p.hasNext(): 

3788 p.moveToNext() 

3789 formats = set(str(f) for f in md.formats()) 

3790 ev.setDropAction(DropAction.IgnoreAction) 

3791 ev.accept() 

3792 hookres = g.doHook("outlinedrop", c=c, p=p, dropevent=ev, formats=formats) 

3793 if hookres: 

3794 # A plugin handled the drop. 

3795 pass 

3796 else: 

3797 if md.hasUrls(): 

3798 self.urlDrop(md, p) 

3799 else: 

3800 self.nodeDrop(md, p) 

3801 g.app.dragging = False 

3802 #@+node:ekr.20110605121601.18366: *5* LeoQTreeWidget.nodeDrop & helpers 

3803 def nodeDrop(self, md, p): 

3804 """ 

3805 Handle a drop event when not md.urls(). 

3806 This will happen when we drop an outline node. 

3807 We get the copied text from md.text(). 

3808 """ 

3809 c = self.c 

3810 fn, s = self.parseText(md) 

3811 if not s or not fn: 

3812 return 

3813 if fn == self.fileName(): 

3814 if p and p == c.p: 

3815 pass 

3816 elif g.os_path_exists(fn): 

3817 self.intraFileDrop(fn, c.p, p) 

3818 else: 

3819 self.interFileDrop(fn, p, s) 

3820 #@+node:ekr.20110605121601.18367: *6* LeoQTreeWidget.interFileDrop 

3821 def interFileDrop(self, fn, p, s): 

3822 """Paste the mime data after (or as the first child of) p.""" 

3823 c = self.c 

3824 u = c.undoer 

3825 undoType = 'Drag Outline' 

3826 isLeo = g.match(s, 0, g.app.prolog_prefix_string) 

3827 if not isLeo: 

3828 return 

3829 c.selectPosition(p) 

3830 # Paste the node after the presently selected node. 

3831 pasted = c.fileCommands.getLeoOutlineFromClipboard(s) 

3832 if not pasted: 

3833 return 

3834 if c.config.getBool('inter-outline-drag-moves'): 

3835 src_c, src_p = g.app.drag_source 

3836 if src_p.hasVisNext(src_c): 

3837 nxt = src_p.getVisNext(src_c).v 

3838 elif src_p.hasVisBack(src_c): 

3839 nxt = src_p.getVisBack(src_c).v 

3840 else: 

3841 nxt = None 

3842 if nxt is not None: 

3843 src_p.doDelete() 

3844 src_c.selectPosition(src_c.vnode2position(nxt)) 

3845 src_c.setChanged() 

3846 src_c.redraw() 

3847 else: 

3848 g.es("Can't move last node out of outline") 

3849 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

3850 c.validateOutline() 

3851 c.selectPosition(pasted) 

3852 pasted.setDirty() # 2011/02/27: Fix bug 690467. 

3853 c.setChanged() 

3854 back = pasted.back() 

3855 if back and back.isExpanded(): 

3856 pasted.moveToNthChildOf(back, 0) 

3857 # c.setRootPosition(c.findRootPosition(pasted)) 

3858 u.afterInsertNode(pasted, undoType, undoData) 

3859 c.redraw(pasted) 

3860 c.recolor() 

3861 #@+node:ekr.20110605121601.18368: *6* LeoQTreeWidget.intraFileDrop 

3862 def intraFileDrop(self, fn, p1, p2): 

3863 """Move p1 after (or as the first child of) p2.""" 

3864 as_child = self.was_alt_drag 

3865 cloneDrag = self.was_control_drag 

3866 c = self.c 

3867 u = c.undoer 

3868 c.selectPosition(p1) 

3869 if as_child or p2.hasChildren() and p2.isExpanded(): 

3870 # Attempt to move p1 to the first child of p2. 

3871 # parent = p2 

3872 

3873 def move(p1, p2): 

3874 if cloneDrag: 

3875 p1 = p1.clone() 

3876 p1.moveToNthChildOf(p2, 0) 

3877 p1.setDirty() 

3878 return p1 

3879 

3880 else: 

3881 # Attempt to move p1 after p2. 

3882 # parent = p2.parent() 

3883 

3884 def move(p1, p2): 

3885 if cloneDrag: 

3886 p1 = p1.clone() 

3887 p1.moveAfter(p2) 

3888 p1.setDirty() 

3889 return p1 

3890 

3891 ok = ( 

3892 # 2011/10/03: Major bug fix. 

3893 c.checkDrag(p1, p2) and 

3894 c.checkMoveWithParentWithWarning(p1, p2, True)) 

3895 if ok: 

3896 undoData = u.beforeMoveNode(p1) 

3897 p1.setDirty() 

3898 p1 = move(p1, p2) 

3899 if cloneDrag: 

3900 # Set dirty bits for ancestors of *all* cloned nodes. 

3901 for z in p1.self_and_subtree(): 

3902 z.setDirty() 

3903 c.setChanged() 

3904 u.afterMoveNode(p1, 'Drag', undoData) 

3905 if (not as_child or 

3906 p2.isExpanded() or 

3907 c.config.getBool("drag-alt-drag-expands") is not False 

3908 ): 

3909 c.redraw(p1) 

3910 else: 

3911 c.redraw(p2) 

3912 #@+node:ekr.20110605121601.18383: *6* LeoQTreeWidget.parseText 

3913 def parseText(self, md): 

3914 """Parse md.text() into (fn,s)""" 

3915 fn = '' 

3916 s = md.text() 

3917 if s: 

3918 i = s.find(',') 

3919 if i == -1: 

3920 pass 

3921 else: 

3922 fn = s[:i] 

3923 s = s[i + 1 :] 

3924 return fn, s 

3925 #@+node:ekr.20110605121601.18369: *5* LeoQTreeWidget.urlDrop & helpers 

3926 def urlDrop(self, md, p): 

3927 """Handle a drop when md.urls().""" 

3928 c, u, undoType = self.c, self.c.undoer, 'Drag Urls' 

3929 urls = md.urls() 

3930 if not urls: 

3931 return 

3932 c.undoer.beforeChangeGroup(c.p, undoType) 

3933 changed = False 

3934 for z in urls: 

3935 url = QtCore.QUrl(z) 

3936 scheme = url.scheme() 

3937 if scheme == 'file': 

3938 changed |= self.doFileUrl(p, url) 

3939 elif scheme in ('http',): # 'ftp','mailto', 

3940 changed |= self.doHttpUrl(p, url) 

3941 if changed: 

3942 c.setChanged() 

3943 u.afterChangeGroup(c.p, undoType, reportFlag=False) 

3944 c.redraw() 

3945 #@+node:ekr.20110605121601.18370: *6* LeoQTreeWidget.doFileUrl & helper 

3946 def doFileUrl(self, p, url): 

3947 """Read the file given by the url and put it in the outline.""" 

3948 # 2014/06/06: Work around a possible bug in QUrl. 

3949 # fn = str(url.path()) # Fails. 

3950 e = sys.getfilesystemencoding() 

3951 fn = g.toUnicode(url.path(), encoding=e) 

3952 if sys.platform.lower().startswith('win'): 

3953 if fn.startswith('/'): 

3954 fn = fn[1:] 

3955 if os.path.isdir(fn): 

3956 # Just insert an @path directory. 

3957 self.doPathUrlHelper(fn, p) 

3958 return True 

3959 if g.os_path_exists(fn): 

3960 try: 

3961 f = open(fn, 'rb') # 2012/03/09: use 'rb' 

3962 except IOError: 

3963 f = None 

3964 if f: 

3965 b = f.read() 

3966 s = g.toUnicode(b) 

3967 f.close() 

3968 return self.doFileUrlHelper(fn, p, s) 

3969 nodeLink = p.get_UNL() 

3970 g.es_print(f"not found: {fn}", nodeLink=nodeLink) 

3971 return False 

3972 #@+node:ekr.20110605121601.18371: *7* LeoQTreeWidget.doFileUrlHelper & helper 

3973 def doFileUrlHelper(self, fn, p, s): 

3974 """ 

3975 Insert s in an @file, @auto or @edit node after p. 

3976 If fn is a .leo file, insert a node containing its top-level nodes as children. 

3977 """ 

3978 c = self.c 

3979 if self.isLeoFile(fn, s) and not self.was_control_drag: 

3980 g.openWithFileName(fn, old_c=c) 

3981 return False # Don't set the changed marker in the original file. 

3982 u, undoType = c.undoer, 'Drag File' 

3983 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

3984 if p.hasChildren() and p.isExpanded(): 

3985 p2 = p.insertAsNthChild(0) 

3986 parent = p 

3987 elif p.h.startswith('@path '): 

3988 # #60: create relative paths & urls when dragging files. 

3989 p2 = p.insertAsNthChild(0) 

3990 p.expand() 

3991 parent = p 

3992 else: 

3993 p2 = p.insertAfter() 

3994 parent = p.parent() 

3995 # #60: create relative paths & urls when dragging files. 

3996 aList = g.get_directives_dict_list(parent) 

3997 path = g.scanAtPathDirectives(c, aList) 

3998 if path: 

3999 fn = os.path.relpath(fn, path) 

4000 self.createAtFileNode(fn, p2, s) 

4001 u.afterInsertNode(p2, undoType, undoData) 

4002 c.selectPosition(p2) 

4003 return True # The original .leo file has changed. 

4004 #@+node:ekr.20110605121601.18372: *8* LeoQTreeWidget.createAtFileNode & helpers (QTreeWidget) 

4005 def createAtFileNode(self, fn, p, s): 

4006 """ 

4007 Set p's headline, body text and possibly descendants 

4008 based on the file's name fn and contents s. 

4009 

4010 If the file is an thin file, create an @file tree. 

4011 Othewise, create an @auto tree. 

4012 If all else fails, create an @edit node. 

4013 

4014 Give a warning if a node with the same headline already exists. 

4015 """ 

4016 c = self.c 

4017 c.init_error_dialogs() 

4018 if self.isLeoFile(fn, s): 

4019 self.createLeoFileTree(fn, p) 

4020 elif self.isThinFile(fn, s): 

4021 self.createAtFileTree(fn, p, s) 

4022 elif self.isAutoFile(fn): 

4023 self.createAtAutoTree(fn, p) 

4024 elif self.isBinaryFile(fn): 

4025 self.createUrlForBinaryFile(fn, p) 

4026 else: 

4027 self.createAtEditNode(fn, p) 

4028 self.warnIfNodeExists(p) 

4029 c.raise_error_dialogs(kind='read') 

4030 #@+node:ekr.20110605121601.18373: *9* LeoQTreeWidget.createAtAutoTree (QTreeWidget) 

4031 def createAtAutoTree(self, fn, p): 

4032 """ 

4033 Make p an @auto node and create the tree using s, the file's contents. 

4034 """ 

4035 c = self.c 

4036 at = c.atFileCommands 

4037 p.h = f"@auto {fn}" 

4038 at.readOneAtAutoNode(p) 

4039 # No error recovery should be needed here. 

4040 p.clearDirty() # Don't automatically rewrite this node. 

4041 #@+node:ekr.20110605121601.18374: *9* LeoQTreeWidget.createAtEditNode 

4042 def createAtEditNode(self, fn, p): 

4043 c = self.c 

4044 at = c.atFileCommands 

4045 # Use the full @edit logic, so dragging will be 

4046 # exactly the same as reading. 

4047 at.readOneAtEditNode(fn, p) 

4048 p.h = f"@edit {fn}" 

4049 p.clearDirty() # Don't automatically rewrite this node. 

4050 #@+node:ekr.20110605121601.18375: *9* LeoQTreeWidget.createAtFileTree 

4051 def createAtFileTree(self, fn, p, s): 

4052 """Make p an @file node and create the tree using s, the file's contents.""" 

4053 c = self.c 

4054 at = c.atFileCommands 

4055 p.h = f"@file {fn}" 

4056 # Read the file into p. 

4057 ok = at.read(root=p.copy(), fromString=s) 

4058 if not ok: 

4059 g.error('Error reading', fn) 

4060 p.b = '' # Safe: will not cause a write later. 

4061 p.clearDirty() # Don't automatically rewrite this node. 

4062 #@+node:ekr.20141007223054.18004: *9* LeoQTreeWidget.createLeoFileTree 

4063 def createLeoFileTree(self, fn, p): 

4064 """Copy all nodes from fn, a .leo file, to the children of p.""" 

4065 c = self.c 

4066 p.h = f"From {g.shortFileName(fn)}" 

4067 c.selectPosition(p) 

4068 # Create a dummy first child of p. 

4069 dummy_p = p.insertAsNthChild(0) 

4070 c.selectPosition(dummy_p) 

4071 c2 = g.openWithFileName(fn, old_c=c, gui=g.app.nullGui) 

4072 for p2 in c2.rootPosition().self_and_siblings(): 

4073 c2.selectPosition(p2) 

4074 s = c2.fileCommands.outline_to_clipboard_string() 

4075 # Paste the outline after the selected node. 

4076 c.fileCommands.getLeoOutlineFromClipboard(s) 

4077 dummy_p.doDelete() 

4078 c.selectPosition(p) 

4079 p.v.contract() 

4080 c2.close() 

4081 g.app.forgetOpenFile(c2.fileName()) # Necessary. 

4082 #@+node:ekr.20120309075544.9882: *9* LeoQTreeWidget.createUrlForBinaryFile 

4083 def createUrlForBinaryFile(self, fn, p): 

4084 # Fix bug 1028986: create relative urls when dragging binary files to Leo. 

4085 c = self.c 

4086 base_fn = g.os_path_normcase(g.os_path_abspath(c.mFileName)) 

4087 abs_fn = g.os_path_normcase(g.os_path_abspath(fn)) 

4088 prefix = os.path.commonprefix([abs_fn, base_fn]) 

4089 if prefix and len(prefix) > 3: # Don't just strip off c:\. 

4090 p.h = abs_fn[len(prefix) :].strip() 

4091 else: 

4092 p.h = f"@url file://{fn}" 

4093 #@+node:ekr.20110605121601.18377: *9* LeoQTreeWidget.isAutoFile (LeoQTreeWidget) 

4094 def isAutoFile(self, fn): 

4095 """Return true if fn (a file name) can be parsed with an @auto parser.""" 

4096 d = g.app.classDispatchDict 

4097 junk, ext = g.os_path_splitext(fn) 

4098 return d.get(ext) 

4099 #@+node:ekr.20120309075544.9881: *9* LeoQTreeWidget.isBinaryFile 

4100 def isBinaryFile(self, fn): 

4101 # The default for unknown files is True. Not great, but safe. 

4102 junk, ext = g.os_path_splitext(fn) 

4103 ext = ext.lower() 

4104 if not ext: 

4105 val = False 

4106 elif ext.startswith('~'): 

4107 val = False 

4108 elif ext in ('.css', '.htm', '.html', '.leo', '.txt'): 

4109 val = False 

4110 # elif ext in ('.bmp','gif','ico',): 

4111 # val = True 

4112 else: 

4113 keys = (z.lower() for z in g.app.extension_dict) 

4114 val = ext not in keys 

4115 return val 

4116 #@+node:ekr.20141007223054.18003: *9* LeoQTreeWidget.isLeoFile 

4117 def isLeoFile(self, fn, s): 

4118 """Return true if fn (a file name) represents an entire .leo file.""" 

4119 return fn.endswith('.leo') and s.startswith(g.app.prolog_prefix_string) 

4120 #@+node:ekr.20110605121601.18376: *9* LeoQTreeWidget.isThinFile 

4121 def isThinFile(self, fn, s): 

4122 """ 

4123 Return true if the file whose contents is s 

4124 was created from an @thin or @file tree. 

4125 """ 

4126 c = self.c 

4127 at = c.atFileCommands 

4128 # Skip lines before the @+leo line. 

4129 i = s.find('@+leo') 

4130 if i == -1: 

4131 return False 

4132 # Like at.isFileLike. 

4133 j, k = g.getLine(s, i) 

4134 line = s[j:k] 

4135 valid, new_df, start, end, isThin = at.parseLeoSentinel(line) 

4136 return valid and new_df and isThin 

4137 #@+node:ekr.20110605121601.18378: *9* LeoQTreeWidget.warnIfNodeExists 

4138 def warnIfNodeExists(self, p): 

4139 c = self.c 

4140 h = p.h 

4141 for p2 in c.all_unique_positions(): 

4142 if p2.h == h and p2 != p: 

4143 g.warning('Warning: duplicate node:', h) 

4144 break 

4145 #@+node:ekr.20110605121601.18379: *7* LeoQTreeWidget.doPathUrlHelper 

4146 def doPathUrlHelper(self, fn, p): 

4147 """Insert fn as an @path node after p.""" 

4148 c = self.c 

4149 u, undoType = c.undoer, 'Drag Directory' 

4150 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

4151 if p.hasChildren() and p.isExpanded(): 

4152 p2 = p.insertAsNthChild(0) 

4153 else: 

4154 p2 = p.insertAfter() 

4155 p2.h = '@path ' + fn 

4156 u.afterInsertNode(p2, undoType, undoData) 

4157 c.selectPosition(p2) 

4158 #@+node:ekr.20110605121601.18380: *6* LeoQTreeWidget.doHttpUrl 

4159 def doHttpUrl(self, p, url): 

4160 """Insert the url in an @url node after p.""" 

4161 c = self.c 

4162 u = c.undoer 

4163 undoType = 'Drag Url' 

4164 s = str(url.toString()).strip() 

4165 if not s: 

4166 return False 

4167 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

4168 if p.hasChildren() and p.isExpanded(): 

4169 p2 = p.insertAsNthChild(0) 

4170 else: 

4171 p2 = p.insertAfter() 

4172 p2.h = '@url' 

4173 p2.b = s 

4174 p2.clearDirty() # Don't automatically rewrite this node. 

4175 u.afterInsertNode(p2, undoType, undoData) 

4176 return True 

4177 #@+node:ekr.20110605121601.18381: *3* LeoQTreeWidget: utils 

4178 #@+node:ekr.20110605121601.18382: *4* LeoQTreeWidget.dump 

4179 def dump(self, ev, p, tag): 

4180 if ev: 

4181 md = ev.mimeData() 

4182 s = g.checkUnicode(md.text(), encoding='utf-8') 

4183 g.trace('md.text:', repr(s) if len(s) < 100 else len(s)) 

4184 for url in md.urls() or []: 

4185 g.trace(' url:', url) 

4186 g.trace(' url.fn:', url.toLocalFile()) 

4187 g.trace('url.text:', url.toString()) 

4188 else: 

4189 g.trace('', tag, '** no event!') 

4190 #@+node:ekr.20141007223054.18002: *4* LeoQTreeWidget.fileName 

4191 def fileName(self): 

4192 """Return the commander's filename.""" 

4193 return self.c.fileName() or '<unsaved file>' 

4194 #@-others 

4195#@+node:ekr.20110605121601.18385: ** class LeoQtSpellTab 

4196class LeoQtSpellTab: 

4197 #@+others 

4198 #@+node:ekr.20110605121601.18386: *3* LeoQtSpellTab.__init__ 

4199 def __init__(self, c, handler, tabName): 

4200 """Ctor for LeoQtSpellTab class.""" 

4201 self.c = c 

4202 top = c.frame.top 

4203 self.handler = handler 

4204 # hack: 

4205 handler.workCtrl = leoFrame.StringTextWrapper(c, 'spell-workctrl') 

4206 self.tabName = tabName 

4207 if hasattr(top, 'leo_spell_label'): 

4208 self.wordLabel = top.leo_spell_label 

4209 self.listBox = top.leo_spell_listBox 

4210 self.fillbox([]) 

4211 else: 

4212 self.handler.loaded = False 

4213 #@+node:ekr.20110605121601.18389: *3* Event handlers 

4214 #@+node:ekr.20110605121601.18390: *4* onAddButton 

4215 def onAddButton(self): 

4216 """Handle a click in the Add button in the Check Spelling dialog.""" 

4217 self.handler.add() 

4218 #@+node:ekr.20110605121601.18391: *4* onChangeButton & onChangeThenFindButton 

4219 def onChangeButton(self, event=None): 

4220 """Handle a click in the Change button in the Spell tab.""" 

4221 state = self.updateButtons() 

4222 if state: 

4223 self.handler.change() 

4224 self.updateButtons() 

4225 

4226 def onChangeThenFindButton(self, event=None): 

4227 """Handle a click in the "Change, Find" button in the Spell tab.""" 

4228 state = self.updateButtons() 

4229 if state: 

4230 self.handler.change() 

4231 if self.handler.change(): 

4232 self.handler.find() 

4233 self.updateButtons() 

4234 #@+node:ekr.20110605121601.18392: *4* onFindButton 

4235 def onFindButton(self): 

4236 """Handle a click in the Find button in the Spell tab.""" 

4237 c = self.c 

4238 self.handler.find() 

4239 self.updateButtons() 

4240 c.invalidateFocus() 

4241 c.bodyWantsFocus() 

4242 #@+node:ekr.20110605121601.18393: *4* onHideButton 

4243 def onHideButton(self): 

4244 """Handle a click in the Hide button in the Spell tab.""" 

4245 self.handler.hide() 

4246 #@+node:ekr.20110605121601.18394: *4* onIgnoreButton 

4247 def onIgnoreButton(self, event=None): 

4248 """Handle a click in the Ignore button in the Check Spelling dialog.""" 

4249 self.handler.ignore() 

4250 #@+node:ekr.20110605121601.18395: *4* onMap 

4251 def onMap(self, event=None): 

4252 """Respond to a Tk <Map> event.""" 

4253 self.update(show=False, fill=False) 

4254 #@+node:ekr.20110605121601.18396: *4* onSelectListBox 

4255 def onSelectListBox(self, event=None): 

4256 """Respond to a click in the selection listBox.""" 

4257 c = self.c 

4258 self.updateButtons() 

4259 c.bodyWantsFocus() 

4260 #@+node:ekr.20110605121601.18397: *3* Helpers 

4261 #@+node:ekr.20110605121601.18398: *4* bringToFront (LeoQtSpellTab) 

4262 def bringToFront(self): 

4263 self.c.frame.log.selectTab('Spell') 

4264 #@+node:ekr.20110605121601.18399: *4* fillbox (LeoQtSpellTab) 

4265 def fillbox(self, alts, word=None): 

4266 """Update the suggestions listBox in the Check Spelling dialog.""" 

4267 self.suggestions = alts 

4268 if not word: 

4269 word = "" 

4270 self.wordLabel.setText("Suggestions for: " + word) 

4271 self.listBox.clear() 

4272 if self.suggestions: 

4273 self.listBox.addItems(self.suggestions) 

4274 self.listBox.setCurrentRow(0) 

4275 #@+node:ekr.20110605121601.18400: *4* getSuggestion (LeoQtSpellTab) 

4276 def getSuggestion(self): 

4277 """Return the selected suggestion from the listBox.""" 

4278 idx = self.listBox.currentRow() 

4279 value = self.suggestions[idx] 

4280 return value 

4281 #@+node:ekr.20141113094129.13: *4* setFocus (LeoQtSpellTab) 

4282 def setFocus(self): 

4283 """Actually put focus in the tab.""" 

4284 # Not a great idea: there is no indication of focus. 

4285 c = self.c 

4286 if c.frame and c.frame.top and hasattr(c.frame.top, 'spellFrame'): 

4287 w = self.c.frame.top.spellFrame 

4288 c.widgetWantsFocus(w) 

4289 #@+node:ekr.20110605121601.18401: *4* update (LeoQtSpellTab) 

4290 def update(self, show=True, fill=False): 

4291 """Update the Spell Check dialog.""" 

4292 c = self.c 

4293 if fill: 

4294 self.fillbox([]) 

4295 self.updateButtons() 

4296 if show: 

4297 self.bringToFront() 

4298 c.bodyWantsFocus() 

4299 #@+node:ekr.20110605121601.18402: *4* updateButtons (spellTab) 

4300 def updateButtons(self): 

4301 """Enable or disable buttons in the Check Spelling dialog.""" 

4302 c = self.c 

4303 top, w = c.frame.top, c.frame.body.wrapper 

4304 state = self.suggestions and w.hasSelection() 

4305 top.leo_spell_btn_Change.setDisabled(not state) 

4306 top.leo_spell_btn_FindChange.setDisabled(not state) 

4307 return state 

4308 #@-others 

4309#@+node:ekr.20110605121601.18438: ** class LeoQtTreeTab 

4310class LeoQtTreeTab: 

4311 """ 

4312 A class representing a so-called tree-tab. 

4313 

4314 Actually, it represents a combo box 

4315 """ 

4316 #@+others 

4317 #@+node:ekr.20110605121601.18439: *3* Birth & death 

4318 #@+node:ekr.20110605121601.18440: *4* ctor (LeoQtTreeTab) 

4319 def __init__(self, c, iconBar): 

4320 """Ctor for LeoQtTreeTab class.""" 

4321 

4322 self.c = c 

4323 self.cc = c.chapterController 

4324 assert self.cc 

4325 self.iconBar = iconBar 

4326 self.lockout = False # True: do not redraw. 

4327 self.tabNames = [] 

4328 # The list of tab names. Changes when tabs are renamed. 

4329 self.w = None # The QComboBox 

4330 # self.reloadSettings() 

4331 self.createControl() 

4332 #@+node:ekr.20110605121601.18441: *4* tt.createControl (defines class LeoQComboBox) 

4333 def createControl(self): 

4334 

4335 

4336 class LeoQComboBox(QtWidgets.QComboBox): # type:ignore 

4337 """Create a subclass in order to handle focusInEvents.""" 

4338 

4339 def __init__(self, tt): 

4340 self.leo_tt = tt 

4341 super().__init__() 

4342 # Fix #458: Chapters drop-down list is not automatically resized. 

4343 self.setSizeAdjustPolicy(SizeAdjustPolicy.AdjustToContents) 

4344 

4345 def focusInEvent(self, event): 

4346 self.leo_tt.setNames() 

4347 QtWidgets.QComboBox.focusInEvent(self, event) # Call the base class 

4348 

4349 tt = self 

4350 frame = QtWidgets.QLabel('Chapters: ') 

4351 tt.iconBar.addWidget(frame) 

4352 tt.w = w = LeoQComboBox(tt) 

4353 tt.setNames() 

4354 tt.iconBar.addWidget(w) 

4355 

4356 def onIndexChanged(s, tt=tt): 

4357 if isinstance(s, int): 

4358 s = '' if s == -1 else tt.w.currentText() 

4359 else: # s is the tab name. 

4360 pass 

4361 if s and not tt.cc.selectChapterLockout: 

4362 tt.selectTab(s) 

4363 

4364 # A change: the argument could now be an int instead of a string. 

4365 w.currentIndexChanged.connect(onIndexChanged) 

4366 #@+node:ekr.20110605121601.18443: *3* tt.createTab 

4367 def createTab(self, tabName, select=True): 

4368 """LeoQtTreeTab.""" 

4369 tt = self 

4370 # Avoid a glitch during initing. 

4371 if tabName != 'main' and tabName not in tt.tabNames: 

4372 tt.tabNames.append(tabName) 

4373 tt.setNames() 

4374 #@+node:ekr.20110605121601.18444: *3* tt.destroyTab 

4375 def destroyTab(self, tabName): 

4376 """LeoQtTreeTab.""" 

4377 tt = self 

4378 if tabName in tt.tabNames: 

4379 tt.tabNames.remove(tabName) 

4380 tt.setNames() 

4381 #@+node:ekr.20110605121601.18445: *3* tt.selectTab 

4382 def selectTab(self, tabName): 

4383 """LeoQtTreeTab.""" 

4384 tt, c, cc = self, self.c, self.cc 

4385 exists = tabName in self.tabNames 

4386 c.treeWantsFocusNow() 

4387 # Fix #969. Somehow this is important. 

4388 if not exists: 

4389 tt.createTab(tabName) # Calls tt.setNames() 

4390 if tt.lockout: 

4391 return 

4392 cc.selectChapterByName(tabName) 

4393 c.redraw() 

4394 c.outerUpdate() 

4395 #@+node:ekr.20110605121601.18446: *3* tt.setTabLabel 

4396 def setTabLabel(self, tabName): 

4397 """LeoQtTreeTab.""" 

4398 w = self.w 

4399 i = w.findText(tabName) 

4400 if i > -1: 

4401 w.setCurrentIndex(i) 

4402 #@+node:ekr.20110605121601.18447: *3* tt.setNames 

4403 def setNames(self): 

4404 """LeoQtTreeTab: Recreate the list of items.""" 

4405 w = self.w 

4406 names = self.cc.setAllChapterNames() 

4407 w.clear() 

4408 w.insertItems(0, names) 

4409 #@-others 

4410#@+node:ekr.20110605121601.18448: ** class LeoTabbedTopLevel (LeoBaseTabWidget) 

4411class LeoTabbedTopLevel(LeoBaseTabWidget): 

4412 """ Toplevel frame for tabbed ui """ 

4413 

4414 def __init__(self, *args, **kwargs): 

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

4416 ## middle click close on tabs -- JMP 20140505 

4417 self.setMovable(False) 

4418 tb = QtTabBarWrapper(self) 

4419 self.setTabBar(tb) 

4420#@+node:peckj.20140505102552.10377: ** class QtTabBarWrapper (QTabBar) 

4421class QtTabBarWrapper(QtWidgets.QTabBar): # type:ignore 

4422 #@+others 

4423 #@+node:peckj.20140516114832.10108: *3* __init__ 

4424 def __init__(self, parent=None): 

4425 super().__init__(parent) 

4426 self.setMovable(True) 

4427 #@+node:peckj.20140516114832.10109: *3* mouseReleaseEvent (QtTabBarWrapper) 

4428 def mouseReleaseEvent(self, event): 

4429 # middle click close on tabs -- JMP 20140505 

4430 # closes Launchpad bug: https://bugs.launchpad.net/leo-editor/+bug/1183528 

4431 if event.button() == MouseButton.MiddleButton: 

4432 self.tabCloseRequested.emit(self.tabAt(event.pos())) 

4433 QtWidgets.QTabBar.mouseReleaseEvent(self, event) 

4434 #@-others 

4435#@+node:ekr.20110605121601.18458: ** class QtMenuWrapper (LeoQtMenu,QMenu) 

4436class QtMenuWrapper(LeoQtMenu, QtWidgets.QMenu): # type:ignore 

4437 #@+others 

4438 #@+node:ekr.20110605121601.18459: *3* ctor and __repr__(QtMenuWrapper) 

4439 def __init__(self, c, frame, parent, label): 

4440 """ctor for QtMenuWrapper class.""" 

4441 assert c 

4442 assert frame 

4443 if parent is None: 

4444 parent = c.frame.top.menuBar() 

4445 # 

4446 # For reasons unknown, the calls must be in this order. 

4447 # Presumably, the order of base classes also matters(!) 

4448 LeoQtMenu.__init__(self, c, frame, label) 

4449 QtWidgets.QMenu.__init__(self, parent) 

4450 label = label.replace('&', '').lower() 

4451 self.leo_menu_label = label 

4452 action = self.menuAction() 

4453 if action: 

4454 action.leo_menu_label = label 

4455 self.aboutToShow.connect(self.onAboutToShow) 

4456 

4457 def __repr__(self): 

4458 return f"<QtMenuWrapper {self.leo_menu_label}>" 

4459 #@+node:ekr.20110605121601.18460: *3* onAboutToShow & helpers (QtMenuWrapper) 

4460 def onAboutToShow(self, *args, **keys): 

4461 

4462 name = self.leo_menu_label 

4463 if not name: 

4464 return 

4465 for action in self.actions(): 

4466 commandName = hasattr(action, 'leo_command_name') and action.leo_command_name 

4467 if commandName: 

4468 self.leo_update_shortcut(action, commandName) 

4469 self.leo_enable_menu_item(action, commandName) 

4470 self.leo_update_menu_label(action, commandName) 

4471 #@+node:ekr.20120120095156.10261: *4* leo_enable_menu_item 

4472 def leo_enable_menu_item(self, action, commandName): 

4473 func = self.c.frame.menu.enable_dict.get(commandName) 

4474 if action and func: 

4475 val = func() 

4476 action.setEnabled(bool(val)) 

4477 #@+node:ekr.20120124115444.10190: *4* leo_update_menu_label 

4478 def leo_update_menu_label(self, action, commandName): 

4479 c = self.c 

4480 if action and commandName == 'mark': 

4481 action.setText('UnMark' if c.p.isMarked() else 'Mark') 

4482 self.leo_update_shortcut(action, commandName) 

4483 # Set the proper shortcut. 

4484 #@+node:ekr.20120120095156.10260: *4* leo_update_shortcut 

4485 def leo_update_shortcut(self, action, commandName): 

4486 

4487 c, k = self.c, self.c.k 

4488 if action: 

4489 s = action.text() 

4490 parts = s.split('\t') 

4491 if len(parts) >= 2: 

4492 s = parts[0] 

4493 key, aList = c.config.getShortcut(commandName) 

4494 if aList: 

4495 result = [] 

4496 for bi in aList: 

4497 # Don't show mode-related bindings. 

4498 if not bi.isModeBinding(): 

4499 accel = k.prettyPrintKey(bi.stroke) 

4500 result.append(accel) 

4501 # Break here if we want to show only one accerator. 

4502 action.setText(f"{s}\t{', '.join(result)}") 

4503 else: 

4504 action.setText(s) 

4505 else: 

4506 g.trace(f"can not happen: no action for {commandName}") 

4507 #@-others 

4508#@+node:ekr.20110605121601.18461: ** class QtSearchWidget 

4509class QtSearchWidget: 

4510 """A dummy widget class to pass to Leo's core find code.""" 

4511 

4512 def __init__(self): 

4513 self.insertPoint = 0 

4514 self.selection = 0, 0 

4515 self.wrapper = self 

4516 self.body = self 

4517 self.text = None 

4518#@+node:ekr.20110605121601.18464: ** class TabbedFrameFactory 

4519class TabbedFrameFactory: 

4520 """ 

4521 'Toplevel' frame builder for tabbed toplevel interface 

4522 

4523 This causes Leo to maintain only one toplevel window, 

4524 with multiple tabs for documents 

4525 """ 

4526 #@+others 

4527 #@+node:ekr.20110605121601.18465: *3* frameFactory.__init__ & __repr__ 

4528 def __init__(self): 

4529 # Will be created when first frame appears. 

4530 # Workaround a problem setting the window title when tabs are shown. 

4531 self.alwaysShowTabs = True 

4532 self.leoFrames = {} # Keys are DynamicWindows, values are frames. 

4533 self.masterFrame = None 

4534 self.createTabCommands() 

4535 #@+node:ekr.20110605121601.18466: *3* frameFactory.createFrame (changed, makes dw) 

4536 def createFrame(self, leoFrame): 

4537 

4538 c = leoFrame.c 

4539 tabw = self.masterFrame 

4540 dw = DynamicWindow(c, tabw) 

4541 self.leoFrames[dw] = leoFrame 

4542 # Shorten the title. 

4543 title = os.path.basename(c.mFileName) if c.mFileName else leoFrame.title 

4544 tip = leoFrame.title 

4545 dw.setWindowTitle(tip) 

4546 idx = tabw.addTab(dw, title) 

4547 if tip: 

4548 tabw.setTabToolTip(idx, tip) 

4549 dw.construct(master=tabw) 

4550 tabw.setCurrentIndex(idx) 

4551 g.app.gui.setFilter(c, dw, dw, tag='tabbed-frame') 

4552 # 

4553 # Work around the problem with missing dirty indicator 

4554 # by always showing the tab. 

4555 tabw.tabBar().setVisible(self.alwaysShowTabs or tabw.count() > 1) 

4556 tabw.setTabsClosable(c.config.getBool('outline-tabs-show-close', True)) 

4557 if not g.unitTesting: 

4558 # #1327: Must always do this. 

4559 # 2021/09/12: but not for new unit tests! 

4560 dw.show() 

4561 tabw.show() 

4562 return dw 

4563 #@+node:ekr.20110605121601.18468: *3* frameFactory.createMaster 

4564 def createMaster(self): 

4565 

4566 window = self.masterFrame = LeoTabbedTopLevel(factory=self) 

4567 tabbar = window.tabBar() 

4568 g.app.gui.attachLeoIcon(window) 

4569 try: 

4570 tabbar.setTabsClosable(True) 

4571 tabbar.tabCloseRequested.connect(self.slotCloseRequest) 

4572 except AttributeError: 

4573 pass # Qt 4.4 does not support setTabsClosable 

4574 window.currentChanged.connect(self.slotCurrentChanged) 

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

4576 g.trace( 

4577 f"minimized: {g.app.start_minimized}, " 

4578 f"maximized: {g.app.start_maximized}, " 

4579 f"fullscreen: {g.app.start_fullscreen}") 

4580 # 

4581 # #1189: We *can* (and should) minimize here, to eliminate flash. 

4582 if g.app.start_minimized: 

4583 window.showMinimized() 

4584 #@+node:ekr.20110605121601.18472: *3* frameFactory.createTabCommands 

4585 def detachTab(self, wdg): 

4586 """ Detach specified tab as individual toplevel window """ 

4587 del self.leoFrames[wdg] 

4588 wdg.setParent(None) 

4589 wdg.show() 

4590 

4591 def createTabCommands(self): 

4592 #@+<< Commands for tabs >> 

4593 #@+node:ekr.20110605121601.18473: *4* << Commands for tabs >> 

4594 @g.command('tab-detach') 

4595 def tab_detach(event): 

4596 """ Detach current tab from tab bar """ 

4597 if len(self.leoFrames) < 2: 

4598 g.es_print_error("Can't detach last tab") 

4599 return 

4600 c = event['c'] 

4601 f = c.frame 

4602 tabwidget = g.app.gui.frameFactory.masterFrame 

4603 tabwidget.detach(tabwidget.indexOf(f.top)) 

4604 f.top.setWindowTitle(f.title + ' [D]') 

4605 

4606 # this is actually not tab-specific, move elsewhere? 

4607 

4608 @g.command('close-others') 

4609 def close_others(event): 

4610 """Close all windows except the present window.""" 

4611 myc = event['c'] 

4612 for c in g.app.commanders(): 

4613 if c is not myc: 

4614 c.close() 

4615 

4616 def tab_cycle(offset): 

4617 

4618 tabw = self.masterFrame 

4619 cur = tabw.currentIndex() 

4620 count = tabw.count() 

4621 # g.es("cur: %s, count: %s, offset: %s" % (cur,count,offset)) 

4622 cur += offset 

4623 if cur < 0: 

4624 cur = count - 1 

4625 elif cur >= count: 

4626 cur = 0 

4627 tabw.setCurrentIndex(cur) 

4628 self.focusCurrentBody() 

4629 

4630 @g.command('tab-cycle-next') 

4631 def tab_cycle_next(event): 

4632 """ Cycle to next tab """ 

4633 tab_cycle(1) 

4634 

4635 @g.command('tab-cycle-previous') 

4636 def tab_cycle_previous(event): 

4637 """ Cycle to next tab """ 

4638 tab_cycle(-1) 

4639 #@-<< Commands for tabs >> 

4640 #@+node:ekr.20110605121601.18467: *3* frameFactory.deleteFrame 

4641 def deleteFrame(self, wdg): 

4642 

4643 if not wdg: 

4644 return 

4645 if wdg not in self.leoFrames: 

4646 # probably detached tab 

4647 self.masterFrame.delete(wdg) 

4648 return 

4649 tabw = self.masterFrame 

4650 idx = tabw.indexOf(wdg) 

4651 tabw.removeTab(idx) 

4652 del self.leoFrames[wdg] 

4653 wdg2 = tabw.currentWidget() 

4654 if wdg2: 

4655 g.app.selectLeoWindow(wdg2.leo_c) 

4656 tabw.tabBar().setVisible(self.alwaysShowTabs or tabw.count() > 1) 

4657 #@+node:ekr.20110605121601.18471: *3* frameFactory.focusCurrentBody 

4658 def focusCurrentBody(self): 

4659 """ Focus body control of current tab """ 

4660 tabw = self.masterFrame 

4661 w = tabw.currentWidget() 

4662 w.setFocus() 

4663 f = self.leoFrames[w] 

4664 c = f.c 

4665 c.bodyWantsFocusNow() 

4666 # Fix bug 690260: correct the log. 

4667 g.app.log = f.log 

4668 #@+node:ekr.20110605121601.18469: *3* frameFactory.setTabForCommander 

4669 def setTabForCommander(self, c): 

4670 tabw = self.masterFrame # a QTabWidget 

4671 for dw in self.leoFrames: # A dict whose keys are DynamicWindows. 

4672 if dw.leo_c == c: 

4673 for i in range(tabw.count()): 

4674 if tabw.widget(i) == dw: 

4675 tabw.setCurrentIndex(i) 

4676 break 

4677 break 

4678 #@+node:ekr.20110605121601.18470: *3* frameFactory.signal handlers 

4679 def slotCloseRequest(self, idx): 

4680 

4681 tabw = self.masterFrame 

4682 w = tabw.widget(idx) 

4683 f = self.leoFrames[w] 

4684 c = f.c 

4685 c.close(new_c=None) 

4686 # 2012/03/04: Don't set the frame here. 

4687 # Wait until the next slotCurrentChanged event. 

4688 # This keeps the log and the QTabbedWidget in sync. 

4689 

4690 def slotCurrentChanged(self, idx): 

4691 # Two events are generated, one for the tab losing focus, 

4692 # and another event for the tab gaining focus. 

4693 tabw = self.masterFrame 

4694 w = tabw.widget(idx) 

4695 f = self.leoFrames.get(w) 

4696 if not f: 

4697 return 

4698 tabw.setWindowTitle(f.title) 

4699 # Don't do this: it would break --minimize. 

4700 # g.app.selectLeoWindow(f.c) 

4701 # Fix bug 690260: correct the log. 

4702 g.app.log = f.log 

4703 # Redraw the tab. 

4704 c = f.c 

4705 if c: 

4706 c.redraw() 

4707 #@-others 

4708#@-others 

4709#@@language python 

4710#@@tabwidth -4 

4711#@@pagewidth 70 

4712#@-leo