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