Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

2#@+leo-ver=5-thin 

3#@+node:ekr.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 

205 # A LeoTabbedTopLevel for tabbed windows. 

206 # None for non-tabbed windows. 

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

208 self.reloadSettings() 

209 main_splitter, secondary_splitter = self.createMainWindow() 

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

211 self.iconBar.setObjectName('icon-bar') 

212 # Required for QMainWindow.saveState(). 

213 self.set_icon_bar_orientation(c) 

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

215 # Calling reloadSettings again would also work. 

216 if not self.show_iconbar: 

217 self.iconBar.hide() 

218 self.leo_menubar = self.menuBar() 

219 self.statusBar = QtWidgets.QStatusBar() 

220 self.setStatusBar(self.statusBar) 

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

222 self.setSplitDirection(main_splitter, secondary_splitter, orientation) 

223 if hasattr(c, 'styleSheetManager'): 

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

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

226 def set_icon_bar_orientation(self, c): 

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

228 d = { 

229 'bottom': ToolBarArea.BottomToolBarArea, 

230 'left': ToolBarArea.LeftToolBarArea, 

231 'right': ToolBarArea.RightToolBarArea, 

232 'top': ToolBarArea.TopToolBarArea, 

233 } 

234 where = self.toolbar_orientation 

235 if not where: 

236 where = 'top' 

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

238 if where: 

239 self.addToolBar(where, self.iconBar) 

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

241 def createMainWindow(self): 

242 """ 

243 Create the component ivars of the main window. 

244 Copied/adapted from qt_main.py. 

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

246 """ 

247 self.setMainWindowOptions() 

248 # 

249 # Legacy code: will not go away. 

250 self.createCentralWidget() 

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

252 # Creates .verticalLayout 

253 if self.bigTree: 

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

255 self.createBodyPane(secondary_splitter) 

256 self.createLogPane(secondary_splitter) 

257 treeFrame = self.createOutlinePane(main_splitter) 

258 main_splitter.addWidget(treeFrame) 

259 main_splitter.addWidget(secondary_splitter) 

260 else: 

261 # Top pane contains outline and log panes. 

262 self.createOutlinePane(secondary_splitter) 

263 self.createLogPane(secondary_splitter) 

264 self.createBodyPane(main_splitter) 

265 self.createMiniBuffer(self.centralwidget) 

266 self.createMenuBar() 

267 self.createStatusBar(self) 

268 # Signals... 

269 QtCore.QMetaObject.connectSlotsByName(self) 

270 return main_splitter, secondary_splitter 

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

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

273 def addNewEditor(self, name): 

274 """Create a new body editor.""" 

275 c, p = self.leo_c, self.leo_c.p 

276 body = c.frame.body 

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

278 # 

279 # Step 1: create the editor. 

280 parent_frame = c.frame.top.leo_body_inner_frame 

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

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

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

284 self.packLabel(widget) 

285 # 

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

287 inner_frame = c.frame.top.leo_body_inner_frame 

288 # Inject ivars *here*. 

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

290 body.updateInjectedIvars(widget, p) 

291 wrapper.setAllText(p.b) 

292 wrapper.see(0) 

293 c.k.completeAllBindingsForWidget(wrapper) 

294 if isinstance(widget, QtWidgets.QTextEdit): 

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

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

297 else: 

298 # Scintilla only. 

299 body.recolorWidget(p, wrapper) 

300 return parent_frame, wrapper 

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

302 def createBodyPane(self, parent): 

303 """ 

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

305 """ 

306 c = self.leo_c 

307 # 

308 # Create widgets. 

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

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

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

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

313 page2 = QtWidgets.QWidget() 

314 self.setName(page2, 'bodyPage2') 

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

316 # 

317 # Pack. 

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

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

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

321 if self.use_gutter: 

322 lineWidget = qt_text.LeoLineTextWidget(c, body) 

323 vLayout.addWidget(lineWidget) 

324 else: 

325 vLayout.addWidget(body) 

326 sw.addWidget(page2) 

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

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

329 self.verticalLayout.addWidget(parent) 

330 # 

331 # Official ivars 

332 self.text_page = page2 

333 self.stackedWidget = sw # used by LeoQtBody 

334 self.richTextEdit = body 

335 self.leo_body_frame = bodyFrame 

336 self.leo_body_inner_frame = innerFrame 

337 return bodyFrame 

338 

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

340 def createCentralWidget(self): 

341 """Create the central widget.""" 

342 dw = self 

343 w = QtWidgets.QWidget(dw) 

344 w.setObjectName("centralwidget") 

345 dw.setCentralWidget(w) 

346 # Official ivars. 

347 self.centralwidget = w 

348 return w 

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

350 def createLogPane(self, parent): 

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

352 c = self.leo_c 

353 # 

354 # Create the log frame. 

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

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

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

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

359 # 

360 # Pack. 

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

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

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

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

365 # 

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

367 findScrollArea = QtWidgets.QScrollArea() 

368 findScrollArea.setObjectName('findScrollArea') 

369 # Find tab. 

370 findTab = QtWidgets.QWidget() 

371 findTab.setObjectName('findTab') 

372 # 

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

374 # 

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

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

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

378 if use_minibuffer or not use_dialog: 

379 tabWidget.addTab(findScrollArea, 'Find') 

380 # Complete the Find tab in LeoFind.finishCreate. 

381 self.findScrollArea = findScrollArea 

382 self.findTab = findTab 

383 # 

384 # Spell tab. 

385 spellTab = QtWidgets.QWidget() 

386 spellTab.setObjectName('spellTab') 

387 tabWidget.addTab(spellTab, 'Spell') 

388 self.createSpellTab(spellTab) 

389 tabWidget.setCurrentIndex(1) 

390 # 

391 # Official ivars 

392 self.tabWidget = tabWidget # Used by LeoQtLog. 

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

394 def finishCreateLogPane(self): 

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

396 assert self.findTab 

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

398 self.findScrollArea.setWidget(self.findTab) 

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

400 def createMainLayout(self, parent): 

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

402 # c = self.leo_c 

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

404 main_splitter = NestedSplitter(parent) 

405 main_splitter.setObjectName('main_splitter') 

406 main_splitter.setOrientation(Orientation.Vertical) 

407 secondary_splitter = NestedSplitter(main_splitter) 

408 secondary_splitter.setObjectName('secondary_splitter') 

409 secondary_splitter.setOrientation(Orientation.Horizontal) 

410 # Official ivar: 

411 self.verticalLayout = vLayout 

412 self.setSizePolicy(secondary_splitter) 

413 self.verticalLayout.addWidget(main_splitter) 

414 return main_splitter, secondary_splitter 

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

416 def createMenuBar(self): 

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

418 dw = self 

419 w = QtWidgets.QMenuBar(dw) 

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

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

422 w.setObjectName("menubar") 

423 dw.setMenuBar(w) 

424 # Official ivars. 

425 self.leo_menubar = w 

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

427 def createMiniBuffer(self, parent): 

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

429 # Create widgets. 

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

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

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

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

434 

435 

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

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

438 

439 def focusInEvent(self, event): 

440 self.parent().show() 

441 super().focusInEvent(event) 

442 # Call the base class method. 

443 

444 def focusOutEvent(self, event): 

445 self.store_selection() 

446 super().focusOutEvent(event) 

447 

448 def restore_selection(self): 

449 w = self 

450 i, j, ins = self._sel_and_insert 

451 if i == j: 

452 w.setCursorPosition(i) 

453 else: 

454 length = j - i 

455 # Set selection is a QLineEditMethod 

456 if ins < j: 

457 w.setSelection(j, -length) 

458 else: 

459 w.setSelection(i, length) 

460 

461 def store_selection(self): 

462 w = self 

463 ins = w.cursorPosition() 

464 if w.hasSelectedText(): 

465 i = w.selectionStart() 

466 s = w.selectedText() 

467 j = i + len(s) 

468 else: 

469 i = j = ins 

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

471 

472 lineEdit = VisLineEdit(frame) 

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

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

475 # Pack. 

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

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

478 hLayout.addWidget(label) 

479 hLayout.addWidget(lineEdit) 

480 self.verticalLayout.addWidget(frame) 

481 label.setBuddy(lineEdit) 

482 # Transfers focus request from label to lineEdit. 

483 # 

484 # Official ivars. 

485 self.lineEdit = lineEdit 

486 # self.leo_minibuffer_frame = frame 

487 # self.leo_minibuffer_layout = layout 

488 return frame 

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

490 def createOutlinePane(self, parent): 

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

492 # Create widgets. 

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

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

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

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

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

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

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

500 # Official ivars... 

501 self.treeWidget = treeWidget 

502 return treeFrame 

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

504 def createStatusBar(self, parent): 

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

506 w = QtWidgets.QStatusBar(parent) 

507 w.setObjectName("statusbar") 

508 parent.setStatusBar(w) 

509 # Official ivars. 

510 self.statusBar = w 

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

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

513 """ 

514 Pack w into the body frame's QVGridLayout. 

515 

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

517 QTextBrowser possibly packed inside a LeoLineTextWidget. 

518 """ 

519 c = self.leo_c 

520 # 

521 # Reuse the grid layout in the body frame. 

522 grid = self.leo_body_frame.layout() 

523 # Pack the label and the text widget. 

524 label = QtWidgets.QLineEdit(None) 

525 label.setObjectName('editorLabel') 

526 label.setText(c.p.h) 

527 if n is None: 

528 n = c.frame.body.numberOfEditors 

529 n = max(0, n - 1) 

530 grid.addWidget(label, 0, n) 

531 grid.addWidget(w, 1, n) 

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

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

534 # Inject the ivar. 

535 w.leo_label = label 

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

537 def setMainWindowOptions(self): 

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

539 dw = self 

540 dw.setObjectName("MainWindow") 

541 dw.resize(691, 635) 

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

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

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

545 w = QtWidgets.QPushButton(parent) 

546 w.setObjectName(name) 

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

548 return w 

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

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

551 w = QtWidgets.QCheckBox(parent) 

552 self.setName(w, name) 

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

554 return w 

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

556 def createFrame(self, parent, name, 

557 hPolicy=None, vPolicy=None, 

558 lineWidth=1, 

559 shadow=None, 

560 shape=None, 

561 ): 

562 """Create a Qt Frame.""" 

563 if shadow is None: 

564 shadow = Shadow.Plain 

565 if shape is None: 

566 shape = Shape.NoFrame 

567 # 

568 w = QtWidgets.QFrame(parent) 

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

570 w.setFrameShape(shape) 

571 w.setFrameShadow(shadow) 

572 w.setLineWidth(lineWidth) 

573 self.setName(w, name) 

574 return w 

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

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

577 w = QtWidgets.QGridLayout(parent) 

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

579 w.setSpacing(spacing) 

580 self.setName(w, name) 

581 return w 

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

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

584 hLayout = QtWidgets.QHBoxLayout(parent) 

585 hLayout.setSpacing(spacing) 

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

587 self.setName(hLayout, name) 

588 return hLayout 

589 

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

591 vLayout = QtWidgets.QVBoxLayout(parent) 

592 vLayout.setSpacing(spacing) 

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

594 self.setName(vLayout, name) 

595 return vLayout 

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

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

598 w = QtWidgets.QLabel(parent) 

599 self.setName(w, name) 

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

601 return w 

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

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

604 

605 w = QtWidgets.QLineEdit(parent) 

606 w.setObjectName(name) 

607 w.leo_disabled = disabled # Inject the ivar. 

608 return w 

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

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

611 w = QtWidgets.QRadioButton(parent) 

612 self.setName(w, name) 

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

614 return w 

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

616 def createStackedWidget(self, parent, name, 

617 lineWidth=1, 

618 hPolicy=None, vPolicy=None, 

619 ): 

620 w = QtWidgets.QStackedWidget(parent) 

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

622 w.setAcceptDrops(True) 

623 w.setLineWidth(1) 

624 self.setName(w, name) 

625 return w 

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

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

628 w = QtWidgets.QTabWidget(parent) 

629 # tb = w.tabBar() 

630 # tb.setTabsClosable(True) 

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

632 self.setName(w, name) 

633 return w 

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

635 def createText(self, parent, name, 

636 lineWidth=0, 

637 shadow=None, 

638 shape=None, 

639 ): 

640 # Create a text widget. 

641 c = self.leo_c 

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

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

644 w = Qsci.QsciScintilla(parent) 

645 self.scintilla_widget = w 

646 else: 

647 if shadow is None: 

648 shadow = Shadow.Plain 

649 if shape is None: 

650 shape = Shape.NoFrame 

651 # 

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

653 w.setFrameShape(shape) 

654 w.setFrameShadow(shadow) 

655 w.setLineWidth(lineWidth) 

656 self.setName(w, name) 

657 return w 

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

659 def createTreeWidget(self, parent, name): 

660 c = self.leo_c 

661 w = LeoQTreeWidget(c, parent) 

662 self.setSizePolicy(w) 

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

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

665 if multiple_selection: 

666 w.setSelectionMode(SelectionMode.ExtendedSelection) 

667 w.setSelectionBehavior(SelectionBehavior.SelectRows) 

668 else: 

669 w.setSelectionMode(SelectionMode.SingleSelection) 

670 w.setSelectionBehavior(SelectionBehavior.SelectItems) 

671 w.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu) 

672 w.setHeaderHidden(False) 

673 self.setName(w, name) 

674 return w 

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

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

677 def createSpellTab(self, parent): 

678 # dw = self 

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

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

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

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

683 table = ( 

684 ('Add', 'Add', 2, 1), 

685 ('Find', 'Find', 2, 0), 

686 ('Change', 'Change', 3, 0), 

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

688 ('Ignore', 'Ignore', 4, 0), 

689 ('Hide', 'Hide', 4, 1), 

690 ) 

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

692 name = f"spell_{label}_button" 

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

694 grid.addWidget(button, row, col) 

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

696 button.clicked.connect(func) 

697 # This name is significant. 

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

699 self.leo_spell_btn_Hide.setCheckable(False) 

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

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

702 listBox = QtWidgets.QListWidget(spellFrame) 

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

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

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

706 listBox.setObjectName("leo_spell_listBox") 

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

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

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

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

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

712 vLayout2.addLayout(grid) 

713 vLayout.addWidget(spellFrame) 

714 listBox.itemDoubleClicked.connect(self.do_leo_spell_btn_FindChange) 

715 # Official ivars. 

716 self.spellFrame = spellFrame 

717 self.spellGrid = grid 

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

719 self.leo_spell_listBox = listBox # Must exist 

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

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

722 def createFindTab(self, parent, tab_widget): 

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

724 c, dw = self.leo_c, self 

725 fc = c.findCommands 

726 assert not fc.ftm 

727 fc.ftm = ftm = FindTabManager(c) 

728 grid = self.create_find_grid(parent) 

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

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

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

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

733 max_row2 = 1 

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

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

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

737 dw.override_events() 

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

739 w = QtWidgets.QWidget() 

740 grid.addWidget(w, row, 0) 

741 grid.addWidget(w, row, 1) 

742 grid.addWidget(w, row, 2) 

743 grid.setRowStretch(row, 100) 

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

745 self.leo_find_widget = tab_widget # A scrollArea. 

746 ftm.init_widgets() 

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

748 def create_find_grid(self, parent): 

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

750 grid.setColumnStretch(0, 100) 

751 grid.setColumnStretch(1, 100) 

752 grid.setColumnStretch(2, 10) 

753 grid.setColumnMinimumWidth(1, 75) 

754 grid.setColumnMinimumWidth(2, 175) 

755 return grid 

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

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

758 if False: 

759 dw = self 

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

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

762 # AlignHCenter 

763 row += 1 

764 return row 

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

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

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

768 c, dw = self.leo_c, self 

769 fc = c.findCommands 

770 ftm = fc.ftm 

771 assert ftm.find_findbox is None 

