Coverage for C:\Repos\leo-editor\leo\plugins\qt_frame.py: 37%
3115 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20140907123524.18774: * @file ../plugins/qt_frame.py
4#@@first
5"""Leo's qt frame classes."""
6#@+<< imports qt_frame.py >>
7#@+node:ekr.20110605121601.18003: ** << imports qt_frame.py >>
8from collections import defaultdict
9import os
10import platform
11import string
12import sys
13import time
14from typing import Any, Callable, Dict, List, Optional, Tuple
15from typing import TYPE_CHECKING
16from leo.core import leoGlobals as g
17from leo.core import leoColor
18from leo.core import leoColorizer
19from leo.core import leoFrame
20from leo.core import leoGui
21from leo.core import leoMenu
22from leo.commands import gotoCommands
23from leo.core.leoQt import isQt5, isQt6, QtCore, QtGui, QtWidgets
24from leo.core.leoQt import QAction, Qsci
25from leo.core.leoQt import Alignment, ContextMenuPolicy, DropAction, FocusReason, KeyboardModifier
26from leo.core.leoQt import MoveOperation, Orientation, MouseButton
27from leo.core.leoQt import Policy, ScrollBarPolicy, SelectionBehavior, SelectionMode, SizeAdjustPolicy
28from leo.core.leoQt import Shadow, Shape, Style
29from leo.core.leoQt import TextInteractionFlag, ToolBarArea, Type, Weight, WindowState, WrapMode
30from leo.plugins import qt_events
31from leo.plugins import qt_text
32from leo.plugins import qt_tree
33from leo.plugins.mod_scripting import build_rclick_tree
34from leo.plugins.nested_splitter import NestedSplitter
35#@-<< imports qt_frame.py >>
36#@+<< type aliases qt_frame.py >>
37#@+node:ekr.20220415080427.1: ** << type aliases qt_frame.py >>
38if TYPE_CHECKING: # Always False at runtime.
39 from leo.core.leoCommands import Commands as Cmdr
40 from leo.core.leoNodes import Position as Pos
41else:
42 Cmdr = Any
43 Pos = Any
44 QComboBox = Any
45Event = Any
46Widget = Any
47Wrapper = Any
48#@-<< type aliases qt_frame.py >>
49#@+others
50#@+node:ekr.20200303082457.1: ** top-level commands (qt_frame.py)
51#@+node:ekr.20200303082511.6: *3* 'contract-body-pane' & 'expand-outline-pane'
52@g.command('contract-body-pane')
53@g.command('expand-outline-pane')
54def contractBodyPane(event: Event) -> None:
55 """Contract the body pane. Expand the outline/log splitter."""
56 c = event.get('c')
57 if not c:
58 return
59 f = c.frame
60 r = min(1.0, f.ratio + 0.1)
61 f.divideLeoSplitter1(r)
63expandOutlinePane = contractBodyPane
64#@+node:ekr.20200303084048.1: *3* 'contract-log-pane'
65@g.command('contract-log-pane')
66def contractLogPane(event: Event) -> None:
67 """Contract the log pane. Expand the outline pane."""
68 c = event.get('c')
69 if not c:
70 return
71 f = c.frame
72 r = min(1.0, f.secondary_ratio + 0.1)
73 f.divideLeoSplitter2(r)
74#@+node:ekr.20200303084225.1: *3* 'contract-outline-pane' & 'expand-body-pane'
75@g.command('contract-outline-pane')
76@g.command('expand-body-pane')
77def contractOutlinePane(event: Event) -> None:
78 """Contract the outline pane. Expand the body pane."""
79 c = event.get('c')
80 if not c:
81 return
82 f = c.frame
83 r = max(0.0, f.ratio - 0.1)
84 f.divideLeoSplitter1(r)
86expandBodyPane = contractOutlinePane
87#@+node:ekr.20200303084226.1: *3* 'expand-log-pane'
88@g.command('expand-log-pane')
89def expandLogPane(event: Event) -> None:
90 """Expand the log pane. Contract the outline pane."""
91 c = event.get('c')
92 if not c:
93 return
94 f = c.frame
95 r = max(0.0, f.secondary_ratio - 0.1)
96 f.divideLeoSplitter2(r)
97#@+node:ekr.20200303084610.1: *3* 'hide-body-pane'
98@g.command('hide-body-pane')
99def hideBodyPane(event: Event) -> None:
100 """Hide the body pane. Fully expand the outline/log splitter."""
101 c = event.get('c')
102 if not c:
103 return
104 c.frame.divideLeoSplitter1(1.0)
105#@+node:ekr.20200303084625.1: *3* 'hide-log-pane'
106@g.command('hide-log-pane')
107def hideLogPane(event: Event) -> None:
108 """Hide the log pane. Fully expand the outline pane."""
109 c = event.get('c')
110 if not c:
111 return
112 c.frame.divideLeoSplitter2(1.0)
113#@+node:ekr.20200303082511.7: *3* 'hide-outline-pane'
114@g.command('hide-outline-pane')
115def hideOutlinePane(event: Event) -> None:
116 """Hide the outline/log splitter. Fully expand the body pane."""
117 c = event.get('c')
118 if not c:
119 return
120 c.frame.divideLeoSplitter1(0.0)
122#@+node:ekr.20210228142208.1: ** decorators (qt_frame.py)
123def body_cmd(name: str) -> Callable:
124 """Command decorator for the LeoQtBody class."""
125 return g.new_cmd_decorator(name, ['c', 'frame', 'body'])
127def frame_cmd(name: str) -> Callable:
128 """Command decorator for the LeoQtFrame class."""
129 return g.new_cmd_decorator(name, ['c', 'frame',])
131def log_cmd(name: str) -> Callable:
132 """Command decorator for the LeoQtLog class."""
133 return g.new_cmd_decorator(name, ['c', 'frame', 'log'])
134#@+node:ekr.20110605121601.18137: ** class DynamicWindow (QMainWindow)
135class DynamicWindow(QtWidgets.QMainWindow): # type:ignore
136 """
137 A class representing all parts of the main Qt window.
139 c.frame.top is a DynamicWindow.
140 c.frame.top.leo_master is a LeoTabbedTopLevel.
141 c.frame.top.parent() is a QStackedWidget()
143 All leoQtX classes use the ivars of this Window class to
144 support operations requested by Leo's core.
145 """
146 #@+others
147 #@+node:ekr.20110605121601.18138: *3* dw.ctor & reloadSettings
148 def __init__(self, c: Cmdr, parent: Widget=None) -> None:
149 """Ctor for the DynamicWindow class. The main window is c.frame.top"""
150 # Called from LeoQtFrame.finishCreate.
151 # parent is a LeoTabbedTopLevel.
152 super().__init__(parent)
153 self.leo_c = c
154 self.leo_master = None # Set in construct.
155 self.leo_menubar = None # Set in createMenuBar.
156 c._style_deltas = defaultdict(lambda: 0) # for adjusting styles dynamically
157 self.reloadSettings()
159 def reloadSettings(self) -> None:
160 c = self.leo_c
161 c.registerReloadSettings(self)
162 self.bigTree = c.config.getBool('big-outline-pane')
163 self.show_iconbar = c.config.getBool('show-iconbar', default=True)
164 self.toolbar_orientation = c.config.getString('qt-toolbar-location') or ''
165 self.use_gutter = c.config.getBool('use-gutter', default=False)
166 if getattr(self, 'iconBar', None):
167 if self.show_iconbar:
168 self.iconBar.show()
169 else:
170 self.iconBar.hide()
171 #@+node:ekr.20110605121601.18172: *3* dw.do_leo_spell_btn_*
172 def doSpellBtn(self, btn: Any) -> None:
173 """Execute btn, a button handler."""
174 # Make *sure* this never crashes.
175 try:
176 tab = self.leo_c.spellCommands.handler.tab
177 button = getattr(tab, btn)
178 button()
179 except Exception:
180 g.es_exception()
182 def do_leo_spell_btn_Add(self) -> None:
183 self.doSpellBtn('onAddButton')
185 def do_leo_spell_btn_Change(self) -> None:
186 self.doSpellBtn('onChangeButton')
188 def do_leo_spell_btn_Find(self) -> None:
189 self.doSpellBtn('onFindButton')
191 def do_leo_spell_btn_FindChange(self) -> None:
192 self.doSpellBtn('onChangeThenFindButton')
194 def do_leo_spell_btn_Hide(self) -> None:
195 self.doSpellBtn('onHideButton')
197 def do_leo_spell_btn_Ignore(self) -> None:
198 self.doSpellBtn('onIgnoreButton')
199 #@+node:ekr.20110605121601.18140: *3* dw.closeEvent
200 def closeEvent(self, event: Event) -> None:
201 """Handle a close event in the Leo window."""
202 c = self.leo_c
203 if not c.exists:
204 # Fixes double-prompt bug on Linux.
205 event.accept()
206 return
207 if c.inCommand:
208 c.requestCloseWindow = True
209 return
210 ok = g.app.closeLeoWindow(c.frame)
211 if ok:
212 event.accept()
213 else:
214 event.ignore()
215 #@+node:ekr.20110605121601.18139: *3* dw.construct & helpers
216 def construct(self, master: Any=None) -> None:
217 """ Factor 'heavy duty' code out from the DynamicWindow ctor """
218 c = self.leo_c
219 self.leo_master = master # A LeoTabbedTopLevel or None for non-tabbed windows.
220 self.useScintilla = c.config.getBool('qt-use-scintilla')
221 self.reloadSettings()
222 main_splitter, secondary_splitter = self.createMainWindow()
223 self.iconBar = self.addToolBar("IconBar")
224 self.iconBar.setObjectName('icon-bar') # Required for QMainWindow.saveState().
225 self.set_icon_bar_orientation(c)
226 # #266 A setting to hide the icon bar.
227 # Calling reloadSettings again would also work.
228 if not self.show_iconbar:
229 self.iconBar.hide()
230 self.leo_menubar = self.menuBar()
231 self.statusBar = QtWidgets.QStatusBar()
232 self.setStatusBar(self.statusBar)
233 orientation = c.config.getString('initial-split-orientation')
234 self.setSplitDirection(main_splitter, secondary_splitter, orientation)
235 if hasattr(c, 'styleSheetManager'):
236 c.styleSheetManager.set_style_sheets(top=self, all=True)
237 #@+node:ekr.20140915062551.19519: *4* dw.set_icon_bar_orientation
238 def set_icon_bar_orientation(self, c: Cmdr) -> None:
239 """Set the orientation of the icon bar based on settings."""
240 d = {
241 'bottom': ToolBarArea.BottomToolBarArea,
242 'left': ToolBarArea.LeftToolBarArea,
243 'right': ToolBarArea.RightToolBarArea,
244 'top': ToolBarArea.TopToolBarArea,
245 }
246 where = self.toolbar_orientation
247 if not where:
248 where = 'top'
249 where = d.get(where.lower())
250 if where:
251 self.addToolBar(where, self.iconBar)
252 #@+node:ekr.20110605121601.18141: *3* dw.createMainWindow & helpers
253 def createMainWindow(self) -> Tuple[Widget, Widget]:
254 """
255 Create the component ivars of the main window.
256 Copied/adapted from qt_main.py.
257 Called instead of uic.loadUi(ui_description_file, self)
258 """
259 self.setMainWindowOptions()
260 # Legacy code: will not go away.
261 self.createCentralWidget()
262 # Create .verticalLayout
263 main_splitter, secondary_splitter = self.createMainLayout(self.centralwidget)
264 if self.bigTree:
265 # Top pane contains only outline. Bottom pane contains body and log panes.
266 self.createBodyPane(secondary_splitter)
267 self.createLogPane(secondary_splitter)
268 treeFrame = self.createOutlinePane(main_splitter)
269 main_splitter.addWidget(treeFrame)
270 main_splitter.addWidget(secondary_splitter)
271 else:
272 # Top pane contains outline and log panes.
273 self.createOutlinePane(secondary_splitter)
274 self.createLogPane(secondary_splitter)
275 self.createBodyPane(main_splitter)
276 self.createMiniBuffer(self.centralwidget)
277 self.createMenuBar()
278 self.createStatusBar(self)
279 # Signals...
280 QtCore.QMetaObject.connectSlotsByName(self)
281 return main_splitter, secondary_splitter
282 #@+node:ekr.20110605121601.18142: *4* dw.top-level
283 #@+node:ekr.20190118150859.10: *5* dw.addNewEditor
284 def addNewEditor(self, name: str) -> Tuple[Widget, Wrapper]:
285 """Create a new body editor."""
286 c, p = self.leo_c, self.leo_c.p
287 body = c.frame.body
288 assert isinstance(body, LeoQtBody), repr(body)
289 # Step 1: create the editor.
290 parent_frame = c.frame.top.leo_body_inner_frame
291 widget = qt_text.LeoQTextBrowser(parent_frame, c, self)
292 widget.setObjectName('richTextEdit') # Will be changed later.
293 wrapper = qt_text.QTextEditWrapper(widget, name='body', c=c)
294 self.packLabel(widget)
295 # Step 2: inject ivars, set bindings, etc.
296 inner_frame = c.frame.top.leo_body_inner_frame # Inject ivars *here*.
297 body.injectIvars(inner_frame, name, p, wrapper)
298 body.updateInjectedIvars(widget, p)
299 wrapper.setAllText(p.b)
300 wrapper.see(0)
301 c.k.completeAllBindingsForWidget(wrapper)
302 if isinstance(widget, QtWidgets.QTextEdit):
303 colorizer = leoColorizer.make_colorizer(c, widget)
304 colorizer.highlighter.setDocument(widget.document())
305 else:
306 # Scintilla only.
307 body.recolorWidget(p, wrapper)
308 return parent_frame, wrapper
309 #@+node:ekr.20110605121601.18143: *5* dw.createBodyPane
310 def createBodyPane(self, parent: Widget) -> Widget:
311 """
312 Create the *pane* for the body, not the actual QTextBrowser.
313 """
314 c = self.leo_c
315 #
316 # Create widgets.
317 bodyFrame = self.createFrame(parent, 'bodyFrame')
318 innerFrame = self.createFrame(bodyFrame, 'innerBodyFrame')
319 sw = self.createStackedWidget(innerFrame, 'bodyStackedWidget',
320 hPolicy=Policy.Expanding, vPolicy=Policy.Expanding)
321 page2 = QtWidgets.QWidget()
322 self.setName(page2, 'bodyPage2')
323 body = self.createText(page2, 'richTextEdit') # A LeoQTextBrowser
324 #
325 # Pack.
326 vLayout = self.createVLayout(page2, 'bodyVLayout', spacing=0)
327 grid = self.createGrid(bodyFrame, 'bodyGrid')
328 innerGrid = self.createGrid(innerFrame, 'bodyInnerGrid')
329 if self.use_gutter:
330 lineWidget = qt_text.LeoLineTextWidget(c, body)
331 vLayout.addWidget(lineWidget)
332 else:
333 vLayout.addWidget(body)
334 sw.addWidget(page2)
335 innerGrid.addWidget(sw, 0, 0, 1, 1)
336 grid.addWidget(innerFrame, 0, 0, 1, 1)
337 self.verticalLayout.addWidget(parent)
338 #
339 # Official ivars
340 self.text_page = page2
341 self.stackedWidget = sw # used by LeoQtBody
342 self.richTextEdit = body
343 self.leo_body_frame = bodyFrame
344 self.leo_body_inner_frame = innerFrame
345 return bodyFrame
347 #@+node:ekr.20110605121601.18144: *5* dw.createCentralWidget
348 def createCentralWidget(self) -> Widget:
349 """Create the central widget."""
350 dw = self
351 w = QtWidgets.QWidget(dw)
352 w.setObjectName("centralwidget")
353 dw.setCentralWidget(w)
354 # Official ivars.
355 self.centralwidget = w
356 return w
357 #@+node:ekr.20110605121601.18145: *5* dw.createLogPane & helpers
358 def createLogPane(self, parent: Any) -> None:
359 """Create all parts of Leo's log pane."""
360 c = self.leo_c
361 #
362 # Create the log frame.
363 logFrame = self.createFrame(parent, 'logFrame', vPolicy=Policy.Minimum)
364 innerFrame = self.createFrame(logFrame, 'logInnerFrame',
365 hPolicy=Policy.Preferred, vPolicy=Policy.Expanding)
366 tabWidget = self.createTabWidget(innerFrame, 'logTabWidget')
367 #
368 # Pack.
369 innerGrid = self.createGrid(innerFrame, 'logInnerGrid')
370 innerGrid.addWidget(tabWidget, 0, 0, 1, 1)
371 outerGrid = self.createGrid(logFrame, 'logGrid')
372 outerGrid.addWidget(innerFrame, 0, 0, 1, 1)
373 #
374 # Create the Find tab, embedded in a QScrollArea.
375 findScrollArea = QtWidgets.QScrollArea()
376 findScrollArea.setObjectName('findScrollArea')
377 # Find tab.
378 findTab = QtWidgets.QWidget()
379 findTab.setObjectName('findTab')
380 #
381 # #516 and #1507: Create a Find tab unless we are using a dialog.
382 #
383 # Careful: @bool minibuffer-ding-mode overrides @bool use-find-dialog.
384 use_minibuffer = c.config.getBool('minibuffer-find-mode', default=False)
385 use_dialog = c.config.getBool('use-find-dialog', default=False)
386 if use_minibuffer or not use_dialog:
387 tabWidget.addTab(findScrollArea, 'Find')
388 # Complete the Find tab in LeoFind.finishCreate.
389 self.findScrollArea = findScrollArea
390 self.findTab = findTab
391 #
392 # Spell tab.
393 spellTab = QtWidgets.QWidget()
394 spellTab.setObjectName('spellTab')
395 tabWidget.addTab(spellTab, 'Spell')
396 self.createSpellTab(spellTab)
397 tabWidget.setCurrentIndex(1)
398 #
399 # Official ivars
400 self.tabWidget = tabWidget # Used by LeoQtLog.
401 #@+node:ekr.20131118172620.16858: *6* dw.finishCreateLogPane
402 def finishCreateLogPane(self) -> None:
403 """It's useful to create this late, because c.config is now valid."""
404 assert self.findTab
405 self.createFindTab(self.findTab, self.findScrollArea)
406 self.findScrollArea.setWidget(self.findTab)
407 #@+node:ekr.20110605121601.18146: *5* dw.createMainLayout
408 def createMainLayout(self, parent: Widget) -> Tuple[Widget, Widget]:
409 """Create the layout for Leo's main window."""
410 # c = self.leo_c
411 vLayout = self.createVLayout(parent, 'mainVLayout', margin=3)
412 main_splitter = NestedSplitter(parent)
413 main_splitter.setObjectName('main_splitter')
414 main_splitter.setOrientation(Orientation.Vertical)
415 secondary_splitter = NestedSplitter(main_splitter)
416 secondary_splitter.setObjectName('secondary_splitter')
417 secondary_splitter.setOrientation(Orientation.Horizontal)
418 # Official ivar:
419 self.verticalLayout = vLayout
420 self.setSizePolicy(secondary_splitter)
421 self.verticalLayout.addWidget(main_splitter)
422 return main_splitter, secondary_splitter
423 #@+node:ekr.20110605121601.18147: *5* dw.createMenuBar
424 def createMenuBar(self) -> None:
425 """Create Leo's menu bar."""
426 dw = self
427 w = QtWidgets.QMenuBar(dw)
428 w.setNativeMenuBar(platform.system() == 'Darwin')
429 w.setGeometry(QtCore.QRect(0, 0, 957, 22))
430 w.setObjectName("menubar")
431 dw.setMenuBar(w)
432 # Official ivars.
433 self.leo_menubar = w
434 #@+node:ekr.20110605121601.18148: *5* dw.createMiniBuffer (class VisLineEdit)
435 def createMiniBuffer(self, parent: Widget) -> Widget:
436 """Create the widgets for Leo's minibuffer area."""
437 # Create widgets.
438 frame = self.createFrame(parent, 'minibufferFrame',
439 hPolicy=Policy.MinimumExpanding, vPolicy=Policy.Fixed)
440 frame.setMinimumSize(QtCore.QSize(100, 0))
441 label = self.createLabel(frame, 'minibufferLabel', 'Minibuffer:')
444 class VisLineEdit(QtWidgets.QLineEdit): # type:ignore
445 """In case user has hidden minibuffer with gui-minibuffer-hide"""
447 def focusInEvent(self, event: Event) -> None:
448 self.parent().show()
449 super().focusInEvent(event) # Call the base class method.
451 def focusOutEvent(self, event: Event) -> None:
452 self.store_selection()
453 super().focusOutEvent(event)
455 def restore_selection(self) -> None:
456 w = self
457 i, j, ins = self._sel_and_insert
458 if i == j:
459 w.setCursorPosition(i)
460 else:
461 length = j - i
462 # Set selection is a QLineEditMethod
463 if ins < j:
464 w.setSelection(j, -length)
465 else:
466 w.setSelection(i, length)
468 def store_selection(self) -> None:
469 w = self
470 ins = w.cursorPosition()
471 if w.hasSelectedText():
472 i = w.selectionStart()
473 s = w.selectedText()
474 j = i + len(s)
475 else:
476 i = j = ins
477 w._sel_and_insert = (i, j, ins)
479 lineEdit = VisLineEdit(frame)
480 lineEdit._sel_and_insert = (0, 0, 0)
481 lineEdit.setObjectName('lineEdit') # name important.
482 # Pack.
483 hLayout = self.createHLayout(frame, 'minibufferHLayout', spacing=4)
484 hLayout.setContentsMargins(3, 2, 2, 0)
485 hLayout.addWidget(label)
486 hLayout.addWidget(lineEdit)
487 self.verticalLayout.addWidget(frame)
488 # Transfers focus request from label to lineEdit.
489 label.setBuddy(lineEdit)
490 #
491 # Official ivars.
492 self.lineEdit = lineEdit
493 # self.leo_minibuffer_frame = frame
494 # self.leo_minibuffer_layout = layout
495 return frame
496 #@+node:ekr.20110605121601.18149: *5* dw.createOutlinePane
497 def createOutlinePane(self, parent: Widget) -> Widget:
498 """Create the widgets and ivars for Leo's outline."""
499 # Create widgets.
500 treeFrame = self.createFrame(parent, 'outlineFrame', vPolicy=Policy.Expanding)
501 innerFrame = self.createFrame(treeFrame, 'outlineInnerFrame', hPolicy=Policy.Preferred)
502 treeWidget = self.createTreeWidget(innerFrame, 'treeWidget')
503 grid = self.createGrid(treeFrame, 'outlineGrid')
504 grid.addWidget(innerFrame, 0, 0, 1, 1)
505 innerGrid = self.createGrid(innerFrame, 'outlineInnerGrid')
506 innerGrid.addWidget(treeWidget, 0, 0, 1, 1)
507 # Official ivars...
508 self.treeWidget = treeWidget
509 return treeFrame
510 #@+node:ekr.20110605121601.18150: *5* dw.createStatusBar
511 def createStatusBar(self, parent: Widget) -> None:
512 """Create the widgets and ivars for Leo's status area."""
513 w = QtWidgets.QStatusBar(parent)
514 w.setObjectName("statusbar")
515 parent.setStatusBar(w)
516 # Official ivars.
517 self.statusBar = w
518 #@+node:ekr.20110605121601.18212: *5* dw.packLabel
519 def packLabel(self, w: Wrapper, n: int=None) -> None:
520 """
521 Pack w into the body frame's QVGridLayout.
523 The type of w does not affect the following code. In fact, w is a
524 QTextBrowser possibly packed inside a LeoLineTextWidget.
525 """
526 c = self.leo_c
527 #
528 # Reuse the grid layout in the body frame.
529 grid = self.leo_body_frame.layout()
530 # Pack the label and the text widget.
531 label = QtWidgets.QLineEdit(None)
532 label.setObjectName('editorLabel')
533 label.setText(c.p.h)
534 if n is None:
535 n = c.frame.body.numberOfEditors
536 n = max(0, n - 1)
537 # mypy error: grid is a QGridLayout, not a QLayout.
538 grid.addWidget(label, 0, n) # type:ignore
539 grid.addWidget(w, 1, n) # type:ignore
540 grid.setRowStretch(0, 0) # Don't grow the label vertically.
541 grid.setRowStretch(1, 1) # Give row 1 as much as vertical room as possible.
542 # Inject the ivar.
543 w.leo_label = label
544 #@+node:ekr.20110605121601.18151: *5* dw.setMainWindowOptions
545 def setMainWindowOptions(self) -> None:
546 """Set default options for Leo's main window."""
547 dw = self
548 dw.setObjectName("MainWindow")
549 dw.resize(691, 635)
550 #@+node:ekr.20110605121601.18152: *4* dw.widgets
551 #@+node:ekr.20110605121601.18153: *5* dw.createButton
552 def createButton(self, parent: Widget, name: str, label: str) -> Widget:
553 w = QtWidgets.QPushButton(parent)
554 w.setObjectName(name)
555 w.setText(self.tr(label))
556 return w
557 #@+node:ekr.20110605121601.18154: *5* dw.createCheckBox
558 def createCheckBox(self, parent: Widget, name: str, label: str) -> Widget:
559 w = QtWidgets.QCheckBox(parent)
560 self.setName(w, name)
561 w.setText(self.tr(label))
562 return w
563 #@+node:ekr.20110605121601.18155: *5* dw.createFrame
564 def createFrame(
565 self,
566 parent: Widget,
567 name: str,
568 hPolicy: Any=None,
569 vPolicy: Any=None,
570 lineWidth: int=1,
571 shadow: Any=None,
572 shape: Any=None,
573 ) -> Widget:
574 """Create a Qt Frame."""
575 if shadow is None:
576 shadow = Shadow.Plain
577 if shape is None:
578 shape = Shape.NoFrame
579 #
580 w = QtWidgets.QFrame(parent)
581 self.setSizePolicy(w, kind1=hPolicy, kind2=vPolicy)
582 w.setFrameShape(shape)
583 w.setFrameShadow(shadow)
584 w.setLineWidth(lineWidth)
585 self.setName(w, name)
586 return w
587 #@+node:ekr.20110605121601.18156: *5* dw.createGrid
588 def createGrid(self, parent: Widget, name: str, margin: int=0, spacing: int=0) -> Widget:
589 w = QtWidgets.QGridLayout(parent)
590 w.setContentsMargins(QtCore.QMargins(margin, margin, margin, margin))
591 w.setSpacing(spacing)
592 self.setName(w, name)
593 return w
594 #@+node:ekr.20110605121601.18157: *5* dw.createHLayout & createVLayout
595 def createHLayout(self, parent: Widget, name: str, margin: int=0, spacing: int=0) -> Any:
596 hLayout = QtWidgets.QHBoxLayout(parent)
597 hLayout.setSpacing(spacing)
598 hLayout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
599 self.setName(hLayout, name)
600 return hLayout
602 def createVLayout(self, parent: Widget, name: str, margin: int=0, spacing: int=0) -> Any:
603 vLayout = QtWidgets.QVBoxLayout(parent)
604 vLayout.setSpacing(spacing)
605 vLayout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
606 self.setName(vLayout, name)
607 return vLayout
608 #@+node:ekr.20110605121601.18158: *5* dw.createLabel
609 def createLabel(self, parent: Widget, name: str, label: str) -> Widget:
610 w = QtWidgets.QLabel(parent)
611 self.setName(w, name)
612 w.setText(self.tr(label))
613 return w
614 #@+node:ekr.20110605121601.18159: *5* dw.createLineEdit
615 def createLineEdit(self, parent: Widget, name: str, disabled: bool=True) -> Widget:
617 w = QtWidgets.QLineEdit(parent)
618 w.setObjectName(name)
619 w.leo_disabled = disabled # Inject the ivar.
620 return w
621 #@+node:ekr.20110605121601.18160: *5* dw.createRadioButton
622 def createRadioButton(self, parent: Widget, name: str, label: str) -> Widget:
623 w = QtWidgets.QRadioButton(parent)
624 self.setName(w, name)
625 w.setText(self.tr(label))
626 return w
627 #@+node:ekr.20110605121601.18161: *5* dw.createStackedWidget
628 def createStackedWidget(
629 self,
630 parent: Widget,
631 name: str,
632 lineWidth: int=1,
633 hPolicy: Any=None,
634 vPolicy: Any=None,
635 ) -> Widget:
636 w = QtWidgets.QStackedWidget(parent)
637 self.setSizePolicy(w, kind1=hPolicy, kind2=vPolicy)
638 w.setAcceptDrops(True)
639 w.setLineWidth(1)
640 self.setName(w, name)
641 return w
642 #@+node:ekr.20110605121601.18162: *5* dw.createTabWidget
643 def createTabWidget(self, parent: Widget, name: str, hPolicy: Any=None, vPolicy: Any=None) -> Widget:
644 w = QtWidgets.QTabWidget(parent)
645 # tb = w.tabBar()
646 # tb.setTabsClosable(True)
647 self.setSizePolicy(w, kind1=hPolicy, kind2=vPolicy)
648 self.setName(w, name)
649 return w
650 #@+node:ekr.20110605121601.18163: *5* dw.createText (creates QTextBrowser)
651 def createText(
652 self,
653 parent: Widget,
654 name: str,
655 lineWidth: int=0,
656 shadow: Any=None,
657 shape: Any=None,
658 ) -> Widget:
659 # Create a text widget.
660 c = self.leo_c
661 if name == 'richTextEdit' and self.useScintilla and Qsci:
662 # Do this in finishCreate, when c.frame.body exists.
663 w = Qsci.QsciScintilla(parent)
664 self.scintilla_widget = w
665 else:
666 if shadow is None:
667 shadow = Shadow.Plain
668 if shape is None:
669 shape = Shape.NoFrame
670 #
671 w = qt_text.LeoQTextBrowser(parent, c, None)
672 w.setFrameShape(shape)
673 w.setFrameShadow(shadow)
674 w.setLineWidth(lineWidth)
675 self.setName(w, name)
676 return w
677 #@+node:ekr.20110605121601.18164: *5* dw.createTreeWidget
678 def createTreeWidget(self, parent: Widget, name: str) -> Widget:
679 c = self.leo_c
680 w = LeoQTreeWidget(c, parent)
681 self.setSizePolicy(w)
682 # 12/01/07: add new config setting.
683 multiple_selection = c.config.getBool('qt-tree-multiple-selection', default=True)
684 if multiple_selection:
685 w.setSelectionMode(SelectionMode.ExtendedSelection)
686 w.setSelectionBehavior(SelectionBehavior.SelectRows)
687 else:
688 w.setSelectionMode(SelectionMode.SingleSelection)
689 w.setSelectionBehavior(SelectionBehavior.SelectItems)
690 w.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu)
691 w.setHeaderHidden(False)
692 self.setName(w, name)
693 return w
694 #@+node:ekr.20110605121601.18165: *4* dw.log tabs
695 #@+node:ekr.20110605121601.18167: *5* dw.createSpellTab
696 def createSpellTab(self, parent: Widget) -> None:
697 # dw = self
698 vLayout = self.createVLayout(parent, 'spellVLayout', margin=2)
699 spellFrame = self.createFrame(parent, 'spellFrame')
700 vLayout2 = self.createVLayout(spellFrame, 'spellVLayout')
701 grid = self.createGrid(None, 'spellGrid', spacing=2)
702 table = (
703 ('Add', 'Add', 2, 1),
704 ('Find', 'Find', 2, 0),
705 ('Change', 'Change', 3, 0),
706 ('FindChange', 'Change,Find', 3, 1),
707 ('Ignore', 'Ignore', 4, 0),
708 ('Hide', 'Hide', 4, 1),
709 )
710 for (ivar, label, row, col) in table:
711 name = f"spell_{label}_button"
712 button = self.createButton(spellFrame, name, label)
713 grid.addWidget(button, row, col)
714 func = getattr(self, f"do_leo_spell_btn_{ivar}")
715 button.clicked.connect(func)
716 # This name is significant.
717 setattr(self, f"leo_spell_btn_{ivar}", button)
718 self.leo_spell_btn_Hide.setCheckable(False)
719 spacerItem = QtWidgets.QSpacerItem(20, 40, Policy.Minimum, Policy.Expanding)
720 grid.addItem(spacerItem, 5, 0, 1, 1)
721 listBox = QtWidgets.QListWidget(spellFrame)
722 self.setSizePolicy(listBox, kind1=Policy.MinimumExpanding, kind2=Policy.Expanding)
723 listBox.setMinimumSize(QtCore.QSize(0, 0))
724 listBox.setMaximumSize(QtCore.QSize(150, 150))
725 listBox.setObjectName("leo_spell_listBox")
726 grid.addWidget(listBox, 1, 0, 1, 2)
727 spacerItem1 = QtWidgets.QSpacerItem(40, 20, Policy.Expanding, Policy.Minimum)
728 grid.addItem(spacerItem1, 2, 2, 1, 1)
729 lab = self.createLabel(spellFrame, 'spellLabel', 'spellLabel')
730 grid.addWidget(lab, 0, 0, 1, 2)
731 vLayout2.addLayout(grid)
732 vLayout.addWidget(spellFrame)
733 listBox.itemDoubleClicked.connect(self.do_leo_spell_btn_FindChange)
734 # Official ivars.
735 self.spellFrame = spellFrame
736 self.spellGrid = grid
737 self.leo_spell_widget = parent # 2013/09/20: To allow bindings to be set.
738 self.leo_spell_listBox = listBox # Must exist
739 self.leo_spell_label = lab # Must exist (!!)
740 #@+node:ekr.20110605121601.18166: *5* dw.createFindTab & helpers
741 def createFindTab(self, parent: Widget, tab_widget: Widget) -> None:
742 """Create a Find Tab in the given parent."""
743 c, dw = self.leo_c, self
744 fc = c.findCommands
745 assert not fc.ftm
746 fc.ftm = ftm = FindTabManager(c)
747 grid = self.create_find_grid(parent)
748 row = 0 # The index for the present row.
749 row = dw.create_find_header(grid, parent, row)
750 row = dw.create_find_findbox(grid, parent, row)
751 row = dw.create_find_replacebox(grid, parent, row)
752 max_row2 = 1
753 max_row2 = dw.create_find_checkboxes(grid, parent, max_row2, row)
754 row = dw.create_find_buttons(grid, parent, max_row2, row)
755 row = dw.create_help_row(grid, parent, row)
756 dw.override_events()
757 # Last row: Widgets that take all additional vertical space.
758 w = QtWidgets.QWidget()
759 grid.addWidget(w, row, 0)
760 grid.addWidget(w, row, 1)
761 grid.addWidget(w, row, 2)
762 grid.setRowStretch(row, 100)
763 # Official ivars (in addition to checkbox ivars).
764 self.leo_find_widget = tab_widget # A scrollArea.
765 ftm.init_widgets()
766 #@+node:ekr.20131118152731.16847: *6* dw.create_find_grid
767 def create_find_grid(self, parent: Widget) -> Any:
768 grid = self.createGrid(parent, 'findGrid', margin=10, spacing=10)
769 grid.setColumnStretch(0, 100)
770 grid.setColumnStretch(1, 100)
771 grid.setColumnStretch(2, 10)
772 grid.setColumnMinimumWidth(1, 75)
773 grid.setColumnMinimumWidth(2, 175)
774 return grid
775 #@+node:ekr.20131118152731.16849: *6* dw.create_find_header
776 def create_find_header(self, grid: Any, parent: Widget, row: int) -> int:
777 if False:
778 dw = self
779 lab1 = dw.createLabel(parent, 'findHeading', 'Find/Change Settings...')
780 grid.addWidget(lab1, row, 0, 1, 2, Alignment.AlignLeft) # AlignHCenter
781 row += 1
782 return row
783 #@+node:ekr.20131118152731.16848: *6* dw.create_find_findbox
784 def create_find_findbox(self, grid: Any, parent: Widget, row: int) -> int:
785 """Create the Find: label and text area."""
786 c, dw = self.leo_c, self
787 fc = c.findCommands
788 ftm = fc.ftm
789 assert ftm.find_findbox is None
790 ftm.find_findbox = w = dw.createLineEdit(
791 parent, 'findPattern', disabled=fc.expert_mode)
792 lab2 = self.createLabel(parent, 'findLabel', 'Find:')
793 grid.addWidget(lab2, row, 0)
794 grid.addWidget(w, row, 1, 1, 2)
795 row += 1
796 return row
797 #@+node:ekr.20131118152731.16850: *6* dw.create_find_replacebox
798 def create_find_replacebox(self, grid: Any, parent: Widget, row: int) -> int:
799 """Create the Replace: label and text area."""
800 c, dw = self.leo_c, self
801 fc = c.findCommands
802 ftm = fc.ftm
803 assert ftm.find_replacebox is None
804 ftm.find_replacebox = w = dw.createLineEdit(
805 parent, 'findChange', disabled=fc.expert_mode)
806 lab3 = dw.createLabel(parent, 'changeLabel', 'Replace:') # Leo 4.11.1.
807 grid.addWidget(lab3, row, 0)
808 grid.addWidget(w, row, 1, 1, 2)
809 row += 1
810 return row
811 #@+node:ekr.20131118152731.16851: *6* dw.create_find_checkboxes
812 def create_find_checkboxes(self, grid: Any, parent: Widget, max_row2: int, row: int) -> int:
813 """Create check boxes and radio buttons."""
814 c, dw = self.leo_c, self
815 fc = c.findCommands
816 ftm = fc.ftm
818 def mungeName(kind: str, label: str) -> str:
819 # The returned value is the namve of an ivar.
820 kind = 'check_box_' if kind == 'box' else 'radio_button_'
821 name = label.replace(' ', '_').replace('&', '').lower()
822 return f"{kind}{name}"
824 # Rows for check boxes, radio buttons & execution buttons...
826 d = {
827 'box': dw.createCheckBox,
828 'rb': dw.createRadioButton,
829 }
830 table = (
831 # Note: the Ampersands create Alt bindings when the log pane is enable.
832 # The QShortcut class is the workaround.
833 # First row.
834 ('box', 'whole &Word', 0, 0),
835 ('rb', '&Entire outline', 0, 1),
836 # Second row.
837 ('box', '&Ignore case', 1, 0),
838 ('rb', '&Suboutline only', 1, 1),
839 # Third row.
840 ('box', 'rege&Xp', 2, 0),
841 ('rb', '&Node only', 2, 1),
842 # Fourth row.
843 ('box', 'mark &Finds', 3, 0),
844 ('rb', 'file &only', 3, 1),
845 # Fifth row.
846 ('box', 'mark &Changes', 4, 0),
847 ('box', 'search &Headline', 4, 1),
848 # Sixth Row
849 ('box', 'search &Body', 5, 1),
851 # ('box', 'rege&Xp', 2, 0),
852 # ('rb', '&Node only', 2, 1),
853 # # Fourth row.
854 # ('box', 'mark &Finds', 3, 0),
855 # ('box', 'search &Headline', 3, 1),
856 # # Fifth row.
857 # ('box', 'mark &Changes', 4, 0),
858 # ('box', 'search &Body', 4, 1),
859 # ('rb', 'File &Only', 5, 1),
861 # Sixth row.
862 # ('box', 'wrap &Around', 5, 0),
863 # a,b,c,e,f,h,i,n,rs,w
864 )
865 for kind, label, row2, col in table:
866 max_row2 = max(max_row2, row2)
867 name = mungeName(kind, label)
868 func = d.get(kind)
869 assert func
870 # Fix the greedy checkbox bug:
871 label = label.replace('&', '')
872 w = func(parent, name, label)
873 grid.addWidget(w, row + row2, col)
874 # The the checkbox ivars in dw and ftm classes.
875 assert getattr(ftm, name) is None
876 setattr(ftm, name, w)
877 return max_row2
878 #@+node:ekr.20131118152731.16853: *6* dw.create_help_row
879 def create_help_row(self, grid: Any, parent: Widget, row: int) -> int:
880 # Help row.
881 if False:
882 w = self.createLabel(parent,
883 'findHelp', 'For help: <alt-x>help-for-find-commands<return>')
884 grid.addWidget(w, row, 0, 1, 3)
885 row += 1
886 return row
887 #@+node:ekr.20131118152731.16852: *6* dw.create_find_buttons
888 def create_find_buttons(self, grid: Any, parent: Widget, max_row2: int, row: int) -> int:
889 """
890 Per #1342, this method now creates labels, not real buttons.
891 """
892 dw, k = self, self.leo_c.k
894 # Create Buttons in column 2 (Leo 4.11.1.)
895 table = (
896 (0, 2, 'find-next'), # 'findButton',
897 (1, 2, 'find-prev'), # 'findPreviousButton',
898 (2, 2, 'find-all'), # 'findAllButton',
899 (3, 2, 'replace'), # 'changeButton',
900 (4, 2, 'replace-then-find'), # 'changeThenFindButton',
901 (5, 2, 'replace-all'), # 'changeAllButton',
902 )
903 for row2, col, cmd_name in table:
904 stroke = k.getStrokeForCommandName(cmd_name)
905 if stroke:
906 label = f"{cmd_name}: {k.prettyPrintKey(stroke)}"
907 else:
908 label = cmd_name
909 # #1342: Create a label, not a button.
910 w = dw.createLabel(parent, cmd_name, label)
911 w.setObjectName('find-label')
912 grid.addWidget(w, row + row2, col)
913 row += max_row2
914 row += 2
915 return row
916 #@+node:ekr.20150618072619.1: *6* dw.create_find_status
917 if 0:
919 def create_find_status(self, grid: Any, parent: Widget, row: int) -> None:
920 """Create the status line."""
921 dw = self
922 status_label = dw.createLabel(parent, 'status-label', 'Status')
923 status_line = dw.createLineEdit(parent, 'find-status', disabled=True)
924 grid.addWidget(status_label, row, 0)
925 grid.addWidget(status_line, row, 1, 1, 2)
926 # Official ivars.
927 dw.find_status_label = status_label
928 dw.find_status_edit = status_line
929 #@+node:ekr.20131118172620.16891: *6* dw.override_events
930 def override_events(self) -> None:
931 # dw = self
932 c = self.leo_c
933 fc = c.findCommands
934 ftm = fc.ftm
935 # Define class EventWrapper.
936 #@+others
937 #@+node:ekr.20131118172620.16892: *7* class EventWrapper
938 class EventWrapper:
940 def __init__(self, c: Cmdr, w: Wrapper, next_w: Wrapper, func: Callable) -> None:
941 self.c = c
942 self.d = self.create_d() # Keys: stroke.s; values: command-names.
943 self.w = w
944 self.next_w = next_w
945 self.eventFilter = qt_events.LeoQtEventFilter(c, w, 'EventWrapper')
946 self.func = func
947 self.oldEvent = w.event
948 w.event = self.wrapper
950 #@+others
951 #@+node:ekr.20131120054058.16281: *8* EventWrapper.create_d
952 def create_d(self) -> Dict[str, str]:
953 """Create self.d dictionary."""
954 c = self.c
955 d = {}
956 table = (
957 'toggle-find-ignore-case-option',
958 'toggle-find-in-body-option',
959 'toggle-find-in-headline-option',
960 'toggle-find-mark-changes-option',
961 'toggle-find-mark-finds-option',
962 'toggle-find-regex-option',
963 'toggle-find-word-option',
964 'toggle-find-wrap-around-option',
965 # New in Leo 5.2: Support these in the Find Dialog.
966 'find-all',
967 'find-next',
968 'find-prev',
969 'hide-find-tab',
970 'replace',
971 'replace-all',
972 'replace-then-find',
973 'set-find-everywhere',
974 'set-find-node-only',
975 'set-find-suboutline-only',
976 # #2041 & # 2094 (Leo 6.4): Support Alt-x.
977 'full-command',
978 'keyboard-quit', # Might as well :-)
979 )
980 for cmd_name in table:
981 stroke = c.k.getStrokeForCommandName(cmd_name)
982 if stroke:
983 d[stroke.s] = cmd_name
984 return d
985 #@+node:ekr.20131118172620.16893: *8* EventWrapper.wrapper
986 def wrapper(self, event: Event) -> Any:
988 type_ = event.type()
989 # Must intercept KeyPress for events that generate FocusOut!
990 if type_ == Type.KeyPress:
991 return self.keyPress(event)
992 if type_ == Type.KeyRelease:
993 return self.keyRelease(event)
994 return self.oldEvent(event)
995 #@+node:ekr.20131118172620.16894: *8* EventWrapper.keyPress
996 def keyPress(self, event: Event) -> Any:
998 s = event.text()
999 out = s and s in '\t\r\n'
1000 if out:
1001 # Move focus to next widget.
1002 if s == '\t':
1003 if self.next_w:
1004 self.next_w.setFocus(FocusReason.TabFocusReason)
1005 else:
1006 # Do the normal processing.
1007 return self.oldEvent(event)
1008 elif self.func:
1009 self.func()
1010 return True
1011 binding, ch, lossage = self.eventFilter.toBinding(event)
1012 # #2094: Use code similar to the end of LeoQtEventFilter.eventFilter.
1013 # The ctor converts <Alt-X> to <Atl-x> !!
1014 # That is, we must use the stroke, not the binding.
1015 key_event = leoGui.LeoKeyEvent(
1016 c=self.c, char=ch, event=event, binding=binding, w=self.w)
1017 if key_event.stroke:
1018 cmd_name = self.d.get(key_event.stroke)
1019 if cmd_name:
1020 self.c.k.simulateCommand(cmd_name)
1021 return True
1022 # Do the normal processing.
1023 return self.oldEvent(event)
1024 #@+node:ekr.20131118172620.16895: *8* EventWrapper.keyRelease
1025 def keyRelease(self, event: Event) -> None:
1026 return self.oldEvent(event)
1027 #@-others
1028 #@-others
1029 EventWrapper(c, w=ftm.find_findbox, next_w=ftm.find_replacebox, func=fc.find_next)
1030 EventWrapper(c, w=ftm.find_replacebox, next_w=ftm.find_next_button, func=fc.find_next)
1031 # Finally, checkBoxMarkChanges goes back to ftm.find_findBox.
1032 EventWrapper(c, w=ftm.check_box_mark_changes, next_w=ftm.find_findbox, func=None)
1033 #@+node:ekr.20110605121601.18168: *4* dw.utils
1034 #@+node:ekr.20110605121601.18169: *5* dw.setName
1035 def setName(self, widget: Widget, name: str) -> None:
1036 if name:
1037 # if not name.startswith('leo_'):
1038 # name = 'leo_' + name
1039 widget.setObjectName(name)
1040 #@+node:ekr.20110605121601.18170: *5* dw.setSizePolicy
1041 def setSizePolicy(self, widget: Widget, kind1: Any=None, kind2: Any=None) -> None:
1042 if kind1 is None:
1043 kind1 = Policy.Ignored
1044 if kind2 is None:
1045 kind2 = Policy.Ignored
1046 sizePolicy = QtWidgets.QSizePolicy(kind1, kind2)
1047 sizePolicy.setHorizontalStretch(0)
1048 sizePolicy.setVerticalStretch(0)
1049 sizePolicy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth())
1050 widget.setSizePolicy(sizePolicy)
1051 #@+node:ekr.20110605121601.18171: *5* dw.tr
1052 def tr(self, s: str) -> str:
1053 # pylint: disable=no-member
1054 if isQt5 or isQt6:
1055 # QApplication.UnicodeUTF8 no longer exists.
1056 return QtWidgets.QApplication.translate('MainWindow', s, None)
1057 return QtWidgets.QApplication.translate(
1058 'MainWindow', s, None, QtWidgets.QApplication.UnicodeUTF8)
1059 #@+node:ekr.20110605121601.18173: *3* dw.select
1060 def select(self, c: Cmdr) -> None:
1061 """Select the window or tab for c."""
1062 # Called from the save commands.
1063 self.leo_master.select(c)
1064 #@+node:ekr.20110605121601.18178: *3* dw.setGeometry
1065 def setGeometry(self, rect: Any) -> None:
1066 """Set the window geometry, but only once when using the qt gui."""
1067 m = self.leo_master
1068 assert self.leo_master
1069 # Only set the geometry once, even for new files.
1070 if not hasattr(m, 'leo_geom_inited'):
1071 m.leo_geom_inited = True
1072 self.leo_master.setGeometry(rect)
1073 super().setGeometry(rect)
1075 #@+node:ekr.20110605121601.18177: *3* dw.setLeoWindowIcon
1076 def setLeoWindowIcon(self) -> None:
1077 """ Set icon visible in title bar and task bar """
1078 # self.setWindowIcon(QtGui.QIcon(g.app.leoDir + "/Icons/leoapp32.png"))
1079 g.app.gui.attachLeoIcon(self)
1080 #@+node:ekr.20110605121601.18174: *3* dw.setSplitDirection
1081 def setSplitDirection(self, main_splitter: Widget, secondary_splitter: Widget, orientation: Any) -> None:
1082 """Set the orientations of the splitters in the Leo main window."""
1083 # c = self.leo_c
1084 vert = orientation and orientation.lower().startswith('v')
1085 h, v = Orientation.Horizontal, Orientation.Vertical
1086 orientation1 = v if vert else h
1087 orientation2 = h if vert else v
1088 main_splitter.setOrientation(orientation1)
1089 secondary_splitter.setOrientation(orientation2)
1090 #@+node:ekr.20130804061744.12425: *3* dw.setWindowTitle
1091 if 0: # Override for debugging only.
1093 def setWindowTitle(self, s: str) -> None:
1094 g.trace('***(DynamicWindow)', s, self.parent())
1095 # Call the base class method.
1096 QtWidgets.QMainWindow.setWindowTitle(self, s)
1097 #@-others
1098#@+node:ekr.20131117054619.16698: ** class FindTabManager (qt_frame.py)
1099class FindTabManager:
1100 """A helper class for the LeoFind class."""
1101 #@+others
1102 #@+node:ekr.20131117120458.16794: *3* ftm.ctor
1103 def __init__(self, c: Cmdr) -> None:
1104 """Ctor for the FindTabManager class."""
1105 self.c = c
1106 self.entry_focus = None # The widget that had focus before find-pane entered.
1107 # Find/change text boxes.
1108 self.find_findbox = None
1109 self.find_replacebox = None
1110 # Check boxes.
1111 self.check_box_ignore_case = None
1112 self.check_box_mark_changes = None
1113 self.check_box_mark_finds = None
1114 self.check_box_regexp = None
1115 self.check_box_search_body = None
1116 self.check_box_search_headline = None
1117 self.check_box_whole_word = None
1118 # self.check_box_wrap_around = None
1119 # Radio buttons
1120 self.radio_button_entire_outline = None
1121 self.radio_button_node_only = None
1122 self.radio_button_suboutline_only = None
1123 self.radio_button_file_only = None
1124 # Push buttons
1125 self.find_next_button = None
1126 self.find_prev_button = None
1127 self.find_all_button = None
1128 self.help_for_find_commands_button = None
1129 self.replace_button = None
1130 self.replace_then_find_button = None
1131 self.replace_all_button = None
1132 #@+node:ekr.20131119185305.16478: *3* ftm.clear_focus & init_focus & set_entry_focus
1133 def clear_focus(self) -> None:
1134 self.entry_focus = None
1135 self.find_findbox.clearFocus()
1137 def init_focus(self) -> None:
1138 self.set_entry_focus()
1139 w = self.find_findbox
1140 w.setFocus()
1141 s = w.text()
1142 w.setSelection(0, len(s))
1144 def set_entry_focus(self) -> None:
1145 # Remember the widget that had focus, changing headline widgets
1146 # to the tree pane widget. Headline widgets can disappear!
1147 c = self.c
1148 w = g.app.gui.get_focus(raw=True)
1149 if w != c.frame.body.wrapper.widget:
1150 w = c.frame.tree.treeWidget
1151 self.entry_focus = w
1152 #@+node:ekr.20210110143917.1: *3* ftm.get_settings
1153 def get_settings(self) -> Any:
1154 """
1155 Return a g.bunch representing all widget values.
1157 Similar to LeoFind.default_settings, but only for find-tab values.
1158 """
1159 return g.Bunch(
1160 # Find/change strings...
1161 find_text=self.find_findbox.text(),
1162 change_text=self.find_replacebox.text(),
1163 # Find options...
1164 ignore_case=self.check_box_ignore_case.isChecked(),
1165 mark_changes=self.check_box_mark_changes.isChecked(),
1166 mark_finds=self.check_box_mark_finds.isChecked(),
1167 node_only=self.radio_button_node_only.isChecked(),
1168 pattern_match=self.check_box_regexp.isChecked(),
1169 # reverse = False,
1170 search_body=self.check_box_search_body.isChecked(),
1171 search_headline=self.check_box_search_headline.isChecked(),
1172 suboutline_only=self.radio_button_suboutline_only.isChecked(),
1173 whole_word=self.check_box_whole_word.isChecked(),
1174 # wrapping = self.check_box_wrap_around.isChecked(),
1175 )
1176 #@+node:ekr.20131117120458.16789: *3* ftm.init_widgets (creates callbacks)
1177 def init_widgets(self) -> None:
1178 """
1179 Init widgets and ivars from c.config settings.
1180 Create callbacks that always keep the LeoFind ivars up to date.
1181 """
1182 c = self.c
1183 find = c.findCommands
1184 # Find/change text boxes.
1185 table1 = (
1186 ('find_findbox', 'find_text', '<find pattern here>'),
1187 ('find_replacebox', 'change_text', ''),
1188 )
1189 for ivar, setting_name, default in table1:
1190 s = c.config.getString(setting_name) or default
1191 w = getattr(self, ivar)
1192 w.insert(s)
1193 if find.minibuffer_mode:
1194 w.clearFocus()
1195 else:
1196 w.setSelection(0, len(s))
1197 # Check boxes.
1198 table2 = (
1199 ('ignore_case', self.check_box_ignore_case),
1200 ('mark_changes', self.check_box_mark_changes),
1201 ('mark_finds', self.check_box_mark_finds),
1202 ('pattern_match', self.check_box_regexp),
1203 ('search_body', self.check_box_search_body),
1204 ('search_headline', self.check_box_search_headline),
1205 ('whole_word', self.check_box_whole_word),
1206 # ('wrap', self.check_box_wrap_around),
1207 )
1208 for setting_name, w in table2:
1209 val = c.config.getBool(setting_name, default=False)
1210 # The setting name is also the name of the LeoFind ivar.
1211 assert hasattr(find, setting_name), setting_name
1212 setattr(find, setting_name, val)
1213 if val:
1214 w.toggle()
1216 def check_box_callback(n: int, setting_name: str=setting_name, w: str=w) -> None:
1217 # The focus has already change when this gets called.
1218 # focus_w = QtWidgets.QApplication.focusWidget()
1219 val = w.isChecked()
1220 assert hasattr(find, setting_name), setting_name
1221 setattr(find, setting_name, val)
1222 # Too kludgy: we must use an accurate setting.
1223 # It would be good to have an "about to change" signal.
1224 # Put focus in minibuffer if minibuffer find is in effect.
1225 c.bodyWantsFocusNow()
1227 w.stateChanged.connect(check_box_callback)
1228 # Radio buttons
1229 table3 = (
1230 ('node_only', 'node_only', self.radio_button_node_only),
1231 ('entire_outline', None, self.radio_button_entire_outline),
1232 ('suboutline_only', 'suboutline_only', self.radio_button_suboutline_only),
1233 ('file_only', 'file_only', self.radio_button_file_only)
1234 )
1235 for setting_name, ivar, w in table3:
1236 val = c.config.getBool(setting_name, default=False)
1237 # The setting name is also the name of the LeoFind ivar.
1238 if ivar is not None:
1239 assert hasattr(find, setting_name), setting_name
1240 setattr(find, setting_name, val)
1241 w.toggle()
1243 def radio_button_callback(n: int, ivar: str=ivar, setting_name: str=setting_name, w: str=w) -> None:
1244 val = w.isChecked()
1245 if ivar:
1246 assert hasattr(find, ivar), ivar
1247 setattr(find, ivar, val)
1249 w.toggled.connect(radio_button_callback)
1250 # Ensure one radio button is set.
1251 if not find.node_only and not find.suboutline_only and not find.file_only:
1252 w = self.radio_button_entire_outline
1253 w.toggle()
1254 #@+node:ekr.20210923060904.1: *3* ftm.init_widgets_from_dict (new)
1255 def set_widgets_from_dict(self, d: Dict[str, str]) -> None:
1256 """Set all settings from d."""
1257 # Similar to ftm.init_widgets, which has already been called.
1258 c = self.c
1259 find = c.findCommands
1260 # Set find text.
1261 find_text = d.get('find_text')
1262 self.set_find_text(find_text)
1263 find.find_text = find_text
1264 # Set change text.
1265 change_text = d.get('change_text')
1266 self.set_change_text(change_text)
1267 find.change_text = change_text
1268 # Check boxes...
1269 table1 = (
1270 ('ignore_case', self.check_box_ignore_case),
1271 ('mark_changes', self.check_box_mark_changes),
1272 ('mark_finds', self.check_box_mark_finds),
1273 ('pattern_match', self.check_box_regexp),
1274 ('search_body', self.check_box_search_body),
1275 ('search_headline', self.check_box_search_headline),
1276 ('whole_word', self.check_box_whole_word),
1277 )
1278 for setting_name, w in table1:
1279 val = d.get(setting_name, False)
1280 # The setting name is also the name of the LeoFind ivar.
1281 assert hasattr(find, setting_name), setting_name
1282 setattr(find, setting_name, val)
1283 w.setChecked(val)
1284 # Radio buttons...
1285 table2 = (
1286 ('node_only', 'node_only', self.radio_button_node_only),
1287 ('entire_outline', None, self.radio_button_entire_outline),
1288 ('suboutline_only', 'suboutline_only', self.radio_button_suboutline_only),
1289 ('file_only', 'file_only', self.radio_button_file_only),
1290 )
1291 for setting_name, ivar, w in table2:
1292 val = d.get(setting_name, False)
1293 # The setting name is also the name of the LeoFind ivar.
1294 if ivar is not None:
1295 assert hasattr(find, setting_name), setting_name
1296 setattr(find, setting_name, val)
1297 w.setChecked(val)
1298 # Ensure one radio button is set.
1299 if not find.node_only and not find.suboutline_only and not find.file_only:
1300 w = self.radio_button_entire_outline
1301 w.setChecked(val)
1302 #@+node:ekr.20210312120503.1: *3* ftm.set_body_and_headline_checkbox
1303 def set_body_and_headline_checkbox(self) -> None:
1304 """Return the search-body and search-headline checkboxes to their defaults."""
1305 # #1840: headline-only one-shot
1306 c = self.c
1307 find = c.findCommands
1308 if not find:
1309 return
1310 table = (
1311 ('search_body', self.check_box_search_body),
1312 ('search_headline', self.check_box_search_headline),
1313 )
1314 for setting_name, w in table:
1315 val = c.config.getBool(setting_name, default=False)
1316 if val != w.isChecked():
1317 w.toggle()
1318 if find.minibuffer_mode:
1319 find.show_find_options_in_status_area()
1320 #@+node:ekr.20150619082825.1: *3* ftm.set_ignore_case
1321 def set_ignore_case(self, aBool: bool) -> None:
1322 """Set the ignore-case checkbox to the given value."""
1323 c = self.c
1324 c.findCommands.ignore_case = aBool
1325 w = self.check_box_ignore_case
1326 w.setChecked(aBool)
1327 #@+node:ekr.20131117120458.16792: *3* ftm.set_radio_button
1328 def set_radio_button(self, name: str) -> None:
1329 """Set the value of the radio buttons"""
1330 c = self.c
1331 find = c.findCommands
1332 d = {
1333 # Name is not an ivar. Set by find.setFindScope... commands.
1334 'node-only': self.radio_button_node_only,
1335 'entire-outline': self.radio_button_entire_outline,
1336 'suboutline-only': self.radio_button_suboutline_only,
1337 'file-only': self.radio_button_file_only,
1338 }
1339 w = d.get(name)
1340 # Most of the work will be done in the radio button callback.
1341 if not w.isChecked():
1342 w.toggle()
1343 if find.minibuffer_mode:
1344 find.show_find_options_in_status_area()
1345 #@+node:ekr.20131117164142.16853: *3* ftm.text getters/setters
1346 def get_find_text(self) -> str:
1347 s = self.find_findbox.text()
1348 if s and s[-1] in ('\r', '\n'):
1349 s = s[:-1]
1350 return s
1352 def get_change_text(self) -> str:
1353 s = self.find_replacebox.text()
1354 if s and s[-1] in ('\r', '\n'):
1355 s = s[:-1]
1356 return s
1358 getChangeText = get_change_text
1360 def set_find_text(self, s: str) -> None:
1361 w = self.find_findbox
1362 s = g.checkUnicode(s)
1363 w.clear()
1364 w.insert(s)
1366 def set_change_text(self, s: str) -> None:
1367 w = self.find_replacebox
1368 s = g.checkUnicode(s)
1369 w.clear()
1370 w.insert(s)
1371 #@+node:ekr.20131117120458.16791: *3* ftm.toggle_checkbox
1372 #@@nobeautify
1374 def toggle_checkbox(self, checkbox_name: str) -> None:
1375 """Toggle the value of the checkbox whose name is given."""
1376 c = self.c
1377 find = c.findCommands
1378 if not find:
1379 return
1380 d = {
1381 'ignore_case': self.check_box_ignore_case,
1382 'mark_changes': self.check_box_mark_changes,
1383 'mark_finds': self.check_box_mark_finds,
1384 'pattern_match': self.check_box_regexp,
1385 'search_body': self.check_box_search_body,
1386 'search_headline': self.check_box_search_headline,
1387 'whole_word': self.check_box_whole_word,
1388 # 'wrap': self.check_box_wrap_around,
1389 }
1390 w = d.get(checkbox_name)
1391 assert w
1392 assert hasattr(find, checkbox_name), checkbox_name
1393 w.toggle() # The checkbox callback toggles the ivar.
1394 if find.minibuffer_mode:
1395 find.show_find_options_in_status_area()
1396 #@-others
1397#@+node:ekr.20131115120119.17376: ** class LeoBaseTabWidget(QTabWidget)
1398class LeoBaseTabWidget(QtWidgets.QTabWidget): # type:ignore
1399 """Base class for all QTabWidgets in Leo."""
1400 #@+others
1401 #@+node:ekr.20131115120119.17390: *3* qt_base_tab.__init__
1402 def __init__(self, *args: Any, **kwargs: Any) -> None:
1403 #
1404 # Called from frameFactory.createMaster.
1405 #
1406 self.factory = kwargs.get('factory')
1407 if self.factory:
1408 del kwargs['factory']
1409 super().__init__(*args, **kwargs)
1410 self.detached: List[Any] = []
1411 self.setMovable(True)
1413 def tabContextMenu(point: str) -> None:
1414 index = self.tabBar().tabAt(point)
1415 if index < 0: # or (self.count() < 1 and not self.detached):
1416 return
1417 menu = QtWidgets.QMenu()
1418 # #310: Create new file on right-click in file tab in UI.
1419 if True:
1420 a = menu.addAction("New Outline")
1421 a.triggered.connect(lambda checked: self.new_outline(index))
1422 if self.count() > 1:
1423 a = menu.addAction("Detach")
1424 a.triggered.connect(lambda checked: self.detach(index))
1425 a = menu.addAction("Horizontal tile")
1426 a.triggered.connect(
1427 lambda checked: self.tile(index, orientation='H'))
1428 a = menu.addAction("Vertical tile")
1429 a.triggered.connect(
1430 lambda checked: self.tile(index, orientation='V'))
1431 if self.detached:
1432 a = menu.addAction("Re-attach All")
1433 a.triggered.connect(lambda checked: self.reattach_all())
1435 global_point = self.mapToGlobal(point)
1436 menu.exec_(global_point)
1437 self.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu)
1438 self.customContextMenuRequested.connect(tabContextMenu)
1439 #@+node:ekr.20180123082452.1: *3* qt_base_tab.new_outline
1440 def new_outline(self, index: int) -> None:
1441 """Open a new outline tab."""
1442 w = self.widget(index)
1443 c = w.leo_c
1444 c.new()
1445 #@+node:ekr.20131115120119.17391: *3* qt_base_tab.detach
1446 def detach(self, index: int) -> Widget:
1447 """detach tab (from tab's context menu)"""
1448 w = self.widget(index)
1449 name = self.tabText(index)
1450 self.detached.append((name, w))
1451 self.factory.detachTab(w)
1452 icon = g.app.gui.getImageFinder("application-x-leo-outline.png")
1453 icon = QtGui.QIcon(icon)
1454 if icon:
1455 w.window().setWindowIcon(icon)
1456 c = w.leo_c
1457 if c.styleSheetManager:
1458 c.styleSheetManager.set_style_sheets(w=w)
1459 if platform.system() == 'Windows':
1460 # Windows (XP and 7) put the windows title bar off screen.
1461 w.move(20, 20)
1462 return w
1463 #@+node:ekr.20131115120119.17392: *3* qt_base_tab.tile
1464 def tile(self, index: int, orientation: str='V') -> None:
1465 """detach tab and tile with parent window"""
1466 w = self.widget(index)
1467 window = w.window()
1468 # window.showMaximized()
1469 # this doesn't happen until we've returned to main even loop
1470 # user needs to do it before using this function
1471 fg = window.frameGeometry()
1472 geom = window.geometry()
1473 x, y, fw, fh = fg.x(), fg.y(), fg.width(), fg.height()
1474 ww, wh = geom.width(), geom.height()
1475 w = self.detach(index)
1476 if window.isMaximized():
1477 window.showNormal()
1478 if orientation == 'V':
1479 # follow MS Windows convention for which way is horizontal/vertical
1480 window.resize(ww / 2, wh)
1481 window.move(x, y)
1482 w.resize(ww / 2, wh)
1483 w.move(x + fw / 2, y)
1484 else:
1485 window.resize(ww, wh / 2)
1486 window.move(x, y)
1487 w.resize(ww, wh / 2)
1488 w.move(x, y + fh / 2)
1489 #@+node:ekr.20131115120119.17393: *3* qt_base_tab.reattach_all
1490 def reattach_all(self) -> None:
1491 """reattach all detached tabs"""
1492 for name, w in self.detached:
1493 self.addTab(w, name)
1494 self.factory.leoFrames[w] = w.leo_c.frame
1495 self.detached = []
1496 #@+node:ekr.20131115120119.17394: *3* qt_base_tab.delete
1497 def delete(self, w: Wrapper) -> None:
1498 """called by TabbedFrameFactory to tell us a detached tab
1499 has been deleted"""
1500 self.detached = [i for i in self.detached if i[1] != w]
1501 #@+node:ekr.20131115120119.17395: *3* qt_base_tab.setChanged
1502 def setChanged(self, c: Cmdr, changed: bool) -> None:
1503 """Set the changed indicator in c's tab."""
1504 # Find the tab corresponding to c.
1505 dw = c.frame.top # A DynamicWindow
1506 i = self.indexOf(dw)
1507 if i < 0:
1508 return
1509 s = self.tabText(i)
1510 if len(s) > 2:
1511 if changed:
1512 if not s.startswith('* '):
1513 title = "* " + s
1514 self.setTabText(i, title)
1515 else:
1516 if s.startswith('* '):
1517 title = s[2:]
1518 self.setTabText(i, title)
1519 #@+node:ekr.20131115120119.17396: *3* qt_base_tab.setTabName
1520 def setTabName(self, c: Cmdr, fileName: str) -> None:
1521 """Set the tab name for c's tab to fileName."""
1522 # Find the tab corresponding to c.
1523 dw = c.frame.top # A DynamicWindow
1524 i = self.indexOf(dw)
1525 if i > -1:
1526 self.setTabText(i, g.shortFileName(fileName))
1527 #@+node:ekr.20131115120119.17397: *3* qt_base_tab.closeEvent
1528 def closeEvent(self, event: Event) -> None:
1529 """Handle a close event."""
1530 g.app.gui.close_event(event)
1531 #@+node:ekr.20131115120119.17398: *3* qt_base_tab.select (leoTabbedTopLevel)
1532 def select(self, c: Cmdr) -> None:
1533 """Select the tab for c."""
1534 dw = c.frame.top # A DynamicWindow
1535 i = self.indexOf(dw)
1536 self.setCurrentIndex(i)
1537 # Fix bug 844953: tell Unity which menu to use.
1538 # c.enableMenuBar()
1539 #@-others
1540#@+node:ekr.20110605121601.18180: ** class LeoQtBody(leoFrame.LeoBody)
1541class LeoQtBody(leoFrame.LeoBody):
1542 """A class that represents the body pane of a Qt window."""
1543 #@+others
1544 #@+node:ekr.20150521061618.1: *3* LeoQtBody.body_cmd (decorator)
1545 #@+node:ekr.20110605121601.18181: *3* LeoQtBody.Birth
1546 #@+node:ekr.20110605121601.18182: *4* LeoQtBody.ctor
1547 def __init__(self, frame: Wrapper, parentFrame: Widget) -> None:
1548 """Ctor for LeoQtBody class."""
1549 # Call the base class constructor.
1550 super().__init__(frame, parentFrame)
1551 c = self.c
1552 assert c.frame == frame and frame.c == c
1553 self.colorizer: Any = None
1554 self.wrapper: Wrapper = None
1555 self.widget: Widget = None
1556 self.reloadSettings()
1557 self.set_widget() # Sets self.widget and self.wrapper.
1558 self.setWrap(c.p)
1559 # For multiple body editors.
1560 self.editor_name = None
1561 self.editor_v = None
1562 self.numberOfEditors = 1
1563 self.totalNumberOfEditors = 1
1564 # For renderer panes.
1565 self.canvasRenderer = None
1566 self.canvasRendererLabel: Widget = None
1567 self.canvasRendererVisible = False
1568 self.textRenderer: Widget = None
1569 self.textRendererLabel: Widget = None
1570 self.textRendererVisible = False
1571 self.textRendererWrapper: Wrapper = None
1572 #@+node:ekr.20110605121601.18185: *5* LeoQtBody.get_name
1573 def getName(self) -> str:
1574 return 'body-widget'
1575 #@+node:ekr.20140901062324.18562: *5* LeoQtBody.reloadSettings
1576 def reloadSettings(self) -> None:
1577 c = self.c
1578 self.useScintilla = c.config.getBool('qt-use-scintilla')
1579 self.use_chapters = c.config.getBool('use-chapters')
1580 self.use_gutter = c.config.getBool('use-gutter', default=False)
1581 #@+node:ekr.20160309074124.1: *5* LeoQtBody.set_invisibles
1582 def set_invisibles(self, c: Cmdr) -> None:
1583 """Set the show-invisibles bit in the document."""
1584 d = c.frame.body.wrapper.widget.document()
1585 option = QtGui.QTextOption()
1586 if c.frame.body.colorizer.showInvisibles:
1587 # The following works with both Qt5 and Qt6.
1588 # pylint: disable=no-member
1589 option.setFlags(option.Flag.ShowTabsAndSpaces)
1590 d.setDefaultTextOption(option)
1591 #@+node:ekr.20140901062324.18563: *5* LeoQtBody.set_widget
1592 def set_widget(self) -> None:
1593 """Set the actual gui widget."""
1594 c = self.c
1595 top = c.frame.top
1596 sw = getattr(top, 'stackedWidget', None)
1597 if sw:
1598 sw.setCurrentIndex(1)
1599 if self.useScintilla and not Qsci:
1600 g.trace('Can not import Qsci: ignoring @bool qt-use-scintilla')
1601 if self.useScintilla and Qsci:
1602 # A Qsci.QsciSintilla object.
1603 # dw.createText sets self.scintilla_widget
1604 self.widget = c.frame.top.scintilla_widget
1605 self.wrapper = qt_text.QScintillaWrapper(self.widget, name='body', c=c)
1606 self.colorizer = leoColorizer.QScintillaColorizer(c, self.widget)
1607 else:
1608 self.widget = top.richTextEdit # A LeoQTextBrowser
1609 self.wrapper = qt_text.QTextEditWrapper(self.widget, name='body', c=c)
1610 self.widget.setAcceptRichText(False)
1611 self.colorizer = leoColorizer.make_colorizer(c, self.widget)
1612 #@+node:ekr.20110605121601.18183: *5* LeoQtBody.forceWrap and setWrap
1613 def forceWrap(self, p: Pos) -> None:
1614 """Set **only** the wrap bits in the body."""
1615 if not p or self.useScintilla:
1616 return
1617 c = self.c
1618 w = c.frame.body.wrapper.widget
1619 wrap = WrapMode.WrapAtWordBoundaryOrAnywhere
1620 w.setWordWrapMode(wrap)
1622 def setWrap(self, p: Pos) -> None:
1623 """Set **only** the wrap bits in the body."""
1624 if not p or self.useScintilla:
1625 return
1626 c = self.c
1627 w = c.frame.body.wrapper.widget
1628 wrap = g.scanAllAtWrapDirectives(c, p)
1629 policy = ScrollBarPolicy.ScrollBarAlwaysOff if wrap else ScrollBarPolicy.ScrollBarAsNeeded
1630 w.setHorizontalScrollBarPolicy(policy)
1631 wrap = WrapMode.WrapAtWordBoundaryOrAnywhere if wrap else WrapMode.NoWrap # type:ignore
1632 w.setWordWrapMode(wrap)
1633 #@+node:ekr.20110605121601.18193: *3* LeoQtBody.Editors
1634 #@+node:ekr.20110605121601.18194: *4* LeoQtBody.entries
1635 #@+node:ekr.20110605121601.18195: *5* LeoQtBody.add_editor_command
1636 # An override of leoFrame.addEditor.
1638 @body_cmd('editor-add')
1639 @body_cmd('add-editor')
1640 def add_editor_command(self, event: Event=None) -> None:
1641 """Add another editor to the body pane."""
1642 c, p = self.c, self.c.p
1643 d = self.editorWrappers
1644 dw = c.frame.top
1645 wrapper = c.frame.body.wrapper # A QTextEditWrapper
1646 widget = wrapper.widget
1647 self.totalNumberOfEditors += 1
1648 self.numberOfEditors += 1
1649 if self.totalNumberOfEditors == 2:
1650 d['1'] = wrapper
1651 # Pack the original body editor.
1652 # Fix #1021: Pack differently depending on whether the gutter exists.
1653 if self.use_gutter:
1654 dw.packLabel(widget.parent(), n=1)
1655 widget.leo_label = widget.parent().leo_label
1656 else:
1657 dw.packLabel(widget, n=1)
1658 name = f"{self.totalNumberOfEditors}"
1659 f, wrapper = dw.addNewEditor(name)
1660 assert g.isTextWrapper(wrapper), wrapper
1661 assert g.isTextWidget(widget), widget
1662 assert isinstance(f, QtWidgets.QFrame), f
1663 d[name] = wrapper
1664 if self.numberOfEditors == 2:
1665 # Inject the ivars into the first editor.
1666 # The name of the last editor need not be '1'
1667 keys = list(d.keys())
1668 old_name = keys[0]
1669 old_wrapper = d.get(old_name)
1670 old_w = old_wrapper.widget
1671 self.injectIvars(f, old_name, p, old_wrapper)
1672 self.updateInjectedIvars(old_w, p)
1673 # Immediately create the label in the old editor.
1674 self.selectLabel(old_wrapper)
1675 # Switch editors.
1676 c.frame.body.wrapper = wrapper
1677 self.selectLabel(wrapper)
1678 self.selectEditor(wrapper)
1679 self.updateEditors()
1680 c.bodyWantsFocus()
1681 #@+node:ekr.20110605121601.18197: *5* LeoQtBody.assignPositionToEditor
1682 def assignPositionToEditor(self, p: Pos) -> None:
1683 """Called *only* from tree.select to select the present body editor."""
1684 c = self.c
1685 wrapper = c.frame.body.wrapper
1686 w = wrapper and wrapper.widget
1687 if w: # Careful: w may not exist during unit testing.
1688 self.updateInjectedIvars(w, p)
1689 self.selectLabel(wrapper)
1690 #@+node:ekr.20110605121601.18198: *5* LeoQtBody.cycleEditorFocus
1691 # Use the base class method.
1692 #@+node:ekr.20110605121601.18199: *5* LeoQtBody.delete_editor_command
1693 @body_cmd('delete-editor')
1694 @body_cmd('editor-delete')
1695 def delete_editor_command(self, event: Event=None) -> None:
1696 """Delete the presently selected body text editor."""
1697 c, d = self.c, self.editorWrappers
1698 wrapper = c.frame.body.wrapper
1699 w = wrapper.widget
1700 assert g.isTextWrapper(wrapper), wrapper
1701 assert g.isTextWidget(w), w
1702 # Fix bug 228: make *sure* the old text is saved.
1703 c.p.b = wrapper.getAllText()
1704 name = getattr(w, 'leo_name', None)
1705 if len(list(d.keys())) <= 1 or name == '1':
1706 g.warning('can not delete main editor')
1707 return
1708 #
1709 # Actually delete the widget.
1710 del d[name]
1711 f = c.frame.top.leo_body_frame
1712 layout = f.layout()
1713 for z in (w, w.leo_label):
1714 if z:
1715 self.unpackWidget(layout, z)
1716 #
1717 # Select another editor.
1718 new_wrapper = list(d.values())[0]
1719 self.numberOfEditors -= 1
1720 if self.numberOfEditors == 1:
1721 w = new_wrapper.widget
1722 label = getattr(w, 'leo_label', None)
1723 if label:
1724 self.unpackWidget(layout, label)
1725 w.leo_label = None
1726 self.selectEditor(new_wrapper)
1727 #@+node:ekr.20110605121601.18200: *5* LeoQtBody.findEditorForChapter
1728 def findEditorForChapter(self, chapter: Any, p: Pos) -> None:
1729 """Return an editor to be assigned to chapter."""
1730 c, d = self.c, self.editorWrappers
1731 values = list(d.values())
1732 # First, try to match both the chapter and position.
1733 if p:
1734 for w in values:
1735 if (
1736 hasattr(w, 'leo_chapter') and w.leo_chapter == chapter and
1737 hasattr(w, 'leo_p') and w.leo_p and w.leo_p == p
1738 ):
1739 return w
1740 # Next, try to match just the chapter.
1741 for w in values:
1742 if hasattr(w, 'leo_chapter') and w.leo_chapter == chapter:
1743 return w
1744 # As a last resort, return the present editor widget.
1745 return c.frame.body.wrapper
1746 #@+node:ekr.20110605121601.18201: *5* LeoQtBody.select/unselectLabel
1747 def unselectLabel(self, wrapper: Wrapper) -> None:
1748 # pylint: disable=arguments-differ
1749 pass
1750 # self.createChapterIvar(wrapper)
1752 def selectLabel(self, wrapper: str) -> None:
1753 # pylint: disable=arguments-differ
1754 c = self.c
1755 w = wrapper.widget
1756 label = getattr(w, 'leo_label', None)
1757 if label:
1758 label.setEnabled(True)
1759 label.setText(c.p.h)
1760 label.setEnabled(False)
1761 #@+node:ekr.20110605121601.18202: *5* LeoQtBody.selectEditor & helpers
1762 selectEditorLockout = False
1764 def selectEditor(self, wrapper: Wrapper) -> None:
1765 """Select editor w and node w.leo_p."""
1766 # pylint: disable=arguments-differ
1767 trace = 'select' in g.app.debug and not g.unitTesting
1768 tag = 'qt_body.selectEditor'
1769 c = self.c
1770 if not wrapper:
1771 return
1772 if self.selectEditorLockout:
1773 return
1774 w = wrapper.widget
1775 assert g.isTextWrapper(wrapper), wrapper
1776 assert g.isTextWidget(w), w
1777 if trace:
1778 print(f"{tag:>30}: {wrapper} {c.p.h}")
1779 if wrapper and wrapper == c.frame.body.wrapper:
1780 self.deactivateEditors(wrapper)
1781 if hasattr(w, 'leo_p') and w.leo_p and w.leo_p != c.p:
1782 c.selectPosition(w.leo_p)
1783 c.bodyWantsFocus()
1784 return
1785 try:
1786 self.selectEditorLockout = True
1787 self.selectEditorHelper(wrapper)
1788 finally:
1789 self.selectEditorLockout = False
1790 #@+node:ekr.20110605121601.18203: *6* LeoQtBody.selectEditorHelper
1791 def selectEditorHelper(self, wrapper: Wrapper) -> None:
1792 c = self.c
1793 w = wrapper.widget
1794 assert g.isTextWrapper(wrapper), wrapper
1795 assert g.isTextWidget(w), w
1796 if not w.leo_p:
1797 g.trace('no w.leo_p')
1798 return
1799 # The actual switch.
1800 self.deactivateEditors(wrapper)
1801 self.recolorWidget(w.leo_p, wrapper) # switches colorizers.
1802 c.frame.body.wrapper = wrapper
1803 # 2014/09/04: Must set both wrapper.widget and body.widget.
1804 c.frame.body.wrapper.widget = w
1805 c.frame.body.widget = w
1806 w.leo_active = True
1807 self.switchToChapter(wrapper)
1808 self.selectLabel(wrapper)
1809 if not self.ensurePositionExists(w):
1810 g.trace('***** no position editor!')
1811 return
1812 if not (hasattr(w, 'leo_p') and w.leo_p):
1813 g.trace('***** no w.leo_p', w)
1814 return
1815 p = w.leo_p
1816 assert p, p
1817 c.expandAllAncestors(p)
1818 # Calls assignPositionToEditor.
1819 # Calls p.v.restoreCursorAndScroll.
1820 c.selectPosition(p)
1821 c.redraw()
1822 c.recolor()
1823 c.bodyWantsFocus()
1824 #@+node:ekr.20110605121601.18205: *5* LeoQtBody.updateEditors
1825 # Called from addEditor and assignPositionToEditor
1827 def updateEditors(self) -> None:
1828 c, p = self.c, self.c.p
1829 body = p.b
1830 d = self.editorWrappers
1831 if len(list(d.keys())) < 2:
1832 return # There is only the main widget
1833 w0 = c.frame.body.wrapper
1834 i, j = w0.getSelectionRange()
1835 ins = w0.getInsertPoint()
1836 sb0 = w0.widget.verticalScrollBar()
1837 pos0 = sb0.sliderPosition()
1838 for key in d:
1839 wrapper = d.get(key)
1840 w = wrapper.widget
1841 v = hasattr(w, 'leo_p') and w.leo_p.v
1842 if v and v == p.v and w != w0:
1843 sb = w.verticalScrollBar()
1844 pos = sb.sliderPosition()
1845 wrapper.setAllText(body)
1846 self.recolorWidget(p, wrapper)
1847 sb.setSliderPosition(pos)
1848 c.bodyWantsFocus()
1849 w0.setSelectionRange(i, j, insert=ins)
1850 sb0.setSliderPosition(pos0)
1851 #@+node:ekr.20110605121601.18206: *4* LeoQtBody.utils
1852 #@+node:ekr.20110605121601.18207: *5* LeoQtBody.computeLabel
1853 def computeLabel(self, w: Wrapper) -> str:
1854 if hasattr(w, 'leo_label') and w.leo_label: # 2011/11/12
1855 s = w.leo_label.text()
1856 else:
1857 s = ''
1858 if hasattr(w, 'leo_chapter') and w.leo_chapter:
1859 s = f"{w.leo_chapter}: {s}"
1860 return s
1861 #@+node:ekr.20110605121601.18208: *5* LeoQtBody.createChapterIvar
1862 def createChapterIvar(self, w: Wrapper) -> None:
1863 c = self.c
1864 cc = c.chapterController
1865 if hasattr(w, 'leo_chapter') and w.leo_chapter:
1866 pass
1867 elif cc and self.use_chapters:
1868 w.leo_chapter = cc.getSelectedChapter()
1869 else:
1870 w.leo_chapter = None
1871 #@+node:ekr.20110605121601.18209: *5* LeoQtBody.deactivateEditors
1872 def deactivateEditors(self, wrapper: Wrapper) -> None:
1873 """Deactivate all editors except wrapper's editor."""
1874 d = self.editorWrappers
1875 # Don't capture ivars here! assignPositionToEditor keeps them up-to-date. (??)
1876 for key in d:
1877 wrapper2 = d.get(key)
1878 w2 = wrapper2.widget
1879 if hasattr(w2, 'leo_active'):
1880 active = w2.leo_active
1881 else:
1882 active = True
1883 if wrapper2 != wrapper and active:
1884 w2.leo_active = False
1885 self.unselectLabel(wrapper2)
1886 self.onFocusOut(w2)
1887 #@+node:ekr.20110605121601.18210: *5* LeoQtBody.ensurePositionExists
1888 def ensurePositionExists(self, w: Wrapper) -> bool:
1889 """Return True if w.leo_p exists or can be reconstituted."""
1890 c = self.c
1891 if c.positionExists(w.leo_p):
1892 return True
1893 for p2 in c.all_unique_positions():
1894 if p2.v and p2.v == w.leo_p.v:
1895 w.leo_p = p2.copy()
1896 return True
1897 # This *can* happen when selecting a deleted node.
1898 w.leo_p = c.p.copy()
1899 return False
1900 #@+node:ekr.20110605121601.18211: *5* LeoQtBody.injectIvars
1901 def injectIvars(self, parentFrame: Wrapper, name: str, p: Pos, wrapper: Wrapper) -> None:
1903 trace = g.app.debug == 'select' and not g.unitTesting
1904 tag = 'qt_body.injectIvars'
1905 w = wrapper.widget
1906 assert g.isTextWrapper(wrapper), wrapper
1907 assert g.isTextWidget(w), w
1908 if trace:
1909 print(f"{tag:>30}: {wrapper!r} {g.callers(1)}")
1910 # Inject ivars
1911 if name == '1':
1912 w.leo_p = None # Will be set when the second editor is created.
1913 else:
1914 w.leo_p = p and p.copy()
1915 w.leo_active = True
1916 w.leo_bodyBar = None
1917 w.leo_bodyXBar = None
1918 w.leo_chapter = None
1919 # w.leo_colorizer injected by JEditColorizer ctor.
1920 # w.leo_label injected by packLabel.
1921 w.leo_frame = parentFrame
1922 w.leo_name = name
1923 w.leo_wrapper = wrapper
1924 #@+node:ekr.20110605121601.18213: *5* LeoQtBody.recolorWidget (QScintilla only)
1925 def recolorWidget(self, p: Pos, wrapper: Wrapper) -> None:
1926 """Support QScintillaColorizer.colorize."""
1927 # pylint: disable=arguments-differ
1928 c = self.c
1929 colorizer = c.frame.body.colorizer
1930 if p and colorizer and hasattr(colorizer, 'colorize'):
1931 g.trace('=====', hasattr(colorizer, 'colorize'), p.h, g.callers())
1932 old_wrapper = c.frame.body.wrapper
1933 c.frame.body.wrapper = wrapper
1934 try:
1935 colorizer.colorize(p)
1936 finally:
1937 # Restore.
1938 c.frame.body.wrapper = old_wrapper
1939 #@+node:ekr.20110605121601.18214: *5* LeoQtBody.switchToChapter
1940 def switchToChapter(self, w: Wrapper) -> None:
1941 """select w.leo_chapter."""
1942 c = self.c
1943 cc = c.chapterController
1944 if hasattr(w, 'leo_chapter') and w.leo_chapter:
1945 chapter = w.leo_chapter
1946 name = chapter and chapter.name
1947 oldChapter = cc.getSelectedChapter()
1948 if chapter != oldChapter:
1949 cc.selectChapterByName(name)
1950 c.bodyWantsFocus()
1951 #@+node:ekr.20110605121601.18216: *5* LeoQtBody.unpackWidget
1952 def unpackWidget(self, layout: Widget, w: Wrapper) -> None:
1954 index = layout.indexOf(w)
1955 if index == -1:
1956 return
1957 item = layout.itemAt(index)
1958 if item:
1959 item.setGeometry(QtCore.QRect(0, 0, 0, 0))
1960 layout.removeItem(item)
1961 #@+node:ekr.20110605121601.18215: *5* LeoQtBody.updateInjectedIvars
1962 def updateInjectedIvars(self, w: Wrapper, p: Pos) -> None:
1964 c = self.c
1965 cc = c.chapterController
1966 assert g.isTextWidget(w), w
1967 if cc and self.use_chapters:
1968 w.leo_chapter = cc.getSelectedChapter()
1969 else:
1970 w.leo_chapter = None
1971 w.leo_p = p.copy()
1972 #@+node:ekr.20110605121601.18223: *3* LeoQtBody.Event handlers
1973 #@+node:ekr.20110930174206.15472: *4* LeoQtBody.onFocusIn
1974 def onFocusIn(self, obj: Any) -> None:
1975 """Handle a focus-in event in the body pane."""
1976 trace = 'select' in g.app.debug and not g.unitTesting
1977 tag = 'qt_body.onFocusIn'
1978 if obj.objectName() == 'richTextEdit':
1979 wrapper = getattr(obj, 'leo_wrapper', None)
1980 if trace:
1981 print(f"{tag:>30}: {wrapper}")
1982 if wrapper and wrapper != self.wrapper:
1983 self.selectEditor(wrapper)
1984 self.onFocusColorHelper('focus-in', obj)
1985 if hasattr(obj, 'leo_copy_button') and obj.leo_copy_button:
1986 obj.setReadOnly(True)
1987 else:
1988 obj.setReadOnly(False)
1989 obj.setFocus() # Weird, but apparently necessary.
1990 #@+node:ekr.20110930174206.15473: *4* LeoQtBody.onFocusOut
1991 def onFocusOut(self, obj: Any) -> None:
1992 """Handle a focus-out event in the body pane."""
1993 # Apparently benign.
1994 if obj.objectName() == 'richTextEdit':
1995 self.onFocusColorHelper('focus-out', obj)
1996 if hasattr(obj, 'setReadOnly'):
1997 obj.setReadOnly(True)
1998 #@+node:ekr.20110605121601.18224: *4* LeoQtBody.qtBody.onFocusColorHelper (revised)
1999 def onFocusColorHelper(self, kind: str, obj: Any) -> None:
2000 """Handle changes of style when focus changes."""
2001 c, vc = self.c, self.c.vimCommands
2002 if vc and c.vim_mode:
2003 try:
2004 assert kind in ('focus-in', 'focus-out')
2005 w = c.frame.body.wrapper.widget
2006 vc.set_border(w=w, activeFlag=kind == 'focus-in')
2007 except Exception:
2008 # g.es_exception()
2009 pass
2010 #@+node:ekr.20110605121601.18217: *3* LeoQtBody.Renderer panes
2011 #@+node:ekr.20110605121601.18218: *4* LeoQtBody.hideCanvasRenderer
2012 def hideCanvasRenderer(self, event: Event=None) -> None:
2013 """Hide canvas pane."""
2014 c, d = self.c, self.editorWrappers
2015 wrapper = c.frame.body.wrapper
2016 w = wrapper.widget
2017 name = w.leo_name
2018 assert name
2019 assert wrapper == d.get(name), 'wrong wrapper'
2020 assert g.isTextWrapper(wrapper), wrapper
2021 assert g.isTextWidget(w), w
2022 if len(list(d.keys())) <= 1:
2023 return
2024 #
2025 # At present, can not delete the first column.
2026 if name == '1':
2027 g.warning('can not delete leftmost editor')
2028 return
2029 #
2030 # Actually delete the widget.
2031 del d[name]
2032 f = c.frame.top.leo_body_inner_frame
2033 layout = f.layout()
2034 for z in (w, w.leo_label):
2035 if z:
2036 self.unpackWidget(layout, z)
2037 #
2038 # Select another editor.
2039 w.leo_label = None
2040 new_wrapper = list(d.values())[0]
2041 self.numberOfEditors -= 1
2042 if self.numberOfEditors == 1:
2043 w = new_wrapper.widget
2044 if w.leo_label: # 2011/11/12
2045 self.unpackWidget(layout, w.leo_label)
2046 w.leo_label = None # 2011/11/12
2047 self.selectEditor(new_wrapper)
2048 #@+node:ekr.20110605121601.18219: *4* LeoQtBody.hideTextRenderer
2049 def hideCanvas(self, event: Event=None) -> None:
2050 """Hide canvas pane."""
2051 c, d = self.c, self.editorWrappers
2052 wrapper = c.frame.body.wrapper
2053 w = wrapper.widget
2054 name = w.leo_name
2055 assert name
2056 assert wrapper == d.get(name), 'wrong wrapper'
2057 assert g.isTextWrapper(wrapper), wrapper
2058 assert g.isTextWidget(w), w
2059 if len(list(d.keys())) <= 1:
2060 return
2061 # At present, can not delete the first column.
2062 if name == '1':
2063 g.warning('can not delete leftmost editor')
2064 return
2065 #
2066 # Actually delete the widget.
2067 del d[name]
2068 f = c.frame.top.leo_body_inner_frame
2069 layout = f.layout()
2070 for z in (w, w.leo_label):
2071 if z:
2072 self.unpackWidget(layout, z)
2073 #
2074 # Select another editor.
2075 w.leo_label = None
2076 new_wrapper = list(d.values())[0]
2077 self.numberOfEditors -= 1
2078 if self.numberOfEditors == 1:
2079 w = new_wrapper.widget
2080 if w.leo_label:
2081 self.unpackWidget(layout, w.leo_label)
2082 w.leo_label = None
2083 self.selectEditor(new_wrapper)
2084 #@+node:ekr.20110605121601.18220: *4* LeoQtBody.packRenderer
2085 def packRenderer(self, f: str, name: str, w: Wrapper) -> Widget:
2086 n = max(1, self.numberOfEditors)
2087 assert isinstance(f, QtWidgets.QFrame), f
2088 layout = f.layout()
2089 f.setObjectName(f"{name} Frame")
2090 # Create the text: to do: use stylesheet to set font, height.
2091 lab = QtWidgets.QLineEdit(f)
2092 lab.setObjectName(f"{name} Label")
2093 lab.setText(name)
2094 # Pack the label and the widget.
2095 layout.addWidget(lab, 0, max(0, n - 1), Alignment.AlignVCenter)
2096 layout.addWidget(w, 1, max(0, n - 1))
2097 layout.setRowStretch(0, 0)
2098 layout.setRowStretch(1, 1) # Give row 1 as much as possible.
2099 return lab
2100 #@+node:ekr.20110605121601.18221: *4* LeoQtBody.showCanvasRenderer
2101 # An override of leoFrame.addEditor.
2103 def showCanvasRenderer(self, event: Event=None) -> None:
2104 """Show the canvas area in the body pane, creating it if necessary."""
2105 c = self.c
2106 f = c.frame.top.leo_body_inner_frame
2107 assert isinstance(f, QtWidgets.QFrame), f
2108 if not self.canvasRenderer:
2109 name = 'Graphics Renderer'
2110 self.canvasRenderer = w = QtWidgets.QGraphicsView(f)
2111 w.setObjectName(name)
2112 if not self.canvasRendererVisible:
2113 self.canvasRendererLabel = self.packRenderer(f, name, w)
2114 self.canvasRendererVisible = True
2115 #@+node:ekr.20110605121601.18222: *4* LeoQtBody.showTextRenderer
2116 # An override of leoFrame.addEditor.
2118 def showTextRenderer(self, event: Event=None) -> None:
2119 """Show the canvas area in the body pane, creating it if necessary."""
2120 c = self.c
2121 f = c.frame.top.leo_body_inner_frame
2122 name = 'Text Renderer'
2123 w = self.textRenderer
2124 assert isinstance(f, QtWidgets.QFrame), f
2125 if w:
2126 self.textRenderer = qt_text.LeoQTextBrowser(f, c, self)
2127 w = self.textRenderer
2128 w.setObjectName(name)
2129 self.textRendererWrapper = qt_text.QTextEditWrapper(w, name='text-renderer', c=c)
2130 if not self.textRendererVisible:
2131 self.textRendererLabel = self.packRenderer(f, name, w)
2132 self.textRendererVisible = True
2133 #@-others
2134#@+node:ekr.20110605121601.18245: ** class LeoQtFrame (leoFrame)
2135class LeoQtFrame(leoFrame.LeoFrame):
2136 """A class that represents a Leo window rendered in qt."""
2137 #@+others
2138 #@+node:ekr.20110605121601.18246: *3* qtFrame.Birth & Death
2139 #@+node:ekr.20110605121601.18247: *4* qtFrame.__init__ & reloadSettings
2140 def __init__(self, c: Cmdr, title: str, gui: Any) -> None:
2142 super().__init__(c, gui)
2143 assert self.c == c
2144 leoFrame.LeoFrame.instances += 1 # Increment the class var.
2145 # Official ivars...
2146 self.iconBar = None
2147 self.iconBarClass = self.QtIconBarClass # type:ignore
2148 self.initComplete = False # Set by initCompleteHint().
2149 self.minibufferVisible = True
2150 self.statusLineClass = self.QtStatusLineClass # type:ignore
2151 self.title = title
2152 self.setIvars()
2153 self.reloadSettings()
2155 def reloadSettings(self) -> None:
2156 c = self.c
2157 self.cursorStay = c.config.getBool("cursor-stay-on-paste", default=True)
2158 self.use_chapters = c.config.getBool('use-chapters')
2159 self.use_chapter_tabs = c.config.getBool('use-chapter-tabs')
2160 #@+node:ekr.20110605121601.18248: *5* qtFrame.setIvars
2161 def setIvars(self) -> None:
2162 # "Official ivars created in createLeoFrame and its allies.
2163 self.bar1 = None
2164 self.bar2 = None
2165 self.body = None
2166 self.f1 = self.f2 = None
2167 self.findPanel = None # Inited when first opened.
2168 self.iconBarComponentName = 'iconBar'
2169 self.iconFrame = None
2170 self.log = None
2171 self.canvas = None
2172 self.outerFrame = None
2173 self.statusFrame = None
2174 self.statusLineComponentName = 'statusLine'
2175 self.statusText = None
2176 self.statusLabel = None
2177 self.top = None # This will be a class Window object.
2178 self.tree = None
2179 # Used by event handlers...
2180 self.controlKeyIsDown = False # For control-drags
2181 self.isActive = True
2182 self.redrawCount = 0
2183 self.wantedWidget = None
2184 self.wantedCallbackScheduled = False
2185 self.scrollWay = None
2186 #@+node:ekr.20110605121601.18249: *4* qtFrame.__repr__
2187 def __repr__(self) -> str:
2188 return f"<LeoQtFrame: {self.title}>"
2189 #@+node:ekr.20110605121601.18250: *4* qtFrame.finishCreate & helpers
2190 def finishCreate(self) -> None:
2191 """Finish creating the outline's frame."""
2192 # Called from app.newCommander, Commands.__init__
2193 t1 = time.process_time()
2194 c = self.c
2195 assert c
2196 frameFactory = g.app.gui.frameFactory
2197 if not frameFactory.masterFrame:
2198 frameFactory.createMaster()
2199 self.top = frameFactory.createFrame(leoFrame=self)
2200 self.createIconBar() # A base class method.
2201 self.createSplitterComponents()
2202 self.createStatusLine() # A base class method.
2203 self.createFirstTreeNode() # Call the base-class method.
2204 self.menu: Wrapper = LeoQtMenu(c, self, label='top-level-menu')
2205 g.app.windowList.append(self)
2206 t2 = time.process_time()
2207 self.setQtStyle() # Slow, but only the first time it is called.
2208 t3 = time.process_time()
2209 self.miniBufferWidget: Wrapper = qt_text.QMinibufferWrapper(c)
2210 c.bodyWantsFocus()
2211 t4 = time.process_time()
2212 if 'speed' in g.app.debug:
2213 print('qtFrame.finishCreate')
2214 print(
2215 f" 1: {t2-t1:5.2f}\n" # 0.20 sec: before.
2216 f" 2: {t3-t2:5.2f}\n" # 0.19 sec: setQtStyle (only once)
2217 f" 3: {t4-t3:5.2f}\n" # 0.00 sec: after.
2218 f"total: {t4-t1:5.2f}"
2219 )
2220 #@+node:ekr.20110605121601.18251: *5* qtFrame.createSplitterComponents
2221 def createSplitterComponents(self) -> None:
2223 c = self.c
2224 self.tree: Wrapper = qt_tree.LeoQtTree(c, self)
2225 self.log: Wrapper = LeoQtLog(self, None)
2226 self.body: Wrapper = LeoQtBody(self, None)
2227 self.splitVerticalFlag, ratio, secondary_ratio = self.initialRatios()
2228 self.resizePanesToRatio(ratio, secondary_ratio)
2229 #@+node:ekr.20190412044556.1: *5* qtFrame.setQtStyle
2230 def setQtStyle(self) -> None:
2231 """
2232 Set the default Qt style. Based on pyzo code.
2234 Copyright (C) 2013-2018, the Pyzo development team
2236 Pyzo is distributed under the terms of the (new) BSD License.
2237 The full license can be found in 'license.txt'.
2238 """
2239 # Fix #1936: very slow new command. Only do this once!
2240 if g.app.initStyleFlag:
2241 return
2242 g.app.initStyleFlag = True
2243 c = self.c
2244 trace = 'themes' in g.app.debug
2245 #
2246 # Get the requested style name.
2247 stylename = c.config.getString('qt-style-name') or ''
2248 if trace:
2249 g.trace(repr(stylename))
2250 if not stylename:
2251 return
2252 #
2253 # Return if the style does not exist.
2254 styles = [z.lower() for z in QtWidgets.QStyleFactory.keys()]
2255 if stylename.lower() not in styles:
2256 g.es_print(f"ignoring unknown Qt style name: {stylename!r}")
2257 g.printObj(styles)
2258 return
2259 #
2260 # Change the style and palette.
2261 app = g.app.gui.qtApp
2262 if isQt5 or isQt6:
2263 qstyle = app.setStyle(stylename)
2264 if not qstyle:
2265 g.es_print(f"failed to set Qt style name: {stylename!r}")
2266 else:
2267 QtWidgets.qApp.nativePalette = QtWidgets.qApp.palette()
2268 qstyle = QtWidgets.qApp.setStyle(stylename)
2269 if not qstyle:
2270 g.es_print(f"failed to set Qt style name: {stylename!r}")
2271 return
2272 app.setPalette(QtWidgets.qApp.nativePalette)
2273 #@+node:ekr.20110605121601.18252: *4* qtFrame.initCompleteHint
2274 def initCompleteHint(self) -> None:
2275 """A kludge: called to enable text changed events."""
2276 self.initComplete = True
2277 #@+node:ekr.20110605121601.18253: *4* Destroying the qtFrame
2278 #@+node:ekr.20110605121601.18254: *5* qtFrame.destroyAllObjects (not used)
2279 def destroyAllObjects(self) -> None:
2280 """Clear all links to objects in a Leo window."""
2281 c = self.c
2282 # g.printGcAll()
2283 # Do this first.
2284 #@+<< clear all vnodes in the tree >>
2285 #@+node:ekr.20110605121601.18255: *6* << clear all vnodes in the tree>> (qtFrame)
2286 vList = [z for z in c.all_unique_nodes()]
2287 for v in vList:
2288 g.clearAllIvars(v)
2289 vList = [] # Remove these references immediately.
2290 #@-<< clear all vnodes in the tree >>
2291 # Destroy all ivars in subcommanders.
2292 g.clearAllIvars(c.atFileCommands)
2293 if c.chapterController: # New in Leo 4.4.3 b1.
2294 g.clearAllIvars(c.chapterController)
2295 g.clearAllIvars(c.fileCommands)
2296 g.clearAllIvars(c.keyHandler) # New in Leo 4.4.3 b1.
2297 g.clearAllIvars(c.importCommands)
2298 g.clearAllIvars(c.tangleCommands)
2299 g.clearAllIvars(c.undoer)
2300 g.clearAllIvars(c)
2302 #@+node:ekr.20110605121601.18256: *5* qtFrame.destroySelf
2303 def destroySelf(self) -> None:
2304 # Remember these: we are about to destroy all of our ivars!
2305 c, top = self.c, self.top
2306 if hasattr(g.app.gui, 'frameFactory'):
2307 g.app.gui.frameFactory.deleteFrame(top)
2308 # Indicate that the commander is no longer valid.
2309 c.exists = False
2310 if 0: # We can't do this unless we unhook the event filter.
2311 # Destroys all the objects of the commander.
2312 self.destroyAllObjects()
2313 c.exists = False # Make sure this one ivar has not been destroyed.
2314 # print('destroySelf: qtFrame: %s' % c,g.callers(4))
2315 top.close()
2316 #@+node:ekr.20110605121601.18257: *3* qtFrame.class QtStatusLineClass
2317 class QtStatusLineClass:
2318 """A class representing the status line."""
2319 #@+others
2320 #@+node:ekr.20110605121601.18258: *4* QtStatusLineClass.ctor
2321 def __init__(self, c: Cmdr, parentFrame: Widget) -> None:
2322 """Ctor for LeoQtFrame class."""
2323 self.c = c
2324 self.statusBar = c.frame.top.statusBar
2325 self.lastFcol = 0
2326 self.lastRow = 0
2327 self.lastCol = 0
2328 # Create the text widgets.
2329 self.textWidget1 = w1 = QtWidgets.QLineEdit(self.statusBar)
2330 self.textWidget2 = w2 = QtWidgets.QLineEdit(self.statusBar)
2331 w1.setObjectName('status1')
2332 w2.setObjectName('status2')
2333 w1.setReadOnly(True)
2334 w2.setReadOnly(True)
2335 splitter = QtWidgets.QSplitter()
2336 self.statusBar.addWidget(splitter, True)
2337 sizes = c.config.getString('status-line-split-sizes') or '1 2'
2338 sizes = [int(i) for i in sizes.replace(',', ' ').split()]
2339 for n, i in enumerate(sizes):
2340 w = [w1, w2][n]
2341 policy = w.sizePolicy()
2342 policy.setHorizontalStretch(i)
2343 policy.setHorizontalPolicy(Policy.Minimum)
2344 w.setSizePolicy(policy)
2345 splitter.addWidget(w1)
2346 splitter.addWidget(w2)
2347 self.put('')
2348 self.update()
2349 #@+node:ekr.20110605121601.18260: *4* QtStatusLineClass.clear, get & put/1
2350 def clear(self) -> None:
2351 self.put('')
2353 def get(self) -> str:
2354 return self.textWidget2.text()
2356 def put(self, s: str, bg: str=None, fg: str=None) -> None:
2357 self.put_helper(s, self.textWidget2, bg, fg)
2359 def put1(self, s: str, bg: str=None, fg: str=None) -> None:
2360 self.put_helper(s, self.textWidget1, bg, fg)
2362 # Keys are widgets, values are stylesheets.
2363 styleSheetCache: Dict[Any, str] = {}
2365 def put_helper(self, s: str, w: Wrapper, bg: str=None, fg: str=None) -> None:
2366 """Put string s in the indicated widget, with proper colors."""
2367 c = self.c
2368 bg = bg or c.config.getColor('status-bg') or 'white'
2369 fg = fg or c.config.getColor('status-fg') or 'black'
2370 if True:
2371 # Work around #804. w is a QLineEdit.
2372 w.setStyleSheet(f"background: {bg}; color: {fg};")
2373 else:
2374 # Rather than put(msg, explicit_color, explicit_color) we should use
2375 # put(msg, status) where status is None, 'info', or 'fail'.
2376 # Just as a quick hack to avoid dealing with propagating those changes
2377 # back upstream, infer status like this:
2378 if (
2379 fg == c.config.getColor('find-found-fg') and
2380 bg == c.config.getColor('find-found-bg')
2381 ):
2382 status = 'info'
2383 elif (
2384 fg == c.config.getColor('find-not-found-fg') and
2385 bg == c.config.getColor('find-not-found-bg')
2386 ):
2387 status = 'fail'
2388 else:
2389 status = None
2390 d = self.styleSheetCache
2391 if status != d.get(w, '__undefined__'):
2392 d[w] = status
2393 c.styleSheetManager.mng.remove_sclass(w, ['info', 'fail'])
2394 c.styleSheetManager.mng.add_sclass(w, status)
2395 c.styleSheetManager.mng.update_view(w) # force appearance update
2396 w.setText(s)
2397 #@+node:chris.20180320072817.1: *4* QtStatusLineClass.update & helpers
2398 def update(self) -> None:
2399 if g.app.killed:
2400 return
2401 c, body = self.c, self.c.frame.body
2402 if not c.p:
2403 return
2404 te = body.widget
2405 if not isinstance(te, QtWidgets.QTextEdit):
2406 return
2407 cursor = te.textCursor()
2408 block = cursor.block()
2409 row = block.blockNumber() + 1
2410 col, fcol = self.compute_columns(block, cursor)
2411 words = len(c.p.b.split(None))
2412 self.put_status_line(col, fcol, row, words)
2413 self.lastRow = row
2414 self.lastCol = col
2415 self.lastFcol = fcol
2416 #@+node:ekr.20190118082646.1: *5* qstatus.compute_columns
2417 def compute_columns(self, block: Any, cursor: Any) -> Tuple[int, int]:
2419 c = self.c
2420 line = block.text()
2421 col = cursor.columnNumber()
2422 offset = c.p.textOffset()
2423 fcol_offset = 0
2424 s2 = line[0:col]
2425 col = g.computeWidth(s2, c.tab_width)
2426 #
2427 # #195: fcol when using @first directive is inaccurate
2428 i = line.find('<<')
2429 j = line.find('>>')
2430 if -1 < i < j or g.match_word(line.strip(), 0, '@others'):
2431 offset = None
2432 else:
2433 for tag in ('@first ', '@last '):
2434 if line.startswith(tag):
2435 fcol_offset = len(tag)
2436 break
2437 #
2438 # fcol is '' if there is no ancestor @<file> node.
2439 fcol = None if offset is None else max(0, col + offset - fcol_offset)
2440 return col, fcol
2441 #@+node:chris.20180320072817.2: *5* qstatus.file_line (not used)
2442 def file_line(self) -> Optional[Pos]:
2443 """
2444 Return the line of the first line of c.p in its external file.
2445 Return None if c.p is not part of an external file.
2446 """
2447 c, p = self.c, self.c.p
2448 if p:
2449 goto = gotoCommands.GoToCommands(c)
2450 return goto.find_node_start(p)
2451 return None
2452 #@+node:ekr.20190118082047.1: *5* qstatus.put_status_line
2453 def put_status_line(self, col: int, fcol: int, row: int, words: int) -> None:
2455 if 1:
2456 fcol_part = '' if fcol is None else f" fcol: {fcol}"
2457 # For now, it seems to0 difficult to get alignment *exactly* right.
2458 self.put1(f"line: {row:d} col: {col:d} {fcol_part} words: {words}")
2459 else:
2460 # #283 is not ready yet, and probably will never be.
2461 fline = self.file_line()
2462 fline = '' if fline is None else fline + row
2463 self.put1(
2464 f"fline: {fline:2} line: {row:2d} col: {col:2} fcol: {fcol:2}")
2465 #@-others
2466 #@+node:ekr.20110605121601.18262: *3* qtFrame.class QtIconBarClass
2467 class QtIconBarClass:
2468 """A class representing the singleton Icon bar"""
2469 #@+others
2470 #@+node:ekr.20110605121601.18263: *4* QtIconBar.ctor & reloadSettings
2471 def __init__(self, c: Cmdr, parentFrame: Widget) -> None:
2472 """Ctor for QtIconBarClass."""
2473 # Copy ivars
2474 self.c = c
2475 self.parentFrame = parentFrame
2476 # Status ivars.
2477 self.actions: List[Any] = []
2478 self.chapterController = None
2479 self.toolbar = self
2480 self.w = c.frame.top.iconBar # A QToolBar.
2481 self.reloadSettings()
2483 def reloadSettings(self) -> None:
2484 c = self.c
2485 c.registerReloadSettings(self)
2486 self.buttonColor = c.config.getString('qt-button-color')
2487 self.toolbar_orientation = c.config.getString('qt-toolbar-location')
2488 #@+node:ekr.20110605121601.18264: *4* QtIconBar.do-nothings
2489 # These *are* called from Leo's core.
2491 def addRow(self, height: int=None) -> None:
2492 pass # To do.
2494 def getNewFrame(self) -> None:
2495 return None # To do
2496 #@+node:ekr.20110605121601.18265: *4* QtIconBar.add
2497 def add(self, *args: Any, **keys: Any) -> Any:
2498 """Add a button to the icon bar."""
2499 c = self.c
2500 if not self.w:
2501 return None
2502 command: Callable = keys.get('command')
2503 text: str = keys.get('text')
2504 # able to specify low-level QAction directly (QPushButton not forced)
2505 qaction: Any = keys.get('qaction')
2506 if not text and not qaction:
2507 g.es('bad toolbar item')
2508 kind: str = keys.get('kind') or 'generic-button'
2509 # imagefile = keys.get('imagefile')
2510 # image = keys.get('image')
2513 class leoIconBarButton(QtWidgets.QWidgetAction): # type:ignore
2515 # toolbar is a QtIconBarClass object, not a QWidget.
2516 def __init__(self, parent: Widget, text: str, toolbar: Any) -> None:
2517 super().__init__(parent)
2518 self.button: Widget = None # set below
2519 self.text = text
2520 self.toolbar = toolbar
2522 def createWidget(self, parent: Widget) -> None:
2523 self.button = b = QtWidgets.QPushButton(self.text, parent)
2524 self.button.setProperty('button_kind', kind) # for styling
2525 return b
2527 action: Any
2528 if qaction is None:
2529 action = leoIconBarButton(parent=self.w, text=text, toolbar=self)
2530 button_name = text
2531 else:
2532 action = qaction
2533 button_name = action.text()
2534 self.w.addAction(action)
2535 self.actions.append(action)
2536 b = self.w.widgetForAction(action)
2537 # Set the button's object name so we can use the stylesheet to color it.
2538 if not button_name:
2539 button_name = 'unnamed'
2540 button_name = button_name + '-button'
2541 b.setObjectName(button_name)
2542 b.setContextMenuPolicy(ContextMenuPolicy.ActionsContextMenu)
2544 def delete_callback(checked: str, action: str=action) -> None:
2545 self.w.removeAction(action)
2547 b.leo_removeAction = rb = QAction('Remove Button', b)
2548 b.addAction(rb)
2549 rb.triggered.connect(delete_callback)
2550 if command:
2552 def button_callback(event: Event, c: Cmdr=c, command: Callable=command) -> None:
2553 val = command()
2554 if c.exists:
2555 # c.bodyWantsFocus()
2556 c.outerUpdate()
2557 return val
2559 b.clicked.connect(button_callback)
2560 return action
2561 #@+node:ekr.20110605121601.18266: *4* QtIconBar.addRowIfNeeded (not used)
2562 def addRowIfNeeded(self) -> None:
2563 """Add a new icon row if there are too many widgets."""
2564 # n = g.app.iconWidgetCount
2565 # if n >= self.widgets_per_row:
2566 # g.app.iconWidgetCount = 0
2567 # self.addRow()
2568 # g.app.iconWidgetCount += 1
2569 #@+node:ekr.20110605121601.18267: *4* QtIconBar.addWidget
2570 def addWidget(self, w: Wrapper) -> None:
2571 self.w.addWidget(w)
2572 #@+node:ekr.20110605121601.18268: *4* QtIconBar.clear
2573 def clear(self) -> None:
2574 """Destroy all the widgets in the icon bar"""
2575 self.w.clear()
2576 self.actions = []
2577 g.app.iconWidgetCount = 0
2578 #@+node:ekr.20110605121601.18269: *4* QtIconBar.createChaptersIcon
2579 def createChaptersIcon(self) -> Optional[Wrapper]:
2581 c = self.c
2582 f = c.frame
2583 if f.use_chapters and f.use_chapter_tabs:
2584 return LeoQtTreeTab(c, f.iconBar)
2585 return None
2586 #@+node:ekr.20110605121601.18270: *4* QtIconBar.deleteButton
2587 def deleteButton(self, w: Wrapper) -> None:
2588 """ w is button """
2589 self.w.removeAction(w)
2590 self.c.bodyWantsFocus()
2591 self.c.outerUpdate()
2592 #@+node:ekr.20141031053508.14: *4* QtIconBar.goto_command
2593 def goto_command(self, controller: Any, gnx: str) -> None:
2594 """
2595 Select the node corresponding to the given gnx.
2596 controller is a ScriptingController instance.
2597 """
2598 # Fix bug 74: command_p may be in another outline.
2599 c = self.c
2600 c2, p = controller.open_gnx(c, gnx)
2601 if p:
2602 assert c2.positionExists(p)
2603 if c == c2:
2604 c2.selectPosition(p)
2605 else:
2606 g.app.selectLeoWindow(c2)
2607 # Fix #367: Process events before selecting.
2608 g.app.gui.qtApp.processEvents()
2609 c2.selectPosition(p)
2610 else:
2611 g.trace('not found', gnx)
2612 #@+node:ekr.20110605121601.18271: *4* QtIconBar.setCommandForButton (@rclick nodes) & helper
2613 # qtFrame.QtIconBarClass.setCommandForButton
2614 # Controller is a ScriptingController.
2616 def setCommandForButton(self,
2617 button: Any, command: Callable, command_p: Pos, controller: Any, gnx: str, script: str,
2618 ) -> None:
2619 """
2620 Set the "Goto Script" rlick item of an @button button.
2621 Called from mod_scripting.py plugin.
2623 button is a leoIconBarButton.
2624 command is a callback, defined in mod_scripting.py.
2625 command_p exists only if the @button node exists in the local .leo file.
2626 gnx is the gnx of the @button node.
2627 script is a static script for common @button nodes.
2628 """
2629 if not command:
2630 return
2631 b = button.button
2632 b.clicked.connect(command)
2634 def goto_callback(checked: str, controller: Any=controller, gnx: str=gnx) -> None:
2635 self.goto_command(controller, gnx)
2637 b.goto_script = gts = QAction('Goto Script', b)
2638 b.addAction(gts)
2639 gts.triggered.connect(goto_callback)
2640 rclicks = build_rclick_tree(command_p, top_level=True)
2641 self.add_rclick_menu(b, rclicks, controller, script=script)
2642 #@+node:ekr.20141031053508.15: *5* add_rclick_menu (QtIconBarClass)
2643 def add_rclick_menu(
2644 self,
2645 action_container: Any,
2646 rclicks: str,
2647 controller: Cmdr,
2648 top_level: bool=True,
2649 button: str=None,
2650 script: str=None,
2651 ) -> None:
2652 c = controller.c
2653 top_offset = -2 # insert before the remove button and goto script items
2654 if top_level:
2655 button = action_container
2656 for rc in rclicks:
2657 # pylint: disable=cell-var-from-loop
2658 headline = rc.position.h[8:].strip()
2659 act = QAction(headline, action_container)
2660 if '---' in headline and headline.strip().strip('-') == '':
2661 act.setSeparator(True)
2662 elif rc.position.b.strip():
2664 def cb(checked: str, p: str=rc.position, button: str=button) -> None:
2665 controller.executeScriptFromButton(
2666 b=button,
2667 buttonText=p.h[8:].strip(),
2668 p=p,
2669 script=script,
2670 )
2671 if c.exists:
2672 c.outerUpdate()
2674 act.triggered.connect(cb)
2675 else: # recurse submenu
2676 sub_menu = QtWidgets.QMenu(action_container)
2677 act.setMenu(sub_menu)
2678 self.add_rclick_menu(sub_menu, rc.children, controller,
2679 top_level=False, button=button)
2680 if top_level:
2681 # insert act before Remove Button
2682 action_container.insertAction(
2683 action_container.actions()[top_offset], act)
2684 else:
2685 action_container.addAction(act)
2686 if top_level and rclicks:
2687 act = QAction('---', action_container)
2688 act.setSeparator(True)
2689 action_container.insertAction(
2690 action_container.actions()[top_offset], act)
2691 action_container.setText(
2692 action_container.text() +
2693 (c.config.getString('mod-scripting-subtext') or '')
2694 )
2695 #@-others
2696 #@+node:ekr.20110605121601.18274: *3* qtFrame.Configuration
2697 #@+node:ekr.20110605121601.18275: *4* qtFrame.configureBar
2698 def configureBar(self, bar: Wrapper, verticalFlag: bool) -> None:
2699 c = self.c
2700 # Get configuration settings.
2701 w = c.config.getInt("split-bar-width")
2702 if not w or w < 1:
2703 w = 7
2704 relief = c.config.get("split_bar_relief", "relief")
2705 if not relief:
2706 relief = "flat"
2707 color = c.config.getColor("split-bar-color")
2708 if not color:
2709 color = "LightSteelBlue2"
2710 try:
2711 if verticalFlag:
2712 # Panes arranged vertically; horizontal splitter bar
2713 bar.configure(
2714 relief=relief, height=w, bg=color, cursor="sb_v_double_arrow")
2715 else:
2716 # Panes arranged horizontally; vertical splitter bar
2717 bar.configure(
2718 relief=relief, width=w, bg=color, cursor="sb_h_double_arrow")
2719 except Exception:
2720 # Could be a user error. Use all defaults
2721 g.es("exception in user configuration for splitbar")
2722 g.es_exception()
2723 if verticalFlag:
2724 # Panes arranged vertically; horizontal splitter bar
2725 bar.configure(height=7, cursor="sb_v_double_arrow")
2726 else:
2727 # Panes arranged horizontally; vertical splitter bar
2728 bar.configure(width=7, cursor="sb_h_double_arrow")
2729 #@+node:ekr.20110605121601.18276: *4* qtFrame.configureBarsFromConfig
2730 def configureBarsFromConfig(self) -> None:
2731 c = self.c
2732 w = c.config.getInt("split-bar-width")
2733 if not w or w < 1:
2734 w = 7
2735 relief = c.config.get("split_bar_relief", "relief")
2736 if not relief or relief == "":
2737 relief = "flat"
2738 color = c.config.getColor("split-bar-color")
2739 if not color or color == "":
2740 color = "LightSteelBlue2"
2741 if self.splitVerticalFlag:
2742 bar1, bar2 = self.bar1, self.bar2
2743 else:
2744 bar1, bar2 = self.bar2, self.bar1
2745 try:
2746 bar1.configure(relief=relief, height=w, bg=color)
2747 bar2.configure(relief=relief, width=w, bg=color)
2748 except Exception:
2749 # Could be a user error.
2750 g.es("exception in user configuration for splitbar")
2751 g.es_exception()
2752 #@+node:ekr.20110605121601.18277: *4* qtFrame.reconfigureFromConfig
2753 def reconfigureFromConfig(self) -> None:
2754 """Init the configuration of the Qt frame from settings."""
2755 c, frame = self.c, self
2756 frame.configureBarsFromConfig()
2757 frame.setTabWidth(c.tab_width)
2758 c.redraw()
2759 #@+node:ekr.20110605121601.18278: *4* qtFrame.setInitialWindowGeometry
2760 def setInitialWindowGeometry(self) -> None:
2761 """Set the position and size of the frame to config params."""
2762 c = self.c
2763 h = c.config.getInt("initial-window-height") or 500
2764 w = c.config.getInt("initial-window-width") or 600
2765 x = c.config.getInt("initial-window-left") or 50 # #1190: was 10
2766 y = c.config.getInt("initial-window-top") or 50 # #1190: was 10
2767 if h and w and x and y:
2768 if 'size' in g.app.debug:
2769 g.trace(w, h, x, y)
2770 self.setTopGeometry(w, h, x, y)
2771 #@+node:ekr.20110605121601.18279: *4* qtFrame.setTabWidth
2772 def setTabWidth(self, w: Wrapper) -> None:
2773 # A do-nothing because tab width is set automatically.
2774 # It *is* called from Leo's core.
2775 pass
2776 #@+node:ekr.20110605121601.18280: *4* qtFrame.forceWrap & setWrap
2777 def forceWrap(self, p: Pos=None) -> None:
2778 self.c.frame.body.forceWrap(p)
2780 def setWrap(self, p: Pos=None) -> None:
2781 self.c.frame.body.setWrap(p)
2782 #@+node:ekr.20110605121601.18281: *4* qtFrame.reconfigurePanes
2783 def reconfigurePanes(self) -> None:
2784 c, f = self.c, self
2785 if f.splitVerticalFlag:
2786 r = c.config.getRatio("initial-vertical-ratio")
2787 if r is None or r < 0.0 or r > 1.0:
2788 r = 0.5
2789 r2 = c.config.getRatio("initial-vertical-secondary-ratio")
2790 if r2 is None or r2 < 0.0 or r2 > 1.0:
2791 r2 = 0.8
2792 else:
2793 r = c.config.getRatio("initial-horizontal-ratio")
2794 if r is None or r < 0.0 or r > 1.0:
2795 r = 0.3
2796 r2 = c.config.getRatio("initial-horizontal-secondary-ratio")
2797 if r2 is None or r2 < 0.0 or r2 > 1.0:
2798 r2 = 0.8
2799 f.resizePanesToRatio(r, r2)
2800 #@+node:ekr.20110605121601.18282: *4* qtFrame.resizePanesToRatio
2801 def resizePanesToRatio(self, ratio: float, ratio2: float) -> None:
2802 """Resize splitter1 and splitter2 using the given ratios."""
2803 # pylint: disable=arguments-differ
2804 self.divideLeoSplitter1(ratio)
2805 self.divideLeoSplitter2(ratio2)
2806 #@+node:ekr.20110605121601.18283: *4* qtFrame.divideLeoSplitter1/2
2807 def divideLeoSplitter1(self, frac: float) -> None:
2808 """Divide the main splitter."""
2809 layout = self.c and self.c.free_layout
2810 if not layout:
2811 return
2812 w = layout.get_main_splitter()
2813 if w:
2814 self.divideAnySplitter(frac, w)
2816 def divideLeoSplitter2(self, frac: float) -> None:
2817 """Divide the secondary splitter."""
2818 layout = self.c and self.c.free_layout
2819 if not layout:
2820 return
2821 w = layout.get_secondary_splitter()
2822 if w:
2823 self.divideAnySplitter(frac, w)
2824 #@+node:ekr.20110605121601.18284: *4* qtFrame.divideAnySplitter
2825 # This is the general-purpose placer for splitters.
2826 # It is the only general-purpose splitter code in Leo.
2828 def divideAnySplitter(self, frac: float, splitter: Wrapper) -> None:
2829 """Set the splitter sizes."""
2830 sizes = splitter.sizes()
2831 if len(sizes) != 2:
2832 g.trace(f"{len(sizes)} widget(s) in {id(splitter)}")
2833 return
2834 if frac > 1 or frac < 0:
2835 g.trace(f"split ratio [{frac}] out of range 0 <= frac <= 1")
2836 return
2837 s1, s2 = sizes
2838 s = s1 + s2
2839 s1 = int(s * frac + 0.5)
2840 s2 = s - s1
2841 splitter.setSizes([s1, s2])
2842 #@+node:ekr.20110605121601.18285: *3* qtFrame.Event handlers
2843 #@+node:ekr.20110605121601.18286: *4* qtFrame.OnCloseLeoEvent
2844 # Called from quit logic and when user closes the window.
2845 # Returns True if the close happened.
2847 def OnCloseLeoEvent(self) -> None:
2848 f = self
2849 c = f.c
2850 if c.inCommand:
2851 c.requestCloseWindow = True
2852 else:
2853 g.app.closeLeoWindow(self)
2854 #@+node:ekr.20110605121601.18287: *4* qtFrame.OnControlKeyUp/Down
2855 def OnControlKeyDown(self, event: Event=None) -> None:
2856 self.controlKeyIsDown = True
2858 def OnControlKeyUp(self, event: Event=None) -> None:
2859 self.controlKeyIsDown = False
2860 #@+node:ekr.20110605121601.18290: *4* qtFrame.OnActivateTree
2861 def OnActivateTree(self, event: Event=None) -> None:
2862 pass
2863 #@+node:ekr.20110605121601.18293: *3* qtFrame.Gui-dependent commands
2864 #@+node:ekr.20110605121601.18301: *4* qtFrame.Window Menu...
2865 #@+node:ekr.20110605121601.18302: *5* qtFrame.toggleActivePane
2866 @frame_cmd('toggle-active-pane')
2867 def toggleActivePane(self, event: Event=None) -> None:
2868 """Toggle the focus between the outline and body panes."""
2869 frame = self
2870 c = frame.c
2871 w = c.get_focus()
2872 w_name = g.app.gui.widget_name(w)
2873 if w_name in ('canvas', 'tree', 'treeWidget'):
2874 c.endEditing()
2875 c.bodyWantsFocus()
2876 else:
2877 c.treeWantsFocus()
2878 #@+node:ekr.20110605121601.18303: *5* qtFrame.cascade
2879 @frame_cmd('cascade-windows')
2880 def cascade(self, event: Event=None) -> None:
2881 """Cascade all Leo windows."""
2882 x, y, delta = 50, 50, 50
2883 for frame in g.app.windowList:
2884 w = frame and frame.top
2885 if w:
2886 r = w.geometry() # a Qt.Rect
2887 # 2011/10/26: Fix bug 823601: cascade-windows fails.
2888 w.setGeometry(QtCore.QRect(x, y, r.width(), r.height()))
2889 # Compute the new offsets.
2890 x += 30
2891 y += 30
2892 if x > 200:
2893 x = 10 + delta
2894 y = 40 + delta
2895 delta += 10
2896 #@+node:ekr.20110605121601.18304: *5* qtFrame.equalSizedPanes
2897 @frame_cmd('equal-sized-panes')
2898 def equalSizedPanes(self, event: Event=None) -> None:
2899 """Make the outline and body panes have the same size."""
2900 self.resizePanesToRatio(0.5, self.secondary_ratio)
2901 #@+node:ekr.20110605121601.18305: *5* qtFrame.hideLogWindow
2902 def hideLogWindow(self, event: Event=None) -> None:
2903 """Hide the log pane."""
2904 self.divideLeoSplitter2(0.99)
2905 #@+node:ekr.20110605121601.18306: *5* qtFrame.minimizeAll
2906 @frame_cmd('minimize-all')
2907 def minimizeAll(self, event: Event=None) -> None:
2908 """Minimize all Leo's windows."""
2909 for frame in g.app.windowList:
2910 self.minimize(frame)
2912 def minimize(self, frame: Widget) -> None:
2913 # This unit test will fail when run externally.
2914 if frame and frame.top:
2915 w = frame.top.leo_master or frame.top
2916 if g.unitTesting:
2917 assert hasattr(w, 'setWindowState'), w
2918 else:
2919 w.setWindowState(WindowState.WindowMinimized)
2920 #@+node:ekr.20110605121601.18307: *5* qtFrame.toggleSplitDirection
2921 @frame_cmd('toggle-split-direction')
2922 def toggleSplitDirection(self, event: Event=None) -> None:
2923 """Toggle the split direction in the present Leo window."""
2924 if hasattr(self.c, 'free_layout'):
2925 self.c.free_layout.get_top_splitter().rotate()
2926 #@+node:ekr.20110605121601.18308: *5* qtFrame.resizeToScreen
2927 @frame_cmd('resize-to-screen')
2928 def resizeToScreen(self, event: Event=None) -> None:
2929 """Resize the Leo window so it fill the entire screen."""
2930 frame = self
2931 # This unit test will fail when run externally.
2932 if frame and frame.top:
2933 # frame.top.leo_master is a LeoTabbedTopLevel.
2934 # frame.top is a DynamicWindow.
2935 w = frame.top.leo_master or frame.top
2936 if g.unitTesting:
2937 assert hasattr(w, 'setWindowState'), w
2938 else:
2939 w.setWindowState(WindowState.WindowMaximized)
2940 #@+node:ekr.20110605121601.18309: *4* qtFrame.Help Menu...
2941 #@+node:ekr.20110605121601.18310: *5* qtFrame.leoHelp
2942 @frame_cmd('open-offline-tutorial')
2943 def leoHelp(self, event: Event=None) -> None:
2944 """Open Leo's offline tutorial."""
2945 frame = self
2946 c = frame.c
2947 theFile = g.os_path_join(g.app.loadDir, "..", "doc", "sbooks.chm")
2948 if g.os_path_exists(theFile) and sys.platform.startswith('win'):
2949 # pylint: disable=no-member
2950 os.startfile(theFile)
2951 else:
2952 answer = g.app.gui.runAskYesNoDialog(c,
2953 "Download Tutorial?",
2954 "Download tutorial (sbooks.chm) from SourceForge?")
2955 if answer == "yes":
2956 try:
2957 url = "http://prdownloads.sourceforge.net/leo/sbooks.chm?download"
2958 import webbrowser
2959 os.chdir(g.app.loadDir)
2960 webbrowser.open_new(url)
2961 except Exception:
2962 if 0:
2963 g.es("exception downloading", "sbooks.chm")
2964 g.es_exception()
2965 #@+node:ekr.20160424080647.1: *3* qtFrame.Properties
2966 # The ratio and secondary_ratio properties are read-only.
2967 #@+node:ekr.20160424080815.2: *4* qtFrame.ratio property
2968 def __get_ratio(self) -> float:
2969 """Return splitter ratio of the main splitter."""
2970 c = self.c
2971 free_layout = c.free_layout
2972 if free_layout:
2973 w = free_layout.get_main_splitter()
2974 if w:
2975 aList = w.sizes()
2976 if len(aList) == 2:
2977 n1, n2 = aList
2978 # 2017/06/07: guard against division by zero.
2979 ratio = 0.5 if n1 + n2 == 0 else float(n1) / float(n1 + n2)
2980 return ratio
2981 return 0.5
2983 ratio = property(
2984 __get_ratio, # No setter.
2985 doc="qtFrame.ratio property")
2986 #@+node:ekr.20160424080815.3: *4* qtFrame.secondary_ratio property
2987 def __get_secondary_ratio(self) -> float:
2988 """Return the splitter ratio of the secondary splitter."""
2989 c = self.c
2990 free_layout = c.free_layout
2991 if free_layout:
2992 w = free_layout.get_secondary_splitter()
2993 if w:
2994 aList = w.sizes()
2995 if len(aList) == 2:
2996 n1, n2 = aList
2997 ratio = float(n1) / float(n1 + n2)
2998 return ratio
2999 return 0.5
3001 secondary_ratio = property(
3002 __get_secondary_ratio, # no setter.
3003 doc="qtFrame.secondary_ratio property")
3004 #@+node:ekr.20110605121601.18311: *3* qtFrame.Qt bindings...
3005 #@+node:ekr.20190611053431.1: *4* qtFrame.bringToFront
3006 def bringToFront(self) -> None:
3007 if 'size' in g.app.debug:
3008 g.trace()
3009 self.lift()
3010 #@+node:ekr.20190611053431.2: *4* qtFrame.deiconify
3011 def deiconify(self) -> None:
3012 """Undo --minimized"""
3013 if 'size' in g.app.debug:
3014 g.trace(
3015 'top:', bool(self.top),
3016 'isMinimized:', self.top and self.top.isMinimized())
3017 if self.top and self.top.isMinimized(): # Bug fix: 400739.
3018 self.lift()
3019 #@+node:ekr.20190611053431.4: *4* qtFrame.get_window_info
3020 def get_window_info(self) -> Tuple[int, int, int, int]:
3021 """Return the geometry of the top window."""
3022 if getattr(self.top, 'leo_master', None):
3023 f = self.top.leo_master
3024 else:
3025 f = self.top
3026 rect = f.geometry()
3027 topLeft = rect.topLeft()
3028 x, y = topLeft.x(), topLeft.y()
3029 w, h = rect.width(), rect.height()
3030 if 'size' in g.app.debug:
3031 g.trace('\n', w, h, x, y)
3032 return w, h, x, y
3033 #@+node:ekr.20190611053431.3: *4* qtFrame.getFocus
3034 def getFocus(self) -> None:
3035 return g.app.gui.get_focus(self.c) # Bug fix: 2009/6/30.
3036 #@+node:ekr.20190611053431.7: *4* qtFrame.getTitle
3037 def getTitle(self) -> None:
3038 # Fix https://bugs.launchpad.net/leo-editor/+bug/1194209
3039 # For qt, leo_master (a LeoTabbedTopLevel) contains the QMainWindow.
3040 w = self.top.leo_master
3041 return w.windowTitle()
3042 #@+node:ekr.20190611053431.5: *4* qtFrame.iconify
3043 def iconify(self) -> None:
3044 if 'size' in g.app.debug:
3045 g.trace(bool(self.top))
3046 if self.top:
3047 self.top.showMinimized()
3048 #@+node:ekr.20190611053431.6: *4* qtFrame.lift
3049 def lift(self) -> None:
3050 if 'size' in g.app.debug:
3051 g.trace(bool(self.top), self.top and self.top.isMinimized())
3052 if not self.top:
3053 return
3054 if self.top.isMinimized(): # Bug 379141
3055 self.top.showNormal()
3056 self.top.activateWindow()
3057 self.top.raise_()
3058 #@+node:ekr.20190611053431.8: *4* qtFrame.setTitle
3059 def setTitle(self, s: str) -> None:
3060 # pylint: disable=arguments-differ
3061 if self.top:
3062 # Fix https://bugs.launchpad.net/leo-editor/+bug/1194209
3063 # When using tabs, leo_master (a LeoTabbedTopLevel) contains the QMainWindow.
3064 w = self.top.leo_master
3065 w.setWindowTitle(s)
3066 #@+node:ekr.20190611053431.9: *4* qtFrame.setTopGeometry
3067 def setTopGeometry(self, w: int, h: int, x: int, y: int) -> None:
3068 # self.top is a DynamicWindow.
3069 if self.top:
3070 if 'size' in g.app.debug:
3071 g.trace(w, h, x, y, self.c.shortFileName(), g.callers())
3072 self.top.setGeometry(QtCore.QRect(x, y, w, h))
3073 #@+node:ekr.20190611053431.10: *4* qtFrame.update
3074 def update(self, *args: Any, **keys: Any) -> None:
3075 if 'size' in g.app.debug:
3076 g.trace(bool(self.top))
3077 self.top.update()
3078 #@-others
3079#@+node:ekr.20110605121601.18312: ** class LeoQtLog (LeoLog)
3080class LeoQtLog(leoFrame.LeoLog):
3081 """A class that represents the log pane of a Qt window."""
3082 #@+others
3083 #@+node:ekr.20110605121601.18313: *3* LeoQtLog.Birth
3084 #@+node:ekr.20110605121601.18314: *4* LeoQtLog.__init__ & reloadSettings
3085 def __init__(self, frame: Wrapper, parentFrame: Widget) -> None:
3086 """Ctor for LeoQtLog class."""
3087 super().__init__(frame, parentFrame) # Calls createControl.
3088 # Set in finishCreate.
3089 # Important: depending on the log *tab*,
3090 # logCtrl may be either a wrapper or a widget.
3091 assert self.logCtrl is None, self.logCtrl # type:ignore
3092 self.c = c = frame.c # Also set in the base constructor, but we need it here.
3093 self.contentsDict: Dict[str, Widget] = {} # Keys are tab names. Values are widgets.
3094 self.eventFilters: List = [] # Apparently needed to make filters work!
3095 self.logCtrl: Any = None
3096 self.logDict: Dict[str, Widget] = {} # Keys are tab names; values are the widgets.
3097 self.logWidget: Widget = None # Set in finishCreate.
3098 self.menu: Widget = None # A menu that pops up on right clicks in the hull or in tabs.
3099 self.tabWidget: Widget = c.frame.top.tabWidget # The Qt.QTabWidget that holds all the tabs.
3100 tw = self.tabWidget
3102 # Bug 917814: Switching Log Pane tabs is done incompletely.
3103 tw.currentChanged.connect(self.onCurrentChanged)
3104 if 0: # Not needed to make onActivateEvent work.
3105 # Works only for .tabWidget, *not* the individual tabs!
3106 theFilter = qt_events.LeoQtEventFilter(c, w=tw, tag='tabWidget')
3107 tw.installEventFilter(theFilter)
3108 # Partial fix for bug 1251755: Log-pane refinements
3109 tw.setMovable(True)
3110 self.reloadSettings()
3112 def reloadSettings(self) -> None:
3113 c = self.c
3114 self.wrap = bool(c.config.getBool('log-pane-wraps'))
3115 #@+node:ekr.20110605121601.18315: *4* LeoQtLog.finishCreate
3116 def finishCreate(self) -> None:
3117 """Finish creating the LeoQtLog class."""
3118 c, log, w = self.c, self, self.tabWidget
3119 #
3120 # Create the log tab as the leftmost tab.
3121 log.createTab('Log')
3122 self.logWidget = self.contentsDict.get('Log')
3123 logWidget = self.logWidget
3124 logWidget.setWordWrapMode(WrapMode.WordWrap if self.wrap else WrapMode.NoWrap)
3125 w.insertTab(0, logWidget, 'Log') # Required.
3126 #
3127 # set up links in log handling
3128 logWidget.setTextInteractionFlags(
3129 TextInteractionFlag.LinksAccessibleByMouse
3130 | TextInteractionFlag.TextEditable
3131 | TextInteractionFlag.TextSelectableByMouse
3132 )
3133 logWidget.setOpenLinks(False)
3134 logWidget.setOpenExternalLinks(False)
3135 logWidget.anchorClicked.connect(self.linkClicked)
3136 #
3137 # Show the spell tab.
3138 c.spellCommands.openSpellTab()
3139 #
3140 #794: Clicking Find Tab should do exactly what pushing Ctrl-F does
3142 def tab_callback(index: str) -> None:
3143 name = w.tabText(index)
3144 if name == 'Find':
3145 c.findCommands.startSearch(event=None)
3147 w.currentChanged.connect(tab_callback)
3148 # #1286.
3149 w.customContextMenuRequested.connect(self.onContextMenu)
3150 #@+node:ekr.20110605121601.18316: *4* LeoQtLog.getName
3151 def getName(self) -> str:
3152 return 'log' # Required for proper pane bindings.
3153 #@+node:ekr.20150717102728.1: *3* LeoQtLog: clear-log & dump-log
3154 @log_cmd('clear-log')
3155 @log_cmd('log-clear')
3156 def clearLog(self, event: Event=None) -> None:
3157 """Clear the log pane."""
3158 # self.logCtrl may be either a wrapper or a widget.
3159 w = self.logCtrl.widget # type:ignore
3160 if w:
3161 w.clear()
3163 @log_cmd('dump-log')
3164 @log_cmd('log-dump')
3165 def dumpLog(self, event: Event=None) -> None:
3166 """Clear the log pane."""
3167 # self.logCtrl may be either a wrapper or a widget.
3168 w = self.logCtrl.widget # type:ignore
3169 if not w:
3170 return
3172 fn = self.c.shortFileName()
3173 printable = string.ascii_letters + string.digits + string.punctuation + ' '
3175 def dump(s: str) -> str:
3176 return ''.join(c if c in printable else r'\x{0:02x}'.format(ord(c)) for c in s)
3178 g.printObj([dump(z) for z in w.toPlainText().split('\n')], tag=f"{fn}: w.toPlainText")
3179 g.printObj([f"{dump(z)}<br />" for z in w.toHtml().split('<br />')], tag=f"{fn}: w.toHtml")
3182 #@+node:ekr.20110605121601.18333: *3* LeoQtLog.color tab stuff
3183 def createColorPicker(self, tabName: str) -> None:
3184 g.warning('color picker not ready for qt')
3185 #@+node:ekr.20110605121601.18334: *3* LeoQtLog.font tab stuff
3186 #@+node:ekr.20110605121601.18335: *4* LeoQtLog.createFontPicker
3187 def createFontPicker(self, tabName: str) -> None:
3188 # log = self
3189 font, ok = QtWidgets.QFontDialog.getFont()
3190 if not (font and ok):
3191 return
3192 style = font.style()
3193 table1 = (
3194 (Style.StyleNormal, 'normal'), # #2330.
3195 (Style.StyleItalic, 'italic'),
3196 (Style.StyleOblique, 'oblique'))
3197 for val, name in table1:
3198 if style == val:
3199 style = name
3200 break
3201 else:
3202 style = ''
3203 weight = font.weight()
3204 table2 = (
3205 (Weight.Light, 'light'), # #2330.
3206 (Weight.Normal, 'normal'),
3207 (Weight.DemiBold, 'demibold'),
3208 (Weight.Bold, 'bold'),
3209 (Weight.Black, 'black'))
3210 for val2, name2 in table2:
3211 if weight == val2:
3212 weight = name2
3213 break
3214 else:
3215 weight = ''
3216 table3 = (
3217 ('family', str(font.family())),
3218 ('size ', font.pointSize()),
3219 ('style ', style),
3220 ('weight', weight),
3221 )
3222 for key3, val3 in table3:
3223 if val3:
3224 g.es(key3, val3, tabName='Fonts')
3225 #@+node:ekr.20110605121601.18339: *4* LeoQtLog.hideFontTab
3226 def hideFontTab(self, event: Event=None) -> None:
3227 c = self.c
3228 c.frame.log.selectTab('Log')
3229 c.bodyWantsFocus()
3230 #@+node:ekr.20111120124732.10184: *3* LeoQtLog.isLogWidget
3231 def isLogWidget(self, w: Wrapper) -> bool:
3232 val = w == self or w in list(self.contentsDict.values())
3233 return val
3234 #@+node:tbnorth.20171220123648.1: *3* LeoQtLog.linkClicked
3235 def linkClicked(self, link: str) -> None:
3236 """linkClicked - link clicked in log
3238 :param QUrl link: link that was clicked
3239 """
3240 # see addition of '/' in LeoQtLog.put()
3241 url = s = g.toUnicode(link.toString())
3242 if platform.system() == 'Windows':
3243 for scheme in 'file', 'unl':
3244 if s.startswith(scheme + ':///') and s[len(scheme) + 5] == ':':
3245 url = s.replace(':///', '://', 1)
3246 break
3247 g.handleUrl(url, c=self.c)
3248 #@+node:ekr.20120304214900.9940: *3* LeoQtLog.onCurrentChanged
3249 def onCurrentChanged(self, idx: int) -> None:
3251 tabw = self.tabWidget
3252 w = tabw.widget(idx)
3253 #
3254 # #917814: Switching Log Pane tabs is done incompletely
3255 wrapper: Wrapper = getattr(w, 'leo_log_wrapper', None)
3256 #
3257 # #1161: Don't change logs unless the wrapper is correct.
3258 if wrapper and isinstance(wrapper, qt_text.QTextEditWrapper):
3259 self.logCtrl = wrapper
3260 #@+node:ekr.20200304132424.1: *3* LeoQtLog.onContextMenu
3261 def onContextMenu(self, point: Any) -> None:
3262 """LeoQtLog: Callback for customContextMenuRequested events."""
3263 # #1286.
3264 c, w = self.c, self
3265 g.app.gui.onContextMenu(c, w, point)
3266 #@+node:ekr.20110605121601.18321: *3* LeoQtLog.put and helpers
3267 #@+node:ekr.20110605121601.18322: *4* LeoQtLog.put & helper
3268 def put(self, s: str, color: str=None, tabName: str='Log', from_redirect: bool=False, nodeLink: str=None) -> None:
3269 """
3270 Put s to the Qt Log widget, converting to html.
3271 All output to the log stream eventually comes here.
3273 The from_redirect keyword argument is no longer used.
3274 """
3275 c = self.c
3276 if g.app.quitting or not c or not c.exists:
3277 return
3278 #
3279 # *Note*: For reasons that I don't fully understand,
3280 # all lines sent to the log must now end in a newline.
3281 #
3282 s = s.rstrip() + '\n'
3283 color = self.resolve_color(color)
3284 self.selectTab(tabName or 'Log')
3285 # Must be done after the call to selectTab.
3286 wrapper = self.logCtrl
3287 if not isinstance(wrapper, qt_text.QTextEditWrapper):
3288 g.trace('BAD wrapper', wrapper.__class__.__name__)
3289 return
3290 w = wrapper.widget
3291 if not isinstance(w, QtWidgets.QTextEdit):
3292 g.trace('BAD widget', w.__class__.__name__)
3293 return
3294 sb = w.horizontalScrollBar()
3295 s = self.to_html(color, s)
3296 if nodeLink:
3297 url = nodeLink
3298 for scheme in 'file', 'unl':
3299 # QUrl requires paths start with '/'
3300 if (
3301 url.startswith(scheme + '://') and not
3302 url.startswith(scheme + ':///')
3303 ):
3304 url = url.replace('://', ':///', 1)
3305 s = f'<a href="{url}" title="{nodeLink}">{s}</a>'
3306 w.insertHtml(s)
3307 w.moveCursor(MoveOperation.End)
3308 sb.setSliderPosition(0) # Force the slider to the initial position.
3309 w.repaint() # Slow, but essential.
3310 #@+node:ekr.20220411085334.1: *5* LeoQtLog.to_html
3311 def to_html(self, color: str, s: str) -> str:
3312 """Convert s to html."""
3313 s = s.replace('&', '&').replace('<', '<').replace('>', '>')
3314 # #884: Always convert leading blanks and tabs to  .
3315 n = len(s) - len(s.lstrip())
3316 if n > 0 and s.strip():
3317 s = ' ' * (n) + s[n:]
3318 if not self.wrap:
3319 # Convert all other blanks to
3320 s = s.replace(' ', ' ')
3321 s = s.replace('\n', '<br>') # The caller is responsible for newlines!
3322 s = f'<font color="{color}">{s}</font>'
3323 return s
3324 #@+node:ekr.20110605121601.18323: *4* LeoQtLog.putnl
3325 def putnl(self, tabName: str='Log') -> None:
3326 """Put a newline to the Qt log."""
3327 #
3328 # This is not called normally.
3329 if g.app.quitting:
3330 return
3331 if tabName:
3332 self.selectTab(tabName)
3333 wrapper = self.logCtrl
3334 if not isinstance(wrapper, qt_text.QTextEditWrapper):
3335 g.trace('BAD wrapper', wrapper.__class__.__name__)
3336 return
3337 w = wrapper.widget
3338 if not isinstance(w, QtWidgets.QTextEdit):
3339 g.trace('BAD widget', w.__class__.__name__)
3340 return
3341 sb = w.horizontalScrollBar()
3342 pos = sb.sliderPosition()
3343 # Not needed!
3344 # contents = w.toHtml()
3345 # w.setHtml(contents + '\n')
3346 w.moveCursor(MoveOperation.End)
3347 sb.setSliderPosition(pos)
3348 w.repaint() # Slow, but essential.
3349 #@+node:ekr.20220411085427.1: *4* LeoQtLog.resolve_color
3350 def resolve_color(self, color: str) -> str:
3351 """Resolve the given color name to an actual color name."""
3352 c = self.c
3353 # Note: g.actualColor does all color translation.
3354 if color:
3355 color = leoColor.getColor(color)
3356 if not color:
3357 # #788: First, fall back to 'log_black_color', not 'black.
3358 color = c.config.getColor('log-black-color')
3359 if not color:
3360 # Should never be necessary.
3361 color = 'black'
3362 return color
3363 #@+node:ekr.20150205181818.5: *4* LeoQtLog.scrollToEnd
3364 def scrollToEnd(self, tabName: str='Log') -> None:
3365 """Scroll the log to the end."""
3366 if g.app.quitting:
3367 return
3368 if tabName:
3369 self.selectTab(tabName)
3370 w = self.logCtrl.widget
3371 if not w:
3372 return
3373 sb = w.horizontalScrollBar()
3374 pos = sb.sliderPosition()
3375 w.moveCursor(MoveOperation.End)
3376 sb.setSliderPosition(pos)
3377 w.repaint() # Slow, but essential.
3378 #@+node:ekr.20110605121601.18324: *3* LeoQtLog.Tab
3379 #@+node:ekr.20110605121601.18325: *4* LeoQtLog.clearTab
3380 def clearTab(self, tabName: str, wrap: str='none') -> None:
3381 w = self.logDict.get(tabName)
3382 if w:
3383 w.clear() # w is a QTextBrowser.
3384 #@+node:ekr.20110605121601.18326: *4* LeoQtLog.createTab
3385 def createTab(self,
3386 tabName: str, createText: bool=True, widget: Widget=None, wrap: str='none',
3387 ) -> Any: # Widget or LeoQTextBrowser.
3388 """
3389 Create a new tab in tab widget
3390 if widget is None, Create a QTextBrowser,
3391 suitable for log functionality.
3392 """
3393 c = self.c
3394 contents: Any
3395 if widget is None:
3396 # widget is subclass of QTextBrowser.
3397 widget = qt_text.LeoQTextBrowser(parent=None, c=c, wrapper=self)
3398 # contents is a wrapper.
3399 contents = qt_text.QTextEditWrapper(widget=widget, name='log', c=c)
3400 # Inject an ivar into the QTextBrowser that points to the wrapper.
3401 widget.leo_log_wrapper = contents
3402 widget.setWordWrapMode(WrapMode.WordWrap if self.wrap else WrapMode.NoWrap)
3403 widget.setReadOnly(False) # Allow edits.
3404 self.logDict[tabName] = widget
3405 if tabName == 'Log':
3406 self.logCtrl = contents
3407 widget.setObjectName('log-widget')
3408 # Set binding on all log pane widgets.
3409 g.app.gui.setFilter(c, widget, self, tag='log')
3410 self.contentsDict[tabName] = widget
3411 self.tabWidget.addTab(widget, tabName)
3412 else:
3413 # #1161: Don't set the wrapper unless it has the correct type.
3414 contents = widget # Unlike text widgets, contents is the actual widget.
3415 if isinstance(contents, qt_text.QTextEditWrapper):
3416 widget.leo_log_wrapper = widget # The leo_log_wrapper is the widget itself.
3417 else:
3418 widget.leo_log_wrapper = None # Tell the truth.
3419 g.app.gui.setFilter(c, widget, contents, 'tabWidget')
3420 self.contentsDict[tabName] = contents
3421 self.tabWidget.addTab(contents, tabName)
3422 return contents
3423 #@+node:ekr.20110605121601.18328: *4* LeoQtLog.deleteTab
3424 def deleteTab(self, tabName: str) -> None:
3425 """
3426 Delete the tab if it exists. Otherwise do *nothing*.
3427 """
3428 c = self.c
3429 w = self.tabWidget
3430 i = self.findTabIndex(tabName)
3431 if i is None:
3432 return
3433 w.removeTab(i)
3434 self.selectTab('Log')
3435 c.invalidateFocus()
3436 c.bodyWantsFocus()
3437 #@+node:ekr.20190603062456.1: *4* LeoQtLog.findTabIndex
3438 def findTabIndex(self, tabName: str) -> Optional[int]:
3439 """Return the tab index for tabName, or None."""
3440 w = self.tabWidget
3441 for i in range(w.count()):
3442 if tabName == w.tabText(i):
3443 return i
3444 return None
3445 #@+node:ekr.20110605121601.18329: *4* LeoQtLog.hideTab
3446 def hideTab(self, tabName: str) -> None:
3447 self.selectTab('Log')
3448 #@+node:ekr.20111122080923.10185: *4* LeoQtLog.orderedTabNames
3449 def orderedTabNames(self, LeoLog: str=None) -> List[str]: # Unused: LeoLog
3450 """Return a list of tab names in the order in which they appear in the QTabbedWidget."""
3451 w = self.tabWidget
3452 return [w.tabText(i) for i in range(w.count())]
3453 #@+node:ekr.20110605121601.18330: *4* LeoQtLog.numberOfVisibleTabs
3454 def numberOfVisibleTabs(self) -> int:
3455 # **Note**: the base-class version of this uses frameDict.
3456 return len([val for val in self.contentsDict.values() if val is not None])
3457 #@+node:ekr.20110605121601.18331: *4* LeoQtLog.selectTab & helpers
3458 def selectTab(self, tabName: str, wrap: str='none') -> None:
3459 """Create the tab if necessary and make it active."""
3460 i = self.findTabIndex(tabName)
3461 if i is None:
3462 self.createTab(tabName, wrap=wrap)
3463 self.finishCreateTab(tabName)
3464 self.finishSelectTab(tabName)
3465 #@+node:ekr.20190603064815.1: *5* LeoQtLog.finishCreateTab
3466 def finishCreateTab(self, tabName: str) -> None:
3467 """Finish creating the given tab. Do not set focus!"""
3468 c = self.c
3469 i = self.findTabIndex(tabName)
3470 if i is None:
3471 g.trace('Can not happen', tabName)
3472 self.tabName = None
3473 return
3474 # #1161.
3475 if tabName == 'Log':
3476 wrapper: Wrapper = None
3477 widget = self.contentsDict.get('Log') # a qt_text.QTextEditWrapper
3478 if widget:
3479 wrapper = getattr(widget, 'leo_log_wrapper', None)
3480 if wrapper and isinstance(wrapper, qt_text.QTextEditWrapper):
3481 self.logCtrl = wrapper
3482 if not wrapper:
3483 g.trace('NO LOG WRAPPER')
3484 if tabName == 'Find':
3485 # Do *not* set focus here!
3486 # #1254861: Ctrl-f doesn't ensure find input field visible.
3487 if c.config.getBool('auto-scroll-find-tab', default=True):
3488 # This is the cause of unwanted scrolling.
3489 findbox = c.findCommands.ftm.find_findbox
3490 if hasattr(widget, 'ensureWidgetVisible'):
3491 widget.ensureWidgetVisible(findbox)
3492 else:
3493 findbox.setFocus()
3494 if tabName == 'Spell':
3495 # Set a flag for the spell system.
3496 widget = self.tabWidget.widget(i)
3497 self.frameDict['Spell'] = widget
3498 #@+node:ekr.20190603064816.1: *5* LeoQtLog.finishSelectTab
3499 def finishSelectTab(self, tabName: str) -> None:
3500 """Select the proper tab."""
3501 w = self.tabWidget
3502 # Special case for Spell tab.
3503 if tabName == 'Spell':
3504 return
3505 i = self.findTabIndex(tabName)
3506 if i is None:
3507 g.trace('can not happen', tabName)
3508 self.tabName = None
3509 return
3510 w.setCurrentIndex(i)
3511 self.tabName = tabName
3512 #@-others
3513#@+node:ekr.20110605121601.18340: ** class LeoQtMenu (LeoMenu)
3514class LeoQtMenu(leoMenu.LeoMenu):
3516 #@+others
3517 #@+node:ekr.20110605121601.18341: *3* LeoQtMenu.__init__
3518 def __init__(self, c: Cmdr, frame: Wrapper, label: str) -> None:
3519 """ctor for LeoQtMenu class."""
3520 assert frame
3521 assert frame.c
3522 super().__init__(frame)
3523 self.leo_menu_label = label.replace('&', '').lower()
3524 self.frame = frame
3525 self.c = c
3526 self.menuBar: Wrapper = c.frame.top.menuBar()
3527 assert self.menuBar is not None
3528 # Inject this dict into the commander.
3529 if not hasattr(c, 'menuAccels'):
3530 setattr(c, 'menuAccels', {})
3531 if 0:
3532 self.font = c.config.getFontFromParams(
3533 'menu_text_font_family', 'menu_text_font_size',
3534 'menu_text_font_slant', 'menu_text_font_weight',
3535 c.config.defaultMenuFontSize)
3536 #@+node:ekr.20120306130648.9848: *3* LeoQtMenu.__repr__
3537 def __repr__(self) -> str:
3538 return f"<LeoQtMenu: {self.leo_menu_label}>"
3540 __str__ = __repr__
3541 #@+node:ekr.20110605121601.18342: *3* LeoQtMenu.Tkinter menu bindings
3542 # See the Tk docs for what these routines are to do
3543 #@+node:ekr.20110605121601.18343: *4* LeoQtMenu.Methods with Tk spellings
3544 #@+node:ekr.20110605121601.18344: *5* LeoQtMenu.add_cascade
3545 def add_cascade(self, parent: Widget, label: str, menu: Wrapper, underline: int) -> Wrapper:
3546 """Wrapper for the Tkinter add_cascade menu method.
3548 Adds a submenu to the parent menu, or the menubar."""
3549 # menu and parent are a QtMenuWrappers, subclasses of QMenu.
3550 n = underline
3551 if -1 < n < len(label):
3552 label = label[:n] + '&' + label[n:]
3553 menu.setTitle(label)
3554 if parent:
3555 parent.addMenu(menu) # QMenu.addMenu.
3556 else:
3557 self.menuBar.addMenu(menu)
3558 label = label.replace('&', '').lower()
3559 menu.leo_menu_label = label
3560 return menu
3561 #@+node:ekr.20110605121601.18345: *5* LeoQtMenu.add_command (Called by createMenuEntries)
3562 def add_command(self, menu: Widget,
3563 accelerator: str='', command: Callable=None, commandName: str=None, label: str=None, underline: int=0,
3564 ) -> None:
3565 """Wrapper for the Tkinter add_command menu method."""
3566 if not label:
3567 return
3568 if -1 < underline < len(label):
3569 label = label[:underline] + '&' + label[underline:]
3570 if accelerator:
3571 label = f"{label}\t{accelerator}"
3572 action = menu.addAction(label) # type:ignore
3573 # Inject the command name into the action so that it can be enabled/disabled dynamically.
3574 action.leo_command_name = commandName or ''
3575 if command:
3577 def qt_add_command_callback(checked: int, label: str=label, command: Callable=command) -> None:
3578 return command()
3580 action.triggered.connect(qt_add_command_callback)
3581 #@+node:ekr.20110605121601.18346: *5* LeoQtMenu.add_separator
3582 def add_separator(self, menu: Wrapper) -> None:
3583 """Wrapper for the Tkinter add_separator menu method."""
3584 if menu:
3585 action = menu.addSeparator()
3586 action.leo_menu_label = '*seperator*'
3587 #@+node:ekr.20110605121601.18347: *5* LeoQtMenu.delete
3588 def delete(self, menu: Wrapper, realItemName: str='<no name>') -> None:
3589 """Wrapper for the Tkinter delete menu method."""
3590 # if menu:
3591 # return menu.delete(realItemName)
3592 #@+node:ekr.20110605121601.18348: *5* LeoQtMenu.delete_range
3593 def delete_range(self, menu: Wrapper, n1: int, n2: int) -> None:
3594 """Wrapper for the Tkinter delete menu method."""
3595 # Menu is a subclass of QMenu and LeoQtMenu.
3596 for z in menu.actions()[n1:n2]:
3597 menu.removeAction(z)
3598 #@+node:ekr.20110605121601.18349: *5* LeoQtMenu.destroy
3599 def destroy(self, menu: Wrapper) -> None:
3600 """Wrapper for the Tkinter destroy menu method."""
3601 # Fixed bug https://bugs.launchpad.net/leo-editor/+bug/1193870
3602 if menu:
3603 menu.menuBar.removeAction(menu.menuAction())
3604 #@+node:ekr.20110605121601.18350: *5* LeoQtMenu.index
3605 def index(self, label: str) -> int:
3606 """Return the index of the menu with the given label."""
3607 return 0
3608 #@+node:ekr.20110605121601.18351: *5* LeoQtMenu.insert
3609 def insert(self,
3610 menuName: str, position: int, label: str, command: Callable, underline: int=None,
3611 ) -> None:
3613 menu = self.getMenu(menuName)
3614 if menu and label:
3615 n = underline or 0
3616 if -1 > n > len(label):
3617 label = label[:n] + '&' + label[n:]
3618 action = menu.addAction(label)
3619 if command:
3621 def insert_callback(checked: str, label: str=label, command: Callable=command) -> None:
3622 command()
3624 action.triggered.connect(insert_callback)
3625 #@+node:ekr.20110605121601.18352: *5* LeoQtMenu.insert_cascade
3626 def insert_cascade(self,
3627 parent: Widget,
3628 index: int,
3629 label: str,
3630 menu: Widget,
3631 underline: int, # Not used
3632 ) -> Widget:
3633 """Wrapper for the Tkinter insert_cascade menu method."""
3634 menu.setTitle(label)
3635 label.replace('&', '').lower()
3636 menu.leo_menu_label = label # was leo_label
3637 if parent:
3638 parent.addMenu(menu)
3639 else:
3640 self.menuBar.addMenu(menu)
3641 action = menu.menuAction()
3642 if action:
3643 action.leo_menu_label = label
3644 else:
3645 g.trace('no action for menu', label)
3646 return menu
3647 #@+node:ekr.20110605121601.18353: *5* LeoQtMenu.new_menu
3648 def new_menu(self, parent: Widget, tearoff: int=0, label: str='') -> Any: # label is for debugging.
3649 """Wrapper for the Tkinter new_menu menu method."""
3650 c, leoFrame = self.c, self.frame
3651 # Parent can be None, in which case it will be added to the menuBar.
3652 menu = QtMenuWrapper(c, leoFrame, parent, label)
3653 return menu
3654 #@+node:ekr.20110605121601.18354: *4* LeoQtMenu.Methods with other spellings
3655 #@+node:ekr.20110605121601.18355: *5* LeoQtMenu.clearAccel
3656 def clearAccel(self, menu: Wrapper, name: str) -> None:
3657 pass
3658 # if not menu:
3659 # return
3660 # realName = self.getRealMenuName(name)
3661 # realName = realName.replace("&","")
3662 # menu.entryconfig(realName,accelerator='')
3663 #@+node:ekr.20110605121601.18356: *5* LeoQtMenu.createMenuBar
3664 def createMenuBar(self, frame: Widget) -> None:
3665 """
3666 (LeoQtMenu) Create all top-level menus.
3667 The menuBar itself has already been created.
3668 """
3669 self.createMenusFromTables() # This is LeoMenu.createMenusFromTables.
3670 #@+node:ekr.20110605121601.18357: *5* LeoQtMenu.createOpenWithMenu
3671 def createOpenWithMenu(self, parent: Any, label: str, index: int, amp_index: int) -> Any:
3672 """
3673 Create the File:Open With submenu.
3675 This is called from LeoMenu.createOpenWithMenuFromTable.
3676 """
3677 # Use the existing Open With menu if possible.
3678 menu = self.getMenu('openwith')
3679 if not menu:
3680 menu = self.new_menu(parent, tearoff=False, label=label)
3681 menu.insert_cascade(parent, index, label, menu, underline=amp_index)
3682 return menu
3683 #@+node:ekr.20110605121601.18358: *5* LeoQtMenu.disable/enableMenu (not used)
3684 def disableMenu(self, menu: Wrapper, name: str) -> None:
3685 self.enableMenu(menu, name, False)
3687 def enableMenu(self, menu: Wrapper, name: str, val: bool) -> None:
3688 """Enable or disable the item in the menu with the given name."""
3689 if menu and name:
3690 for action in menu.actions():
3691 s = g.checkUnicode(action.text()).replace('&', '')
3692 if s.startswith(name):
3693 action.setEnabled(val)
3694 break
3695 #@+node:ekr.20110605121601.18359: *5* LeoQtMenu.getMenuLabel
3696 def getMenuLabel(self, menu: Wrapper, name: str) -> None:
3697 """Return the index of the menu item whose name (or offset) is given.
3698 Return None if there is no such menu item."""
3699 # At present, it is valid to always return None.
3700 #@+node:ekr.20110605121601.18360: *5* LeoQtMenu.setMenuLabel
3701 def setMenuLabel(self, menu: Wrapper, name: str, label: str, underline: int=-1) -> None:
3703 def munge(s: str) -> str:
3704 return (s or '').replace('&', '')
3706 # menu is a QtMenuWrapper.
3707 if not menu:
3708 return
3710 realName = munge(self.getRealMenuName(name))
3711 realLabel = self.getRealMenuName(label)
3712 for action in menu.actions():
3713 s = munge(action.text())
3714 s = s.split('\t')[0]
3715 if s == realName:
3716 action.setText(realLabel)
3717 break
3718 #@+node:ekr.20110605121601.18361: *3* LeoQtMenu.activateMenu & helper
3719 def activateMenu(self, menuName: str) -> None:
3720 """Activate the menu with the given name"""
3721 # Menu is a QtMenuWrapper, a subclass of both QMenu and LeoQtMenu.
3722 menu = self.getMenu(menuName)
3723 if menu:
3724 self.activateAllParentMenus(menu)
3725 else:
3726 g.trace(f"No such menu: {menuName}")
3727 #@+node:ekr.20120922041923.10607: *4* LeoQtMenu.activateAllParentMenus
3728 def activateAllParentMenus(self, menu: Wrapper) -> None:
3729 """menu is a QtMenuWrapper. Activate it and all parent menus."""
3730 parent = menu.parent()
3731 action = menu.menuAction()
3732 if action:
3733 if parent and isinstance(parent, QtWidgets.QMenuBar):
3734 parent.setActiveAction(action)
3735 elif parent:
3736 self.activateAllParentMenus(parent)
3737 parent.setActiveAction(action)
3738 else:
3739 g.trace(f"can not happen: no parent for {menu}")
3740 else:
3741 g.trace(f"can not happen: no action for {menu}")
3742 #@+node:ekr.20120922041923.10613: *3* LeoQtMenu.deactivateMenuBar
3743 # def deactivateMenuBar (self):
3744 # """Activate the menu with the given name"""
3745 # menubar = self.c.frame.top.leo_menubar
3746 # menubar.setActiveAction(None)
3747 # menubar.repaint()
3748 #@+node:ekr.20110605121601.18362: *3* LeoQtMenu.getMacHelpMenu
3749 def getMacHelpMenu(self, table: List) -> None:
3750 return None
3751 #@-others
3752#@+node:ekr.20110605121601.18363: ** class LeoQTreeWidget (QTreeWidget)
3753class LeoQTreeWidget(QtWidgets.QTreeWidget): # type:ignore
3755 def __init__(self, c: Cmdr, parent: Widget) -> None:
3756 super().__init__(parent)
3757 self.setAcceptDrops(True)
3758 enable_drag = c.config.getBool('enable-tree-dragging')
3759 self.setDragEnabled(bool(enable_drag))
3760 self.c = c
3761 self.was_alt_drag = False
3762 self.was_control_drag = False
3764 def __repr__(self) -> str:
3765 return f"LeoQTreeWidget: {id(self)}"
3767 __str__ = __repr__
3770 def dragMoveEvent(self, ev: Event) -> None: # Called during drags.
3771 pass
3773 #@+others
3774 #@+node:ekr.20111022222228.16980: *3* LeoQTreeWidget: Event handlers
3775 #@+node:ekr.20110605121601.18364: *4* LeoQTreeWidget.dragEnterEvent & helper
3776 def dragEnterEvent(self, ev: Event) -> None:
3777 """Export c.p's tree as a Leo mime-data."""
3778 c = self.c
3779 if not ev:
3780 g.trace('no event!')
3781 return
3782 md = ev.mimeData()
3783 if not md:
3784 g.trace('No mimeData!')
3785 return
3786 c.endEditing()
3787 # Fix bug 135: cross-file drag and drop is broken.
3788 # This handler may be called several times for the same drag.
3789 # Only the first should should set g.app.drag_source.
3790 if g.app.dragging:
3791 pass
3792 else:
3793 g.app.dragging = True
3794 g.app.drag_source = c, c.p
3795 self.setText(md)
3796 # Always accept the drag, even if we are already dragging.
3797 ev.accept()
3798 #@+node:ekr.20110605121601.18384: *5* LeoQTreeWidget.setText
3799 def setText(self, md: Any) -> None:
3800 c = self.c
3801 fn = self.fileName()
3802 s = c.fileCommands.outline_to_clipboard_string()
3803 md.setText(f"{fn},{s}")
3804 #@+node:ekr.20110605121601.18365: *4* LeoQTreeWidget.dropEvent & helpers
3805 def dropEvent(self, ev: Event) -> None:
3806 """Handle a drop event in the QTreeWidget."""
3807 if not ev:
3808 return
3809 md = ev.mimeData()
3810 if not md:
3811 g.trace('no mimeData!')
3812 return
3813 try:
3814 mods = ev.modifiers() if isQt6 else int(ev.keyboardModifiers())
3815 self.was_alt_drag = bool(mods & KeyboardModifier.AltModifier)
3816 self.was_control_drag = bool(mods & KeyboardModifier.ControlModifier)
3817 except Exception: # Defensive.
3818 g.es_exception()
3819 g.app.dragging = False
3820 return
3821 c, tree = self.c, self.c.frame.tree
3822 p = None
3823 point = ev.position().toPoint() if isQt6 else ev.pos()
3824 item = self.itemAt(point)
3825 if item:
3826 itemHash = tree.itemHash(item)
3827 p = tree.item2positionDict.get(itemHash)
3828 if not p:
3829 # #59: Drop at last node.
3830 p = c.rootPosition()
3831 while p.hasNext():
3832 p.moveToNext()
3833 formats = set(str(f) for f in md.formats())
3834 ev.setDropAction(DropAction.IgnoreAction)
3835 ev.accept()
3836 hookres = g.doHook("outlinedrop", c=c, p=p, dropevent=ev, formats=formats)
3837 if hookres:
3838 # A plugin handled the drop.
3839 pass
3840 else:
3841 if md.hasUrls():
3842 self.urlDrop(md, p)
3843 else:
3844 self.nodeDrop(md, p)
3845 g.app.dragging = False
3846 #@+node:ekr.20110605121601.18366: *5* LeoQTreeWidget.nodeDrop & helpers
3847 def nodeDrop(self, md: Any, p: Pos) -> None:
3848 """
3849 Handle a drop event when not md.urls().
3850 This will happen when we drop an outline node.
3851 We get the copied text from md.text().
3852 """
3853 c = self.c
3854 fn, s = self.parseText(md)
3855 if not s or not fn:
3856 return
3857 if fn == self.fileName():
3858 if p and p == c.p:
3859 pass
3860 elif g.os_path_exists(fn):
3861 self.intraFileDrop(fn, c.p, p)
3862 else:
3863 self.interFileDrop(fn, p, s)
3864 #@+node:ekr.20110605121601.18367: *6* LeoQTreeWidget.interFileDrop
3865 def interFileDrop(self, fn: str, p: Pos, s: str) -> None:
3866 """Paste the mime data after (or as the first child of) p."""
3867 c = self.c
3868 u = c.undoer
3869 undoType = 'Drag Outline'
3870 isLeo = g.match(s, 0, g.app.prolog_prefix_string)
3871 if not isLeo:
3872 return
3873 c.selectPosition(p)
3874 # Paste the node after the presently selected node.
3875 pasted = c.fileCommands.getLeoOutlineFromClipboard(s)
3876 if not pasted:
3877 return
3878 if c.config.getBool('inter-outline-drag-moves'):
3879 src_c, src_p = g.app.drag_source
3880 if src_p.hasVisNext(src_c):
3881 nxt = src_p.getVisNext(src_c).v
3882 elif src_p.hasVisBack(src_c):
3883 nxt = src_p.getVisBack(src_c).v
3884 else:
3885 nxt = None
3886 if nxt is not None:
3887 src_p.doDelete()
3888 src_c.selectPosition(src_c.vnode2position(nxt))
3889 src_c.setChanged()
3890 src_c.redraw()
3891 else:
3892 g.es("Can't move last node out of outline")
3893 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[])
3894 c.validateOutline()
3895 c.selectPosition(pasted)
3896 pasted.setDirty() # 2011/02/27: Fix bug 690467.
3897 c.setChanged()
3898 back = pasted.back()
3899 if back and back.isExpanded():
3900 pasted.moveToNthChildOf(back, 0)
3901 # c.setRootPosition(c.findRootPosition(pasted))
3902 u.afterInsertNode(pasted, undoType, undoData)
3903 c.redraw(pasted)
3904 c.recolor()
3905 #@+node:ekr.20110605121601.18368: *6* LeoQTreeWidget.intraFileDrop
3906 def intraFileDrop(self, fn: str, p1: Pos, p2: Pos) -> None:
3907 """Move p1 after (or as the first child of) p2."""
3908 as_child = self.was_alt_drag
3909 cloneDrag = self.was_control_drag
3910 c = self.c
3911 u = c.undoer
3912 c.selectPosition(p1)
3913 if as_child or p2.hasChildren() and p2.isExpanded():
3914 # Attempt to move p1 to the first child of p2.
3915 # parent = p2
3917 def move(p1: Pos, p2: Pos) -> Pos:
3918 if cloneDrag:
3919 p1 = p1.clone()
3920 p1.moveToNthChildOf(p2, 0)
3921 p1.setDirty()
3922 return p1
3924 else:
3925 # Attempt to move p1 after p2.
3926 # parent = p2.parent()
3928 def move(p1: Pos, p2: Pos) -> Pos:
3929 if cloneDrag:
3930 p1 = p1.clone()
3931 p1.moveAfter(p2)
3932 p1.setDirty()
3933 return p1
3935 ok = (
3936 # 2011/10/03: Major bug fix.
3937 c.checkDrag(p1, p2) and
3938 c.checkMoveWithParentWithWarning(p1, p2, True))
3939 if ok:
3940 undoData = u.beforeMoveNode(p1)
3941 p1.setDirty()
3942 p1 = move(p1, p2)
3943 if cloneDrag:
3944 # Set dirty bits for ancestors of *all* cloned nodes.
3945 for z in p1.self_and_subtree():
3946 z.setDirty()
3947 c.setChanged()
3948 u.afterMoveNode(p1, 'Drag', undoData)
3949 if (not as_child or
3950 p2.isExpanded() or
3951 c.config.getBool("drag-alt-drag-expands") is not False
3952 ):
3953 c.redraw(p1)
3954 else:
3955 c.redraw(p2)
3956 #@+node:ekr.20110605121601.18383: *6* LeoQTreeWidget.parseText
3957 def parseText(self, md: Any) -> Tuple[str, str]:
3958 """Parse md.text() into (fn,s)"""
3959 fn = ''
3960 s = md.text()
3961 if s:
3962 i = s.find(',')
3963 if i == -1:
3964 pass
3965 else:
3966 fn = s[:i]
3967 s = s[i + 1 :]
3968 return fn, s
3969 #@+node:ekr.20110605121601.18369: *5* LeoQTreeWidget.urlDrop & helpers
3970 def urlDrop(self, md: Any, p: Pos) -> None:
3971 """Handle a drop when md.urls()."""
3972 c, u, undoType = self.c, self.c.undoer, 'Drag Urls'
3973 urls = md.urls()
3974 if not urls:
3975 return
3976 c.undoer.beforeChangeGroup(c.p, undoType)
3977 changed = False
3978 for z in urls:
3979 url = QtCore.QUrl(z)
3980 scheme = url.scheme()
3981 if scheme == 'file':
3982 changed |= self.doFileUrl(p, url)
3983 elif scheme in ('http',): # 'ftp','mailto',
3984 changed |= self.doHttpUrl(p, url)
3985 if changed:
3986 c.setChanged()
3987 u.afterChangeGroup(c.p, undoType, reportFlag=False)
3988 c.redraw()
3989 #@+node:ekr.20110605121601.18370: *6* LeoQTreeWidget.doFileUrl & helper
3990 def doFileUrl(self, p: Pos, url: str) -> bool:
3991 """Read the file given by the url and put it in the outline."""
3992 # 2014/06/06: Work around a possible bug in QUrl.
3993 # fn = str(url.path()) # Fails.
3994 e = sys.getfilesystemencoding()
3995 fn = g.toUnicode(url.path(), encoding=e)
3996 if sys.platform.lower().startswith('win'):
3997 if fn.startswith('/'):
3998 fn = fn[1:]
3999 if os.path.isdir(fn):
4000 # Just insert an @path directory.
4001 self.doPathUrlHelper(fn, p)
4002 return True
4003 if g.os_path_exists(fn):
4004 try:
4005 f = open(fn, 'rb') # 2012/03/09: use 'rb'
4006 except IOError:
4007 f = None
4008 if f:
4009 b = f.read()
4010 s = g.toUnicode(b)
4011 f.close()
4012 return self.doFileUrlHelper(fn, p, s)
4013 nodeLink = p.get_UNL()
4014 g.es_print(f"not found: {fn}", nodeLink=nodeLink)
4015 return False
4016 #@+node:ekr.20110605121601.18371: *7* LeoQTreeWidget.doFileUrlHelper & helper
4017 def doFileUrlHelper(self, fn: str, p: Pos, s: str) -> bool:
4018 """
4019 Insert s in an @file, @auto or @edit node after p.
4020 If fn is a .leo file, insert a node containing its top-level nodes as children.
4021 """
4022 c = self.c
4023 if self.isLeoFile(fn, s) and not self.was_control_drag:
4024 g.openWithFileName(fn, old_c=c)
4025 return False # Don't set the changed marker in the original file.
4026 u, undoType = c.undoer, 'Drag File'
4027 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[])
4028 if p.hasChildren() and p.isExpanded():
4029 p2 = p.insertAsNthChild(0)
4030 parent = p
4031 elif p.h.startswith('@path '):
4032 # #60: create relative paths & urls when dragging files.
4033 p2 = p.insertAsNthChild(0)
4034 p.expand()
4035 parent = p
4036 else:
4037 p2 = p.insertAfter()
4038 parent = p.parent()
4039 # #60: create relative paths & urls when dragging files.
4040 aList = g.get_directives_dict_list(parent)
4041 path = g.scanAtPathDirectives(c, aList)
4042 if path:
4043 fn = os.path.relpath(fn, path)
4044 self.createAtFileNode(fn, p2, s)
4045 u.afterInsertNode(p2, undoType, undoData)
4046 c.selectPosition(p2)
4047 return True # The original .leo file has changed.
4048 #@+node:ekr.20110605121601.18372: *8* LeoQTreeWidget.createAtFileNode & helpers (QTreeWidget)
4049 def createAtFileNode(self, fn: str, p: Pos, s: str) -> None:
4050 """
4051 Set p's headline, body text and possibly descendants
4052 based on the file's name fn and contents s.
4054 If the file is an thin file, create an @file tree.
4055 Othewise, create an @auto tree.
4056 If all else fails, create an @edit node.
4058 Give a warning if a node with the same headline already exists.
4059 """
4060 c = self.c
4061 c.init_error_dialogs()
4062 if self.isLeoFile(fn, s):
4063 self.createLeoFileTree(fn, p)
4064 elif self.isThinFile(fn, s):
4065 self.createAtFileTree(fn, p, s)
4066 elif self.isAutoFile(fn):
4067 self.createAtAutoTree(fn, p)
4068 elif self.isBinaryFile(fn):
4069 self.createUrlForBinaryFile(fn, p)
4070 else:
4071 self.createAtEditNode(fn, p)
4072 self.warnIfNodeExists(p)
4073 c.raise_error_dialogs(kind='read')
4074 #@+node:ekr.20110605121601.18373: *9* LeoQTreeWidget.createAtAutoTree (QTreeWidget)
4075 def createAtAutoTree(self, fn: str, p: Pos) -> None:
4076 """
4077 Make p an @auto node and create the tree using s, the file's contents.
4078 """
4079 c = self.c
4080 at = c.atFileCommands
4081 p.h = f"@auto {fn}"
4082 at.readOneAtAutoNode(p)
4083 # No error recovery should be needed here.
4084 p.clearDirty() # Don't automatically rewrite this node.
4085 #@+node:ekr.20110605121601.18374: *9* LeoQTreeWidget.createAtEditNode
4086 def createAtEditNode(self, fn: str, p: Pos) -> None:
4087 c = self.c
4088 at = c.atFileCommands
4089 # Use the full @edit logic, so dragging will be
4090 # exactly the same as reading.
4091 at.readOneAtEditNode(fn, p)
4092 p.h = f"@edit {fn}"
4093 p.clearDirty() # Don't automatically rewrite this node.
4094 #@+node:ekr.20110605121601.18375: *9* LeoQTreeWidget.createAtFileTree
4095 def createAtFileTree(self, fn: str, p: Pos, s: str) -> None:
4096 """Make p an @file node and create the tree using s, the file's contents."""
4097 c = self.c
4098 at = c.atFileCommands
4099 p.h = f"@file {fn}"
4100 # Read the file into p.
4101 ok = at.read(root=p.copy(), fromString=s)
4102 if not ok:
4103 g.error('Error reading', fn)
4104 p.b = '' # Safe: will not cause a write later.
4105 p.clearDirty() # Don't automatically rewrite this node.
4106 #@+node:ekr.20141007223054.18004: *9* LeoQTreeWidget.createLeoFileTree
4107 def createLeoFileTree(self, fn: str, p: Pos) -> None:
4108 """Copy all nodes from fn, a .leo file, to the children of p."""
4109 c = self.c
4110 p.h = f"From {g.shortFileName(fn)}"
4111 c.selectPosition(p)
4112 # Create a dummy first child of p.
4113 dummy_p = p.insertAsNthChild(0)
4114 c.selectPosition(dummy_p)
4115 c2 = g.openWithFileName(fn, old_c=c, gui=g.app.nullGui)
4116 for p2 in c2.rootPosition().self_and_siblings():
4117 c2.selectPosition(p2)
4118 s = c2.fileCommands.outline_to_clipboard_string()
4119 # Paste the outline after the selected node.
4120 c.fileCommands.getLeoOutlineFromClipboard(s)
4121 dummy_p.doDelete()
4122 c.selectPosition(p)
4123 p.v.contract()
4124 c2.close()
4125 g.app.forgetOpenFile(c2.fileName()) # Necessary.
4126 #@+node:ekr.20120309075544.9882: *9* LeoQTreeWidget.createUrlForBinaryFile
4127 def createUrlForBinaryFile(self, fn: str, p: Pos) -> None:
4128 # Fix bug 1028986: create relative urls when dragging binary files to Leo.
4129 c = self.c
4130 base_fn = g.os_path_normcase(g.os_path_abspath(c.mFileName))
4131 abs_fn = g.os_path_normcase(g.os_path_abspath(fn))
4132 prefix = os.path.commonprefix([abs_fn, base_fn])
4133 if prefix and len(prefix) > 3: # Don't just strip off c:\.
4134 p.h = abs_fn[len(prefix) :].strip()
4135 else:
4136 p.h = f"@url file://{fn}"
4137 #@+node:ekr.20110605121601.18377: *9* LeoQTreeWidget.isAutoFile (LeoQTreeWidget)
4138 def isAutoFile(self, fn: str) -> bool:
4139 """Return true if fn (a file name) can be parsed with an @auto parser."""
4140 d = g.app.classDispatchDict
4141 junk, ext = g.os_path_splitext(fn)
4142 return bool(d.get(ext))
4143 #@+node:ekr.20120309075544.9881: *9* LeoQTreeWidget.isBinaryFile
4144 def isBinaryFile(self, fn: str) -> bool:
4145 # The default for unknown files is True. Not great, but safe.
4146 junk, ext = g.os_path_splitext(fn)
4147 ext = ext.lower()
4148 if not ext:
4149 val = False
4150 elif ext.startswith('~'):
4151 val = False
4152 elif ext in ('.css', '.htm', '.html', '.leo', '.txt'):
4153 val = False
4154 # elif ext in ('.bmp','gif','ico',):
4155 # val = True
4156 else:
4157 keys = (z.lower() for z in g.app.extension_dict)
4158 val = ext not in keys
4159 return val
4160 #@+node:ekr.20141007223054.18003: *9* LeoQTreeWidget.isLeoFile
4161 def isLeoFile(self, fn: str, s: str) -> bool:
4162 """Return true if fn (a file name) represents an entire .leo file."""
4163 return fn.endswith('.leo') and s.startswith(g.app.prolog_prefix_string)
4164 #@+node:ekr.20110605121601.18376: *9* LeoQTreeWidget.isThinFile
4165 def isThinFile(self, fn: str, s: str) -> bool:
4166 """
4167 Return true if the file whose contents is s
4168 was created from an @thin or @file tree.
4169 """
4170 c = self.c
4171 at = c.atFileCommands
4172 # Skip lines before the @+leo line.
4173 i = s.find('@+leo')
4174 if i == -1:
4175 return False
4176 # Like at.isFileLike.
4177 j, k = g.getLine(s, i)
4178 line = s[j:k]
4179 valid, new_df, start, end, isThin = at.parseLeoSentinel(line)
4180 return valid and new_df and isThin
4181 #@+node:ekr.20110605121601.18378: *9* LeoQTreeWidget.warnIfNodeExists
4182 def warnIfNodeExists(self, p: Pos) -> None:
4183 c = self.c
4184 h = p.h
4185 for p2 in c.all_unique_positions():
4186 if p2.h == h and p2 != p:
4187 g.warning('Warning: duplicate node:', h)
4188 break
4189 #@+node:ekr.20110605121601.18379: *7* LeoQTreeWidget.doPathUrlHelper
4190 def doPathUrlHelper(self, fn: str, p: Pos) -> None:
4191 """Insert fn as an @path node after p."""
4192 c = self.c
4193 u, undoType = c.undoer, 'Drag Directory'
4194 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[])
4195 if p.hasChildren() and p.isExpanded():
4196 p2 = p.insertAsNthChild(0)
4197 else:
4198 p2 = p.insertAfter()
4199 p2.h = '@path ' + fn
4200 u.afterInsertNode(p2, undoType, undoData)
4201 c.selectPosition(p2)
4202 #@+node:ekr.20110605121601.18380: *6* LeoQTreeWidget.doHttpUrl
4203 def doHttpUrl(self, p: Pos, url: str) -> bool:
4204 """Insert the url in an @url node after p."""
4205 c = self.c
4206 u = c.undoer
4207 undoType = 'Drag Url'
4208 s = str(url.toString()).strip()
4209 if not s:
4210 return False
4211 undoData = u.beforeInsertNode(p, pasteAsClone=False, copiedBunchList=[])
4212 if p.hasChildren() and p.isExpanded():
4213 p2 = p.insertAsNthChild(0)
4214 else:
4215 p2 = p.insertAfter()
4216 p2.h = '@url'
4217 p2.b = s
4218 p2.clearDirty() # Don't automatically rewrite this node.
4219 u.afterInsertNode(p2, undoType, undoData)
4220 return True
4221 #@+node:ekr.20110605121601.18381: *3* LeoQTreeWidget: utils
4222 #@+node:ekr.20110605121601.18382: *4* LeoQTreeWidget.dump
4223 def dump(self, ev: Event, p: Pos, tag: str) -> None:
4224 if ev:
4225 md = ev.mimeData()
4226 s = g.checkUnicode(md.text(), encoding='utf-8')
4227 g.trace('md.text:', repr(s) if len(s) < 100 else len(s))
4228 for url in md.urls() or []:
4229 g.trace(' url:', url)
4230 g.trace(' url.fn:', url.toLocalFile())
4231 g.trace('url.text:', url.toString())
4232 else:
4233 g.trace('', tag, '** no event!')
4234 #@+node:ekr.20141007223054.18002: *4* LeoQTreeWidget.fileName
4235 def fileName(self) -> str:
4236 """Return the commander's filename."""
4237 return self.c.fileName() or '<unsaved file>'
4238 #@-others
4239#@+node:ekr.20110605121601.18385: ** class LeoQtSpellTab
4240class LeoQtSpellTab:
4241 #@+others
4242 #@+node:ekr.20110605121601.18386: *3* LeoQtSpellTab.__init__
4243 def __init__(self, c: Cmdr, handler: Callable, tabName: str) -> None:
4244 """Ctor for LeoQtSpellTab class."""
4245 self.c = c
4246 top = c.frame.top
4247 self.handler = handler
4248 # hack:
4249 handler.workCtrl = leoFrame.StringTextWrapper(c, 'spell-workctrl')
4250 self.tabName = tabName
4251 if hasattr(top, 'leo_spell_label'):
4252 self.wordLabel = top.leo_spell_label
4253 self.listBox = top.leo_spell_listBox
4254 self.fillbox([])
4255 else:
4256 self.handler.loaded = False
4257 #@+node:ekr.20110605121601.18389: *3* Event handlers
4258 #@+node:ekr.20110605121601.18390: *4* onAddButton
4259 def onAddButton(self) -> None:
4260 """Handle a click in the Add button in the Check Spelling dialog."""
4261 self.handler.add()
4262 #@+node:ekr.20110605121601.18391: *4* onChangeButton & onChangeThenFindButton
4263 def onChangeButton(self, event: Event=None) -> None:
4264 """Handle a click in the Change button in the Spell tab."""
4265 state = self.updateButtons()
4266 if state:
4267 self.handler.change()
4268 self.updateButtons()
4270 def onChangeThenFindButton(self, event: Event=None) -> None:
4271 """Handle a click in the "Change, Find" button in the Spell tab."""
4272 state = self.updateButtons()
4273 if state:
4274 self.handler.change()
4275 if self.handler.change():
4276 self.handler.find()
4277 self.updateButtons()
4278 #@+node:ekr.20110605121601.18392: *4* onFindButton
4279 def onFindButton(self) -> None:
4280 """Handle a click in the Find button in the Spell tab."""
4281 c = self.c
4282 self.handler.find()
4283 self.updateButtons()
4284 c.invalidateFocus()
4285 c.bodyWantsFocus()
4286 #@+node:ekr.20110605121601.18393: *4* onHideButton
4287 def onHideButton(self) -> None:
4288 """Handle a click in the Hide button in the Spell tab."""
4289 self.handler.hide()
4290 #@+node:ekr.20110605121601.18394: *4* onIgnoreButton
4291 def onIgnoreButton(self, event: Event=None) -> None:
4292 """Handle a click in the Ignore button in the Check Spelling dialog."""
4293 self.handler.ignore()
4294 #@+node:ekr.20110605121601.18395: *4* onMap
4295 def onMap(self, event: Event=None) -> None:
4296 """Respond to a Tk <Map> event."""
4297 self.update(show=False, fill=False)
4298 #@+node:ekr.20110605121601.18396: *4* onSelectListBox
4299 def onSelectListBox(self, event: Event=None) -> None:
4300 """Respond to a click in the selection listBox."""
4301 c = self.c
4302 self.updateButtons()
4303 c.bodyWantsFocus()
4304 #@+node:ekr.20110605121601.18397: *3* Helpers
4305 #@+node:ekr.20110605121601.18398: *4* bringToFront (LeoQtSpellTab)
4306 def bringToFront(self) -> None:
4307 self.c.frame.log.selectTab('Spell')
4308 #@+node:ekr.20110605121601.18399: *4* fillbox (LeoQtSpellTab)
4309 def fillbox(self, alts: List[str], word: str=None) -> None:
4310 """Update the suggestions listBox in the Check Spelling dialog."""
4311 self.suggestions = alts
4312 if not word:
4313 word = ""
4314 self.wordLabel.setText("Suggestions for: " + word)
4315 self.listBox.clear()
4316 if self.suggestions:
4317 self.listBox.addItems(self.suggestions)
4318 self.listBox.setCurrentRow(0)
4319 #@+node:ekr.20110605121601.18400: *4* getSuggestion (LeoQtSpellTab)
4320 def getSuggestion(self) -> str:
4321 """Return the selected suggestion from the listBox."""
4322 idx = self.listBox.currentRow()
4323 value = self.suggestions[idx]
4324 return value
4325 #@+node:ekr.20141113094129.13: *4* setFocus (LeoQtSpellTab)
4326 def setFocus(self) -> None:
4327 """Actually put focus in the tab."""
4328 # Not a great idea: there is no indication of focus.
4329 c = self.c
4330 if c.frame and c.frame.top and hasattr(c.frame.top, 'spellFrame'):
4331 w = self.c.frame.top.spellFrame
4332 c.widgetWantsFocus(w)
4333 #@+node:ekr.20110605121601.18401: *4* update (LeoQtSpellTab)
4334 def update(self, show: bool=True, fill: bool=False) -> None:
4335 """Update the Spell Check dialog."""
4336 c = self.c
4337 if fill:
4338 self.fillbox([])
4339 self.updateButtons()
4340 if show:
4341 self.bringToFront()
4342 c.bodyWantsFocus()
4343 #@+node:ekr.20110605121601.18402: *4* updateButtons (spellTab)
4344 def updateButtons(self) -> bool:
4345 """Enable or disable buttons in the Check Spelling dialog."""
4346 c = self.c
4347 top, w = c.frame.top, c.frame.body.wrapper
4348 state = bool(self.suggestions and w.hasSelection())
4349 top.leo_spell_btn_Change.setDisabled(not state)
4350 top.leo_spell_btn_FindChange.setDisabled(not state)
4351 return state
4352 #@-others
4353#@+node:ekr.20110605121601.18438: ** class LeoQtTreeTab
4354class LeoQtTreeTab:
4355 """
4356 A class representing a so-called tree-tab.
4358 Actually, it represents a combo box
4359 """
4360 #@+others
4361 #@+node:ekr.20110605121601.18439: *3* Birth & death
4362 #@+node:ekr.20110605121601.18440: *4* ctor (LeoQtTreeTab)
4363 def __init__(self, c: Cmdr, iconBar: Widget) -> None:
4364 """Ctor for LeoQtTreeTab class."""
4366 self.c = c
4367 self.cc = c.chapterController
4368 assert self.cc
4369 self.iconBar = iconBar
4370 self.lockout = False # True: do not redraw.
4371 self.tabNames: List[str] = [] # The list of tab names. Changes when tabs are renamed.
4372 self.w: Any = None # The QComboBox, not a QWidget.
4373 # self.reloadSettings()
4374 self.createControl()
4375 #@+node:ekr.20110605121601.18441: *4* tt.createControl (defines class LeoQComboBox)
4376 def createControl(self) -> None:
4379 class LeoQComboBox(QtWidgets.QComboBox): # type:ignore
4380 """Create a subclass in order to handle focusInEvents."""
4382 def __init__(self, tt: Wrapper) -> None:
4383 self.leo_tt = tt
4384 super().__init__()
4385 # Fix #458: Chapters drop-down list is not automatically resized.
4386 self.setSizeAdjustPolicy(SizeAdjustPolicy.AdjustToContents)
4388 def focusInEvent(self, event: Event) -> None:
4389 self.leo_tt.setNames()
4390 QtWidgets.QComboBox.focusInEvent(self, event) # Call the base class
4392 tt = self
4393 frame = QtWidgets.QLabel('Chapters: ')
4394 tt.iconBar.addWidget(frame)
4395 tt.w = w = LeoQComboBox(tt)
4396 tt.setNames()
4397 tt.iconBar.addWidget(w)
4399 def onIndexChanged(s: Any, tt: Any=tt) -> None:
4400 if isinstance(s, int):
4401 s = '' if s == -1 else tt.w.currentText()
4402 else: # s is the tab name.
4403 pass
4404 if s and not tt.cc.selectChapterLockout:
4405 tt.selectTab(s)
4407 # A change: the argument could now be an int instead of a string.
4408 w.currentIndexChanged.connect(onIndexChanged)
4409 #@+node:ekr.20110605121601.18443: *3* tt.createTab
4410 def createTab(self, tabName: str, select: bool=True) -> None:
4411 """LeoQtTreeTab."""
4412 tt = self
4413 # Avoid a glitch during initing.
4414 if tabName != 'main' and tabName not in tt.tabNames:
4415 tt.tabNames.append(tabName)
4416 tt.setNames()
4417 #@+node:ekr.20110605121601.18444: *3* tt.destroyTab
4418 def destroyTab(self, tabName: str) -> None:
4419 """LeoQtTreeTab."""
4420 tt = self
4421 if tabName in tt.tabNames:
4422 tt.tabNames.remove(tabName)
4423 tt.setNames()
4424 #@+node:ekr.20110605121601.18445: *3* tt.selectTab
4425 def selectTab(self, tabName: str) -> None:
4426 """LeoQtTreeTab."""
4427 tt, c, cc = self, self.c, self.cc
4428 exists = tabName in self.tabNames
4429 c.treeWantsFocusNow() # Fix #969. Somehow this is important.
4430 if not exists:
4431 tt.createTab(tabName) # Calls tt.setNames()
4432 if tt.lockout:
4433 return
4434 cc.selectChapterByName(tabName)
4435 c.redraw()
4436 c.outerUpdate()
4437 #@+node:ekr.20110605121601.18446: *3* tt.setTabLabel
4438 def setTabLabel(self, tabName: str) -> None:
4439 """LeoQtTreeTab."""
4440 w = self.w
4441 i = w.findText(tabName)
4442 if i > -1:
4443 w.setCurrentIndex(i)
4444 #@+node:ekr.20110605121601.18447: *3* tt.setNames
4445 def setNames(self) -> None:
4446 """LeoQtTreeTab: Recreate the list of items."""
4447 w = self.w
4448 names = self.cc.setAllChapterNames()
4449 w.clear()
4450 w.insertItems(0, names)
4451 #@-others
4452#@+node:ekr.20110605121601.18448: ** class LeoTabbedTopLevel (LeoBaseTabWidget)
4453class LeoTabbedTopLevel(LeoBaseTabWidget):
4454 """ Toplevel frame for tabbed ui """
4456 def __init__(self, *args: Any, **kwargs: Any) -> None:
4457 super().__init__(*args, **kwargs)
4458 ## middle click close on tabs -- JMP 20140505
4459 self.setMovable(False)
4460 tb = QtTabBarWrapper(self)
4461 self.setTabBar(tb)
4462#@+node:peckj.20140505102552.10377: ** class QtTabBarWrapper (QTabBar)
4463class QtTabBarWrapper(QtWidgets.QTabBar): # type:ignore
4464 #@+others
4465 #@+node:peckj.20140516114832.10108: *3* __init__
4466 def __init__(self, parent: Widget=None) -> None:
4467 super().__init__(parent)
4468 self.setMovable(True)
4469 #@+node:peckj.20140516114832.10109: *3* mouseReleaseEvent (QtTabBarWrapper)
4470 def mouseReleaseEvent(self, event: Event) -> None:
4471 # middle click close on tabs -- JMP 20140505
4472 # closes Launchpad bug: https://bugs.launchpad.net/leo-editor/+bug/1183528
4473 if event.button() == MouseButton.MiddleButton:
4474 self.tabCloseRequested.emit(self.tabAt(event.pos()))
4475 QtWidgets.QTabBar.mouseReleaseEvent(self, event)
4476 #@-others
4477#@+node:ekr.20110605121601.18458: ** class QtMenuWrapper (LeoQtMenu,QMenu)
4478class QtMenuWrapper(LeoQtMenu, QtWidgets.QMenu): # type:ignore
4479 #@+others
4480 #@+node:ekr.20110605121601.18459: *3* ctor and __repr__(QtMenuWrapper)
4481 def __init__(self, c: Cmdr, frame: Widget, parent: Widget, label: str) -> None:
4482 """ctor for QtMenuWrapper class."""
4483 assert c
4484 assert frame
4485 if parent is None:
4486 parent = c.frame.top.menuBar()
4487 #
4488 # For reasons unknown, the calls must be in this order.
4489 # Presumably, the order of base classes also matters(!)
4490 LeoQtMenu.__init__(self, c, frame, label)
4491 QtWidgets.QMenu.__init__(self, parent)
4492 label = label.replace('&', '').lower()
4493 self.leo_menu_label = label
4494 action = self.menuAction()
4495 if action:
4496 action.leo_menu_label = label
4497 self.aboutToShow.connect(self.onAboutToShow)
4499 def __repr__(self) -> str:
4500 return f"<QtMenuWrapper {self.leo_menu_label}>"
4501 #@+node:ekr.20110605121601.18460: *3* onAboutToShow & helpers (QtMenuWrapper)
4502 def onAboutToShow(self, *args: Any, **keys: Any) -> None:
4504 name = self.leo_menu_label
4505 if not name:
4506 return
4507 for action in self.actions():
4508 commandName = hasattr(action, 'leo_command_name') and action.leo_command_name
4509 if commandName:
4510 self.leo_update_shortcut(action, commandName)
4511 self.leo_enable_menu_item(action, commandName)
4512 self.leo_update_menu_label(action, commandName)
4513 #@+node:ekr.20120120095156.10261: *4* leo_enable_menu_item
4514 def leo_enable_menu_item(self, action: Any, commandName: str) -> None:
4515 func = self.c.frame.menu.enable_dict.get(commandName)
4516 if action and func:
4517 val = func()
4518 action.setEnabled(bool(val))
4519 #@+node:ekr.20120124115444.10190: *4* leo_update_menu_label
4520 def leo_update_menu_label(self, action: Any, commandName: str) -> None:
4521 c = self.c
4522 if action and commandName == 'mark':
4523 action.setText('UnMark' if c.p.isMarked() else 'Mark')
4524 # Set the proper shortcut.
4525 self.leo_update_shortcut(action, commandName)
4526 #@+node:ekr.20120120095156.10260: *4* leo_update_shortcut
4527 def leo_update_shortcut(self, action: Any, commandName: str) -> None:
4529 c, k = self.c, self.c.k
4530 if action:
4531 s = action.text()
4532 parts = s.split('\t')
4533 if len(parts) >= 2:
4534 s = parts[0]
4535 key, aList = c.config.getShortcut(commandName)
4536 if aList:
4537 result = []
4538 for bi in aList:
4539 # Don't show mode-related bindings.
4540 if not bi.isModeBinding():
4541 accel = k.prettyPrintKey(bi.stroke)
4542 result.append(accel)
4543 # Break here if we want to show only one accerator.
4544 action.setText(f"{s}\t{', '.join(result)}")
4545 else:
4546 action.setText(s)
4547 else:
4548 g.trace(f"can not happen: no action for {commandName}")
4549 #@-others
4550#@+node:ekr.20110605121601.18461: ** class QtSearchWidget
4551class QtSearchWidget:
4552 """A dummy widget class to pass to Leo's core find code."""
4554 def __init__(self) -> None:
4555 self.insertPoint = 0
4556 self.selection = 0, 0
4557 self.wrapper = self
4558 self.body = self
4559 self.text = None
4560#@+node:ekr.20110605121601.18464: ** class TabbedFrameFactory
4561class TabbedFrameFactory:
4562 """
4563 'Toplevel' frame builder for tabbed toplevel interface
4565 This causes Leo to maintain only one toplevel window,
4566 with multiple tabs for documents
4567 """
4568 #@+others
4569 #@+node:ekr.20110605121601.18465: *3* frameFactory.__init__ & __repr__
4570 def __init__(self) -> None:
4571 # Will be created when first frame appears.
4572 # Workaround a problem setting the window title when tabs are shown.
4573 self.alwaysShowTabs = True
4574 self.leoFrames: Dict[Any, Widget] = {} # Keys are DynamicWindows, values are frames.
4575 self.masterFrame: Widget = None
4576 self.createTabCommands()
4577 #@+node:ekr.20110605121601.18466: *3* frameFactory.createFrame (changed, makes dw)
4578 def createFrame(self, leoFrame: Widget) -> Widget:
4580 c = leoFrame.c
4581 tabw = self.masterFrame
4582 dw = DynamicWindow(c, tabw)
4583 self.leoFrames[dw] = leoFrame
4584 # Shorten the title.
4585 title = os.path.basename(c.mFileName) if c.mFileName else leoFrame.title
4586 tip = leoFrame.title
4587 dw.setWindowTitle(tip)
4588 idx = tabw.addTab(dw, title)
4589 if tip:
4590 tabw.setTabToolTip(idx, tip)
4591 dw.construct(master=tabw)
4592 tabw.setCurrentIndex(idx)
4593 g.app.gui.setFilter(c, dw, dw, tag='tabbed-frame')
4594 #
4595 # Work around the problem with missing dirty indicator
4596 # by always showing the tab.
4597 tabw.tabBar().setVisible(self.alwaysShowTabs or tabw.count() > 1)
4598 tabw.setTabsClosable(c.config.getBool('outline-tabs-show-close', True))
4599 if not g.unitTesting:
4600 # #1327: Must always do this.
4601 # 2021/09/12: but not for new unit tests!
4602 dw.show()
4603 tabw.show()
4604 return dw
4605 #@+node:ekr.20110605121601.18468: *3* frameFactory.createMaster
4606 def createMaster(self) -> None:
4608 window = self.masterFrame = LeoTabbedTopLevel(factory=self)
4609 tabbar = window.tabBar()
4610 g.app.gui.attachLeoIcon(window)
4611 try:
4612 tabbar.setTabsClosable(True)
4613 tabbar.tabCloseRequested.connect(self.slotCloseRequest)
4614 except AttributeError:
4615 pass # Qt 4.4 does not support setTabsClosable
4616 window.currentChanged.connect(self.slotCurrentChanged)
4617 if 'size' in g.app.debug:
4618 g.trace(
4619 f"minimized: {g.app.start_minimized}, "
4620 f"maximized: {g.app.start_maximized}, "
4621 f"fullscreen: {g.app.start_fullscreen}")
4622 #
4623 # #1189: We *can* (and should) minimize here, to eliminate flash.
4624 if g.app.start_minimized:
4625 window.showMinimized()
4626 #@+node:ekr.20110605121601.18472: *3* frameFactory.createTabCommands
4627 def detachTab(self, wdg: Widget) -> None:
4628 """ Detach specified tab as individual toplevel window """
4629 del self.leoFrames[wdg]
4630 wdg.setParent(None)
4631 wdg.show()
4633 def createTabCommands(self) -> None:
4634 #@+<< Commands for tabs >>
4635 #@+node:ekr.20110605121601.18473: *4* << Commands for tabs >>
4636 @g.command('tab-detach')
4637 def tab_detach(event: Event) -> None:
4638 """ Detach current tab from tab bar """
4639 if len(self.leoFrames) < 2:
4640 g.es_print_error("Can't detach last tab")
4641 return
4642 c = event['c']
4643 f = c.frame
4644 tabwidget = g.app.gui.frameFactory.masterFrame
4645 tabwidget.detach(tabwidget.indexOf(f.top))
4646 f.top.setWindowTitle(f.title + ' [D]')
4648 # this is actually not tab-specific, move elsewhere?
4650 @g.command('close-others')
4651 def close_others(event: Event) -> None:
4652 """Close all windows except the present window."""
4653 myc = event['c']
4654 for c in g.app.commanders():
4655 if c is not myc:
4656 c.close()
4658 def tab_cycle(offset: int) -> None:
4660 tabw = self.masterFrame
4661 cur = tabw.currentIndex()
4662 count = tabw.count()
4663 # g.es("cur: %s, count: %s, offset: %s" % (cur,count,offset))
4664 cur += offset
4665 if cur < 0:
4666 cur = count - 1
4667 elif cur >= count:
4668 cur = 0
4669 tabw.setCurrentIndex(cur)
4670 self.focusCurrentBody()
4672 @g.command('tab-cycle-next')
4673 def tab_cycle_next(event: Event) -> None:
4674 """ Cycle to next tab """
4675 tab_cycle(1)
4677 @g.command('tab-cycle-previous')
4678 def tab_cycle_previous(event: Event) -> None:
4679 """ Cycle to next tab """
4680 tab_cycle(-1)
4681 #@-<< Commands for tabs >>
4682 #@+node:ekr.20110605121601.18467: *3* frameFactory.deleteFrame
4683 def deleteFrame(self, wdg: Widget) -> None:
4685 if not wdg:
4686 return
4687 if wdg not in self.leoFrames:
4688 # probably detached tab
4689 self.masterFrame.delete(wdg)
4690 return
4691 tabw = self.masterFrame
4692 idx = tabw.indexOf(wdg)
4693 tabw.removeTab(idx)
4694 del self.leoFrames[wdg]
4695 wdg2 = tabw.currentWidget()
4696 if wdg2:
4697 g.app.selectLeoWindow(wdg2.leo_c)
4698 tabw.tabBar().setVisible(self.alwaysShowTabs or tabw.count() > 1)
4699 #@+node:ekr.20110605121601.18471: *3* frameFactory.focusCurrentBody
4700 def focusCurrentBody(self) -> None:
4701 """ Focus body control of current tab """
4702 tabw = self.masterFrame
4703 w = tabw.currentWidget()
4704 w.setFocus()
4705 f = self.leoFrames[w]
4706 c = f.c
4707 c.bodyWantsFocusNow()
4708 # Fix bug 690260: correct the log.
4709 g.app.log = f.log
4710 #@+node:ekr.20110605121601.18469: *3* frameFactory.setTabForCommander
4711 def setTabForCommander(self, c: Cmdr) -> None:
4712 tabw = self.masterFrame # a QTabWidget
4713 for dw in self.leoFrames: # A dict whose keys are DynamicWindows.
4714 if dw.leo_c == c:
4715 for i in range(tabw.count()):
4716 if tabw.widget(i) == dw:
4717 tabw.setCurrentIndex(i)
4718 break
4719 break
4720 #@+node:ekr.20110605121601.18470: *3* frameFactory.signal handlers
4721 def slotCloseRequest(self, idx: int) -> None:
4723 tabw = self.masterFrame
4724 w = tabw.widget(idx)
4725 f = self.leoFrames[w]
4726 c = f.c
4727 # 2012/03/04: Don't set the frame here.
4728 # Wait until the next slotCurrentChanged event.
4729 # This keeps the log and the QTabbedWidget in sync.
4730 c.close(new_c=None)
4732 def slotCurrentChanged(self, idx: str) -> None:
4733 # Two events are generated, one for the tab losing focus,
4734 # and another event for the tab gaining focus.
4735 tabw = self.masterFrame
4736 w = tabw.widget(idx)
4737 f = self.leoFrames.get(w)
4738 if not f:
4739 return
4740 tabw.setWindowTitle(f.title)
4741 # Don't do this: it would break --minimize.
4742 # g.app.selectLeoWindow(f.c)
4743 # Fix bug 690260: correct the log.
4744 g.app.log = f.log
4745 # Redraw the tab.
4746 c = f.c
4747 if c:
4748 c.redraw()
4749 #@-others
4750#@-others
4751#@@language python
4752#@@tabwidth -4
4753#@@pagewidth 70
4754#@-leo