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

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