772 ftm.find_findbox = w = dw.createLineEdit( 

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

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

775 grid.addWidget(lab2, row, 0) 

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

777 row += 1 

778 return row 

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

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

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

782 c, dw = self.leo_c, self 

783 fc = c.findCommands 

784 ftm = fc.ftm 

785 assert ftm.find_replacebox is None 

786 ftm.find_replacebox = w = dw.createLineEdit( 

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

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

789 grid.addWidget(lab3, row, 0) 

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

791 row += 1 

792 return row 

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

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

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

796 c, dw = self.leo_c, self 

797 fc = c.findCommands 

798 ftm = fc.ftm 

799 

800 def mungeName(kind, label): 

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

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

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

804 return f"{kind}{name}" 

805 

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

807 

808 d = { 

809 'box': dw.createCheckBox, 

810 'rb': dw.createRadioButton, 

811 } 

812 table = ( 

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

814 # The QShortcut class is the workaround. 

815 # First row. 

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

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

818 # Second row. 

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

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

821 # Third row. 

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

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

824 # Fourth row. 

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

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

827 # Fifth row. 

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

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

830 # Sixth row. 

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

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

833 ) 

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

835 max_row2 = max(max_row2, row2) 

836 name = mungeName(kind, label) 

837 func = d.get(kind) 

838 assert func 

839 # Fix the greedy checkbox bug: 

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

841 w = func(parent, name, label) 

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

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

844 assert getattr(ftm, name) is None 

845 setattr(ftm, name, w) 

846 return max_row2 

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

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

849 """ 

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

851 """ 

852 dw, k = self, self.leo_c.k 

853 

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

855 table = ( 

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

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

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

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

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

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

862 ) 

863 for row2, col, cmd_name in table: 

864 stroke = k.getStrokeForCommandName(cmd_name) 

865 if stroke: 

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

867 else: 

868 label = cmd_name 

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

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

871 w.setObjectName('find-label') 

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

873 row += max_row2 

874 row += 2 

875 return row 

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

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

878 # Help row. 

879 if False: 

880 w = self.createLabel(parent, 

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

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

883 row += 1 

884 return row 

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

886 if 0: 

887 

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

889 """Create the status line.""" 

890 dw = self 

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

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

893 grid.addWidget(status_label, row, 0) 

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

895 # Official ivars. 

896 dw.find_status_label = status_label 

897 dw.find_status_edit = status_line 

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

899 def override_events(self): 

900 # dw = self 

901 c = self.leo_c 

902 fc = c.findCommands 

903 ftm = fc.ftm 

904 # Define class EventWrapper. 

905 #@+others 

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

907 class EventWrapper: 

908 

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

910 self.c = c 

911 self.d = self.create_d() 

912 # Keys: stroke.s; values: command-names. 

913 self.w = w 

914 self.next_w = next_w 

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

916 self.func = func 

917 self.oldEvent = w.event 

918 w.event = self.wrapper 

919 #@+others 

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

921 def create_d(self): 

922 """Create self.d dictionary.""" 

923 c = self.c 

924 d = {} 

925 table = ( 

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

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

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

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

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

931 'toggle-find-regex-option', 

932 'toggle-find-word-option', 

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

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

935 'find-all', 

936 'find-next', 

937 'find-prev', 

938 'hide-find-tab', 

939 'replace', 

940 'replace-all', 

941 'replace-then-find', 

942 'set-find-everywhere', 

943 'set-find-node-only', 

944 'set-find-suboutline-only', 

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

946 'full-command', 

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

948 ) 

949 for cmd_name in table: 

950 stroke = c.k.getStrokeForCommandName(cmd_name) 

951 if stroke: 

952 d[stroke.s] = cmd_name 

953 return d 

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

955 def wrapper(self, event): 

956 

957 type_ = event.type() 

958 # Must intercept KeyPress for events that generate FocusOut! 

959 if type_ == Type.KeyPress: 

960 return self.keyPress(event) 

961 if type_ == Type.KeyRelease: 

962 return self.keyRelease(event) 

963 return self.oldEvent(event) 

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

965 def keyPress(self, event): 

966 

967 s = event.text() 

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

969 if out: 

970 # Move focus to next widget. 

971 if s == '\t': 

972 if self.next_w: 

973 self.next_w.setFocus(FocusReason.TabFocusReason) 

974 else: 

975 # Do the normal processing. 

976 return self.oldEvent(event) 

977 elif self.func: 

978 self.func() 

979 return True 

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

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

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

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

984 key_event = leoGui.LeoKeyEvent( 

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

986 if key_event.stroke: 

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

988 if cmd_name: 

989 self.c.k.simulateCommand(cmd_name) 

990 return True 

991 # Do the normal processing. 

992 return self.oldEvent(event) 

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

994 def keyRelease(self, event): 

995 return self.oldEvent(event) 

996 #@-others 

997 #@-others 

998 EventWrapper( 

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

1000 EventWrapper( 

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

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

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

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

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

1006 def setName(self, widget, name): 

1007 if name: 

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

1009 # name = 'leo_' + name 

1010 widget.setObjectName(name) 

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

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

1013 if kind1 is None: 

1014 kind1 = Policy.Ignored 

1015 if kind2 is None: 

1016 kind2 = Policy.Ignored 

1017 sizePolicy = QtWidgets.QSizePolicy(kind1, kind2) 

1018 sizePolicy.setHorizontalStretch(0) 

1019 sizePolicy.setVerticalStretch(0) 

1020 sizePolicy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth()) 

1021 widget.setSizePolicy(sizePolicy) 

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

1023 def tr(self, s): 

1024 # pylint: disable=no-member 

1025 if isQt5 or isQt6: 

1026 # QApplication.UnicodeUTF8 no longer exists. 

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

1028 return QtWidgets.QApplication.translate( 

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

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

1031 def select(self, c): 

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

1033 # Called from the save commands. 

1034 self.leo_master.select(c) 

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

1036 def setGeometry(self, rect): 

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

1038 m = self.leo_master 

1039 assert self.leo_master 

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

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

1042 m.leo_geom_inited = True 

1043 self.leo_master.setGeometry(rect) 

1044 super().setGeometry(rect) 

1045 

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

1047 def setLeoWindowIcon(self): 

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

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

1050 g.app.gui.attachLeoIcon(self) 

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

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

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

1054 # c = self.leo_c 

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

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

1057 orientation1 = v if vert else h 

1058 orientation2 = h if vert else v 

1059 main_splitter.setOrientation(orientation1) 

1060 secondary_splitter.setOrientation(orientation2) 

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

1062 if 0: # Override for debugging only. 

1063 

1064 def setWindowTitle(self, s): 

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

1066 # Call the base class method. 

1067 QtWidgets.QMainWindow.setWindowTitle(self, s) 

1068 #@-others 

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

1070class FindTabManager: 

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

1072 #@+others 

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

1074 def __init__(self, c): 

1075 """Ctor for the FindTabManager class.""" 

1076 self.c = c 

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

1078 # Find/change text boxes. 

1079 self.find_findbox = None 

1080 self.find_replacebox = None 

1081 # Check boxes. 

1082 self.check_box_ignore_case = None 

1083 self.check_box_mark_changes = None 

1084 self.check_box_mark_finds = None 

1085 self.check_box_regexp = None 

1086 self.check_box_search_body = None 

1087 self.check_box_search_headline = None 

1088 self.check_box_whole_word = None 

1089 # self.check_box_wrap_around = None 

1090 # Radio buttons 

1091 self.radio_button_entire_outline = None 

1092 self.radio_button_node_only = None 

1093 self.radio_button_suboutline_only = None 

1094 # Push buttons 

1095 self.find_next_button = None 

1096 self.find_prev_button = None 

1097 self.find_all_button = None 

1098 self.help_for_find_commands_button = None 

1099 self.replace_button = None 

1100 self.replace_then_find_button = None 

1101 self.replace_all_button = None 

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

1103 def clear_focus(self): 

1104 self.entry_focus = None 

1105 self.find_findbox.clearFocus() 

1106 

1107 def init_focus(self): 

1108 self.set_entry_focus() 

1109 w = self.find_findbox 

1110 w.setFocus() 

1111 s = w.text() 

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

1113 

1114 def set_entry_focus(self): 

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

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

1117 c = self.c 

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

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

1120 w = c.frame.tree.treeWidget 

1121 self.entry_focus = w 

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

1123 def get_settings(self): 

1124 """ 

1125 Return a g.bunch representing all widget values. 

1126 

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

1128 """ 

1129 return g.Bunch( 

1130 # Find/change strings... 

1131 find_text=self.find_findbox.text(), 

1132 change_text=self.find_replacebox.text(), 

1133 # Find options... 

1134 ignore_case=self.check_box_ignore_case.isChecked(), 

1135 mark_changes=self.check_box_mark_changes.isChecked(), 

1136 mark_finds=self.check_box_mark_finds.isChecked(), 

1137 node_only=self.radio_button_node_only.isChecked(), 

1138 pattern_match=self.check_box_regexp.isChecked(), 

1139 # reverse = False, 

1140 search_body=self.check_box_search_body.isChecked(), 

1141 search_headline=self.check_box_search_headline.isChecked(), 

1142 suboutline_only=self.radio_button_suboutline_only.isChecked(), 

1143 whole_word=self.check_box_whole_word.isChecked(), 

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

1145 ) 

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

1147 def init_widgets(self): 

1148 """ 

1149 Init widgets and ivars from c.config settings. 

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

1151 """ 

1152 c = self.c 

1153 find = c.findCommands 

1154 # Find/change text boxes. 

1155 table1 = ( 

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

1157 ('find_replacebox', 'change_text', ''), 

1158 ) 

1159 for ivar, setting_name, default in table1: 

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

1161 w = getattr(self, ivar) 

1162 w.insert(s) 

1163 if find.minibuffer_mode: 

1164 w.clearFocus() 

1165 else: 

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

1167 # Check boxes. 

1168 table2 = ( 

1169 ('ignore_case', self.check_box_ignore_case), 

1170 ('mark_changes', self.check_box_mark_changes), 

1171 ('mark_finds', self.check_box_mark_finds), 

1172 ('pattern_match', self.check_box_regexp), 

1173 ('search_body', self.check_box_search_body), 

1174 ('search_headline', self.check_box_search_headline), 

1175 ('whole_word', self.check_box_whole_word), 

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

1177 ) 

1178 for setting_name, w in table2: 

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

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

1181 assert hasattr(find, setting_name), setting_name 

1182 setattr(find, setting_name, val) 

1183 if val: 

1184 w.toggle() 

1185 

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

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

1188 # focus_w = QtWidgets.QApplication.focusWidget() 

1189 val = w.isChecked() 

1190 assert hasattr(find, setting_name), setting_name 

1191 setattr(find, setting_name, val) 

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

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

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

1195 c.bodyWantsFocusNow() 

1196 

1197 w.stateChanged.connect(check_box_callback) 

1198 # Radio buttons 

1199 table3 = ( 

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

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

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

1203 ) 

1204 for setting_name, ivar, w in table3: 

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

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

1207 if ivar is not None: 

1208 assert hasattr(find, setting_name), setting_name 

1209 setattr(find, setting_name, val) 

1210 w.toggle() 

1211 

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

1213 val = w.isChecked() 

1214 if ivar: 

1215 assert hasattr(find, ivar), ivar 

1216 setattr(find, ivar, val) 

1217 

1218 w.toggled.connect(radio_button_callback) 

1219 # Ensure one radio button is set. 

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

1221 w = self.radio_button_entire_outline 

1222 w.toggle() 

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

1224 def set_widgets_from_dict(self, d): 

1225 """Set all settings from d.""" 

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

1227 c = self.c 

1228 find = c.findCommands 

1229 # Set find text. 

1230 find_text = d.get('find_text') 

1231 self.set_find_text(find_text) 

1232 find.find_text = find_text 

1233 # Set change text. 

1234 change_text = d.get('change_text') 

1235 self.set_change_text(change_text) 

1236 find.change_text = change_text 

1237 # Check boxes... 

1238 table1 = ( 

1239 ('ignore_case', self.check_box_ignore_case), 

1240 ('mark_changes', self.check_box_mark_changes), 

1241 ('mark_finds', self.check_box_mark_finds), 

1242 ('pattern_match', self.check_box_regexp), 

1243 ('search_body', self.check_box_search_body), 

1244 ('search_headline', self.check_box_search_headline), 

1245 ('whole_word', self.check_box_whole_word), 

1246 ) 

1247 for setting_name, w in table1: 

1248 val = d.get(setting_name, False) 

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

1250 assert hasattr(find, setting_name), setting_name 

1251 setattr(find, setting_name, val) 

1252 w.setChecked(val) 

1253 # Radio buttons... 

1254 table2 = ( 

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

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

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

1258 ) 

1259 for setting_name, ivar, w in table2: 

1260 val = d.get(setting_name, False) 

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

1262 if ivar is not None: 

1263 assert hasattr(find, setting_name), setting_name 

1264 setattr(find, setting_name, val) 

1265 w.setChecked(val) 

1266 # Ensure one radio button is set. 

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

1268 w = self.radio_button_entire_outline 

1269 w.setChecked(val) 

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

1271 def set_body_and_headline_checkbox(self): 

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

1273 # #1840: headline-only one-shot 

1274 c = self.c 

1275 find = c.findCommands 

1276 if not find: 

1277 return 

1278 table = ( 

1279 ('search_body', self.check_box_search_body), 

1280 ('search_headline', self.check_box_search_headline), 

1281 ) 

1282 for setting_name, w in table: 

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

1284 if val != w.isChecked(): 

1285 w.toggle() 

1286 if find.minibuffer_mode: 

1287 find.show_find_options_in_status_area() 

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

1289 def set_ignore_case(self, aBool): 

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

1291 c = self.c 

1292 c.findCommands.ignore_case = aBool 

1293 w = self.check_box_ignore_case 

1294 w.setChecked(aBool) 

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

1296 def set_radio_button(self, name): 

1297 """Set the value of the radio buttons""" 

1298 c = self.c 

1299 find = c.findCommands 

1300 d = { 

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

1302 'node-only': self.radio_button_node_only, 

1303 'entire-outline': self.radio_button_entire_outline, 

1304 'suboutline-only': self.radio_button_suboutline_only, 

1305 } 

1306 w = d.get(name) 

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

1308 if not w.isChecked(): 

1309 w.toggle() 

1310 if find.minibuffer_mode: 

1311 find.show_find_options_in_status_area() 

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

1313 def get_find_text(self): 

1314 s = self.find_findbox.text() 

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

1316 s = s[:-1] 

1317 return s 

1318 

1319 def get_change_text(self): 

1320 s = self.find_replacebox.text() 

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

1322 s = s[:-1] 

1323 return s 

1324 

1325 getChangeText = get_change_text 

1326 

1327 def set_find_text(self, s): 

1328 w = self.find_findbox 

1329 s = g.checkUnicode(s) 

1330 w.clear() 

1331 w.insert(s) 

1332 

1333 def set_change_text(self, s): 

1334 w = self.find_replacebox 

1335 s = g.checkUnicode(s) 

1336 w.clear() 

1337 w.insert(s) 

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

1339 #@@nobeautify 

1340 

1341 def toggle_checkbox(self, checkbox_name): 

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

1343 c = self.c 

1344 find = c.findCommands 

1345 if not find: 

1346 return 

1347 d = { 

1348 'ignore_case': self.check_box_ignore_case, 

1349 'mark_changes': self.check_box_mark_changes, 

1350 'mark_finds': self.check_box_mark_finds, 

1351 'pattern_match': self.check_box_regexp, 

1352 'search_body': self.check_box_search_body, 

1353 'search_headline': self.check_box_search_headline, 

1354 'whole_word': self.check_box_whole_word, 

1355 # 'wrap': self.check_box_wrap_around, 

1356 } 

1357 w = d.get(checkbox_name) 

1358 assert w 

1359 assert hasattr(find, checkbox_name), checkbox_name 

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

1361 if find.minibuffer_mode: 

1362 find.show_find_options_in_status_area() 

1363 #@-others 

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

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

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

1367 #@+others 

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

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

1370 

1371 # 

1372 # Called from frameFactory.createMaster. 

1373 # 

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

1375 if self.factory: 

1376 del kwargs['factory'] 

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

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

1379 self.setMovable(True) 

1380 

1381 def tabContextMenu(point): 

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

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

1384 return 

1385 menu = QtWidgets.QMenu() 

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

1387 if True: 

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

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

1390 if self.count() > 1: 

1391 a = menu.addAction("Detach") 

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

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

1394 a.triggered.connect( 

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

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

1397 a.triggered.connect( 

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

1399 if self.detached: 

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

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

1402 

1403 global_point = self.mapToGlobal(point) 

1404 menu.exec_(global_point) 

1405 self.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu) 

1406 self.customContextMenuRequested.connect(tabContextMenu) 

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

1408 def new_outline(self, index): 

1409 """Open a new outline tab.""" 

1410 w = self.widget(index) 

1411 c = w.leo_c 

1412 c.new() 

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

1414 def detach(self, index): 

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

1416 w = self.widget(index) 

1417 name = self.tabText(index) 

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

1419 self.factory.detachTab(w) 

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

1421 icon = QtGui.QIcon(icon) 

1422 if icon: 

1423 w.window().setWindowIcon(icon) 

1424 c = w.leo_c 

1425 if c.styleSheetManager: 

1426 c.styleSheetManager.set_style_sheets(w=w) 

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

1428 w.move(20, 20) 

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

1430 return w 

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

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

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

1434 w = self.widget(index) 

1435 window = w.window() 

1436 # window.showMaximized() 

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

1438 # user needs to do it before using this function 

1439 fg = window.frameGeometry() 

1440 geom = window.geometry() 

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

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

1443 w = self.detach(index) 

1444 if window.isMaximized(): 

1445 window.showNormal() 

1446 if orientation == 'V': 

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

1448 window.resize(ww / 2, wh) 

1449 window.move(x, y) 

1450 w.resize(ww / 2, wh) 

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

1452 else: 

1453 window.resize(ww, wh / 2) 

1454 window.move(x, y) 

1455 w.resize(ww, wh / 2) 

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

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

1458 def reattach_all(self): 

1459 """reattach all detached tabs""" 

1460 for name, w in self.detached: 

1461 self.addTab(w, name) 

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

1463 self.detached = [] 

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

1465 def delete(self, w): 

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

1467 has been deleted""" 

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

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

1470 def setChanged(self, c, changed): 

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

1472 # Find the tab corresponding to c. 

1473 dw = c.frame.top # A DynamicWindow 

1474 i = self.indexOf(dw) 

1475 if i < 0: 

1476 return 

1477 s = self.tabText(i) 

1478 if len(s) > 2: 

1479 if changed: 

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

1481 title = "* " + s 

1482 self.setTabText(i, title) 

1483 else: 

1484 if s.startswith('* '): 

1485 title = s[2:] 

1486 self.setTabText(i, title) 

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

1488 def setTabName(self, c, fileName): 

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

1490 # Find the tab corresponding to c. 

1491 dw = c.frame.top # A DynamicWindow 

1492 i = self.indexOf(dw) 

1493 if i > -1: 

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

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

1496 def closeEvent(self, event): 

1497 """Handle a close event.""" 

1498 g.app.gui.close_event(event) 

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

1500 def select(self, c): 

1501 """Select the tab for c.""" 

1502 dw = c.frame.top # A DynamicWindow 

1503 i = self.indexOf(dw) 

1504 self.setCurrentIndex(i) 

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

1506 # c.enableMenuBar() 

1507 #@-others 

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

1509class LeoQtBody(leoFrame.LeoBody): 

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

1511 #@+others 

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

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

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

1515 def __init__(self, frame, parentFrame): 

1516 """Ctor for LeoQtBody class.""" 

1517 # Call the base class constructor. 

1518 super().__init__(frame, parentFrame) 

1519 c = self.c 

1520 assert c.frame == frame and frame.c == c 

1521 self.reloadSettings() 

1522 self.set_widget() 

1523 # Sets self.widget and self.wrapper. 

1524 self.setWrap(c.p) 

1525 # For multiple body editors. 

1526 self.editor_name = None 

1527 self.editor_v = None 

1528 self.numberOfEditors = 1 

1529 self.totalNumberOfEditors = 1 

1530 # For renderer panes. 

1531 self.canvasRenderer = None 

1532 self.canvasRendererLabel = None 

1533 self.canvasRendererVisible = False 

1534 self.textRenderer = None 

1535 self.textRendererLabel = None 

1536 self.textRendererVisible = False 

1537 self.textRendererWrapper = None 

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

1539 def getName(self): 

1540 return 'body-widget' 

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

1542 def reloadSettings(self): 

1543 c = self.c 

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

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

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

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

1548 def set_invisibles(self, c): 

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

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

1551 option = QtGui.QTextOption() 

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

1553 # The following works with both Qt5 and Qt6. 

1554 # pylint: disable=no-member 

1555 option.setFlags(option.Flag.ShowTabsAndSpaces) 

1556 d.setDefaultTextOption(option) 

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

1558 def set_widget(self): 

1559 """Set the actual gui widget.""" 

1560 c = self.c 

1561 top = c.frame.top 

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

1563 if sw: 

1564 sw.setCurrentIndex(1) 

1565 if self.useScintilla and not Qsci: 

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

1567 if self.useScintilla and Qsci: 

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

1569 # A Qsci.QsciSintilla object. 

1570 # dw.createText sets self.scintilla_widget 

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

1572 self.colorizer = leoColorizer.QScintillaColorizer( 

1573 c, self.widget, self.wrapper) 

1574 else: 

1575 self.widget = top.richTextEdit # A LeoQTextBrowser 

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

1577 self.widget.setAcceptRichText(False) 

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

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

1580 def forceWrap(self, p): 

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

1582 if not p or self.useScintilla: 

1583 return 

1584 c = self.c 

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

1586 wrap = WrapMode.WrapAtWordBoundaryOrAnywhere 

1587 w.setWordWrapMode(wrap) 

1588 

1589 def setWrap(self, p): 

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

1591 if not p or self.useScintilla: 

1592 return 

1593 c = self.c 

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

1595 wrap = g.scanAllAtWrapDirectives(c, p) 

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

1597 w.setHorizontalScrollBarPolicy(policy) 

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

1599 w.setWordWrapMode(wrap) 

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

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

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

1603 # An override of leoFrame.addEditor. 

1604 

1605 @body_cmd('editor-add') 

1606 @body_cmd('add-editor') 

1607 def add_editor_command(self, event=None): 

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

1609 c, p = self.c, self.c.p 

1610 d = self.editorWrappers 

1611 dw = c.frame.top 

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

1613 widget = wrapper.widget 

1614 self.totalNumberOfEditors += 1 

1615 self.numberOfEditors += 1 

1616 if self.totalNumberOfEditors == 2: 

1617 d['1'] = wrapper 

1618 # Pack the original body editor. 

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

1620 if self.use_gutter: 

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

1622 widget.leo_label = widget.parent().leo_label 

1623 else: 

1624 dw.packLabel(widget, n=1) 

1625 name = f"{self.totalNumberOfEditors}" 

1626 f, wrapper = dw.addNewEditor(name) 

1627 assert g.isTextWrapper(wrapper), wrapper 

1628 assert g.isTextWidget(widget), widget 

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

1630 d[name] = wrapper 

1631 if self.numberOfEditors == 2: 

1632 # Inject the ivars into the first editor. 

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

1634 keys = list(d.keys()) 

1635 old_name = keys[0] 

1636 old_wrapper = d.get(old_name) 

1637 old_w = old_wrapper.widget 

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

1639 self.updateInjectedIvars(old_w, p) 

1640 self.selectLabel(old_wrapper) 

1641 # Immediately create the label in the old editor. 

1642 # Switch editors. 

1643 c.frame.body.wrapper = wrapper 

1644 self.selectLabel(wrapper) 

1645 self.selectEditor(wrapper) 

1646 self.updateEditors() 

1647 c.bodyWantsFocus() 

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

1649 def assignPositionToEditor(self, p): 

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

1651 c = self.c 

1652 wrapper = c.frame.body.wrapper 

1653 w = wrapper and wrapper.widget 

1654 # Careful: w may not exist during unit testing. 

1655 if w: 

1656 self.updateInjectedIvars(w, p) 

1657 self.selectLabel(wrapper) 

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

1659 # Use the base class method. 

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

1661 @body_cmd('delete-editor') 

1662 @body_cmd('editor-delete') 

1663 def delete_editor_command(self, event=None): 

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

1665 c, d = self.c, self.editorWrappers 

1666 wrapper = c.frame.body.wrapper 

1667 w = wrapper.widget 

1668 assert g.isTextWrapper(wrapper), wrapper 

1669 assert g.isTextWidget(w), w 

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

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

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

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

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

1675 return 

1676 # 

1677 # Actually delete the widget. 

1678 del d[name] 

1679 f = c.frame.top.leo_body_frame 

1680 layout = f.layout() 

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

1682 if z: 

1683 self.unpackWidget(layout, z) 

1684 # 

1685 # Select another editor. 

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

1687 self.numberOfEditors -= 1 

1688 if self.numberOfEditors == 1: 

1689 w = new_wrapper.widget 

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

1691 if label: 

1692 self.unpackWidget(layout, label) 

1693 w.leo_label = None 

1694 self.selectEditor(new_wrapper) 

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

1696 def findEditorForChapter(self, chapter, p): 

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

1698 c, d = self.c, self.editorWrappers 

1699 values = list(d.values()) 

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

1701 if p: 

1702 for w in values: 

1703 if ( 

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

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

1706 ): 

1707 return w 

1708 # Next, try to match just the chapter. 

1709 for w in values: 

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

1711 return w 

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

1713 return c.frame.body.wrapper 

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

1715 def unselectLabel(self, wrapper): 

1716 # pylint: disable=arguments-differ 

1717 pass 

1718 # self.createChapterIvar(wrapper) 

1719 

1720 def selectLabel(self, wrapper): 

1721 # pylint: disable=arguments-differ 

1722 c = self.c 

1723 w = wrapper.widget 

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

1725 if label: 

1726 label.setEnabled(True) 

1727 label.setText(c.p.h) 

1728 label.setEnabled(False) 

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

1730 selectEditorLockout = False 

1731 

1732 def selectEditor(self, wrapper): 

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

1734 # pylint: disable=arguments-differ 

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

1736 tag = 'qt_body.selectEditor' 

1737 c = self.c 

1738 if not wrapper: 

1739 return c.frame.body.wrapper 

1740 if self.selectEditorLockout: 

1741 return None 

1742 w = wrapper.widget 

1743 assert g.isTextWrapper(wrapper), wrapper 

1744 assert g.isTextWidget(w), w 

1745 if trace: 

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

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

1748 self.deactivateEditors(wrapper) 

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

1750 c.selectPosition(w.leo_p) 

1751 c.bodyWantsFocus() 

1752 return None 

1753 try: 

1754 val = None 

1755 self.selectEditorLockout = True 

1756 val = self.selectEditorHelper(wrapper) 

1757 finally: 

1758 self.selectEditorLockout = False 

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

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

1761 def selectEditorHelper(self, wrapper): 

1762 

1763 c = self.c 

1764 w = wrapper.widget 

1765 assert g.isTextWrapper(wrapper), wrapper 

1766 assert g.isTextWidget(w), w 

1767 if not w.leo_p: 

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

1769 return 'break' 

1770 # The actual switch. 

1771 self.deactivateEditors(wrapper) 

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

1773 c.frame.body.wrapper = wrapper 

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

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

1776 c.frame.body.widget = w 

1777 w.leo_active = True 

1778 self.switchToChapter(wrapper) 

1779 self.selectLabel(wrapper) 

1780 if not self.ensurePositionExists(w): 

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

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

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

1784 return None 

1785 p = w.leo_p 

1786 assert p, p 

1787 c.expandAllAncestors(p) 

1788 c.selectPosition(p) 

1789 # Calls assignPositionToEditor. 

1790 # Calls p.v.restoreCursorAndScroll. 

1791 c.redraw() 

1792 c.recolor() 

1793 c.bodyWantsFocus() 

1794 return None 

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

1796 # Called from addEditor and assignPositionToEditor 

1797 

1798 def updateEditors(self): 

1799 c, p = self.c, self.c.p 

1800 body = p.b 

1801 d = self.editorWrappers 

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

1803 return # There is only the main widget 

1804 w0 = c.frame.body.wrapper 

1805 i, j = w0.getSelectionRange() 

1806 ins = w0.getInsertPoint() 

1807 sb0 = w0.widget.verticalScrollBar() 

1808 pos0 = sb0.sliderPosition() 

1809 for key in d: 

1810 wrapper = d.get(key) 

1811 w = wrapper.widget 

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

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

1814 sb = w.verticalScrollBar() 

1815 pos = sb.sliderPosition() 

1816 wrapper.setAllText(body) 

1817 self.recolorWidget(p, wrapper) 

1818 sb.setSliderPosition(pos) 

1819 c.bodyWantsFocus() 

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

1821 sb0.setSliderPosition(pos0) 

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

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

1824 def computeLabel(self, w): 

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

1826 s = w.leo_label.text() 

1827 else: 

1828 s = '' 

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

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

1831 return s 

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

1833 def createChapterIvar(self, w): 

1834 c = self.c 

1835 cc = c.chapterController 

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

1837 pass 

1838 elif cc and self.use_chapters: 

1839 w.leo_chapter = cc.getSelectedChapter() 

1840 else: 

1841 w.leo_chapter = None 

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

1843 def deactivateEditors(self, wrapper): 

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

1845 d = self.editorWrappers 

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

1847 for key in d: 

1848 wrapper2 = d.get(key) 

1849 w2 = wrapper2.widget 

1850 if hasattr(w2, 'leo_active'): 

1851 active = w2.leo_active 

1852 else: 

1853 active = True 

1854 if wrapper2 != wrapper and active: 

1855 w2.leo_active = False 

1856 self.unselectLabel(wrapper2) 

1857 self.onFocusOut(w2) 

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

1859 def ensurePositionExists(self, w): 

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

1861 c = self.c 

1862 if c.positionExists(w.leo_p): 

1863 return True 

1864 for p2 in c.all_unique_positions(): 

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

1866 w.leo_p = p2.copy() 

1867 return True 

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

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

1870 return False 

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

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

1873 

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

1875 tag = 'qt_body.injectIvars' 

1876 w = wrapper.widget 

1877 assert g.isTextWrapper(wrapper), wrapper 

1878 assert g.isTextWidget(w), w 

1879 if trace: 

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

1881 # Inject ivars 

1882 if name == '1': 

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

1884 else: 

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

1886 w.leo_active = True 

1887 w.leo_bodyBar = None 

1888 w.leo_bodyXBar = None 

1889 w.leo_chapter = None 

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

1891 w.leo_frame = parentFrame 

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

1893 w.leo_name = name 

1894 w.leo_wrapper = wrapper 

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

1896 def recolorWidget(self, p, wrapper): 

1897 """Support QScintillaColorizer.colorize.""" 

1898 # pylint: disable=arguments-differ 

1899 c = self.c 

1900 colorizer = c.frame.body.colorizer 

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

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

1903 old_wrapper = c.frame.body.wrapper 

1904 c.frame.body.wrapper = wrapper 

1905 try: 

1906 colorizer.colorize(p) 

1907 finally: 

1908 # Restore. 

1909 c.frame.body.wrapper = old_wrapper 

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

1911 def switchToChapter(self, w): 

1912 """select w.leo_chapter.""" 

1913 c = self.c 

1914 cc = c.chapterController 

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

1916 chapter = w.leo_chapter 

1917 name = chapter and chapter.name 

1918 oldChapter = cc.getSelectedChapter() 

1919 if chapter != oldChapter: 

1920 cc.selectChapterByName(name) 

1921 c.bodyWantsFocus() 

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

1923 def unpackWidget(self, layout, w): 

1924 

1925 index = layout.indexOf(w) 

1926 if index == -1: 

1927 return 

1928 item = layout.itemAt(index) 

1929 if item: 

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

1931 layout.removeItem(item) 

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

1933 def updateInjectedIvars(self, w, p): 

1934 

1935 c = self.c 

1936 cc = c.chapterController 

1937 assert g.isTextWidget(w), w 

1938 if cc and self.use_chapters: 

1939 w.leo_chapter = cc.getSelectedChapter() 

1940 else: 

1941 w.leo_chapter = None 

1942 w.leo_p = p.copy() 

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

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

1945 def onFocusIn(self, obj): 

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

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

1948 tag = 'qt_body.onFocusIn' 

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

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

1951 if trace: 

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

1953 if wrapper and wrapper != self.wrapper: 

1954 self.selectEditor(wrapper) 

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

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

1957 obj.setReadOnly(True) 

1958 else: 

1959 obj.setReadOnly(False) 

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

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

1962 def onFocusOut(self, obj): 

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

1964 # Apparently benign. 

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

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

1967 if hasattr(obj, 'setReadOnly'): 

1968 obj.setReadOnly(True) 

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

1970 def onFocusColorHelper(self, kind, obj): 

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

1972 c, vc = self.c, self.c.vimCommands 

1973 if vc and c.vim_mode: 

1974 try: 

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

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

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

1978 except Exception: 

1979 # g.es_exception() 

1980 pass 

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

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

1983 def hideCanvasRenderer(self, event=None): 

1984 """Hide canvas pane.""" 

1985 c, d = self.c, self.editorWrappers 

1986 wrapper = c.frame.body.wrapper 

1987 w = wrapper.widget 

1988 name = w.leo_name 

1989 assert name 

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

1991 assert g.isTextWrapper(wrapper), wrapper 

1992 assert g.isTextWidget(w), w 

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

1994 return 

1995 # 

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

1997 if name == '1': 

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

1999 return 

2000 # 

2001 # Actually delete the widget. 

2002 del d[name] 

2003 f = c.frame.top.leo_body_inner_frame 

2004 layout = f.layout() 

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

2006 if z: 

2007 self.unpackWidget(layout, z) 

2008 # 

2009 # Select another editor. 

2010 w.leo_label = None 

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

2012 self.numberOfEditors -= 1 

2013 if self.numberOfEditors == 1: 

2014 w = new_wrapper.widget 

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

2016 self.unpackWidget(layout, w.leo_label) 

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

2018 self.selectEditor(new_wrapper) 

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

2020 def hideCanvas(self, event=None): 

2021 """Hide canvas pane.""" 

2022 c, d = self.c, self.editorWrappers 

2023 wrapper = c.frame.body.wrapper 

2024 w = wrapper.widget 

2025 name = w.leo_name 

2026 assert name 

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

2028 assert g.isTextWrapper(wrapper), wrapper 

2029 assert g.isTextWidget(w), w 

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

2031 return 

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

2033 if name == '1': 

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

2035 return 

2036 # 

2037 # Actually delete the widget. 

2038 del d[name] 

2039 f = c.frame.top.leo_body_inner_frame 

2040 layout = f.layout() 

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

2042 if z: 

2043 self.unpackWidget(layout, z) 

2044 # 

2045 # Select another editor. 

2046 w.leo_label = None 

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

2048 self.numberOfEditors -= 1 

2049 if self.numberOfEditors == 1: 

2050 w = new_wrapper.widget 

2051 if w.leo_label: 

2052 self.unpackWidget(layout, w.leo_label) 

2053 w.leo_label = None 

2054 self.selectEditor(new_wrapper) 

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

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

2057 n = max(1, self.numberOfEditors) 

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

2059 layout = f.layout() 

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

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

2062 lab = QtWidgets.QLineEdit(f) 

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

2064 lab.setText(name) 

2065 # Pack the label and the widget. 

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

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

2068 layout.setRowStretch(0, 0) 

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

2070 return lab 

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

2072 # An override of leoFrame.addEditor. 

2073 

2074 def showCanvasRenderer(self, event=None): 

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

2076 c = self.c 

2077 f = c.frame.top.leo_body_inner_frame 

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

2079 if not self.canvasRenderer: 

2080 name = 'Graphics Renderer' 

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

2082 w.setObjectName(name) 

2083 if not self.canvasRendererVisible: 

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

2085 self.canvasRendererVisible = True 

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

2087 # An override of leoFrame.addEditor. 

2088 

2089 def showTextRenderer(self, event=None): 

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

2091 c = self.c 

2092 f = c.frame.top.leo_body_inner_frame 

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

2094 if not self.textRenderer: 

2095 name = 'Text Renderer' 

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

2097 w.setObjectName(name) 

2098 self.textRendererWrapper = qt_text.QTextEditWrapper( 

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

2100 if not self.textRendererVisible: 

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

2102 self.textRendererVisible = True 

2103 #@-others 

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

2105class LeoQtFrame(leoFrame.LeoFrame): 

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

2107 #@+others 

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

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

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

2111 

2112 super().__init__(c, gui) 

2113 assert self.c == c 

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

2115 # Official ivars... 

2116 self.iconBar = None 

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

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

2119 self.minibufferVisible = True 

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

2121 self.title = title 

2122 self.setIvars() 

2123 self.reloadSettings() 

2124 

2125 def reloadSettings(self): 

2126 c = self.c 

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

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

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

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

2131 def setIvars(self): 

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

2133 self.bar1 = None 

2134 self.bar2 = None 

2135 self.body = None 

2136 self.f1 = self.f2 = None 

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

2138 self.iconBarComponentName = 'iconBar' 

2139 self.iconFrame = None 

2140 self.log = None 

2141 self.canvas = None 

2142 self.outerFrame = None 

2143 self.statusFrame = None 

2144 self.statusLineComponentName = 'statusLine' 

2145 self.statusText = None 

2146 self.statusLabel = None 

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

2148 self.tree = None 

2149 # Used by event handlers... 

2150 self.controlKeyIsDown = False # For control-drags 

2151 self.isActive = True 

2152 self.redrawCount = 0 

2153 self.wantedWidget = None 

2154 self.wantedCallbackScheduled = False 

2155 self.scrollWay = None 

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

2157 def __repr__(self): 

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

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

2160 def finishCreate(self): 

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

2162 # Called from app.newCommander, Commands.__init__ 

2163 t1 = time.process_time() 

2164 c = self.c 

2165 assert c 

2166 frameFactory = g.app.gui.frameFactory 

2167 if not frameFactory.masterFrame: 

2168 frameFactory.createMaster() 

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

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

2171 self.createSplitterComponents() 

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

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

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

2175 g.app.windowList.append(self) 

2176 t2 = time.process_time() 

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

2178 t3 = time.process_time() 

2179 self.miniBufferWidget = qt_text.QMinibufferWrapper(c) 

2180 c.bodyWantsFocus() 

2181 t4 = time.process_time() 

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

2183 print('qtFrame.finishCreate') 

2184 print( 

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

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

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

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

2189 ) 

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

2191 def createSplitterComponents(self): 

2192 

2193 c = self.c 

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

2195 self.log = LeoQtLog(self, None) 

2196 self.body = LeoQtBody(self, None) 

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

2198 self.resizePanesToRatio(ratio, secondary_ratio) 

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

2200 def setQtStyle(self): 

2201 """ 

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

2203 

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

2205 

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

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

2208 """ 

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

2210 if g.app.initStyleFlag: 

2211 return 

2212 g.app.initStyleFlag = True 

2213 c = self.c 

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

2215 # 

2216 # Get the requested style name. 

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

2218 if trace: 

2219 g.trace(repr(stylename)) 

2220 if not stylename: 

2221 return 

2222 # 

2223 # Return if the style does not exist. 

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

2225 if stylename.lower() not in styles: 

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

2227 g.printObj(styles) 

2228 return 

2229 # 

2230 # Change the style and palette. 

2231 app = g.app.gui.qtApp 

2232 if isQt5 or isQt6: 

2233 qstyle = app.setStyle(stylename) 

2234 if not qstyle: 

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

2236 else: 

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

2238 qstyle = QtWidgets.qApp.setStyle(stylename) 

2239 if not qstyle: 

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

2241 return 

2242 app.setPalette(QtWidgets.qApp.nativePalette) 

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

2244 def initCompleteHint(self): 

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

2246 self.initComplete = True 

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

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

2249 def destroyAllObjects(self): 

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

2251 c = self.c 

2252 # g.printGcAll() 

2253 # Do this first. 

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

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

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

2257 for v in vList: 

2258 g.clearAllIvars(v) 

2259 vList = [] # Remove these references immediately. 

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

2261 # Destroy all ivars in subcommanders. 

2262 g.clearAllIvars(c.atFileCommands) 

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

2264 g.clearAllIvars(c.chapterController) 

2265 g.clearAllIvars(c.fileCommands) 

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

2267 g.clearAllIvars(c.importCommands) 

2268 g.clearAllIvars(c.tangleCommands) 

2269 g.clearAllIvars(c.undoer) 

2270 g.clearAllIvars(c) 

2271 

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

2273 def destroySelf(self): 

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

2275 c, top = self.c, self.top 

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

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

2278 # Indicate that the commander is no longer valid. 

2279 c.exists = False 

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

2281 # Destroys all the objects of the commander. 

2282 self.destroyAllObjects() 

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

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

2285 top.close() 

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

2287 class QtStatusLineClass: 

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

2289 #@+others 

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

2291 def __init__(self, c, parentFrame): 

2292 """Ctor for LeoQtFrame class.""" 

2293 self.c = c 

2294 self.statusBar = c.frame.top.statusBar 

2295 self.lastFcol = 0 

2296 self.lastRow = 0 

2297 self.lastCol = 0 

2298 # Create the text widgets. 

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

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

2301 w1.setObjectName('status1') 

2302 w2.setObjectName('status2') 

2303 w1.setReadOnly(True) 

2304 w2.setReadOnly(True) 

2305 splitter = QtWidgets.QSplitter() 

2306 self.statusBar.addWidget(splitter, True) 

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

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

2309 for n, i in enumerate(sizes): 

2310 w = [w1, w2][n] 

2311 policy = w.sizePolicy() 

2312 policy.setHorizontalStretch(i) 

2313 policy.setHorizontalPolicy(Policy.Minimum) 

2314 w.setSizePolicy(policy) 

2315 splitter.addWidget(w1) 

2316 splitter.addWidget(w2) 

2317 self.put('') 

2318 self.update() 

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

2320 def clear(self): 

2321 self.put('') 

2322 

2323 def get(self): 

2324 return self.textWidget2.text() 

2325 

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

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

2328 

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

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

2331 

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

2333 # Keys are widgets, values are stylesheets. 

2334 

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

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

2337 c = self.c 

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

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

2340 if True: 

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

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

2343 else: 

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

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

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

2347 # back upstream, infer status like this: 

2348 if ( 

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

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

2351 ): 

2352 status = 'info' 

2353 elif ( 

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

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

2356 ): 

2357 status = 'fail' 

2358 else: 

2359 status = None 

2360 d = self.styleSheetCache 

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

2362 d[w] = status 

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

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

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

2366 w.setText(s) 

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

2368 def update(self): 

2369 if g.app.killed: 

2370 return 

2371 c, body = self.c, self.c.frame.body 

2372 if not c.p: 

2373 return 

2374 te = body.widget 

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

2376 return 

2377 cursor = te.textCursor() 

2378 block = cursor.block() 

2379 row = block.blockNumber() + 1 

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

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

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

2383 self.lastRow = row 

2384 self.lastCol = col 

2385 self.lastFcol = fcol 

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

2387 def compute_columns(self, block, cursor): 

2388 

2389 c = self.c 

2390 line = block.text() 

2391 col = cursor.columnNumber() 

2392 offset = c.p.textOffset() 

2393 fcol_offset = 0 

2394 s2 = line[0:col] 

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

2396 # 

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

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

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

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

2401 offset = None 

2402 else: 

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

2404 if line.startswith(tag): 

2405 fcol_offset = len(tag) 

2406 break 

2407 # 

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

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

2410 return col, fcol 

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

2412 def file_line(self): 

2413 """ 

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

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

2416 """ 

2417 c, p = self.c, self.c.p 

2418 if p: 

2419 goto = gotoCommands.GoToCommands(c) 

2420 return goto.find_node_start(p) 

2421 return None 

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

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

2424 

2425 if 1: 

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

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

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

2429 else: 

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

2431 fline = self.file_line() 

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

2433 self.put1( 

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

2435 #@-others 

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

2437 class QtIconBarClass: 

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

2439 #@+others 

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

2441 def __init__(self, c, parentFrame): 

2442 """Ctor for QtIconBarClass.""" 

2443 # Copy ivars 

2444 self.c = c 

2445 self.parentFrame = parentFrame 

2446 # Status ivars. 

2447 self.actions = [] 

2448 self.chapterController = None 

2449 self.toolbar = self 

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

2451 self.reloadSettings() 

2452 

2453 def reloadSettings(self): 

2454 c = self.c 

2455 c.registerReloadSettings(self) 

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

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

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

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

2460 

2461 def addRow(self, height=None): 

2462 pass # To do. 

2463 

2464 def getNewFrame(self): 

2465 return None # To do 

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

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

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

2469 c = self.c 

2470 if not self.w: 

2471 return None 

2472 command = keys.get('command') 

2473 text = keys.get('text') 

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

2475 qaction = keys.get('qaction') 

2476 if not text and not qaction: 

2477 g.es('bad toolbar item') 

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

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

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

2481 

2482 

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

2484 

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

2486 super().__init__(parent) 

2487 self.button = None # set below 

2488 self.text = text 

2489 self.toolbar = toolbar 

2490 

2491 def createWidget(self, parent): 

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

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

2494 return b 

2495 

2496 if qaction is None: 

2497 action = leoIconBarButton(parent=self.w, text=text, toolbar=self) 

2498 button_name = text 

2499 else: 

2500 action = qaction 

2501 button_name = action.text() 

2502 self.w.addAction(action) 

2503 self.actions.append(action) 

2504 b = self.w.widgetForAction(action) 

2505 # Set the button's object name so we can use the stylesheet to color it. 

2506 if not button_name: 

2507 button_name = 'unnamed' 

2508 button_name = button_name + '-button' 

2509 b.setObjectName(button_name) 

2510 b.setContextMenuPolicy(ContextMenuPolicy.ActionsContextMenu) 

2511 

2512 def delete_callback(checked, action=action,): 

2513 self.w.removeAction(action) 

2514 

2515 b.leo_removeAction = rb = QAction('Remove Button', b) 

2516 b.addAction(rb) 

2517 rb.triggered.connect(delete_callback) 

2518 if command: 

2519 

2520 def button_callback(event, c=c, command=command): 

2521 val = command() 

2522 if c.exists: 

2523 # c.bodyWantsFocus() 

2524 c.outerUpdate() 

2525 return val 

2526 

2527 b.clicked.connect(button_callback) 

2528 return action 

2529 #@+node:ekr.20110605121601.18266: *4* QtIconBar.addRowIfNeeded (not used) 

2530 def addRowIfNeeded(self): 

2531 """Add a new icon row if there are too many widgets.""" 

2532 # n = g.app.iconWidgetCount 

2533 # if n >= self.widgets_per_row: 

2534 # g.app.iconWidgetCount = 0 

2535 # self.addRow() 

2536 # g.app.iconWidgetCount += 1 

2537 #@+node:ekr.20110605121601.18267: *4* QtIconBar.addWidget 

2538 def addWidget(self, w): 

2539 self.w.addWidget(w) 

2540 #@+node:ekr.20110605121601.18268: *4* QtIconBar.clear 

2541 def clear(self): 

2542 """Destroy all the widgets in the icon bar""" 

2543 self.w.clear() 

2544 self.actions = [] 

2545 g.app.iconWidgetCount = 0 

2546 #@+node:ekr.20110605121601.18269: *4* QtIconBar.createChaptersIcon 

2547 def createChaptersIcon(self): 

2548 

2549 c = self.c 

2550 f = c.frame 

2551 if f.use_chapters and f.use_chapter_tabs: 

2552 return LeoQtTreeTab(c, f.iconBar) 

2553 return None 

2554 #@+node:ekr.20110605121601.18270: *4* QtIconBar.deleteButton 

2555 def deleteButton(self, w): 

2556 """ w is button """ 

2557 self.w.removeAction(w) 

2558 self.c.bodyWantsFocus() 

2559 self.c.outerUpdate() 

2560 #@+node:ekr.20141031053508.14: *4* QtIconBar.goto_command 

2561 def goto_command(self, controller, gnx): 

2562 """ 

2563 Select the node corresponding to the given gnx. 

2564 controller is a ScriptingController instance. 

2565 """ 

2566 # Fix bug 74: command_p may be in another outline. 

2567 c = self.c 

2568 c2, p = controller.open_gnx(c, gnx) 

2569 if p: 

2570 assert c2.positionExists(p) 

2571 if c == c2: 

2572 c2.selectPosition(p) 

2573 else: 

2574 g.app.selectLeoWindow(c2) 

2575 # Fix #367: Process events before selecting. 

2576 g.app.gui.qtApp.processEvents() 

2577 c2.selectPosition(p) 

2578 else: 

2579 g.trace('not found', gnx) 

2580 #@+node:ekr.20110605121601.18271: *4* QtIconBar.setCommandForButton (@rclick nodes) & helper 

2581 # qtFrame.QtIconBarClass.setCommandForButton 

2582 

2583 def setCommandForButton(self, button, command, command_p, controller, gnx, script): 

2584 """ 

2585 Set the "Goto Script" rlick item of an @button button. 

2586 Called from mod_scripting.py plugin. 

2587 

2588 button is a leoIconBarButton. 

2589 command is a callback, defined in mod_scripting.py. 

2590 command_p exists only if the @button node exists in the local .leo file. 

2591 gnx is the gnx of the @button node. 

2592 script is a static script for common @button nodes. 

2593 """ 

2594 if not command: 

2595 return 

2596 b = button.button 

2597 b.clicked.connect(command) 

2598 

2599 # Fix bug 74: use the controller and gnx arguments. 

2600 

2601 def goto_callback(checked, controller=controller, gnx=gnx): 

2602 self.goto_command(controller, gnx) 

2603 b.goto_script = gts = QAction('Goto Script', b) 

2604 b.addAction(gts) 

2605 gts.triggered.connect(goto_callback) 

2606 rclicks = build_rclick_tree(command_p, top_level=True) 

2607 self.add_rclick_menu(b, rclicks, controller, script=script) 

2608 #@+node:ekr.20141031053508.15: *5* add_rclick_menu (QtIconBarClass) 

2609 def add_rclick_menu(self, action_container, rclicks, controller, 

2610 top_level=True, 

2611 button=None, 

2612 script=None 

2613 ): 

2614 c = controller.c 

2615 top_offset = -2 # insert before the remove button and goto script items 

2616 if top_level: 

2617 button = action_container 

2618 for rc in rclicks: 

2619 # pylint: disable=cell-var-from-loop 

2620 headline = rc.position.h[8:].strip() 

2621 act = QAction(headline, action_container) 

2622 if '---' in headline and headline.strip().strip('-') == '': 

2623 act.setSeparator(True) 

2624 elif rc.position.b.strip(): 

2625 

2626 def cb(checked, p=rc.position, button=button): 

2627 controller.executeScriptFromButton( 

2628 b=button, 

2629 buttonText=p.h[8:].strip(), 

2630 p=p, 

2631 script=script, 

2632 ) 

2633 if c.exists: 

2634 c.outerUpdate() 

2635 

2636 act.triggered.connect(cb) 

2637 else: # recurse submenu 

2638 sub_menu = QtWidgets.QMenu(action_container) 

2639 act.setMenu(sub_menu) 

2640 self.add_rclick_menu(sub_menu, rc.children, controller, 

2641 top_level=False, button=button) 

2642 if top_level: 

2643 # insert act before Remove Button 

2644 action_container.insertAction( 

2645 action_container.actions()[top_offset], act) 

2646 else: 

2647 action_container.addAction(act) 

2648 if top_level and rclicks: 

2649 act = QAction('---', action_container) 

2650 act.setSeparator(True) 

2651 action_container.insertAction( 

2652 action_container.actions()[top_offset], act) 

2653 action_container.setText( 

2654 action_container.text() + 

2655 (c.config.getString('mod-scripting-subtext') or '') 

2656 ) 

2657 #@-others 

2658 #@+node:ekr.20110605121601.18274: *3* qtFrame.Configuration 

2659 #@+node:ekr.20110605121601.18275: *4* qtFrame.configureBar 

2660 def configureBar(self, bar, verticalFlag): 

2661 c = self.c 

2662 # Get configuration settings. 

2663 w = c.config.getInt("split-bar-width") 

2664 if not w or w < 1: 

2665 w = 7 

2666 relief = c.config.get("split_bar_relief", "relief") 

2667 if not relief: 

2668 relief = "flat" 

2669 color = c.config.getColor("split-bar-color") 

2670 if not color: 

2671 color = "LightSteelBlue2" 

2672 try: 

2673 if verticalFlag: 

2674 # Panes arranged vertically; horizontal splitter bar 

2675 bar.configure( 

2676 relief=relief, height=w, bg=color, cursor="sb_v_double_arrow") 

2677 else: 

2678 # Panes arranged horizontally; vertical splitter bar 

2679 bar.configure( 

2680 relief=relief, width=w, bg=color, cursor="sb_h_double_arrow") 

2681 except Exception: 

2682 # Could be a user error. Use all defaults 

2683 g.es("exception in user configuration for splitbar") 

2684 g.es_exception() 

2685 if verticalFlag: 

2686 # Panes arranged vertically; horizontal splitter bar 

2687 bar.configure(height=7, cursor="sb_v_double_arrow") 

2688 else: 

2689 # Panes arranged horizontally; vertical splitter bar 

2690 bar.configure(width=7, cursor="sb_h_double_arrow") 

2691 #@+node:ekr.20110605121601.18276: *4* qtFrame.configureBarsFromConfig 

2692 def configureBarsFromConfig(self): 

2693 c = self.c 

2694 w = c.config.getInt("split-bar-width") 

2695 if not w or w < 1: 

2696 w = 7 

2697 relief = c.config.get("split_bar_relief", "relief") 

2698 if not relief or relief == "": 

2699 relief = "flat" 

2700 color = c.config.getColor("split-bar-color") 

2701 if not color or color == "": 

2702 color = "LightSteelBlue2" 

2703 if self.splitVerticalFlag: 

2704 bar1, bar2 = self.bar1, self.bar2 

2705 else: 

2706 bar1, bar2 = self.bar2, self.bar1 

2707 try: 

2708 bar1.configure(relief=relief, height=w, bg=color) 

2709 bar2.configure(relief=relief, width=w, bg=color) 

2710 except Exception: 

2711 # Could be a user error. 

2712 g.es("exception in user configuration for splitbar") 

2713 g.es_exception() 

2714 #@+node:ekr.20110605121601.18277: *4* qtFrame.reconfigureFromConfig 

2715 def reconfigureFromConfig(self): 

2716 """Init the configuration of the Qt frame from settings.""" 

2717 c, frame = self.c, self 

2718 frame.configureBarsFromConfig() 

2719 frame.setTabWidth(c.tab_width) 

2720 c.redraw() 

2721 #@+node:ekr.20110605121601.18278: *4* qtFrame.setInitialWindowGeometry 

2722 def setInitialWindowGeometry(self): 

2723 """Set the position and size of the frame to config params.""" 

2724 c = self.c 

2725 h = c.config.getInt("initial-window-height") or 500 

2726 w = c.config.getInt("initial-window-width") or 600 

2727 x = c.config.getInt("initial-window-left") or 50 # #1190: was 10 

2728 y = c.config.getInt("initial-window-top") or 50 # #1190: was 10 

2729 if h and w and x and y: 

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

2731 g.trace(w, h, x, y) 

2732 self.setTopGeometry(w, h, x, y) 

2733 #@+node:ekr.20110605121601.18279: *4* qtFrame.setTabWidth 

2734 def setTabWidth(self, w): 

2735 # A do-nothing because tab width is set automatically. 

2736 # It *is* called from Leo's core. 

2737 pass 

2738 #@+node:ekr.20110605121601.18280: *4* qtFrame.forceWrap & setWrap 

2739 def forceWrap(self, p=None): 

2740 return self.c.frame.body.forceWrap(p) 

2741 

2742 def setWrap(self, p=None): 

2743 return self.c.frame.body.setWrap(p) 

2744 

2745 #@+node:ekr.20110605121601.18281: *4* qtFrame.reconfigurePanes 

2746 def reconfigurePanes(self): 

2747 c, f = self.c, self 

2748 if f.splitVerticalFlag: 

2749 r = c.config.getRatio("initial-vertical-ratio") 

2750 if r is None or r < 0.0 or r > 1.0: 

2751 r = 0.5 

2752 r2 = c.config.getRatio("initial-vertical-secondary-ratio") 

2753 if r2 is None or r2 < 0.0 or r2 > 1.0: 

2754 r2 = 0.8 

2755 else: 

2756 r = c.config.getRatio("initial-horizontal-ratio") 

2757 if r is None or r < 0.0 or r > 1.0: 

2758 r = 0.3 

2759 r2 = c.config.getRatio("initial-horizontal-secondary-ratio") 

2760 if r2 is None or r2 < 0.0 or r2 > 1.0: 

2761 r2 = 0.8 

2762 f.resizePanesToRatio(r, r2) 

2763 #@+node:ekr.20110605121601.18282: *4* qtFrame.resizePanesToRatio 

2764 def resizePanesToRatio(self, ratio, ratio2): 

2765 """Resize splitter1 and splitter2 using the given ratios.""" 

2766 # pylint: disable=arguments-differ 

2767 self.divideLeoSplitter1(ratio) 

2768 self.divideLeoSplitter2(ratio2) 

2769 #@+node:ekr.20110605121601.18283: *4* qtFrame.divideLeoSplitter1/2 

2770 def divideLeoSplitter1(self, frac): 

2771 """Divide the main splitter.""" 

2772 layout = self.c and self.c.free_layout 

2773 if not layout: 

2774 return 

2775 w = layout.get_main_splitter() 

2776 if w: 

2777 self.divideAnySplitter(frac, w) 

2778 

2779 def divideLeoSplitter2(self, frac): 

2780 """Divide the secondary splitter.""" 

2781 layout = self.c and self.c.free_layout 

2782 if not layout: 

2783 return 

2784 w = layout.get_secondary_splitter() 

2785 if w: 

2786 self.divideAnySplitter(frac, w) 

2787 #@+node:ekr.20110605121601.18284: *4* qtFrame.divideAnySplitter 

2788 # This is the general-purpose placer for splitters. 

2789 # It is the only general-purpose splitter code in Leo. 

2790 

2791 def divideAnySplitter(self, frac, splitter): 

2792 """Set the splitter sizes.""" 

2793 sizes = splitter.sizes() 

2794 if len(sizes) != 2: 

2795 g.trace(f"{len(sizes)} widget(s) in {id(splitter)}") 

2796 return 

2797 if frac > 1 or frac < 0: 

2798 g.trace(f"split ratio [{frac}] out of range 0 <= frac <= 1") 

2799 return 

2800 s1, s2 = sizes 

2801 s = s1 + s2 

2802 s1 = int(s * frac + 0.5) 

2803 s2 = s - s1 

2804 splitter.setSizes([s1, s2]) 

2805 #@+node:ekr.20110605121601.18285: *3* qtFrame.Event handlers 

2806 #@+node:ekr.20110605121601.18286: *4* qtFrame.OnCloseLeoEvent 

2807 # Called from quit logic and when user closes the window. 

2808 # Returns True if the close happened. 

2809 

2810 def OnCloseLeoEvent(self): 

2811 f = self 

2812 c = f.c 

2813 if c.inCommand: 

2814 c.requestCloseWindow = True 

2815 else: 

2816 g.app.closeLeoWindow(self) 

2817 #@+node:ekr.20110605121601.18287: *4* qtFrame.OnControlKeyUp/Down 

2818 def OnControlKeyDown(self, event=None): 

2819 self.controlKeyIsDown = True 

2820 

2821 def OnControlKeyUp(self, event=None): 

2822 self.controlKeyIsDown = False 

2823 #@+node:ekr.20110605121601.18290: *4* qtFrame.OnActivateTree 

2824 def OnActivateTree(self, event=None): 

2825 pass 

2826 #@+node:ekr.20110605121601.18291: *4* qtFrame.OnBodyClick, OnBodyRClick (not used) 

2827 # At present, these are not called, 

2828 # but they could be called by LeoQTextBrowser. 

2829 

2830 def OnBodyClick(self, event=None): 

2831 g.trace() 

2832 try: 

2833 c, p = self.c, self.c.p 

2834 if g.doHook("bodyclick1", c=c, p=p, event=event): 

2835 g.doHook("bodyclick2", c=c, p=p, event=event) 

2836 return 

2837 c.k.showStateAndMode(w=c.frame.body.wrapper) 

2838 g.doHook("bodyclick2", c=c, p=p, event=event) 

2839 except Exception: 

2840 g.es_event_exception("bodyclick") 

2841 

2842 def OnBodyRClick(self, event=None): 

2843 try: 

2844 c, p = self.c, self.c.p 

2845 if g.doHook("bodyrclick1", c=c, p=p, event=event): 

2846 g.doHook("bodyrclick2", c=c, p=p, event=event) 

2847 return 

2848 c.k.showStateAndMode(w=c.frame.body.wrapper) 

2849 g.doHook("bodyrclick2", c=c, p=p, event=event) 

2850 except Exception: 

2851 g.es_event_exception("iconrclick") 

2852 #@+node:ekr.20110605121601.18292: *4* qtFrame.OnBodyDoubleClick (Events) (not used) 

2853 # Not called 

2854 

2855 def OnBodyDoubleClick(self, event=None): 

2856 try: 

2857 c, p = self.c, self.c.p 

2858 if event and not g.doHook("bodydclick1", c=c, p=p, event=event): 

2859 c.editCommands.extendToWord(event) # Handles unicode properly. 

2860 c.k.showStateAndMode(w=c.frame.body.wrapper) 

2861 g.doHook("bodydclick2", c=c, p=p, event=event) 

2862 except Exception: 

2863 g.es_event_exception("bodydclick") 

2864 return "break" # Restore this to handle proper double-click logic. 

2865 #@+node:ekr.20110605121601.18293: *3* qtFrame.Gui-dependent commands 

2866 #@+node:ekr.20110605121601.18301: *4* qtFrame.Window Menu... 

2867 #@+node:ekr.20110605121601.18302: *5* qtFrame.toggleActivePane 

2868 @frame_cmd('toggle-active-pane') 

2869 def toggleActivePane(self, event=None): 

2870 """Toggle the focus between the outline and body panes.""" 

2871 frame = self 

2872 c = frame.c 

2873 w = c.get_focus() 

2874 w_name = g.app.gui.widget_name(w) 

2875 if w_name in ('canvas', 'tree', 'treeWidget'): 

2876 c.endEditing() 

2877 c.bodyWantsFocus() 

2878 else: 

2879 c.treeWantsFocus() 

2880 #@+node:ekr.20110605121601.18303: *5* qtFrame.cascade 

2881 @frame_cmd('cascade-windows') 

2882 def cascade(self, event=None): 

2883 """Cascade all Leo windows.""" 

2884 x, y, delta = 50, 50, 50 

2885 for frame in g.app.windowList: 

2886 w = frame and frame.top 

2887 if w: 

2888 r = w.geometry() # a Qt.Rect 

2889 # 2011/10/26: Fix bug 823601: cascade-windows fails. 

2890 w.setGeometry(QtCore.QRect(x, y, r.width(), r.height())) 

2891 # Compute the new offsets. 

2892 x += 30 

2893 y += 30 

2894 if x > 200: 

2895 x = 10 + delta 

2896 y = 40 + delta 

2897 delta += 10 

2898 #@+node:ekr.20110605121601.18304: *5* qtFrame.equalSizedPanes 

2899 @frame_cmd('equal-sized-panes') 

2900 def equalSizedPanes(self, event=None): 

2901 """Make the outline and body panes have the same size.""" 

2902 self.resizePanesToRatio(0.5, self.secondary_ratio) 

2903 #@+node:ekr.20110605121601.18305: *5* qtFrame.hideLogWindow 

2904 def hideLogWindow(self, event=None): 

2905 """Hide the log pane.""" 

2906 self.divideLeoSplitter2(0.99) 

2907 #@+node:ekr.20110605121601.18306: *5* qtFrame.minimizeAll 

2908 @frame_cmd('minimize-all') 

2909 def minimizeAll(self, event=None): 

2910 """Minimize all Leo's windows.""" 

2911 for frame in g.app.windowList: 

2912 self.minimize(frame) 

2913 

2914 def minimize(self, frame): 

2915 # This unit test will fail when run externally. 

2916 if frame and frame.top: 

2917 w = frame.top.leo_master or frame.top 

2918 if g.unitTesting: 

2919 assert hasattr(w, 'setWindowState'), w 

2920 else: 

2921 w.setWindowState(WindowState.WindowMinimized) 

2922 #@+node:ekr.20110605121601.18307: *5* qtFrame.toggleSplitDirection 

2923 @frame_cmd('toggle-split-direction') 

2924 def toggleSplitDirection(self, event=None): 

2925 """Toggle the split direction in the present Leo window.""" 

2926 if hasattr(self.c, 'free_layout'): 

2927 self.c.free_layout.get_top_splitter().rotate() 

2928 #@+node:ekr.20110605121601.18308: *5* qtFrame.resizeToScreen 

2929 @frame_cmd('resize-to-screen') 

2930 def resizeToScreen(self, event=None): 

2931 """Resize the Leo window so it fill the entire screen.""" 

2932 frame = self 

2933 # This unit test will fail when run externally. 

2934 if frame and frame.top: 

2935 # frame.top.leo_master is a LeoTabbedTopLevel. 

2936 # frame.top is a DynamicWindow. 

2937 w = frame.top.leo_master or frame.top 

2938 if g.unitTesting: 

2939 assert hasattr(w, 'setWindowState'), w 

2940 else: 

2941 w.setWindowState(WindowState.WindowMaximized) 

2942 #@+node:ekr.20110605121601.18309: *4* qtFrame.Help Menu... 

2943 #@+node:ekr.20110605121601.18310: *5* qtFrame.leoHelp 

2944 @frame_cmd('open-offline-tutorial') 

2945 def leoHelp(self, event=None): 

2946 """Open Leo's offline tutorial.""" 

2947 frame = self 

2948 c = frame.c 

2949 theFile = g.os_path_join(g.app.loadDir, "..", "doc", "sbooks.chm") 

2950 if g.os_path_exists(theFile) and sys.platform.startswith('win'): 

2951 # pylint: disable=no-member 

2952 os.startfile(theFile) 

2953 else: 

2954 answer = g.app.gui.runAskYesNoDialog(c, 

2955 "Download Tutorial?", 

2956 "Download tutorial (sbooks.chm) from SourceForge?") 

2957 if answer == "yes": 

2958 try: 

2959 url = "http://prdownloads.sourceforge.net/leo/sbooks.chm?download" 

2960 import webbrowser 

2961 os.chdir(g.app.loadDir) 

2962 webbrowser.open_new(url) 

2963 except Exception: 

2964 if 0: 

2965 g.es("exception downloading", "sbooks.chm") 

2966 g.es_exception() 

2967 #@+node:ekr.20160424080647.1: *3* qtFrame.Properties 

2968 # The ratio and secondary_ratio properties are read-only. 

2969 #@+node:ekr.20160424080815.2: *4* qtFrame.ratio property 

2970 def __get_ratio(self): 

2971 """Return splitter ratio of the main splitter.""" 

2972 c = self.c 

2973 free_layout = c.free_layout 

2974 if free_layout: 

2975 w = free_layout.get_main_splitter() 

2976 if w: 

2977 aList = w.sizes() 

2978 if len(aList) == 2: 

2979 n1, n2 = aList 

2980 # 2017/06/07: guard against division by zero. 

2981 ratio = 0.5 if n1 + n2 == 0 else float(n1) / float(n1 + n2) 

2982 return ratio 

2983 return 0.5 

2984 

2985 ratio = property( 

2986 __get_ratio, # No setter. 

2987 doc="qtFrame.ratio property") 

2988 #@+node:ekr.20160424080815.3: *4* qtFrame.secondary_ratio property 

2989 def __get_secondary_ratio(self): 

2990 """Return the splitter ratio of the secondary splitter.""" 

2991 c = self.c 

2992 free_layout = c.free_layout 

2993 if free_layout: 

2994 w = free_layout.get_secondary_splitter() 

2995 if w: 

2996 aList = w.sizes() 

2997 if len(aList) == 2: 

2998 n1, n2 = aList 

2999 ratio = float(n1) / float(n1 + n2) 

3000 return ratio 

3001 return 0.5 

3002 

3003 secondary_ratio = property( 

3004 __get_secondary_ratio, # no setter. 

3005 doc="qtFrame.secondary_ratio property") 

3006 #@+node:ekr.20110605121601.18311: *3* qtFrame.Qt bindings... 

3007 #@+node:ekr.20190611053431.1: *4* qtFrame.bringToFront 

3008 def bringToFront(self): 

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

3010 g.trace() 

3011 self.lift() 

3012 #@+node:ekr.20190611053431.2: *4* qtFrame.deiconify 

3013 def deiconify(self): 

3014 """Undo --minimized""" 

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

3016 g.trace( 

3017 'top:', bool(self.top), 

3018 'isMinimized:', self.top and self.top.isMinimized()) 

3019 if self.top and self.top.isMinimized(): # Bug fix: 400739. 

3020 self.lift() 

3021 #@+node:ekr.20190611053431.4: *4* qtFrame.get_window_info 

3022 def get_window_info(self): 

3023 """Return the geometry of the top window.""" 

3024 if getattr(self.top, 'leo_master', None): 

3025 f = self.top.leo_master 

3026 else: 

3027 f = self.top 

3028 rect = f.geometry() 

3029 topLeft = rect.topLeft() 

3030 x, y = topLeft.x(), topLeft.y() 

3031 w, h = rect.width(), rect.height() 

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

3033 g.trace('\n', w, h, x, y) 

3034 return w, h, x, y 

3035 #@+node:ekr.20190611053431.3: *4* qtFrame.getFocus 

3036 def getFocus(self): 

3037 return g.app.gui.get_focus(self.c) # Bug fix: 2009/6/30. 

3038 #@+node:ekr.20190611053431.7: *4* qtFrame.getTitle 

3039 def getTitle(self): 

3040 # Fix https://bugs.launchpad.net/leo-editor/+bug/1194209 

3041 # For qt, leo_master (a LeoTabbedTopLevel) contains the QMainWindow. 

3042 w = self.top.leo_master 

3043 return w.windowTitle() 

3044 #@+node:ekr.20190611053431.5: *4* qtFrame.iconify 

3045 def iconify(self): 

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

3047 g.trace(bool(self.top)) 

3048 if self.top: 

3049 self.top.showMinimized() 

3050 #@+node:ekr.20190611053431.6: *4* qtFrame.lift 

3051 def lift(self): 

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

3053 g.trace(bool(self.top), self.top and self.top.isMinimized()) 

3054 if not self.top: 

3055 return 

3056 if self.top.isMinimized(): # Bug 379141 

3057 self.top.showNormal() 

3058 self.top.activateWindow() 

3059 self.top.raise_() 

3060 #@+node:ekr.20190611053431.8: *4* qtFrame.setTitle 

3061 def setTitle(self, s): 

3062 # pylint: disable=arguments-differ 

3063 if self.top: 

3064 # Fix https://bugs.launchpad.net/leo-editor/+bug/1194209 

3065 # When using tabs, leo_master (a LeoTabbedTopLevel) contains the QMainWindow. 

3066 w = self.top.leo_master 

3067 w.setWindowTitle(s) 

3068 #@+node:ekr.20190611053431.9: *4* qtFrame.setTopGeometry 

3069 def setTopGeometry(self, w, h, x, y): 

3070 # self.top is a DynamicWindow. 

3071 if self.top: 

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

3073 g.trace(w, h, x, y, self.c.shortFileName(), g.callers()) 

3074 self.top.setGeometry(QtCore.QRect(x, y, w, h)) 

3075 #@+node:ekr.20190611053431.10: *4* qtFrame.update 

3076 def update(self, *args, **keys): 

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

3078 g.trace(bool(self.top)) 

3079 self.top.update() 

3080 #@-others 

3081#@+node:ekr.20110605121601.18312: ** class LeoQtLog (LeoLog) 

3082class LeoQtLog(leoFrame.LeoLog): 

3083 """A class that represents the log pane of a Qt window.""" 

3084 #@+others 

3085 #@+node:ekr.20110605121601.18313: *3* LeoQtLog.Birth 

3086 #@+node:ekr.20110605121601.18314: *4* LeoQtLog.__init__ & reloadSettings 

3087 def __init__(self, frame, parentFrame): 

3088 """Ctor for LeoQtLog class.""" 

3089 super().__init__(frame, parentFrame) 

3090 # Calls createControl. 

3091 assert self.logCtrl is None, self.logCtrl # type:ignore # Set in finishCreate. 

3092 # Important: depeding on the log *tab*, 

3093 # logCtrl may be either a wrapper or a widget. 

3094 self.c = c = frame.c 

3095 # Also set in the base constructor, but we need it here. 

3096 self.contentsDict = {} 

3097 # Keys are tab names. Values are widgets. 

3098 self.eventFilters = [] 

3099 # Apparently needed to make filters work! 

3100 self.logDict = {} 

3101 # Keys are tab names text widgets. Values are the widgets. 

3102 self.logWidget = None 

3103 # Set in finishCreate. 

3104 self.menu = None 

3105 # A menu that pops up on right clicks in the hull or in tabs. 

3106 self.tabWidget = tw = c.frame.top.tabWidget 

3107 # The Qt.QTabWidget that holds all the tabs. 

3108 # 

3109 # Bug 917814: Switching Log Pane tabs is done incompletely. 

3110 tw.currentChanged.connect(self.onCurrentChanged) 

3111 if 0: # Not needed to make onActivateEvent work. 

3112 # Works only for .tabWidget, *not* the individual tabs! 

3113 theFilter = qt_events.LeoQtEventFilter(c, w=tw, tag='tabWidget') 

3114 tw.installEventFilter(theFilter) 

3115 # 

3116 # 2013/11/15: Partial fix for bug 1251755: Log-pane refinements 

3117 tw.setMovable(True) 

3118 self.reloadSettings() 

3119 

3120 def reloadSettings(self): 

3121 c = self.c 

3122 self.wrap = bool(c.config.getBool('log-pane-wraps')) 

3123 #@+node:ekr.20110605121601.18315: *4* LeoQtLog.finishCreate 

3124 def finishCreate(self): 

3125 """Finish creating the LeoQtLog class.""" 

3126 c, log, w = self.c, self, self.tabWidget 

3127 # 

3128 # Create the log tab as the leftmost tab. 

3129 log.createTab('Log') 

3130 self.logWidget = logWidget = self.contentsDict.get('Log') 

3131 logWidget.setWordWrapMode(WrapMode.WordWrap if self.wrap else WrapMode.NoWrap) 

3132 w.insertTab(0, logWidget, 'Log') 

3133 # Required. 

3134 # 

3135 # set up links in log handling 

3136 logWidget.setTextInteractionFlags( 

3137 TextInteractionFlag.LinksAccessibleByMouse 

3138 | TextInteractionFlag.TextEditable 

3139 | TextInteractionFlag.TextSelectableByMouse 

3140 ) 

3141 logWidget.setOpenLinks(False) 

3142 logWidget.setOpenExternalLinks(False) 

3143 logWidget.anchorClicked.connect(self.linkClicked) 

3144 # 

3145 # Show the spell tab. 

3146 c.spellCommands.openSpellTab() 

3147 # 

3148 #794: Clicking Find Tab should do exactly what pushing Ctrl-F does 

3149 

3150 def tab_callback(index): 

3151 name = w.tabText(index) 

3152 if name == 'Find': 

3153 c.findCommands.startSearch(event=None) 

3154 

3155 w.currentChanged.connect(tab_callback) 

3156 # #1286. 

3157 w.customContextMenuRequested.connect(self.onContextMenu) 

3158 #@+node:ekr.20110605121601.18316: *4* LeoQtLog.getName 

3159 def getName(self): 

3160 return 'log' # Required for proper pane bindings. 

3161 #@+node:ekr.20150717102728.1: *3* LeoQtLog.Commands 

3162 @log_cmd('clear-log') 

3163 def clearLog(self, event=None): 

3164 """Clear the log pane.""" 

3165 w = self.logCtrl.widget # type:ignore # w is a QTextBrowser 

3166 if w: 

3167 w.clear() 

3168 #@+node:ekr.20110605121601.18333: *3* LeoQtLog.color tab stuff 

3169 def createColorPicker(self, tabName): 

3170 g.warning('color picker not ready for qt') 

3171 #@+node:ekr.20110605121601.18334: *3* LeoQtLog.font tab stuff 

3172 #@+node:ekr.20110605121601.18335: *4* LeoQtLog.createFontPicker 

3173 def createFontPicker(self, tabName): 

3174 # log = self 

3175 font, ok = QtWidgets.QFontDialog.getFont() 

3176 if not (font and ok): 

3177 return 

3178 style = font.style() 

3179 table1 = ( 

3180 (Style.StyleNormal, 'normal'), # #2330. 

3181 (Style.StyleItalic, 'italic'), 

3182 (Style.StyleOblique, 'oblique')) 

3183 for val, name in table1: 

3184 if style == val: 

3185 style = name 

3186 break 

3187 else: 

3188 style = '' 

3189 weight = font.weight() 

3190 table2 = ( 

3191 (Weight.Light, 'light'), # #2330. 

3192 (Weight.Normal, 'normal'), 

3193 (Weight.DemiBold, 'demibold'), 

3194 (Weight.Bold, 'bold'), 

3195 (Weight.Black, 'black')) 

3196 for val2, name2 in table2: 

3197 if weight == val2: 

3198 weight = name2 

3199 break 

3200 else: 

3201 weight = '' 

3202 table3 = ( 

3203 ('family', str(font.family())), 

3204 ('size ', font.pointSize()), 

3205 ('style ', style), 

3206 ('weight', weight), 

3207 ) 

3208 for key3, val3 in table3: 

3209 if val3: 

3210 g.es(key3, val3, tabName='Fonts') 

3211 #@+node:ekr.20110605121601.18339: *4* LeoQtLog.hideFontTab 

3212 def hideFontTab(self, event=None): 

3213 c = self.c 

3214 c.frame.log.selectTab('Log') 

3215 c.bodyWantsFocus() 

3216 #@+node:ekr.20111120124732.10184: *3* LeoQtLog.isLogWidget 

3217 def isLogWidget(self, w): 

3218 val = w == self or w in list(self.contentsDict.values()) 

3219 return val 

3220 #@+node:tbnorth.20171220123648.1: *3* LeoQtLog.linkClicked 

3221 def linkClicked(self, link): 

3222 """linkClicked - link clicked in log 

3223 

3224 :param QUrl link: link that was clicked 

3225 """ 

3226 # see addition of '/' in LeoQtLog.put() 

3227 url = s = g.toUnicode(link.toString()) 

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

3229 for scheme in 'file', 'unl': 

3230 if s.startswith(scheme + ':///') and s[len(scheme) + 5] == ':': 

3231 url = s.replace(':///', '://', 1) 

3232 break 

3233 g.handleUrl(url, c=self.c) 

3234 #@+node:ekr.20120304214900.9940: *3* LeoQtLog.onCurrentChanged 

3235 def onCurrentChanged(self, idx): 

3236 

3237 tabw = self.tabWidget 

3238 w = tabw.widget(idx) 

3239 # 

3240 # #917814: Switching Log Pane tabs is done incompletely 

3241 wrapper = getattr(w, 'leo_log_wrapper', None) 

3242 # 

3243 # #1161: Don't change logs unless the wrapper is correct. 

3244 if wrapper and isinstance(wrapper, qt_text.QTextEditWrapper): 

3245 self.logCtrl = wrapper 

3246 #@+node:ekr.20200304132424.1: *3* LeoQtLog.onContextMenu 

3247 def onContextMenu(self, point): 

3248 """LeoQtLog: Callback for customContextMenuRequested events.""" 

3249 # #1286. 

3250 c, w = self.c, self 

3251 g.app.gui.onContextMenu(c, w, point) 

3252 #@+node:ekr.20110605121601.18321: *3* LeoQtLog.put & putnl 

3253 #@+node:ekr.20110605121601.18322: *4* LeoQtLog.put 

3254 def put(self, s, color=None, tabName='Log', from_redirect=False, nodeLink=None): 

3255 """ 

3256 Put s to the Qt Log widget, converting to html. 

3257 All output to the log stream eventually comes here. 

3258 

3259 The from_redirect keyword argument is no longer used. 

3260 """ 

3261 c = self.c 

3262 if g.app.quitting or not c or not c.exists: 

3263 return 

3264 # Note: g.actualColor does all color translation. 

3265 if color: 

3266 color = leoColor.getColor(color) 

3267 if not color: 

3268 # #788: First, fall back to 'log_black_color', not 'black. 

3269 color = c.config.getColor('log-black-color') 

3270 if not color: 

3271 # Should never be necessary. 

3272 color = 'black' 

3273 self.selectTab(tabName or 'Log') 

3274 # Must be done after the call to selectTab. 

3275 wrapper = self.logCtrl 

3276 if not isinstance(wrapper, qt_text.QTextEditWrapper): 

3277 g.trace('BAD wrapper', wrapper.__class__.__name__) 

3278 return 

3279 w = wrapper.widget 

3280 if not isinstance(w, QtWidgets.QTextEdit): 

3281 g.trace('BAD widget', w.__class__.__name__) 

3282 return 

3283 sb = w.horizontalScrollBar() 

3284 s = s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;') 

3285 # #884: Always convert leading blanks and tabs to &nbsp. 

3286 n = len(s) - len(s.lstrip()) 

3287 if n > 0 and s.strip(): 

3288 s = '&nbsp;' * (n) + s[n:] 

3289 if not self.wrap: 

3290 # Convert all other blanks to &nbsp; 

3291 s = s.replace(' ', '&nbsp;') 

3292 s = s.replace('\n', '<br>') 

3293 # The caller is responsible for newlines! 

3294 s = f'<font color="{color}">{s}</font>' 

3295 if nodeLink: 

3296 url = nodeLink 

3297 for scheme in 'file', 'unl': 

3298 # QUrl requires paths start with '/' 

3299 if ( 

3300 url.startswith(scheme + '://') and not 

3301 url.startswith(scheme + ':///') 

3302 ): 

3303 url = url.replace('://', ':///', 1) 

3304 s = f'<a href="{url}" title="{nodeLink}">{s}</a>' 

3305 w.insertHtml(s) 

3306 w.moveCursor(MoveOperation.End) 

3307 sb.setSliderPosition(0) # Force the slider to the initial position. 

3308 w.repaint() # Slow, but essential. 

3309 #@+node:ekr.20110605121601.18323: *4* LeoQtLog.putnl 

3310 def putnl(self, tabName='Log'): 

3311 """Put a newline to the Qt log.""" 

3312 # 

3313 # This is not called normally. 

3314 if g.app.quitting: 

3315 return 

3316 if tabName: 

3317 self.selectTab(tabName) 

3318 wrapper = self.logCtrl 

3319 if not isinstance(wrapper, qt_text.QTextEditWrapper): 

3320 g.trace('BAD wrapper', wrapper.__class__.__name__) 

3321 return 

3322 w = wrapper.widget 

3323 if not isinstance(w, QtWidgets.QTextEdit): 

3324 g.trace('BAD widget', w.__class__.__name__) 

3325 return 

3326 sb = w.horizontalScrollBar() 

3327 pos = sb.sliderPosition() 

3328 # Not needed! 

3329 # contents = w.toHtml() 

3330 # w.setHtml(contents + '\n') 

3331 w.moveCursor(MoveOperation.End) 

3332 sb.setSliderPosition(pos) 

3333 w.repaint() # Slow, but essential. 

3334 #@+node:ekr.20150205181818.5: *4* LeoQtLog.scrollToEnd 

3335 def scrollToEnd(self, tabName='Log'): 

3336 """Scroll the log to the end.""" 

3337 if g.app.quitting: 

3338 return 

3339 if tabName: 

3340 self.selectTab(tabName) 

3341 w = self.logCtrl.widget 

3342 if not w: 

3343 return 

3344 sb = w.horizontalScrollBar() 

3345 pos = sb.sliderPosition() 

3346 w.moveCursor(MoveOperation.End) 

3347 sb.setSliderPosition(pos) 

3348 w.repaint() # Slow, but essential. 

3349 #@+node:ekr.20120913110135.10613: *3* LeoQtLog.putImage 

3350 #@+node:ekr.20110605121601.18324: *3* LeoQtLog.Tab 

3351 #@+node:ekr.20110605121601.18325: *4* LeoQtLog.clearTab 

3352 def clearTab(self, tabName, wrap='none'): 

3353 w = self.logDict.get(tabName) 

3354 if w: 

3355 w.clear() # w is a QTextBrowser. 

3356 #@+node:ekr.20110605121601.18326: *4* LeoQtLog.createTab 

3357 def createTab(self, tabName, createText=True, widget=None, wrap='none'): 

3358 """ 

3359 Create a new tab in tab widget 

3360 if widget is None, Create a QTextBrowser, 

3361 suitable for log functionality. 

3362 """ 

3363 c = self.c 

3364 if widget is None: 

3365 widget = qt_text.LeoQTextBrowser(parent=None, c=c, wrapper=self) 

3366 # widget is subclass of QTextBrowser. 

3367 contents = qt_text.QTextEditWrapper(widget=widget, name='log', c=c) 

3368 # contents a wrapper. 

3369 widget.leo_log_wrapper = contents 

3370 # Inject an ivar into the QTextBrowser that points to the wrapper. 

3371 widget.setWordWrapMode(WrapMode.WordWrap if self.wrap else WrapMode.NoWrap) 

3372 widget.setReadOnly(False) # Allow edits. 

3373 self.logDict[tabName] = widget 

3374 if tabName == 'Log': 

3375 self.logCtrl = contents 

3376 widget.setObjectName('log-widget') 

3377 # Set binding on all log pane widgets. 

3378 g.app.gui.setFilter(c, widget, self, tag='log') 

3379 self.contentsDict[tabName] = widget 

3380 self.tabWidget.addTab(widget, tabName) 

3381 else: 

3382 # #1161: Don't set the wrapper unless it has the correct type. 

3383 contents = widget 

3384 # Unlike text widgets, contents is the actual widget. 

3385 if isinstance(contents, qt_text.QTextEditWrapper): 

3386 widget.leo_log_wrapper = widget 

3387 # The leo_log_wrapper is the widget itself. 

3388 else: 

3389 widget.leo_log_wrapper = None 

3390 # Tell the truth. 

3391 g.app.gui.setFilter(c, widget, contents, 'tabWidget') 

3392 self.contentsDict[tabName] = contents 

3393 self.tabWidget.addTab(contents, tabName) 

3394 return contents 

3395 #@+node:ekr.20110605121601.18328: *4* LeoQtLog.deleteTab 

3396 def deleteTab(self, tabName): 

3397 """ 

3398 Delete the tab if it exists. Otherwise do *nothing*. 

3399 """ 

3400 c = self.c 

3401 w = self.tabWidget 

3402 i = self.findTabIndex(tabName) 

3403 if i is None: 

3404 return 

3405 w.removeTab(i) 

3406 self.selectTab('Log') 

3407 c.invalidateFocus() 

3408 c.bodyWantsFocus() 

3409 #@+node:ekr.20190603062456.1: *4* LeoQtLog.findTabIndex 

3410 def findTabIndex(self, tabName): 

3411 """Return the tab index for tabName, or None.""" 

3412 w = self.tabWidget 

3413 for i in range(w.count()): 

3414 if tabName == w.tabText(i): 

3415 return i 

3416 return None 

3417 #@+node:ekr.20110605121601.18329: *4* LeoQtLog.hideTab 

3418 def hideTab(self, tabName): 

3419 self.selectTab('Log') 

3420 #@+node:ekr.20111122080923.10185: *4* LeoQtLog.orderedTabNames 

3421 def orderedTabNames(self, LeoLog=None): # Unused: LeoLog 

3422 """Return a list of tab names in the order in which they appear in the QTabbedWidget.""" 

3423 w = self.tabWidget 

3424 return [w.tabText(i) for i in range(w.count())] 

3425 #@+node:ekr.20110605121601.18330: *4* LeoQtLog.numberOfVisibleTabs 

3426 def numberOfVisibleTabs(self): 

3427 return len([val for val in self.contentsDict.values() if val is not None]) 

3428 # **Note**: the base-class version of this uses frameDict. 

3429 #@+node:ekr.20110605121601.18331: *4* LeoQtLog.selectTab & helpers 

3430 def selectTab(self, tabName, createText=True, widget=None, wrap='none'): 

3431 """Create the tab if necessary and make it active.""" 

3432 i = self.findTabIndex(tabName) 

3433 if i is None: 

3434 self.createTab(tabName, widget=widget, wrap=wrap) 

3435 self.finishCreateTab(tabName) 

3436 self.finishSelectTab(tabName) 

3437 #@+node:ekr.20190603064815.1: *5* LeoQtLog.finishCreateTab 

3438 def finishCreateTab(self, tabName): 

3439 """Finish creating the given tab. Do not set focus!""" 

3440 c = self.c 

3441 i = self.findTabIndex(tabName) 

3442 if i is None: 

3443 g.trace('Can not happen', tabName) 

3444 self.tabName = None 

3445 return 

3446 # # #1161. 

3447 if tabName == 'Log': 

3448 wrapper = None 

3449 widget = self.contentsDict.get('Log') 

3450 # a qt_text.QTextEditWrapper 

3451 if widget: 

3452 wrapper = getattr(widget, 'leo_log_wrapper', None) 

3453 if wrapper and isinstance(wrapper, qt_text.QTextEditWrapper): 

3454 self.logCtrl = wrapper 

3455 if not wrapper: 

3456 g.trace('NO LOG WRAPPER') 

3457 if tabName == 'Find': 

3458 # Do *not* set focus here! 

3459 # #1254861: Ctrl-f doesn't ensure find input field visible. 

3460 if c.config.getBool('auto-scroll-find-tab', default=True): 

3461 # This is the cause of unwanted scrolling. 

3462 findbox = c.findCommands.ftm.find_findbox 

3463 if hasattr(widget, 'ensureWidgetVisible'): 

3464 widget.ensureWidgetVisible(findbox) 

3465 else: 

3466 findbox.setFocus() 

3467 if tabName == 'Spell': 

3468 # Set a flag for the spell system. 

3469 widget = self.tabWidget.widget(i) 

3470 self.frameDict['Spell'] = widget 

3471 #@+node:ekr.20190603064816.1: *5* LeoQtLog.finishSelectTab 

3472 def finishSelectTab(self, tabName): 

3473 """Select the proper tab.""" 

3474 w = self.tabWidget 

3475 # Special case for Spell tab. 

3476 if tabName == 'Spell': 

3477 return 

3478 i = self.findTabIndex(tabName) 

3479 if i is None: 

3480 g.trace('can not happen', tabName) 

3481 self.tabName = None 

3482 return 

3483 w.setCurrentIndex(i) 

3484 self.tabName = tabName 

3485 #@-others 

3486#@+node:ekr.20110605121601.18340: ** class LeoQtMenu (LeoMenu) 

3487class LeoQtMenu(leoMenu.LeoMenu): 

3488 

3489 #@+others 

3490 #@+node:ekr.20110605121601.18341: *3* LeoQtMenu.__init__ 

3491 def __init__(self, c, frame, label): 

3492 """ctor for LeoQtMenu class.""" 

3493 assert frame 

3494 assert frame.c 

3495 super().__init__(frame) 

3496 self.leo_menu_label = label.replace('&', '').lower() 

3497 self.frame = frame 

3498 self.c = c 

3499 self.menuBar = c.frame.top.menuBar() 

3500 assert self.menuBar is not None 

3501 # Inject this dict into the commander. 

3502 if not hasattr(c, 'menuAccels'): 

3503 setattr(c, 'menuAccels', {}) 

3504 if 0: 

3505 self.font = c.config.getFontFromParams( 

3506 'menu_text_font_family', 'menu_text_font_size', 

3507 'menu_text_font_slant', 'menu_text_font_weight', 

3508 c.config.defaultMenuFontSize) 

3509 #@+node:ekr.20120306130648.9848: *3* LeoQtMenu.__repr__ 

3510 def __repr__(self): 

3511 return f"<LeoQtMenu: {self.leo_menu_label}>" 

3512 

3513 __str__ = __repr__ 

3514 #@+node:ekr.20110605121601.18342: *3* LeoQtMenu.Tkinter menu bindings 

3515 # See the Tk docs for what these routines are to do 

3516 #@+node:ekr.20110605121601.18343: *4* LeoQtMenu.Methods with Tk spellings 

3517 #@+node:ekr.20110605121601.18344: *5* LeoQtMenu.add_cascade 

3518 def add_cascade(self, parent, label, menu, underline): 

3519 """Wrapper for the Tkinter add_cascade menu method. 

3520 

3521 Adds a submenu to the parent menu, or the menubar.""" 

3522 # menu and parent are a QtMenuWrappers, subclasses of QMenu. 

3523 n = underline 

3524 if -1 < n < len(label): 

3525 label = label[:n] + '&' + label[n:] 

3526 menu.setTitle(label) 

3527 if parent: 

3528 parent.addMenu(menu) # QMenu.addMenu. 

3529 else: 

3530 self.menuBar.addMenu(menu) 

3531 label = label.replace('&', '').lower() 

3532 menu.leo_menu_label = label 

3533 return menu 

3534 #@+node:ekr.20110605121601.18345: *5* LeoQtMenu.add_command (Called by createMenuEntries) 

3535 def add_command(self, **keys): 

3536 """Wrapper for the Tkinter add_command menu method.""" 

3537 # pylint: disable=arguments-differ 

3538 accel = keys.get('accelerator') or '' 

3539 command = keys.get('command') or '' 

3540 commandName = keys.get('commandName') 

3541 label = keys.get('label') 

3542 n = keys.get('underline') 

3543 if n is None: 

3544 n = -1 

3545 menu = keys.get('menu') or self 

3546 if not label: 

3547 return 

3548 if -1 < n < len(label): 

3549 label = label[:n] + '&' + label[n:] 

3550 if accel: 

3551 label = f"{label}\t{accel}" 

3552 action = menu.addAction(label) # type:ignore 

3553 # 2012/01/20: Inject the command name into the action 

3554 # so that it can be enabled/disabled dynamically. 

3555 action.leo_command_name = commandName 

3556 if command: 

3557 

3558 def qt_add_command_callback(checked, label=label, command=command): 

3559 return command() 

3560 

3561 action.triggered.connect(qt_add_command_callback) 

3562 #@+node:ekr.20110605121601.18346: *5* LeoQtMenu.add_separator 

3563 def add_separator(self, menu): 

3564 """Wrapper for the Tkinter add_separator menu method.""" 

3565 if menu: 

3566 action = menu.addSeparator() 

3567 action.leo_menu_label = '*seperator*' 

3568 #@+node:ekr.20110605121601.18347: *5* LeoQtMenu.delete 

3569 def delete(self, menu, realItemName='<no name>'): 

3570 """Wrapper for the Tkinter delete menu method.""" 

3571 # if menu: 

3572 # return menu.delete(realItemName) 

3573 #@+node:ekr.20110605121601.18348: *5* LeoQtMenu.delete_range 

3574 def delete_range(self, menu, n1, n2): 

3575 """Wrapper for the Tkinter delete menu method.""" 

3576 # Menu is a subclass of QMenu and LeoQtMenu. 

3577 for z in menu.actions()[n1:n2]: 

3578 menu.removeAction(z) 

3579 #@+node:ekr.20110605121601.18349: *5* LeoQtMenu.destroy 

3580 def destroy(self, menu): 

3581 """Wrapper for the Tkinter destroy menu method.""" 

3582 # Fixed bug https://bugs.launchpad.net/leo-editor/+bug/1193870 

3583 if menu: 

3584 menu.menuBar.removeAction(menu.menuAction()) 

3585 #@+node:ekr.20110605121601.18350: *5* LeoQtMenu.index 

3586 def index(self, label): 

3587 """Return the index of the menu with the given label.""" 

3588 return 0 

3589 #@+node:ekr.20110605121601.18351: *5* LeoQtMenu.insert 

3590 def insert(self, menuName, position, label, command, underline=None): 

3591 

3592 menu = self.getMenu(menuName) 

3593 if menu and label: 

3594 n = underline or 0 

3595 if -1 > n > len(label): 

3596 label = label[:n] + '&' + label[n:] 

3597 action = menu.addAction(label) 

3598 if command: 

3599 

3600 def insert_callback(checked, label=label, command=command): 

3601 command() 

3602 

3603 action.triggered.connect(insert_callback) 

3604 #@+node:ekr.20110605121601.18352: *5* LeoQtMenu.insert_cascade 

3605 def insert_cascade(self, parent, index, label, menu, underline): 

3606 """Wrapper for the Tkinter insert_cascade menu method.""" 

3607 menu.setTitle(label) 

3608 label.replace('&', '').lower() 

3609 menu.leo_menu_label = label # was leo_label 

3610 if parent: 

3611 parent.addMenu(menu) 

3612 else: 

3613 self.menuBar.addMenu(menu) 

3614 action = menu.menuAction() 

3615 if action: 

3616 action.leo_menu_label = label 

3617 else: 

3618 g.trace('no action for menu', label) 

3619 return menu 

3620 #@+node:ekr.20110605121601.18353: *5* LeoQtMenu.new_menu 

3621 def new_menu(self, parent, tearoff=False, label=''): # label is for debugging. 

3622 """Wrapper for the Tkinter new_menu menu method.""" 

3623 c, leoFrame = self.c, self.frame 

3624 # Parent can be None, in which case it will be added to the menuBar. 

3625 menu = QtMenuWrapper(c, leoFrame, parent, label) 

3626 return menu 

3627 #@+node:ekr.20110605121601.18354: *4* LeoQtMenu.Methods with other spellings 

3628 #@+node:ekr.20110605121601.18355: *5* LeoQtMenu.clearAccel 

3629 def clearAccel(self, menu, name): 

3630 pass 

3631 # if not menu: 

3632 # return 

3633 # realName = self.getRealMenuName(name) 

3634 # realName = realName.replace("&","") 

3635 # menu.entryconfig(realName,accelerator='') 

3636 #@+node:ekr.20110605121601.18356: *5* LeoQtMenu.createMenuBar 

3637 def createMenuBar(self, frame): 

3638 """ 

3639 (LeoQtMenu) Create all top-level menus. 

3640 The menuBar itself has already been created. 

3641 """ 

3642 self.createMenusFromTables() 

3643 # This is LeoMenu.createMenusFromTables. 

3644 #@+node:ekr.20110605121601.18357: *5* LeoQtMenu.createOpenWithMenu 

3645 def createOpenWithMenu(self, parent, label, index, amp_index): 

3646 """ 

3647 Create the File:Open With submenu. 

3648 

3649 This is called from LeoMenu.createOpenWithMenuFromTable. 

3650 """ 

3651 # Use the existing Open With menu if possible. 

3652 menu = self.getMenu('openwith') 

3653 if not menu: 

3654 menu = self.new_menu(parent, tearoff=False, label=label) 

3655 menu.insert_cascade(parent, index, label, menu, underline=amp_index) 

3656 return menu 

3657 #@+node:ekr.20110605121601.18358: *5* LeoQtMenu.disable/enableMenu (not used) 

3658 def disableMenu(self, menu, name): 

3659 self.enableMenu(menu, name, False) 

3660 

3661 def enableMenu(self, menu, name, val): 

3662 """Enable or disable the item in the menu with the given name.""" 

3663 if menu and name: 

3664 val = bool(val) 

3665 for action in menu.actions(): 

3666 s = g.checkUnicode(action.text()).replace('&', '') 

3667 if s.startswith(name): 

3668 action.setEnabled(val) 

3669 break 

3670 #@+node:ekr.20110605121601.18359: *5* LeoQtMenu.getMenuLabel 

3671 def getMenuLabel(self, menu, name): 

3672 """Return the index of the menu item whose name (or offset) is given. 

3673 Return None if there is no such menu item.""" 

3674 # At present, it is valid to always return None. 

3675 #@+node:ekr.20110605121601.18360: *5* LeoQtMenu.setMenuLabel 

3676 def setMenuLabel(self, menu, name, label, underline=-1): 

3677 

3678 def munge(s): 

3679 return (s or '').replace('&', '') 

3680 

3681 # menu is a QtMenuWrapper. 

3682 

3683 if not menu: 

3684 

3685 return 

3686 realName = munge(self.getRealMenuName(name)) 

3687 realLabel = self.getRealMenuName(label) 

3688 for action in menu.actions(): 

3689 s = munge(action.text()) 

3690 s = s.split('\t')[0] 

3691 if s == realName: 

3692 action.setText(realLabel) 

3693 break 

3694 #@+node:ekr.20110605121601.18361: *3* LeoQtMenu.activateMenu & helper 

3695 def activateMenu(self, menuName): 

3696 """Activate the menu with the given name""" 

3697 menu = self.getMenu(menuName) 

3698 # Menu is a QtMenuWrapper, a subclass of both QMenu and LeoQtMenu. 

3699 if menu: 

3700 self.activateAllParentMenus(menu) 

3701 else: 

3702 g.trace(f"No such menu: {menuName}") 

3703 #@+node:ekr.20120922041923.10607: *4* LeoQtMenu.activateAllParentMenus 

3704 def activateAllParentMenus(self, menu): 

3705 """menu is a QtMenuWrapper. Activate it and all parent menus.""" 

3706 parent = menu.parent() 

3707 action = menu.menuAction() 

3708 if action: 

3709 if parent and isinstance(parent, QtWidgets.QMenuBar): 

3710 parent.setActiveAction(action) 

3711 elif parent: 

3712 self.activateAllParentMenus(parent) 

3713 parent.setActiveAction(action) 

3714 else: 

3715 g.trace(f"can not happen: no parent for {menu}") 

3716 else: 

3717 g.trace(f"can not happen: no action for {menu}") 

3718 #@+node:ekr.20120922041923.10613: *3* LeoQtMenu.deactivateMenuBar 

3719 # def deactivateMenuBar (self): 

3720 # """Activate the menu with the given name""" 

3721 # menubar = self.c.frame.top.leo_menubar 

3722 # menubar.setActiveAction(None) 

3723 # menubar.repaint() 

3724 #@+node:ekr.20110605121601.18362: *3* LeoQtMenu.getMacHelpMenu 

3725 def getMacHelpMenu(self, table): 

3726 return None 

3727 #@-others 

3728#@+node:ekr.20110605121601.18363: ** class LeoQTreeWidget (QTreeWidget) 

3729class LeoQTreeWidget(QtWidgets.QTreeWidget): # type:ignore 

3730 

3731 # To do: Generate @auto or @file nodes when appropriate. 

3732 

3733 def __init__(self, c, parent): 

3734 super().__init__(parent) 

3735 self.setAcceptDrops(True) 

3736 enable_drag = c.config.getBool('enable-tree-dragging') 

3737 self.setDragEnabled(bool(enable_drag)) 

3738 self.c = c 

3739 self.was_alt_drag = False 

3740 self.was_control_drag = False 

3741 

3742 def __repr__(self): 

3743 return f"LeoQTreeWidget: {id(self)}" 

3744 

3745 __str__ = __repr__ 

3746 

3747 

3748 def dragMoveEvent(self, ev): # Called during drags. 

3749 pass 

3750 

3751 #@+others 

3752 #@+node:ekr.20111022222228.16980: *3* LeoQTreeWidget: Event handlers 

3753 #@+node:ekr.20110605121601.18364: *4* LeoQTreeWidget.dragEnterEvent & helper 

3754 def dragEnterEvent(self, ev): 

3755 """Export c.p's tree as a Leo mime-data.""" 

3756 c = self.c 

3757 if not ev: 

3758 g.trace('no event!') 

3759 return 

3760 md = ev.mimeData() 

3761 if not md: 

3762 g.trace('No mimeData!') 

3763 return 

3764 c.endEditing() 

3765 # Fix bug 135: cross-file drag and drop is broken. 

3766 # This handler may be called several times for the same drag. 

3767 # Only the first should should set g.app.drag_source. 

3768 if g.app.dragging: 

3769 pass 

3770 else: 

3771 g.app.dragging = True 

3772 g.app.drag_source = c, c.p 

3773 self.setText(md) 

3774 # Always accept the drag, even if we are already dragging. 

3775 ev.accept() 

3776 #@+node:ekr.20110605121601.18384: *5* LeoQTreeWidget.setText 

3777 def setText(self, md): 

3778 c = self.c 

3779 fn = self.fileName() 

3780 s = c.fileCommands.outline_to_clipboard_string() 

3781 md.setText(f"{fn},{s}") 

3782 #@+node:ekr.20110605121601.18365: *4* LeoQTreeWidget.dropEvent & helpers 

3783 def dropEvent(self, ev): 

3784 """Handle a drop event in the QTreeWidget.""" 

3785 if not ev: 

3786 return 

3787 md = ev.mimeData() 

3788 if not md: 

3789 g.trace('no mimeData!') 

3790 return 

3791 try: 

3792 mods = ev.modifiers() if isQt6 else int(ev.keyboardModifiers()) 

3793 self.was_alt_drag = bool(mods & KeyboardModifier.AltModifier) 

3794 self.was_control_drag = bool(mods & KeyboardModifier.ControlModifier) 

3795 except Exception: # Defensive. 

3796 g.es_exception() 

3797 g.app.dragging = False 

3798 return 

3799 c, tree = self.c, self.c.frame.tree 

3800 p = None 

3801 point = ev.position().toPoint() if isQt6 else ev.pos() 

3802 item = self.itemAt(point) 

3803 if item: 

3804 itemHash = tree.itemHash(item) 

3805 p = tree.item2positionDict.get(itemHash) 

3806 if not p: 

3807 # #59: Drop at last node. 

3808 p = c.rootPosition() 

3809 while p.hasNext(): 

3810 p.moveToNext() 

3811 formats = set(str(f) for f in md.formats()) 

3812 ev.setDropAction(DropAction.IgnoreAction) 

3813 ev.accept() 

3814 hookres = g.doHook("outlinedrop", c=c, p=p, dropevent=ev, formats=formats) 

3815 if hookres: 

3816 # A plugin handled the drop. 

3817 pass 

3818 else: 

3819 if md.hasUrls(): 

3820 self.urlDrop(md, p) 

3821 else: 

3822 self.nodeDrop(md, p) 

3823 g.app.dragging = False 

3824 #@+node:ekr.20110605121601.18366: *5* LeoQTreeWidget.nodeDrop & helpers 

3825 def nodeDrop(self, md, p): 

3826 """ 

3827 Handle a drop event when not md.urls(). 

3828 This will happen when we drop an outline node. 

3829 We get the copied text from md.text(). 

3830 """ 

3831 c = self.c 

3832 fn, s = self.parseText(md) 

3833 if not s or not fn: 

3834 return 

3835 if fn == self.fileName(): 

3836 if p and p == c.p: 

3837 pass 

3838 elif g.os_path_exists(fn): 

3839 self.intraFileDrop(fn, c.p, p) 

3840 else: 

3841 self.interFileDrop(fn, p, s) 

3842 #@+node:ekr.20110605121601.18367: *6* LeoQTreeWidget.interFileDrop 

3843 def interFileDrop(self, fn, p, s): 

3844 """Paste the mime data after (or as the first child of) p.""" 

3845 c = self.c 

3846 u = c.undoer 

3847 undoType = 'Drag Outline' 

3848 isLeo = g.match(s, 0, g.app.prolog_prefix_string) 

3849 if not isLeo: 

3850 return 

3851 c.selectPosition(p) 

3852 pasted = c.fileCommands.getLeoOutlineFromClipboard(s) 

3853 # Paste the node after the presently selected node. 

3854 if not pasted: 

3855 return 

3856 if c.config.getBool('inter-outline-drag-moves'): 

3857 src_c, src_p = g.app.drag_source 

3858 if src_p.hasVisNext(src_c): 

3859 nxt = src_p.getVisNext(src_c).v 

3860 elif src_p.hasVisBack(src_c): 

3861 nxt = src_p.getVisBack(src_c).v 

3862 else: 

3863 nxt = None 

3864 if nxt is not None: 

3865 src_p.doDelete() 

3866 src_c.selectPosition(src_c.vnode2position(nxt)) 

3867 src_c.setChanged() 

3868 src_c.redraw() 

3869 else: 

3870 g.es("Can't move last node out of outline") 

3871 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

3872 c.validateOutline() 

3873 c.selectPosition(pasted) 

3874 pasted.setDirty() 

3875 # 2011/02/27: Fix bug 690467. 

3876 c.setChanged() 

3877 back = pasted.back() 

3878 if back and back.isExpanded(): 

3879 pasted.moveToNthChildOf(back, 0) 

3880 # c.setRootPosition(c.findRootPosition(pasted)) 

3881 u.afterInsertNode(pasted, undoType, undoData) 

3882 c.redraw(pasted) 

3883 c.recolor() 

3884 #@+node:ekr.20110605121601.18368: *6* LeoQTreeWidget.intraFileDrop 

3885 def intraFileDrop(self, fn, p1, p2): 

3886 """Move p1 after (or as the first child of) p2.""" 

3887 as_child = self.was_alt_drag 

3888 cloneDrag = self.was_control_drag 

3889 c = self.c 

3890 u = c.undoer 

3891 c.selectPosition(p1) 

3892 if as_child or p2.hasChildren() and p2.isExpanded(): 

3893 # Attempt to move p1 to the first child of p2. 

3894 # parent = p2 

3895 

3896 def move(p1, p2): 

3897 if cloneDrag: 

3898 p1 = p1.clone() 

3899 p1.moveToNthChildOf(p2, 0) 

3900 p1.setDirty() 

3901 return p1 

3902 

3903 else: 

3904 # Attempt to move p1 after p2. 

3905 # parent = p2.parent() 

3906 

3907 def move(p1, p2): 

3908 if cloneDrag: 

3909 p1 = p1.clone() 

3910 p1.moveAfter(p2) 

3911 p1.setDirty() 

3912 return p1 

3913 

3914 ok = ( 

3915 # 2011/10/03: Major bug fix. 

3916 c.checkDrag(p1, p2) and 

3917 c.checkMoveWithParentWithWarning(p1, p2, True)) 

3918 if ok: 

3919 undoData = u.beforeMoveNode(p1) 

3920 p1.setDirty() 

3921 p1 = move(p1, p2) 

3922 if cloneDrag: 

3923 # Set dirty bits for ancestors of *all* cloned nodes. 

3924 for z in p1.self_and_subtree(): 

3925 z.setDirty() 

3926 c.setChanged() 

3927 u.afterMoveNode(p1, 'Drag', undoData) 

3928 if (not as_child or 

3929 p2.isExpanded() or 

3930 c.config.getBool("drag-alt-drag-expands") is not False 

3931 ): 

3932 c.redraw(p1) 

3933 else: 

3934 c.redraw(p2) 

3935 #@+node:ekr.20110605121601.18383: *6* LeoQTreeWidget.parseText 

3936 def parseText(self, md): 

3937 """Parse md.text() into (fn,s)""" 

3938 fn = '' 

3939 s = md.text() 

3940 if s: 

3941 i = s.find(',') 

3942 if i == -1: 

3943 pass 

3944 else: 

3945 fn = s[:i] 

3946 s = s[i + 1 :] 

3947 return fn, s 

3948 #@+node:ekr.20110605121601.18369: *5* LeoQTreeWidget.urlDrop & helpers 

3949 def urlDrop(self, md, p): 

3950 """Handle a drop when md.urls().""" 

3951 c, u, undoType = self.c, self.c.undoer, 'Drag Urls' 

3952 urls = md.urls() 

3953 if not urls: 

3954 return 

3955 c.undoer.beforeChangeGroup(c.p, undoType) 

3956 changed = False 

3957 for z in urls: 

3958 url = QtCore.QUrl(z) 

3959 scheme = url.scheme() 

3960 if scheme == 'file': 

3961 changed |= self.doFileUrl(p, url) 

3962 elif scheme in ('http',): # 'ftp','mailto', 

3963 changed |= self.doHttpUrl(p, url) 

3964 if changed: 

3965 c.setChanged() 

3966 u.afterChangeGroup(c.p, undoType, reportFlag=False) 

3967 c.redraw() 

3968 #@+node:ekr.20110605121601.18370: *6* LeoQTreeWidget.doFileUrl & helper 

3969 def doFileUrl(self, p, url): 

3970 """Read the file given by the url and put it in the outline.""" 

3971 # 2014/06/06: Work around a possible bug in QUrl. 

3972 # fn = str(url.path()) # Fails. 

3973 e = sys.getfilesystemencoding() 

3974 fn = g.toUnicode(url.path(), encoding=e) 

3975 if sys.platform.lower().startswith('win'): 

3976 if fn.startswith('/'): 

3977 fn = fn[1:] 

3978 if os.path.isdir(fn): 

3979 # Just insert an @path directory. 

3980 self.doPathUrlHelper(fn, p) 

3981 return True 

3982 if g.os_path_exists(fn): 

3983 try: 

3984 f = open(fn, 'rb') # 2012/03/09: use 'rb' 

3985 except IOError: 

3986 f = None 

3987 if f: 

3988 b = f.read() 

3989 s = g.toUnicode(b) 

3990 f.close() 

3991 return self.doFileUrlHelper(fn, p, s) 

3992 nodeLink = p.get_UNL() 

3993 g.es_print(f"not found: {fn}", nodeLink=nodeLink) 

3994 return False 

3995 #@+node:ekr.20110605121601.18371: *7* LeoQTreeWidget.doFileUrlHelper & helper 

3996 def doFileUrlHelper(self, fn, p, s): 

3997 """ 

3998 Insert s in an @file, @auto or @edit node after p. 

3999 If fn is a .leo file, insert a node containing its top-level nodes as children. 

4000 """ 

4001 c = self.c 

4002 if self.isLeoFile(fn, s) and not self.was_control_drag: 

4003 g.openWithFileName(fn, old_c=c) 

4004 return False # Don't set the changed marker in the original file. 

4005 u, undoType = c.undoer, 'Drag File' 

4006 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

4007 if p.hasChildren() and p.isExpanded(): 

4008 p2 = p.insertAsNthChild(0) 

4009 parent = p 

4010 elif p.h.startswith('@path '): 

4011 # #60: create relative paths & urls when dragging files. 

4012 p2 = p.insertAsNthChild(0) 

4013 p.expand() 

4014 parent = p 

4015 else: 

4016 p2 = p.insertAfter() 

4017 parent = p.parent() 

4018 # #60: create relative paths & urls when dragging files. 

4019 aList = g.get_directives_dict_list(parent) 

4020 path = g.scanAtPathDirectives(c, aList) 

4021 if path: 

4022 fn = os.path.relpath(fn, path) 

4023 self.createAtFileNode(fn, p2, s) 

4024 u.afterInsertNode(p2, undoType, undoData) 

4025 c.selectPosition(p2) 

4026 return True # The original .leo file has changed. 

4027 #@+node:ekr.20110605121601.18372: *8* LeoQTreeWidget.createAtFileNode & helpers (QTreeWidget) 

4028 def createAtFileNode(self, fn, p, s): 

4029 """ 

4030 Set p's headline, body text and possibly descendants 

4031 based on the file's name fn and contents s. 

4032 

4033 If the file is an thin file, create an @file tree. 

4034 Othewise, create an @auto tree. 

4035 If all else fails, create an @edit node. 

4036 

4037 Give a warning if a node with the same headline already exists. 

4038 """ 

4039 c = self.c 

4040 c.init_error_dialogs() 

4041 if self.isLeoFile(fn, s): 

4042 self.createLeoFileTree(fn, p) 

4043 elif self.isThinFile(fn, s): 

4044 self.createAtFileTree(fn, p, s) 

4045 elif self.isAutoFile(fn): 

4046 self.createAtAutoTree(fn, p) 

4047 elif self.isBinaryFile(fn): 

4048 self.createUrlForBinaryFile(fn, p) 

4049 else: 

4050 self.createAtEditNode(fn, p) 

4051 self.warnIfNodeExists(p) 

4052 c.raise_error_dialogs(kind='read') 

4053 #@+node:ekr.20110605121601.18373: *9* LeoQTreeWidget.createAtAutoTree (QTreeWidget) 

4054 def createAtAutoTree(self, fn, p): 

4055 """ 

4056 Make p an @auto node and create the tree using s, the file's contents. 

4057 """ 

4058 c = self.c 

4059 at = c.atFileCommands 

4060 p.h = f"@auto {fn}" 

4061 at.readOneAtAutoNode(p) 

4062 # No error recovery should be needed here. 

4063 p.clearDirty() # Don't automatically rewrite this node. 

4064 #@+node:ekr.20110605121601.18374: *9* LeoQTreeWidget.createAtEditNode 

4065 def createAtEditNode(self, fn, p): 

4066 c = self.c 

4067 at = c.atFileCommands 

4068 # Use the full @edit logic, so dragging will be 

4069 # exactly the same as reading. 

4070 at.readOneAtEditNode(fn, p) 

4071 p.h = f"@edit {fn}" 

4072 p.clearDirty() # Don't automatically rewrite this node. 

4073 #@+node:ekr.20110605121601.18375: *9* LeoQTreeWidget.createAtFileTree 

4074 def createAtFileTree(self, fn, p, s): 

4075 """Make p an @file node and create the tree using s, the file's contents.""" 

4076 c = self.c 

4077 at = c.atFileCommands 

4078 p.h = f"@file {fn}" 

4079 # Read the file into p. 

4080 ok = at.read(root=p.copy(), fromString=s) 

4081 if not ok: 

4082 g.error('Error reading', fn) 

4083 p.b = '' # Safe: will not cause a write later. 

4084 p.clearDirty() # Don't automatically rewrite this node. 

4085 #@+node:ekr.20141007223054.18004: *9* LeoQTreeWidget.createLeoFileTree 

4086 def createLeoFileTree(self, fn, p): 

4087 """Copy all nodes from fn, a .leo file, to the children of p.""" 

4088 c = self.c 

4089 p.h = f"From {g.shortFileName(fn)}" 

4090 c.selectPosition(p) 

4091 # Create a dummy first child of p. 

4092 dummy_p = p.insertAsNthChild(0) 

4093 c.selectPosition(dummy_p) 

4094 c2 = g.openWithFileName(fn, old_c=c, gui=g.app.nullGui) 

4095 for p2 in c2.rootPosition().self_and_siblings(): 

4096 c2.selectPosition(p2) 

4097 s = c2.fileCommands.outline_to_clipboard_string() 

4098 # Paste the outline after the selected node. 

4099 c.fileCommands.getLeoOutlineFromClipboard(s) 

4100 dummy_p.doDelete() 

4101 c.selectPosition(p) 

4102 p.v.contract() 

4103 c2.close() 

4104 g.app.forgetOpenFile(c2.fileName()) # Necessary. 

4105 #@+node:ekr.20120309075544.9882: *9* LeoQTreeWidget.createUrlForBinaryFile 

4106 def createUrlForBinaryFile(self, fn, p): 

4107 # Fix bug 1028986: create relative urls when dragging binary files to Leo. 

4108 c = self.c 

4109 base_fn = g.os_path_normcase(g.os_path_abspath(c.mFileName)) 

4110 abs_fn = g.os_path_normcase(g.os_path_abspath(fn)) 

4111 prefix = os.path.commonprefix([abs_fn, base_fn]) 

4112 if prefix and len(prefix) > 3: # Don't just strip off c:\. 

4113 p.h = abs_fn[len(prefix) :].strip() 

4114 else: 

4115 p.h = f"@url file://{fn}" 

4116 #@+node:ekr.20110605121601.18377: *9* LeoQTreeWidget.isAutoFile (LeoQTreeWidget) 

4117 def isAutoFile(self, fn): 

4118 """Return true if fn (a file name) can be parsed with an @auto parser.""" 

4119 d = g.app.classDispatchDict 

4120 junk, ext = g.os_path_splitext(fn) 

4121 return d.get(ext) 

4122 #@+node:ekr.20120309075544.9881: *9* LeoQTreeWidget.isBinaryFile 

4123 def isBinaryFile(self, fn): 

4124 # The default for unknown files is True. Not great, but safe. 

4125 junk, ext = g.os_path_splitext(fn) 

4126 ext = ext.lower() 

4127 if not ext: 

4128 val = False 

4129 elif ext.startswith('~'): 

4130 val = False 

4131 elif ext in ('.css', '.htm', '.html', '.leo', '.txt'): 

4132 val = False 

4133 # elif ext in ('.bmp','gif','ico',): 

4134 # val = True 

4135 else: 

4136 keys = (z.lower() for z in g.app.extension_dict) 

4137 val = ext not in keys 

4138 return val 

4139 #@+node:ekr.20141007223054.18003: *9* LeoQTreeWidget.isLeoFile 

4140 def isLeoFile(self, fn, s): 

4141 """Return true if fn (a file name) represents an entire .leo file.""" 

4142 return fn.endswith('.leo') and s.startswith(g.app.prolog_prefix_string) 

4143 #@+node:ekr.20110605121601.18376: *9* LeoQTreeWidget.isThinFile 

4144 def isThinFile(self, fn, s): 

4145 """ 

4146 Return true if the file whose contents is s 

4147 was created from an @thin or @file tree. 

4148 """ 

4149 c = self.c 

4150 at = c.atFileCommands 

4151 # Skip lines before the @+leo line. 

4152 i = s.find('@+leo') 

4153 if i == -1: 

4154 return False 

4155 # Like at.isFileLike. 

4156 j, k = g.getLine(s, i) 

4157 line = s[j:k] 

4158 valid, new_df, start, end, isThin = at.parseLeoSentinel(line) 

4159 return valid and new_df and isThin 

4160 #@+node:ekr.20110605121601.18378: *9* LeoQTreeWidget.warnIfNodeExists 

4161 def warnIfNodeExists(self, p): 

4162 c = self.c 

4163 h = p.h 

4164 for p2 in c.all_unique_positions(): 

4165 if p2.h == h and p2 != p: 

4166 g.warning('Warning: duplicate node:', h) 

4167 break 

4168 #@+node:ekr.20110605121601.18379: *7* LeoQTreeWidget.doPathUrlHelper 

4169 def doPathUrlHelper(self, fn, p): 

4170 """Insert fn as an @path node after p.""" 

4171 c = self.c 

4172 u, undoType = c.undoer, 'Drag Directory' 

4173 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

4174 if p.hasChildren() and p.isExpanded(): 

4175 p2 = p.insertAsNthChild(0) 

4176 else: 

4177 p2 = p.insertAfter() 

4178 p2.h = '@path ' + fn 

4179 u.afterInsertNode(p2, undoType, undoData) 

4180 c.selectPosition(p2) 

4181 #@+node:ekr.20110605121601.18380: *6* LeoQTreeWidget.doHttpUrl 

4182 def doHttpUrl(self, p, url): 

4183 """Insert the url in an @url node after p.""" 

4184 c = self.c 

4185 u = c.undoer 

4186 undoType = 'Drag Url' 

4187 s = str(url.toString()).strip() 

4188 if not s: 

4189 return False 

4190 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[]) 

4191 if p.hasChildren() and p.isExpanded(): 

4192 p2 = p.insertAsNthChild(0) 

4193 else: 

4194 p2 = p.insertAfter() 

4195 p2.h = '@url' 

4196 p2.b = s 

4197 p2.clearDirty() # Don't automatically rewrite this node. 

4198 u.afterInsertNode(p2, undoType, undoData) 

4199 return True 

4200 #@+node:ekr.20110605121601.18381: *3* LeoQTreeWidget: utils 

4201 #@+node:ekr.20110605121601.18382: *4* LeoQTreeWidget.dump 

4202 def dump(self, ev, p, tag): 

4203 if ev: 

4204 md = ev.mimeData() 

4205 s = g.checkUnicode(md.text(), encoding='utf-8') 

4206 g.trace('md.text:', repr(s) if len(s) < 100 else len(s)) 

4207 for url in md.urls() or []: 

4208 g.trace(' url:', url) 

4209 g.trace(' url.fn:', url.toLocalFile()) 

4210 g.trace('url.text:', url.toString()) 

4211 else: 

4212 g.trace('', tag, '** no event!') 

4213 #@+node:ekr.20141007223054.18002: *4* LeoQTreeWidget.fileName 

4214 def fileName(self): 

4215 """Return the commander's filename.""" 

4216 return self.c.fileName() or '<unsaved file>' 

4217 #@-others 

4218#@+node:ekr.20110605121601.18385: ** class LeoQtSpellTab 

4219class LeoQtSpellTab: 

4220 #@+others 

4221 #@+node:ekr.20110605121601.18386: *3* LeoQtSpellTab.__init__ 

4222 def __init__(self, c, handler, tabName): 

4223 """Ctor for LeoQtSpellTab class.""" 

4224 self.c = c 

4225 top = c.frame.top 

4226 self.handler = handler 

4227 # hack: 

4228 handler.workCtrl = leoFrame.StringTextWrapper(c, 'spell-workctrl') 

4229 self.tabName = tabName 

4230 if hasattr(top, 'leo_spell_label'): 

4231 self.wordLabel = top.leo_spell_label 

4232 self.listBox = top.leo_spell_listBox 

4233 self.fillbox([]) 

4234 else: 

4235 self.handler.loaded = False 

4236 #@+node:ekr.20110605121601.18389: *3* Event handlers 

4237 #@+node:ekr.20110605121601.18390: *4* onAddButton 

4238 def onAddButton(self): 

4239 """Handle a click in the Add button in the Check Spelling dialog.""" 

4240 self.handler.add() 

4241 #@+node:ekr.20110605121601.18391: *4* onChangeButton & onChangeThenFindButton 

4242 def onChangeButton(self, event=None): 

4243 """Handle a click in the Change button in the Spell tab.""" 

4244 state = self.updateButtons() 

4245 if state: 

4246 self.handler.change() 

4247 self.updateButtons() 

4248 

4249 def onChangeThenFindButton(self, event=None): 

4250 """Handle a click in the "Change, Find" button in the Spell tab.""" 

4251 state = self.updateButtons() 

4252 if state: 

4253 self.handler.change() 

4254 if self.handler.change(): 

4255 self.handler.find() 

4256 self.updateButtons() 

4257 #@+node:ekr.20110605121601.18392: *4* onFindButton 

4258 def onFindButton(self): 

4259 """Handle a click in the Find button in the Spell tab.""" 

4260 c = self.c 

4261 self.handler.find() 

4262 self.updateButtons() 

4263 c.invalidateFocus() 

4264 c.bodyWantsFocus() 

4265 #@+node:ekr.20110605121601.18393: *4* onHideButton 

4266 def onHideButton(self): 

4267 """Handle a click in the Hide button in the Spell tab.""" 

4268 self.handler.hide() 

4269 #@+node:ekr.20110605121601.18394: *4* onIgnoreButton 

4270 def onIgnoreButton(self, event=None): 

4271 """Handle a click in the Ignore button in the Check Spelling dialog.""" 

4272 self.handler.ignore() 

4273 #@+node:ekr.20110605121601.18395: *4* onMap 

4274 def onMap(self, event=None): 

4275 """Respond to a Tk <Map> event.""" 

4276 self.update(show=False, fill=False) 

4277 #@+node:ekr.20110605121601.18396: *4* onSelectListBox 

4278 def onSelectListBox(self, event=None): 

4279 """Respond to a click in the selection listBox.""" 

4280 c = self.c 

4281 self.updateButtons() 

4282 c.bodyWantsFocus() 

4283 #@+node:ekr.20110605121601.18397: *3* Helpers 

4284 #@+node:ekr.20110605121601.18398: *4* bringToFront (LeoQtSpellTab) 

4285 def bringToFront(self): 

4286 self.c.frame.log.selectTab('Spell') 

4287 #@+node:ekr.20110605121601.18399: *4* fillbox (LeoQtSpellTab) 

4288 def fillbox(self, alts, word=None): 

4289 """Update the suggestions listBox in the Check Spelling dialog.""" 

4290 self.suggestions = alts 

4291 if not word: 

4292 word = "" 

4293 self.wordLabel.setText("Suggestions for: " + word) 

4294 self.listBox.clear() 

4295 if self.suggestions: 

4296 self.listBox.addItems(self.suggestions) 

4297 self.listBox.setCurrentRow(0) 

4298 #@+node:ekr.20110605121601.18400: *4* getSuggestion (LeoQtSpellTab) 

4299 def getSuggestion(self): 

4300 """Return the selected suggestion from the listBox.""" 

4301 idx = self.listBox.currentRow() 

4302 value = self.suggestions[idx] 

4303 return value 

4304 #@+node:ekr.20141113094129.13: *4* setFocus (LeoQtSpellTab) 

4305 def setFocus(self): 

4306 """Actually put focus in the tab.""" 

4307 # Not a great idea: there is no indication of focus. 

4308 c = self.c 

4309 if c.frame and c.frame.top and hasattr(c.frame.top, 'spellFrame'): 

4310 w = self.c.frame.top.spellFrame 

4311 c.widgetWantsFocus(w) 

4312 #@+node:ekr.20110605121601.18401: *4* update (LeoQtSpellTab) 

4313 def update(self, show=True, fill=False): 

4314 """Update the Spell Check dialog.""" 

4315 c = self.c 

4316 if fill: 

4317 self.fillbox([]) 

4318 self.updateButtons() 

4319 if show: 

4320 self.bringToFront() 

4321 c.bodyWantsFocus() 

4322 #@+node:ekr.20110605121601.18402: *4* updateButtons (spellTab) 

4323 def updateButtons(self): 

4324 """Enable or disable buttons in the Check Spelling dialog.""" 

4325 c = self.c 

4326 top, w = c.frame.top, c.frame.body.wrapper 

4327 state = self.suggestions and w.hasSelection() 

4328 top.leo_spell_btn_Change.setDisabled(not state) 

4329 top.leo_spell_btn_FindChange.setDisabled(not state) 

4330 return state 

4331 #@-others 

4332#@+node:ekr.20110605121601.18438: ** class LeoQtTreeTab 

4333class LeoQtTreeTab: 

4334 """ 

4335 A class representing a so-called tree-tab. 

4336 

4337 Actually, it represents a combo box 

4338 """ 

4339 #@+others 

4340 #@+node:ekr.20110605121601.18439: *3* Birth & death 

4341 #@+node:ekr.20110605121601.18440: *4* ctor (LeoQtTreeTab) 

4342 def __init__(self, c, iconBar): 

4343 """Ctor for LeoQtTreeTab class.""" 

4344 

4345 self.c = c 

4346 self.cc = c.chapterController 

4347 assert self.cc 

4348 self.iconBar = iconBar 

4349 self.lockout = False # True: do not redraw. 

4350 self.tabNames = [] 

4351 # The list of tab names. Changes when tabs are renamed. 

4352 self.w = None # The QComboBox 

4353 # self.reloadSettings() 

4354 self.createControl() 

4355 #@+node:ekr.20110605121601.18441: *4* tt.createControl (defines class LeoQComboBox) 

4356 def createControl(self): 

4357 

4358 

4359 class LeoQComboBox(QtWidgets.QComboBox): # type:ignore 

4360 """Create a subclass in order to handle focusInEvents.""" 

4361 

4362 def __init__(self, tt): 

4363 self.leo_tt = tt 

4364 super().__init__() 

4365 # Fix #458: Chapters drop-down list is not automatically resized. 

4366 self.setSizeAdjustPolicy(SizeAdjustPolicy.AdjustToContents) 

4367 

4368 def focusInEvent(self, event): 

4369 self.leo_tt.setNames() 

4370 QtWidgets.QComboBox.focusInEvent(self, event) # Call the base class 

4371 

4372 tt = self 

4373 frame = QtWidgets.QLabel('Chapters: ') 

4374 tt.iconBar.addWidget(frame) 

4375 tt.w = w = LeoQComboBox(tt) 

4376 tt.setNames() 

4377 tt.iconBar.addWidget(w) 

4378 

4379 def onIndexChanged(s, tt=tt): 

4380 if isinstance(s, int): 

4381 s = '' if s == -1 else tt.w.currentText() 

4382 else: # s is the tab name. 

4383 pass 

4384 if s and not tt.cc.selectChapterLockout: 

4385 tt.selectTab(s) 

4386 

4387 # A change: the argument could now be an int instead of a string. 

4388 w.currentIndexChanged.connect(onIndexChanged) 

4389 #@+node:ekr.20110605121601.18443: *3* tt.createTab 

4390 def createTab(self, tabName, select=True): 

4391 """LeoQtTreeTab.""" 

4392 tt = self 

4393 # Avoid a glitch during initing. 

4394 if tabName != 'main' and tabName not in tt.tabNames: 

4395 tt.tabNames.append(tabName) 

4396 tt.setNames() 

4397 #@+node:ekr.20110605121601.18444: *3* tt.destroyTab 

4398 def destroyTab(self, tabName): 

4399 """LeoQtTreeTab.""" 

4400 tt = self 

4401 if tabName in tt.tabNames: 

4402 tt.tabNames.remove(tabName) 

4403 tt.setNames() 

4404 #@+node:ekr.20110605121601.18445: *3* tt.selectTab 

4405 def selectTab(self, tabName): 

4406 """LeoQtTreeTab.""" 

4407 tt, c, cc = self, self.c, self.cc 

4408 exists = tabName in self.tabNames 

4409 c.treeWantsFocusNow() 

4410 # Fix #969. Somehow this is important. 

4411 if not exists: 

4412 tt.createTab(tabName) # Calls tt.setNames() 

4413 if tt.lockout: 

4414 return 

4415 cc.selectChapterByName(tabName) 

4416 c.redraw() 

4417 c.outerUpdate() 

4418 #@+node:ekr.20110605121601.18446: *3* tt.setTabLabel 

4419 def setTabLabel(self, tabName): 

4420 """LeoQtTreeTab.""" 

4421 w = self.w 

4422 i = w.findText(tabName) 

4423 if i > -1: 

4424 w.setCurrentIndex(i) 

4425 #@+node:ekr.20110605121601.18447: *3* tt.setNames 

4426 def setNames(self): 

4427 """LeoQtTreeTab: Recreate the list of items.""" 

4428 w = self.w 

4429 names = self.cc.setAllChapterNames() 

4430 w.clear() 

4431 w.insertItems(0, names) 

4432 #@-others 

4433#@+node:ekr.20110605121601.18448: ** class LeoTabbedTopLevel (LeoBaseTabWidget) 

4434class LeoTabbedTopLevel(LeoBaseTabWidget): 

4435 """ Toplevel frame for tabbed ui """ 

4436 

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

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

4439 ## middle click close on tabs -- JMP 20140505 

4440 self.setMovable(False) 

4441 tb = QtTabBarWrapper(self) 

4442 self.setTabBar(tb) 

4443#@+node:peckj.20140505102552.10377: ** class QtTabBarWrapper (QTabBar) 

4444class QtTabBarWrapper(QtWidgets.QTabBar): # type:ignore 

4445 #@+others 

4446 #@+node:peckj.20140516114832.10108: *3* __init__ 

4447 def __init__(self, parent=None): 

4448 super().__init__(parent) 

4449 self.setMovable(True) 

4450 #@+node:peckj.20140516114832.10109: *3* mouseReleaseEvent (QtTabBarWrapper) 

4451 def mouseReleaseEvent(self, event): 

4452 # middle click close on tabs -- JMP 20140505 

4453 # closes Launchpad bug: https://bugs.launchpad.net/leo-editor/+bug/1183528 

4454 if event.button() == MouseButton.MiddleButton: 

4455 self.tabCloseRequested.emit(self.tabAt(event.pos())) 

4456 QtWidgets.QTabBar.mouseReleaseEvent(self, event) 

4457 #@-others 

4458#@+node:ekr.20110605121601.18458: ** class QtMenuWrapper (LeoQtMenu,QMenu) 

4459class QtMenuWrapper(LeoQtMenu, QtWidgets.QMenu): # type:ignore 

4460 #@+others 

4461 #@+node:ekr.20110605121601.18459: *3* ctor and __repr__(QtMenuWrapper) 

4462 def __init__(self, c, frame, parent, label): 

4463 """ctor for QtMenuWrapper class.""" 

4464 assert c 

4465 assert frame 

4466 if parent is None: 

4467 parent = c.frame.top.menuBar() 

4468 # 

4469 # For reasons unknown, the calls must be in this order. 

4470 # Presumably, the order of base classes also matters(!) 

4471 LeoQtMenu.__init__(self, c, frame, label) 

4472 QtWidgets.QMenu.__init__(self, parent) 

4473 label = label.replace('&', '').lower() 

4474 self.leo_menu_label = label 

4475 action = self.menuAction() 

4476 if action: 

4477 action.leo_menu_label = label 

4478 self.aboutToShow.connect(self.onAboutToShow) 

4479 

4480 def __repr__(self): 

4481 return f"<QtMenuWrapper {self.leo_menu_label}>" 

4482 #@+node:ekr.20110605121601.18460: *3* onAboutToShow & helpers (QtMenuWrapper) 

4483 def onAboutToShow(self, *args, **keys): 

4484 

4485 name = self.leo_menu_label 

4486 if not name: 

4487 return 

4488 for action in self.actions(): 

4489 commandName = hasattr(action, 'leo_command_name') and action.leo_command_name 

4490 if commandName: 

4491 self.leo_update_shortcut(action, commandName) 

4492 self.leo_enable_menu_item(action, commandName) 

4493 self.leo_update_menu_label(action, commandName) 

4494 #@+node:ekr.20120120095156.10261: *4* leo_enable_menu_item 

4495 def leo_enable_menu_item(self, action, commandName): 

4496 func = self.c.frame.menu.enable_dict.get(commandName) 

4497 if action and func: 

4498 val = func() 

4499 action.setEnabled(bool(val)) 

4500 #@+node:ekr.20120124115444.10190: *4* leo_update_menu_label 

4501 def leo_update_menu_label(self, action, commandName): 

4502 c = self.c 

4503 if action and commandName == 'mark': 

4504 action.setText('UnMark' if c.p.isMarked() else 'Mark') 

4505 self.leo_update_shortcut(action, commandName) 

4506 # Set the proper shortcut. 

4507 #@+node:ekr.20120120095156.10260: *4* leo_update_shortcut 

4508 def leo_update_shortcut(self, action, commandName): 

4509 

4510 c, k = self.c, self.c.k 

4511 if action: 

4512 s = action.text() 

4513 parts = s.split('\t') 

4514 if len(parts) >= 2: 

4515 s = parts[0] 

4516 key, aList = c.config.getShortcut(commandName) 

4517 if aList: 

4518 result = [] 

4519 for bi in aList: 

4520 # Don't show mode-related bindings. 

4521 if not bi.isModeBinding(): 

4522 accel = k.prettyPrintKey(bi.stroke) 

4523 result.append(accel) 

4524 # Break here if we want to show only one accerator. 

4525 action.setText(f"{s}\t{', '.join(result)}") 

4526 else: 

4527 action.setText(s) 

4528 else: 

4529 g.trace(f"can not happen: no action for {commandName}") 

4530 #@-others 

4531#@+node:ekr.20110605121601.18461: ** class QtSearchWidget 

4532class QtSearchWidget: 

4533 """A dummy widget class to pass to Leo's core find code.""" 

4534 

4535 def __init__(self): 

4536 self.insertPoint = 0 

4537 self.selection = 0, 0 

4538 self.wrapper = self 

4539 self.body = self 

4540 self.text = None 

4541#@+node:ekr.20110605121601.18464: ** class TabbedFrameFactory 

4542class TabbedFrameFactory: 

4543 """ 

4544 'Toplevel' frame builder for tabbed toplevel interface 

4545 

4546 This causes Leo to maintain only one toplevel window, 

4547 with multiple tabs for documents 

4548 """ 

4549 #@+others 

4550 #@+node:ekr.20110605121601.18465: *3* frameFactory.__init__ & __repr__ 

4551 def __init__(self): 

4552 # will be created when first frame appears 

4553 # DynamicWindow => Leo frame map 

4554 self.alwaysShowTabs = True 

4555 # Set to true to workaround a problem 

4556 # setting the window title when tabs are shown. 

4557 self.leoFrames = {} 

4558 # Keys are DynamicWindows, values are frames. 

4559 self.masterFrame = None 

4560 self.createTabCommands() 

4561 #@+node:ekr.20110605121601.18466: *3* frameFactory.createFrame (changed, makes dw) 

4562 def createFrame(self, leoFrame): 

4563 

4564 c = leoFrame.c 

4565 tabw = self.masterFrame 

4566 dw = DynamicWindow(c, tabw) 

4567 self.leoFrames[dw] = leoFrame 

4568 # Shorten the title. 

4569 title = os.path.basename(c.mFileName) if c.mFileName else leoFrame.title 

4570 tip = leoFrame.title 

4571 dw.setWindowTitle(tip) 

4572 idx = tabw.addTab(dw, title) 

4573 if tip: 

4574 tabw.setTabToolTip(idx, tip) 

4575 dw.construct(master=tabw) 

4576 tabw.setCurrentIndex(idx) 

4577 g.app.gui.setFilter(c, dw, dw, tag='tabbed-frame') 

4578 # 

4579 # Work around the problem with missing dirty indicator 

4580 # by always showing the tab. 

4581 tabw.tabBar().setVisible(self.alwaysShowTabs or tabw.count() > 1) 

4582 tabw.setTabsClosable(c.config.getBool('outline-tabs-show-close', True)) 

4583 if not g.unitTesting: 

4584 # #1327: Must always do this. 

4585 # 2021/09/12: but not for new unit tests! 

4586 dw.show() 

4587 tabw.show() 

4588 return dw 

4589 #@+node:ekr.20110605121601.18468: *3* frameFactory.createMaster 

4590 def createMaster(self): 

4591 

4592 window = self.masterFrame = LeoTabbedTopLevel(factory=self) 

4593 tabbar = window.tabBar() 

4594 g.app.gui.attachLeoIcon(window) 

4595 try: 

4596 tabbar.setTabsClosable(True) 

4597 tabbar.tabCloseRequested.connect(self.slotCloseRequest) 

4598 except AttributeError: 

4599 pass # Qt 4.4 does not support setTabsClosable 

4600 window.currentChanged.connect(self.slotCurrentChanged) 

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

4602 g.trace( 

4603 f"minimized: {g.app.start_minimized}, " 

4604 f"maximized: {g.app.start_maximized}, " 

4605 f"fullscreen: {g.app.start_fullscreen}") 

4606 # 

4607 # #1189: We *can* (and should) minimize here, to eliminate flash. 

4608 if g.app.start_minimized: 

4609 window.showMinimized() 

4610 #@+node:ekr.20110605121601.18472: *3* frameFactory.createTabCommands 

4611 def detachTab(self, wdg): 

4612 """ Detach specified tab as individual toplevel window """ 

4613 del self.leoFrames[wdg] 

4614 wdg.setParent(None) 

4615 wdg.show() 

4616 

4617 def createTabCommands(self): 

4618 #@+<< Commands for tabs >> 

4619 #@+node:ekr.20110605121601.18473: *4* << Commands for tabs >> 

4620 @g.command('tab-detach') 

4621 def tab_detach(event): 

4622 """ Detach current tab from tab bar """ 

4623 if len(self.leoFrames) < 2: 

4624 g.es_print_error("Can't detach last tab") 

4625 return 

4626 c = event['c'] 

4627 f = c.frame 

4628 tabwidget = g.app.gui.frameFactory.masterFrame 

4629 tabwidget.detach(tabwidget.indexOf(f.top)) 

4630 f.top.setWindowTitle(f.title + ' [D]') 

4631 

4632 # this is actually not tab-specific, move elsewhere? 

4633 

4634 @g.command('close-others') 

4635 def close_others(event): 

4636 """Close all windows except the present window.""" 

4637 myc = event['c'] 

4638 for c in g.app.commanders(): 

4639 if c is not myc: 

4640 c.close() 

4641 

4642 def tab_cycle(offset): 

4643 

4644 tabw = self.masterFrame 

4645 cur = tabw.currentIndex() 

4646 count = tabw.count() 

4647 # g.es("cur: %s, count: %s, offset: %s" % (cur,count,offset)) 

4648 cur += offset 

4649 if cur < 0: 

4650 cur = count - 1 

4651 elif cur >= count: 

4652 cur = 0 

4653 tabw.setCurrentIndex(cur) 

4654 self.focusCurrentBody() 

4655 

4656 @g.command('tab-cycle-next') 

4657 def tab_cycle_next(event): 

4658 """ Cycle to next tab """ 

4659 tab_cycle(1) 

4660 

4661 @g.command('tab-cycle-previous') 

4662 def tab_cycle_previous(event): 

4663 """ Cycle to next tab """ 

4664 tab_cycle(-1) 

4665 #@-<< Commands for tabs >> 

4666 #@+node:ekr.20110605121601.18467: *3* frameFactory.deleteFrame 

4667 def deleteFrame(self, wdg): 

4668 

4669 if not wdg: 

4670 return 

4671 if wdg not in self.leoFrames: 

4672 # probably detached tab 

4673 self.masterFrame.delete(wdg) 

4674 return 

4675 tabw = self.masterFrame 

4676 idx = tabw.indexOf(wdg) 

4677 tabw.removeTab(idx) 

4678 del self.leoFrames[wdg] 

4679 wdg2 = tabw.currentWidget() 

4680 if wdg2: 

4681 g.app.selectLeoWindow(wdg2.leo_c) 

4682 tabw.tabBar().setVisible(self.alwaysShowTabs or tabw.count() > 1) 

4683 #@+node:ekr.20110605121601.18471: *3* frameFactory.focusCurrentBody 

4684 def focusCurrentBody(self): 

4685 """ Focus body control of current tab """ 

4686 tabw = self.masterFrame 

4687 w = tabw.currentWidget() 

4688 w.setFocus() 

4689 f = self.leoFrames[w] 

4690 c = f.c 

4691 c.bodyWantsFocusNow() 

4692 # Fix bug 690260: correct the log. 

4693 g.app.log = f.log 

4694 #@+node:ekr.20110605121601.18469: *3* frameFactory.setTabForCommander 

4695 def setTabForCommander(self, c): 

4696 tabw = self.masterFrame # a QTabWidget 

4697 for dw in self.leoFrames: # A dict whose keys are DynamicWindows. 

4698 if dw.leo_c == c: 

4699 for i in range(tabw.count()): 

4700 if tabw.widget(i) == dw: 

4701 tabw.setCurrentIndex(i) 

4702 break 

4703 break 

4704 #@+node:ekr.20110605121601.18470: *3* frameFactory.signal handlers 

4705 def slotCloseRequest(self, idx): 

4706 

4707 tabw = self.masterFrame 

4708 w = tabw.widget(idx) 

4709 f = self.leoFrames[w] 

4710 c = f.c 

4711 c.close(new_c=None) 

4712 # 2012/03/04: Don't set the frame here. 

4713 # Wait until the next slotCurrentChanged event. 

4714 # This keeps the log and the QTabbedWidget in sync. 

4715 

4716 def slotCurrentChanged(self, idx): 

4717 # Two events are generated, one for the tab losing focus, 

4718 # and another event for the tab gaining focus. 

4719 tabw = self.masterFrame 

4720 w = tabw.widget(idx) 

4721 f = self.leoFrames.get(w) 

4722 if not f: 

4723 return 

4724 tabw.setWindowTitle(f.title) 

4725 # Don't do this: it would break --minimize. 

4726 # g.app.selectLeoWindow(f.c) 

4727 # Fix bug 690260: correct the log. 

4728 g.app.log = f.log 

4729 # Redraw the tab. 

4730 c = f.c 

4731 if c: 

4732 c.redraw() 

4733 #@-others 

4734#@-others 

4735#@@language python 

4736#@@tabwidth -4 

4737#@@pagewidth 70 

4738#@-leo