Coverage for C:\leo.repo\leo-editor\leo\plugins\qt_text.py : 25%

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.20140831085423.18598: * @file ../plugins/qt_text.py
4#@@first
5"""Text classes for the Qt version of Leo"""
6import time
7assert time
8from leo.core import leoGlobals as g
9from leo.core.leoQt import isQt6, QtCore, QtGui, Qsci, QtWidgets
10from leo.core.leoQt import ContextMenuPolicy, Key, KeyboardModifier, Modifier
11from leo.core.leoQt import MouseButton, MoveMode, MoveOperation
12from leo.core.leoQt import Shadow, Shape, SliderAction, WindowType, WrapMode
14QColor = QtGui.QColor
15FullWidthSelection = 0x06000 # works for both Qt5 and Qt6
17#@+others
18#@+node:ekr.20191001084541.1: ** zoom commands
19#@+node:tbrown.20130411145310.18857: *3* @g.command("zoom-in")
20@g.command("zoom-in")
21def zoom_in(event=None, delta=1):
22 """increase body font size by one
24 @font-size-body must be present in the stylesheet
25 """
26 zoom_helper(event, delta=1)
27#@+node:ekr.20191001084646.1: *3* @g.command("zoom-out")
28@g.command("zoom-out")
29def zoom_out(event=None):
30 """decrease body font size by one
32 @font-size-body must be present in the stylesheet
33 """
34 # zoom_in(event=event, delta=-1)
35 zoom_helper(event=event, delta=-1)
36#@+node:ekr.20191001084612.1: *3* zoom_helper
37def zoom_helper(event, delta):
38 """
39 Common helper for zoom commands.
40 """
41 c = event.get('c')
42 if not c:
43 return
44 if not c.config.getBool('allow-text-zoom', default=True):
45 if 'zoom' in g.app.debug:
46 g.trace('text zoom disabled')
47 return
48 wrapper = c.frame.body.wrapper
49 #
50 # For performance, don't call c.styleSheetManager.reload_style_sheets().
51 # Apply to body widget directly
52 c._style_deltas['font-size-body'] += delta
53 ssm = c.styleSheetManager
54 sheet = ssm.expand_css_constants(c.active_stylesheet)
55 wrapper.widget.setStyleSheet(sheet)
56 #
57 # #490: Honor language-specific settings.
58 colorizer = getattr(c.frame.body, 'colorizer', None)
59 if not colorizer:
60 return
61 c.zoom_delta = delta
62 colorizer.configure_fonts()
63 wrapper.setAllText(wrapper.getAllText())
64 # Recolor everything.
65#@+node:tom.20210904233317.1: ** Show Hilite Settings command
66# Add item to known "help-for" commands
67hilite_doc = r'''
68Changing The Current Line Highlighting Color
69--------------------------------------------
71The highlight color will be computed based on the Leo theme in effect, unless the `line-highlight-color` setting is set to a non-blank string.
73The setting will always override the color computation. If the setting is changed, after the settings are reloaded the new color will take effect the next time the cursor is moved.
75Settings for Current Line Highlighting
76---------------------------------------
77\@bool highlight-body-line -- if True, highlight current line.
79\@string line-highlight-color -- override highlight color with css value.
80Valid values are standard css color names like `lightgrey`, and css rgb values like `#1234ad`.
81'''
83@g.command('help-for-highlight-current-line')
84def helpForLineHighlight(self, event=None):
85 """Displays Settings used by current line highlighter."""
86 self.c.putHelpFor(hilite_doc)
88#@+node:ekr.20140901062324.18719: ** class QTextMixin
89class QTextMixin:
90 """A minimal mixin class for QTextEditWrapper and QScintillaWrapper classes."""
91 #@+others
92 #@+node:ekr.20140901062324.18732: *3* qtm.ctor & helper
93 def __init__(self, c=None):
94 """Ctor for QTextMixin class"""
95 self.c = c
96 self.changingText = False # A lockout for onTextChanged.
97 self.enabled = True
98 self.supportsHighLevelInterface = True
99 # A flag for k.masterKeyHandler and isTextWrapper.
100 self.tags = {}
101 self.permanent = True # False if selecting the minibuffer will make the widget go away.
102 self.configDict = {} # Keys are tags, values are colors (names or values).
103 self.configUnderlineDict = {} # Keys are tags, values are True
104 # self.formatDict = {} # Keys are tags, values are actual QTextFormat objects.
105 self.useScintilla = False # This is used!
106 self.virtualInsertPoint = None
107 if c:
108 self.injectIvars(c)
109 #@+node:ekr.20140901062324.18721: *4* qtm.injectIvars
110 def injectIvars(self, name='1', parentFrame=None):
111 """Inject standard leo ivars into the QTextEdit or QsciScintilla widget."""
112 w = self
113 p = self.c.currentPosition()
114 if name == '1':
115 w.leo_p = None # Will be set when the second editor is created.
116 else:
117 w.leo_p = p and p.copy()
118 w.leo_active = True
119 # New in Leo 4.4.4 final: inject the scrollbar items into the text widget.
120 w.leo_bodyBar = None
121 w.leo_bodyXBar = None
122 w.leo_chapter = None
123 w.leo_frame = None
124 w.leo_name = name
125 w.leo_label = None
126 return w
127 #@+node:ekr.20140901062324.18825: *3* qtm.getName
128 def getName(self):
129 return self.name # Essential.
130 #@+node:ekr.20140901122110.18733: *3* qtm.Event handlers
131 # These are independent of the kind of Qt widget.
132 #@+node:ekr.20140901062324.18716: *4* qtm.onCursorPositionChanged
133 def onCursorPositionChanged(self, event=None):
135 c = self.c
136 name = c.widget_name(self)
137 # Apparently, this does not cause problems
138 # because it generates no events in the body pane.
139 if not name.startswith('body'):
140 return
141 if hasattr(c.frame, 'statusLine'):
142 c.frame.statusLine.update()
143 #@+node:ekr.20140901062324.18714: *4* qtm.onTextChanged
144 def onTextChanged(self):
145 """
146 Update Leo after the body has been changed.
148 tree.tree_select_lockout is True during the entire selection process.
149 """
150 # Important: usually w.changingText is True.
151 # This method very seldom does anything.
152 w = self
153 c, p = self.c, self.c.p
154 tree = c.frame.tree
155 if w.changingText:
156 return
157 if tree.tree_select_lockout:
158 g.trace('*** LOCKOUT', g.callers())
159 return
160 if not p:
161 return
162 newInsert = w.getInsertPoint()
163 newSel = w.getSelectionRange()
164 newText = w.getAllText() # Converts to unicode.
165 # Get the previous values from the VNode.
166 oldText = p.b
167 if oldText == newText:
168 # This can happen as the result of undo.
169 # g.error('*** unexpected non-change')
170 return
171 i, j = p.v.selectionStart, p.v.selectionLength
172 oldSel = (i, i + j)
173 c.undoer.doTyping(p, 'Typing', oldText, newText,
174 oldSel=oldSel, oldYview=None, newInsert=newInsert, newSel=newSel)
175 #@+node:ekr.20140901122110.18734: *3* qtm.Generic high-level interface
176 # These call only wrapper methods.
177 #@+node:ekr.20140902181058.18645: *4* qtm.Enable/disable
178 def disable(self):
179 self.enabled = False
181 def enable(self, enabled=True):
182 self.enabled = enabled
183 #@+node:ekr.20140902181058.18644: *4* qtm.Clipboard
184 def clipboard_append(self, s):
185 s1 = g.app.gui.getTextFromClipboard()
186 g.app.gui.replaceClipboardWith(s1 + s)
188 def clipboard_clear(self):
189 g.app.gui.replaceClipboardWith('')
190 #@+node:ekr.20140901062324.18698: *4* qtm.setFocus
191 def setFocus(self):
192 """QTextMixin"""
193 if 'focus' in g.app.debug:
194 print('BaseQTextWrapper.setFocus', self.widget)
195 # Call the base class
196 assert isinstance(self.widget, (
197 QtWidgets.QTextBrowser,
198 QtWidgets.QLineEdit,
199 QtWidgets.QTextEdit,
200 Qsci and Qsci.QsciScintilla,
201 )), self.widget
202 QtWidgets.QTextBrowser.setFocus(self.widget)
203 #@+node:ekr.20140901062324.18717: *4* qtm.Generic text
204 #@+node:ekr.20140901062324.18703: *5* qtm.appendText
205 def appendText(self, s):
206 """QTextMixin"""
207 s2 = self.getAllText()
208 self.setAllText(s2 + s)
209 self.setInsertPoint(len(s2))
210 #@+node:ekr.20140901141402.18706: *5* qtm.delete
211 def delete(self, i, j=None):
212 """QTextMixin"""
213 i = self.toPythonIndex(i)
214 if j is None:
215 j = i + 1
216 j = self.toPythonIndex(j)
217 # This allows subclasses to use this base class method.
218 if i > j:
219 i, j = j, i
220 s = self.getAllText()
221 self.setAllText(s[:i] + s[j:])
222 # Bug fix: Significant in external tests.
223 self.setSelectionRange(i, i, insert=i)
224 #@+node:ekr.20140901062324.18827: *5* qtm.deleteTextSelection
225 def deleteTextSelection(self):
226 """QTextMixin"""
227 i, j = self.getSelectionRange()
228 self.delete(i, j)
229 #@+node:ekr.20110605121601.18102: *5* qtm.get
230 def get(self, i, j=None):
231 """QTextMixin"""
232 # 2012/04/12: fix the following two bugs by using the vanilla code:
233 # https://bugs.launchpad.net/leo-editor/+bug/979142
234 # https://bugs.launchpad.net/leo-editor/+bug/971166
235 s = self.getAllText()
236 i = self.toPythonIndex(i)
237 j = self.toPythonIndex(j)
238 return s[i:j]
239 #@+node:ekr.20140901062324.18704: *5* qtm.getLastPosition & getLength
240 def getLastPosition(self, s=None):
241 """QTextMixin"""
242 return len(self.getAllText()) if s is None else len(s)
244 def getLength(self, s=None):
245 """QTextMixin"""
246 return len(self.getAllText()) if s is None else len(s)
247 #@+node:ekr.20140901062324.18705: *5* qtm.getSelectedText
248 def getSelectedText(self):
249 """QTextMixin"""
250 i, j = self.getSelectionRange()
251 if i == j:
252 return ''
253 s = self.getAllText()
254 return s[i:j]
255 #@+node:ekr.20140901141402.18702: *5* qtm.insert
256 def insert(self, i, s):
257 """QTextMixin"""
258 s2 = self.getAllText()
259 i = self.toPythonIndex(i)
260 self.setAllText(s2[:i] + s + s2[i:])
261 self.setInsertPoint(i + len(s))
262 return i
263 #@+node:ekr.20140902084950.18634: *5* qtm.seeInsertPoint
264 def seeInsertPoint(self):
265 """Ensure the insert point is visible."""
266 self.see(self.getInsertPoint())
267 # getInsertPoint defined in client classes.
268 #@+node:ekr.20140902135648.18668: *5* qtm.selectAllText
269 def selectAllText(self, s=None):
270 """QTextMixin."""
271 self.setSelectionRange(0, self.getLength(s))
272 #@+node:ekr.20140901141402.18710: *5* qtm.toPythonIndex
273 def toPythonIndex(self, index, s=None):
274 """QTextMixin"""
275 if s is None:
276 s = self.getAllText()
277 i = g.toPythonIndex(s, index)
278 return i
279 #@+node:ekr.20140901141402.18704: *5* qtm.toPythonIndexRowCol
280 def toPythonIndexRowCol(self, index):
281 """QTextMixin"""
282 s = self.getAllText()
283 i = self.toPythonIndex(index)
284 row, col = g.convertPythonIndexToRowCol(s, i)
285 return i, row, col
286 #@+node:ekr.20140901062324.18729: *4* qtm.rememberSelectionAndScroll
287 def rememberSelectionAndScroll(self):
289 w = self
290 v = self.c.p.v # Always accurate.
291 v.insertSpot = w.getInsertPoint()
292 i, j = w.getSelectionRange()
293 if i > j:
294 i, j = j, i
295 assert i <= j
296 v.selectionStart = i
297 v.selectionLength = j - i
298 v.scrollBarSpot = w.getYScrollPosition()
299 #@+node:ekr.20140901062324.18712: *4* qtm.tag_configure
300 def tag_configure(self, *args, **keys):
302 if len(args) == 1:
303 key = args[0]
304 self.tags[key] = keys
305 val = keys.get('foreground')
306 underline = keys.get('underline')
307 if val:
308 self.configDict[key] = val
309 if underline:
310 self.configUnderlineDict[key] = True
311 else:
312 g.trace('oops', args, keys)
314 tag_config = tag_configure
315 #@-others
316#@+node:ekr.20110605121601.18058: ** class QLineEditWrapper(QTextMixin)
317class QLineEditWrapper(QTextMixin):
318 """
319 A class to wrap QLineEdit widgets.
321 The QHeadlineWrapper class is a subclass that merely
322 redefines the do-nothing check method here.
323 """
324 #@+others
325 #@+node:ekr.20110605121601.18060: *3* qlew.Birth
326 def __init__(self, widget, name, c=None):
327 """Ctor for QLineEditWrapper class."""
328 super().__init__(c)
329 self.widget = widget
330 self.name = name
331 self.baseClassName = 'QLineEditWrapper'
333 def __repr__(self):
334 return f"<QLineEditWrapper: widget: {self.widget}"
336 __str__ = __repr__
337 #@+node:ekr.20140901191541.18599: *3* qlew.check
338 def check(self):
339 """
340 QLineEditWrapper.
341 """
342 return True
343 #@+node:ekr.20110605121601.18118: *3* qlew.Widget-specific overrides
344 #@+node:ekr.20110605121601.18120: *4* qlew.getAllText
345 def getAllText(self):
346 """QHeadlineWrapper."""
347 if self.check():
348 w = self.widget
349 return w.text()
350 return ''
351 #@+node:ekr.20110605121601.18121: *4* qlew.getInsertPoint
352 def getInsertPoint(self):
353 """QHeadlineWrapper."""
354 if self.check():
355 return self.widget.cursorPosition()
356 return 0
357 #@+node:ekr.20110605121601.18122: *4* qlew.getSelectionRange
358 def getSelectionRange(self, sort=True):
359 """QHeadlineWrapper."""
360 w = self.widget
361 if self.check():
362 if w.hasSelectedText():
363 i = w.selectionStart()
364 s = w.selectedText()
365 j = i + len(s)
366 else:
367 i = j = w.cursorPosition()
368 return i, j
369 return 0, 0
370 #@+node:ekr.20210104122029.1: *4* qlew.getYScrollPosition
371 def getYScrollPosition(self):
372 return 0 # #1801.
373 #@+node:ekr.20110605121601.18123: *4* qlew.hasSelection
374 def hasSelection(self):
375 """QHeadlineWrapper."""
376 if self.check():
377 return self.widget.hasSelectedText()
378 return False
379 #@+node:ekr.20110605121601.18124: *4* qlew.see & seeInsertPoint
380 def see(self, i):
381 """QHeadlineWrapper."""
382 pass
384 def seeInsertPoint(self):
385 """QHeadlineWrapper."""
386 pass
387 #@+node:ekr.20110605121601.18125: *4* qlew.setAllText
388 def setAllText(self, s):
389 """Set all text of a Qt headline widget."""
390 if self.check():
391 w = self.widget
392 w.setText(s)
393 #@+node:ekr.20110605121601.18128: *4* qlew.setFocus
394 def setFocus(self):
395 """QHeadlineWrapper."""
396 if self.check():
397 g.app.gui.set_focus(self.c, self.widget)
398 #@+node:ekr.20110605121601.18129: *4* qlew.setInsertPoint
399 def setInsertPoint(self, i, s=None):
400 """QHeadlineWrapper."""
401 if not self.check():
402 return
403 w = self.widget
404 if s is None:
405 s = w.text()
406 i = self.toPythonIndex(i)
407 i = max(0, min(i, len(s)))
408 w.setCursorPosition(i)
409 #@+node:ekr.20110605121601.18130: *4* qlew.setSelectionRange
410 def setSelectionRange(self, i, j, insert=None, s=None):
411 """QHeadlineWrapper."""
412 if not self.check():
413 return
414 w = self.widget
415 if i > j:
416 i, j = j, i
417 if s is None:
418 s = w.text()
419 n = len(s)
420 i = self.toPythonIndex(i)
421 j = self.toPythonIndex(j)
422 i = max(0, min(i, n))
423 j = max(0, min(j, n))
424 if insert is None:
425 insert = j
426 else:
427 insert = self.toPythonIndex(insert)
428 insert = max(0, min(insert, n))
429 if i == j:
430 w.setCursorPosition(i)
431 else:
432 length = j - i
433 # Set selection is a QLineEditMethod
434 if insert < j:
435 w.setSelection(j, -length)
436 else:
437 w.setSelection(i, length)
438 # setSelectionRangeHelper = setSelectionRange
439 #@-others
440#@+node:ekr.20150403094619.1: ** class LeoLineTextWidget(QFrame)
441class LeoLineTextWidget(QtWidgets.QFrame): # type:ignore
442 """
443 A QFrame supporting gutter line numbers.
445 This class *has* a QTextEdit.
446 """
447 #@+others
448 #@+node:ekr.20150403094706.9: *3* __init__(LeoLineTextWidget)
449 def __init__(self, c, e, *args):
450 """Ctor for LineTextWidget."""
451 super().__init__(*args)
452 self.c = c
453 Raised = Shadow.Raised if isQt6 else self.StyledPanel
454 NoFrame = Shape.NoFrame if isQt6 else self.NoFrame
455 self.setFrameStyle(Raised)
456 self.edit = e # A QTextEdit
457 e.setFrameStyle(NoFrame)
458 # e.setAcceptRichText(False)
459 self.number_bar = NumberBar(c, e)
460 hbox = QtWidgets.QHBoxLayout(self)
461 hbox.setSpacing(0)
462 hbox.setContentsMargins(0, 0, 0, 0)
463 hbox.addWidget(self.number_bar)
464 hbox.addWidget(e)
465 e.installEventFilter(self)
466 e.viewport().installEventFilter(self)
467 #@+node:ekr.20150403094706.10: *3* eventFilter
468 def eventFilter(self, obj, event):
469 """
470 Update the line numbers for all events on the text edit and the viewport.
471 This is easier than connecting all necessary signals.
472 """
473 if obj in (self.edit, self.edit.viewport()):
474 self.number_bar.update()
475 return False
476 return QtWidgets.QFrame.eventFilter(obj, event)
477 #@-others
478#@+node:ekr.20110605121601.18005: ** class LeoQTextBrowser (QtWidgets.QTextBrowser)
479if QtWidgets:
482 class LeoQTextBrowser(QtWidgets.QTextBrowser): # type:ignore
483 """A subclass of QTextBrowser that overrides the mouse event handlers."""
484 #@+others
485 #@+node:ekr.20110605121601.18006: *3* lqtb.ctor
486 def __init__(self, parent, c, wrapper):
487 """ctor for LeoQTextBrowser class."""
488 for attr in ('leo_c', 'leo_wrapper',):
489 assert not hasattr(QtWidgets.QTextBrowser, attr), attr
490 self.leo_c = c
491 self.leo_s = '' # The cached text.
492 self.leo_wrapper = wrapper
493 self.htmlFlag = True
494 super().__init__(parent)
495 self.setCursorWidth(c.config.getInt('qt-cursor-width') or 1)
497 # Connect event handlers...
498 if 0: # Not a good idea: it will complicate delayed loading of body text.
499 # #1286
500 self.textChanged.connect(self.onTextChanged)
501 self.cursorPositionChanged.connect(self.highlightCurrentLine)
502 self.textChanged.connect(self.highlightCurrentLine)
503 self.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu)
504 self.customContextMenuRequested.connect(self.onContextMenu)
505 # This event handler is the easy way to keep track of the vertical scroll position.
506 self.leo_vsb = vsb = self.verticalScrollBar()
507 vsb.valueChanged.connect(self.onSliderChanged)
508 # For QCompleter
509 self.leo_q_completer = None
510 self.leo_options = None
511 self.leo_model = None
513 hl_color_setting = c.config.getString('line-highlight-color') or ''
514 hl_color = QColor(hl_color_setting)
515 self.hiliter_params = {
516 'lastblock': -2, 'last_style_hash': 0,
517 'last_color_setting': hl_color_setting,
518 'last_hl_color': hl_color
519 }
520 #@+node:ekr.20110605121601.18007: *3* lqtb. __repr__ & __str__
521 def __repr__(self):
522 return f"(LeoQTextBrowser) {id(self)}"
524 __str__ = __repr__
525 #@+node:ekr.20110605121601.18008: *3* lqtb.Auto completion
526 #@+node:ekr.20110605121601.18009: *4* class LeoQListWidget(QListWidget)
527 class LeoQListWidget(QtWidgets.QListWidget): # type:ignore
528 #@+others
529 #@+node:ekr.20110605121601.18010: *5* lqlw.ctor
530 def __init__(self, c):
531 """ctor for LeoQListWidget class"""
532 super().__init__()
533 self.setWindowFlags(WindowType.Popup | self.windowFlags())
534 # Inject the ivars
535 self.leo_w = c.frame.body.wrapper.widget
536 # A LeoQTextBrowser, a subclass of QtWidgets.QTextBrowser.
537 self.leo_c = c
538 # A weird hack.
539 self.leo_geom_set = False # When true, self.geom returns global coords!
540 self.itemClicked.connect(self.select_callback)
541 #@+node:ekr.20110605121601.18011: *5* lqlw.closeEvent
542 def closeEvent(self, event):
543 """Kill completion and close the window."""
544 self.leo_c.k.autoCompleter.abort()
545 #@+node:ekr.20110605121601.18012: *5* lqlw.end_completer
546 def end_completer(self):
547 """End completion."""
548 c = self.leo_c
549 c.in_qt_dialog = False
550 # This is important: it clears the autocompletion state.
551 c.k.keyboardQuit()
552 c.bodyWantsFocusNow()
553 try:
554 self.deleteLater()
555 except RuntimeError:
556 # Avoid bug 1338773: Autocompleter error
557 pass
558 #@+node:ekr.20141024170936.7: *5* lqlw.get_selection
559 def get_selection(self):
560 """Return the presently selected item's text."""
561 return self.currentItem().text()
562 #@+node:ekr.20110605121601.18013: *5* lqlw.keyPressEvent
563 def keyPressEvent(self, event):
564 """Handle a key event from QListWidget."""
565 c = self.leo_c
566 w = c.frame.body.wrapper
567 key = event.key()
568 if event.modifiers() != Modifier.NoModifier and not event.text():
569 # A modifier key on it's own.
570 pass
571 elif key in (Key.Key_Up, Key.Key_Down):
572 QtWidgets.QListWidget.keyPressEvent(self, event)
573 elif key == Key.Key_Tab:
574 self.tab_callback()
575 elif key in (Key.Key_Enter, Key.Key_Return):
576 self.select_callback()
577 else:
578 # Pass all other keys to the autocompleter via the event filter.
579 w.ev_filter.eventFilter(obj=self, event=event)
580 #@+node:ekr.20110605121601.18014: *5* lqlw.select_callback
581 def select_callback(self):
582 """
583 Called when user selects an item in the QListWidget.
584 """
585 c = self.leo_c
586 p = c.p
587 w = c.k.autoCompleter.w or c.frame.body.wrapper
588 oldSel = w.getSelectionRange()
589 oldText = w.getAllText()
590 # Replace the tail of the prefix with the completion.
591 completion = self.currentItem().text()
592 prefix = c.k.autoCompleter.get_autocompleter_prefix()
593 parts = prefix.split('.')
594 if len(parts) > 1:
595 tail = parts[-1]
596 else:
597 tail = prefix
598 if tail != completion:
599 j = w.getInsertPoint()
600 i = j - len(tail)
601 w.delete(i, j)
602 w.insert(i, completion)
603 j = i + len(completion)
604 c.setChanged()
605 w.setInsertPoint(j)
606 c.undoer.doTyping(p, 'Typing', oldText,
607 newText=w.getAllText(),
608 newInsert=w.getInsertPoint(),
609 newSel=w.getSelectionRange(),
610 oldSel=oldSel,
611 )
612 self.end_completer()
613 #@+node:tbrown.20111011094944.27031: *5* lqlw.tab_callback
614 def tab_callback(self):
615 """Called when user hits tab on an item in the QListWidget."""
616 c = self.leo_c
617 w = c.k.autoCompleter.w or c.frame.body.wrapper # 2014/09/19
618 if w is None:
619 return
620 # Replace the tail of the prefix with the completion.
621 prefix = c.k.autoCompleter.get_autocompleter_prefix()
622 parts = prefix.split('.')
623 if len(parts) < 2:
624 return
625 i = j = w.getInsertPoint()
626 s = w.getAllText()
627 while (0 <= i < len(s) and s[i] != '.'):
628 i -= 1
629 i += 1
630 if j > i:
631 w.delete(i, j)
632 w.setInsertPoint(i)
633 c.k.autoCompleter.compute_completion_list()
634 #@+node:ekr.20110605121601.18015: *5* lqlw.set_position
635 def set_position(self, c):
636 """Set the position of the QListWidget."""
638 def glob(obj, pt):
639 """Convert pt from obj's local coordinates to global coordinates."""
640 return obj.mapToGlobal(pt)
642 w = self.leo_w
643 vp = self.viewport()
644 r = w.cursorRect()
645 geom = self.geometry() # In viewport coordinates.
646 gr_topLeft = glob(w, r.topLeft())
647 # As a workaround to the Qt setGeometry bug,
648 # The window is destroyed instead of being hidden.
649 if self.leo_geom_set:
650 g.trace('Error: leo_geom_set')
651 return
652 # This code illustrates the Qt bug...
653 # if self.leo_geom_set:
654 # # Unbelievable: geom is now in *global* coords.
655 # gg_topLeft = geom.topLeft()
656 # else:
657 # # Per documentation, geom in local (viewport) coords.
658 # gg_topLeft = glob(vp,geom.topLeft())
659 gg_topLeft = glob(vp, geom.topLeft())
660 delta_x = gr_topLeft.x() - gg_topLeft.x()
661 delta_y = gr_topLeft.y() - gg_topLeft.y()
662 # These offset are reasonable. Perhaps they should depend on font size.
663 x_offset, y_offset = 10, 60
664 # Compute the new geometry, setting the size by hand.
665 geom2_topLeft = QtCore.QPoint(
666 geom.x() + delta_x + x_offset,
667 geom.y() + delta_y + y_offset)
668 geom2_size = QtCore.QSize(400, 100)
669 geom2 = QtCore.QRect(geom2_topLeft, geom2_size)
670 # These tests fail once offsets are added.
671 if x_offset == 0 and y_offset == 0:
672 if self.leo_geom_set:
673 if geom2.topLeft() != glob(w, r.topLeft()):
674 g.trace(
675 f"Error: geom.topLeft: {geom2.topLeft()}, "
676 f"geom2.topLeft: {glob(w, r.topLeft())}")
677 else:
678 if glob(vp, geom2.topLeft()) != glob(w, r.topLeft()):
679 g.trace(
680 f"Error 2: geom.topLeft: {glob(vp, geom2.topLeft())}, "
681 f"geom2.topLeft: {glob(w, r.topLeft())}")
682 self.setGeometry(geom2)
683 self.leo_geom_set = True
684 #@+node:ekr.20110605121601.18016: *5* lqlw.show_completions
685 def show_completions(self, aList):
686 """Set the QListView contents to aList."""
687 self.clear()
688 self.addItems(aList)
689 self.setCurrentRow(0)
690 self.activateWindow()
691 self.setFocus()
692 #@-others
693 #@+node:ekr.20110605121601.18017: *4* lqtb.lqtb.init_completer
694 def init_completer(self, options):
695 """Connect a QCompleter."""
696 c = self.leo_c
697 self.leo_qc = qc = self.LeoQListWidget(c)
698 # Move the window near the body pane's cursor.
699 qc.set_position(c)
700 # Show the initial completions.
701 c.in_qt_dialog = True
702 qc.show()
703 qc.activateWindow()
704 c.widgetWantsFocusNow(qc)
705 qc.show_completions(options)
706 return qc
707 #@+node:ekr.20110605121601.18018: *4* lqtb.redirections to LeoQListWidget
708 def end_completer(self):
709 if hasattr(self, 'leo_qc'):
710 self.leo_qc.end_completer()
711 delattr(self, 'leo_qc')
713 def show_completions(self, aList):
714 if hasattr(self, 'leo_qc'):
715 self.leo_qc.show_completions(aList)
716 #@+node:tom.20210827230127.1: *3* lqtb Highlight Current Line
717 #@+node:tom.20210827225119.3: *4* lqtb.parse_css
718 #@@language python
719 @staticmethod
720 def parse_css(css_string, clas=''):
721 """Extract colors from a css stylesheet string.
723 This is an extremely simple-minded function. It assumes
724 that no quotation marks are being used, and that the
725 first block in braces with the name clas is the controlling
726 css for our widget.
728 Returns a tuple of strings (color, background).
729 """
730 # Get first block with name matching "clas'
731 block = css_string.split(clas, 1)
732 block = block[1].split('{', 1)
733 block = block[1].split('}', 1)
735 # Split into styles separated by ";"
736 styles = block[0].split(';')
738 # Split into fields separated by ":"
739 fields = [style.split(':') for style in styles if style.strip()]
741 # Only get fields whose names are "color" and "background"
742 color = bg = ''
743 for style, val in fields:
744 style = style.strip()
745 if style == 'color':
746 color = val.strip()
747 elif style == 'background':
748 bg = val.strip()
749 if color and bg:
750 break
751 return color, bg
753 #@+node:tom.20210827225119.4: *4* lqtb.assign_bg
754 #@@language python
755 @staticmethod
756 def assign_bg(fg):
757 """If fg or bg colors are missing, assign
758 reasonable values. Can happen with incorrectly
759 constructed themes, or no-theme color schemes.
761 Intended to be called when bg color is missing.
763 RETURNS
764 a QColor object for the background color
765 """
766 if not fg:
767 fg = 'black' # QTextEdit default
768 bg = 'white' # QTextEdit default
769 if fg == 'black':
770 bg = 'white' # QTextEdit default
771 else:
772 fg_color = QColor(fg)
773 h, s, v, a = fg_color.getHsv()
774 if v < 128: # dark foreground
775 bg = 'white'
776 else:
777 bg = 'black'
778 return QColor(bg)
779 #@+node:tom.20210827225119.5: *4* lqtb.calc_hl
780 #@@language python
781 @staticmethod
782 def calc_hl(bg_color):
783 """Return the line highlight color.
785 ARGUMENT
786 bg_color -- a QColor object for the background color
788 RETURNS
789 a QColor object for the highlight color
790 """
791 h, s, v, a = bg_color.getHsv()
793 if v < 40:
794 v = 60
795 bg_color.setHsv(h, s, v, a)
796 elif v > 240:
797 v = 220
798 bg_color.setHsv(h, s, v, a)
799 elif v < 128:
800 bg_color = bg_color.lighter(130)
801 else:
802 bg_color = bg_color.darker(130)
804 return bg_color
805 #@+node:tom.20210827225119.2: *4* lqtb.highlightCurrentLine
806 #@@language python
807 def highlightCurrentLine(self):
808 """Highlight cursor line."""
809 c = self.leo_c
810 params = self.hiliter_params
811 editor = c.frame.body.wrapper.widget
813 if not c.config.getBool('highlight-body-line', True):
814 editor.setExtraSelections([])
815 return
817 curs = editor.textCursor()
818 blocknum = curs.blockNumber()
820 # Some cursor movements don't change the line: ignore them
821 # if blocknum == params['lastblock'] and blocknum > 0:
822 # return
824 if blocknum == 0: # invalid position
825 blocknum = 1
826 params['lastblock'] = blocknum
828 hl_color = params['last_hl_color']
830 #@+<< Recalculate Color >>
831 #@+node:tom.20210909124441.1: *5* << Recalculate Color >>
832 config_setting = c.config.getString('line-highlight-color') \
833 or ''
834 config_setting = (config_setting.replace("'", '')
835 .replace('"', '').lower()
836 .replace('none', ''))
838 last_color_setting = params['last_color_setting']
839 config_setting_changed = config_setting != last_color_setting
841 if config_setting:
842 if config_setting_changed:
843 hl_color = QColor(config_setting)
844 params['last_hl_color'] = hl_color
845 params['last_color_setting'] = config_setting
846 else:
847 hl_color = params['last_hl_color']
848 else:
849 ssm = c.styleSheetManager
850 sheet = ssm.expand_css_constants(c.active_stylesheet)
851 h = hash(sheet)
852 params['last_color_setting'] = ''
853 if params['last_style_hash'] != h or config_setting_changed:
854 fg, bg = self.parse_css(sheet, 'QTextEdit')
855 bg_color = QColor(bg) if bg else self.assign_bg(fg)
856 hl_color = self.calc_hl(bg_color)
857 # g.trace('fg', fg, 'bg', bg, 'hl_color', hl_color.name())
858 params['last_hl_color'] = hl_color
859 params['last_style_hash'] = h
860 #@-<< Recalculate Color >>
861 #@+<< Apply Highlight >>
862 #@+node:tom.20210909124551.1: *5* << Apply Highlight >>
863 # Based on code from
864 # https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
866 selection = editor.ExtraSelection()
867 selection.format.setBackground(hl_color)
868 selection.format.setProperty(FullWidthSelection, True)
869 selection.cursor = curs
870 selection.cursor.clearSelection()
872 editor.setExtraSelections([selection])
873 #@-<< Apply Highlight >>
874 #@+node:tom.20210905130804.1: *4* Add Help Menu Item
875 # Add entry to Help menu
876 new_entry = ('@item', 'help-for-&highlight-current-line', '')
878 if g.app.config:
879 for item in g.app.config.menusList:
880 if 'Help' in item[0]:
881 for entry in item[1]:
882 if entry[0].lower() == '@menu &open help topics':
883 menu_items = entry[1]
884 menu_items.append(new_entry)
885 menu_items.sort()
886 break
887 #@+node:ekr.20141103061944.31: *3* lqtb.get/setXScrollPosition
888 def getXScrollPosition(self):
889 """Get the horizontal scrollbar position."""
890 w = self
891 sb = w.horizontalScrollBar()
892 pos = sb.sliderPosition()
893 return pos
895 def setXScrollPosition(self, pos):
896 """Set the position of the horizontal scrollbar."""
897 if pos is not None:
898 w = self
899 sb = w.horizontalScrollBar()
900 sb.setSliderPosition(pos)
901 #@+node:ekr.20111002125540.7021: *3* lqtb.get/setYScrollPosition
902 def getYScrollPosition(self):
903 """Get the vertical scrollbar position."""
904 w = self
905 sb = w.verticalScrollBar()
906 pos = sb.sliderPosition()
907 return pos
909 def setYScrollPosition(self, pos):
910 """Set the position of the vertical scrollbar."""
911 w = self
912 if pos is None:
913 pos = 0
914 sb = w.verticalScrollBar()
915 sb.setSliderPosition(pos)
916 #@+node:ekr.20110605121601.18019: *3* lqtb.leo_dumpButton
917 def leo_dumpButton(self, event, tag):
918 button = event.button()
919 table = (
920 (MouseButton.NoButton, 'no button'),
921 (MouseButton.LeftButton, 'left-button'),
922 (MouseButton.RightButton, 'right-button'),
923 (MouseButton.MiddleButton, 'middle-button'),
924 )
925 for val, s in table:
926 if button == val:
927 kind = s
928 break
929 else:
930 kind = f"unknown: {repr(button)}"
931 return kind
932 #@+node:ekr.20200304130514.1: *3* lqtb.onContextMenu
933 def onContextMenu(self, point):
934 """LeoQTextBrowser: Callback for customContextMenuRequested events."""
935 # #1286.
936 c, w = self.leo_c, self
937 g.app.gui.onContextMenu(c, w, point)
938 #@+node:ekr.20120925061642.13506: *3* lqtb.onSliderChanged
939 def onSliderChanged(self, arg):
940 """Handle a Qt onSliderChanged event."""
941 c = self.leo_c
942 p = c.p
943 # Careful: changing nodes changes the scrollbars.
944 if hasattr(c.frame.tree, 'tree_select_lockout'):
945 if c.frame.tree.tree_select_lockout:
946 return
947 # Only scrolling in the body pane should set v.scrollBarSpot.
948 if not c.frame.body or self != c.frame.body.wrapper.widget:
949 return
950 if p:
951 p.v.scrollBarSpot = arg
952 #@+node:ekr.20201204172235.1: *3* lqtb.paintEvent
953 leo_cursor_width = 0
955 leo_vim_mode = None
957 def paintEvent(self, event):
958 """
959 LeoQTextBrowser.paintEvent.
961 New in Leo 6.4: Draw a box around the cursor in command mode.
962 This is as close as possible to vim's look.
963 """
964 c, vc, w = self.leo_c, self.leo_c.vimCommands, self
965 #
966 # First, call the base class paintEvent.
967 QtWidgets.QTextBrowser.paintEvent(self, event)
969 def set_cursor_width(width):
970 """Set the cursor width, but only if necessary."""
971 if self.leo_cursor_width != width:
972 self.leo_cursor_width = width
973 w.setCursorWidth(width)
975 #
976 # Are we in vim mode?
977 if self.leo_vim_mode is None:
978 self.leo_vim_mode = c.config.getBool('vim-mode', default=False)
979 #
980 # Are we in command mode?
981 if self.leo_vim_mode:
982 in_command = vc and vc.state == 'normal' # vim mode.
983 else:
984 in_command = c.k.unboundKeyAction == 'command' # vim emulation.
985 #
986 # Draw the box only in command mode, when w is the body pane, with focus.
987 if (
988 not in_command
989 or w != c.frame.body.widget
990 or w != g.app.gui.get_focus()
991 ):
992 set_cursor_width(c.config.getInt('qt-cursor-width') or 1)
993 return
994 #
995 # Set the width of the cursor.
996 font = w.currentFont()
997 cursor_width = QtGui.QFontMetrics(font).averageCharWidth()
998 set_cursor_width(cursor_width)
999 #
1000 # Draw a box around the cursor.
1001 qp = QtGui.QPainter()
1002 qp.begin(self.viewport())
1003 qp.drawRect(w.cursorRect())
1004 qp.end()
1005 #@+node:tbrown.20130411145310.18855: *3* lqtb.wheelEvent
1006 def wheelEvent(self, event):
1007 """Handle a wheel event."""
1008 if KeyboardModifier.ControlModifier & event.modifiers():
1009 d = {'c': self.leo_c}
1010 try: # Qt5 or later.
1011 point = event.angleDelta()
1012 delta = point.y() or point.x()
1013 except AttributeError:
1014 delta = event.delta() # Qt4.
1015 if delta < 0:
1016 zoom_out(d)
1017 else:
1018 zoom_in(d)
1019 event.accept()
1020 return
1021 QtWidgets.QTextBrowser.wheelEvent(self, event)
1022 #@-others
1023#@+node:ekr.20150403094706.2: ** class NumberBar(QFrame)
1024class NumberBar(QtWidgets.QFrame): # type:ignore
1025 #@+others
1026 #@+node:ekr.20150403094706.3: *3* NumberBar.__init__
1027 def __init__(self, c, e, *args):
1028 """Ctor for NumberBar class."""
1029 super().__init__(*args)
1030 self.c = c
1031 self.edit = e
1032 # A QTextEdit.
1033 self.d = e.document()
1034 # A QTextDocument.
1035 self.fm = self.fontMetrics()
1036 # A QFontMetrics
1037 self.image = QtGui.QImage(g.app.gui.getImageImage(
1038 g.os_path_finalize_join(g.app.loadDir,
1039 '..', 'Icons', 'Tango', '16x16', 'actions', 'stop.png')))
1040 self.highest_line = 0
1041 # The highest line that is currently visibile.
1042 # Set the name to gutter so that the QFrame#gutter style sheet applies.
1043 self.offsets = []
1044 self.setObjectName('gutter')
1045 self.reloadSettings()
1046 #@+node:ekr.20181005093003.1: *3* NumberBar.reloadSettings
1047 def reloadSettings(self):
1048 c = self.c
1049 c.registerReloadSettings(self)
1050 self.w_adjust = c.config.getInt('gutter-w-adjust') or 12
1051 # Extra width for column.
1052 self.y_adjust = c.config.getInt('gutter-y-adjust') or 10
1053 # The y offset of the first line of the gutter.
1054 #@+node:ekr.20181005085507.1: *3* NumberBar.mousePressEvent
1055 def mousePressEvent(self, event):
1057 c = self.c
1059 def find_line(y):
1060 n, last_y = 0, 0
1061 for n, y2 in self.offsets:
1062 if last_y <= y < y2:
1063 return n
1064 last_y = y2
1065 return n if self.offsets else 0
1067 xdb = getattr(g.app, 'xdb', None)
1068 if not xdb:
1069 return
1070 path = xdb.canonic(g.fullPath(c, c.p))
1071 if not path:
1072 return
1073 n = find_line(event.y())
1074 if not xdb.checkline(path, n):
1075 g.trace('FAIL checkline', path, n)
1076 return
1077 if xdb.has_breakpoint(path, n):
1078 xdb.qc.put(f"clear {path}:{n}")
1079 else:
1080 xdb.qc.put(f"b {path}:{n}")
1081 #@+node:ekr.20150403094706.5: *3* NumberBar.update
1082 def update(self, *args):
1083 """
1084 Updates the number bar to display the current set of numbers.
1085 Also, adjusts the width of the number bar if necessary.
1086 """
1087 # w_adjust is used to compensate for the current line being bold.
1088 # Always allocate room for 2 columns
1089 #width = self.fm.width(str(max(1000, self.highest_line))) + self.w_adjust
1090 if isQt6:
1091 width = self.fm.boundingRect(str(max(1000, self.highest_line))).width()
1092 else:
1093 width = self.fm.width(str(max(1000, self.highest_line))) + self.w_adjust
1094 if self.width() != width:
1095 self.setFixedWidth(width)
1096 QtWidgets.QWidget.update(self, *args)
1097 #@+node:ekr.20150403094706.6: *3* NumberBar.paintEvent
1098 def paintEvent(self, event):
1099 """
1100 Enhance QFrame.paintEvent.
1101 Paint all visible text blocks in the editor's document.
1102 """
1103 e = self.edit
1104 d = self.d
1105 layout = d.documentLayout()
1106 # Compute constants.
1107 current_block = d.findBlock(e.textCursor().position())
1108 scroll_y = e.verticalScrollBar().value()
1109 page_bottom = scroll_y + e.viewport().height()
1110 # Paint each visible block.
1111 painter = QtGui.QPainter(self)
1112 block = d.begin()
1113 n = i = 0
1114 c = self.c
1115 translation = c.user_dict.get('line_number_translation', [])
1116 self.offsets = []
1117 while block.isValid():
1118 i = translation[n] if n < len(translation) else n + 1
1119 n += 1
1120 top_left = layout.blockBoundingRect(block).topLeft()
1121 if top_left.y() > page_bottom:
1122 break # Outside the visible area.
1123 bold = block == current_block
1124 self.paintBlock(bold, i, painter, top_left, scroll_y)
1125 block = block.next()
1126 self.highest_line = i
1127 painter.end()
1128 QtWidgets.QWidget.paintEvent(self, event)
1129 # Propagate the event.
1130 #@+node:ekr.20150403094706.7: *3* NumberBar.paintBlock
1131 def paintBlock(self, bold, n, painter, top_left, scroll_y):
1132 """Paint n, right justified in the line number field."""
1133 c = self.c
1134 if bold:
1135 self.setBold(painter, True)
1136 s = str(n)
1137 pad = max(4, len(str(self.highest_line))) - len(s)
1138 s = ' ' * pad + s
1139 # x = self.width() - self.fm.width(s) - self.w_adjust
1140 x = 0
1141 y = round(top_left.y()) - scroll_y + self.fm.ascent() + self.y_adjust
1142 self.offsets.append((n, y),)
1143 painter.drawText(x, y, s)
1144 if bold:
1145 self.setBold(painter, False)
1146 xdb = getattr(g.app, 'xdb', None)
1147 if not xdb:
1148 return
1149 if not xdb.has_breakpoints():
1150 return
1151 path = g.fullPath(c, c.p)
1152 if xdb.has_breakpoint(path, n):
1153 target_r = QtCore.QRect(
1154 self.fm.width(s) + 16,
1155 top_left.y() + self.y_adjust - 2,
1156 16.0, 16.0)
1157 if self.image:
1158 source_r = QtCore.QRect(0.0, 0.0, 16.0, 16.0)
1159 painter.drawImage(target_r, self.image, source_r)
1160 else:
1161 painter.drawEllipse(target_r)
1162 #@+node:ekr.20150403094706.8: *3* NumberBar.setBold
1163 def setBold(self, painter, flag):
1164 """Set or clear bold facing in the painter, depending on flag."""
1165 font = painter.font()
1166 font.setBold(flag)
1167 painter.setFont(font)
1168 #@-others
1169#@+node:ekr.20140901141402.18700: ** class PlainTextWrapper(QTextMixin)
1170class PlainTextWrapper(QTextMixin):
1171 """A Qt class for use by the find code."""
1173 def __init__(self, widget):
1174 """Ctor for the PlainTextWrapper class."""
1175 super().__init__()
1176 self.widget = widget
1177#@+node:ekr.20110605121601.18116: ** class QHeadlineWrapper (QLineEditWrapper)
1178class QHeadlineWrapper(QLineEditWrapper):
1179 """
1180 A wrapper class for QLineEdit widgets in QTreeWidget's.
1181 This class just redefines the check method.
1182 """
1183 #@+others
1184 #@+node:ekr.20110605121601.18117: *3* qhw.Birth
1185 def __init__(self, c, item, name, widget):
1186 """The ctor for the QHeadlineWrapper class."""
1187 assert isinstance(widget, QtWidgets.QLineEdit), widget
1188 super().__init__(widget, name, c)
1189 # Set ivars.
1190 self.c = c
1191 self.item = item
1192 self.name = name
1193 self.permanent = False # Warn the minibuffer that we can go away.
1194 self.widget = widget
1195 # Set the signal.
1196 g.app.gui.setFilter(c, self.widget, self, tag=name)
1198 def __repr__(self):
1199 return f"QHeadlineWrapper: {id(self)}"
1200 #@+node:ekr.20110605121601.18119: *3* qhw.check
1201 def check(self):
1202 """Return True if the tree item exists and it's edit widget exists."""
1203 tree = self.c.frame.tree
1204 try:
1205 e = tree.treeWidget.itemWidget(self.item, 0)
1206 except RuntimeError:
1207 return False
1208 valid = tree.isValidItem(self.item)
1209 result = valid and e == self.widget
1210 return result
1211 #@-others
1212#@+node:ekr.20110605121601.18131: ** class QMinibufferWrapper (QLineEditWrapper)
1213class QMinibufferWrapper(QLineEditWrapper):
1215 def __init__(self, c):
1216 """Ctor for QMinibufferWrapper class."""
1217 self.c = c
1218 w = c.frame.top.lineEdit # QLineEdit
1219 super().__init__(widget=w, name='minibuffer', c=c)
1220 assert self.widget
1221 g.app.gui.setFilter(c, w, self, tag='minibuffer')
1222 # Monkey-patch the event handlers
1223 #@+<< define mouseReleaseEvent >>
1224 #@+node:ekr.20110605121601.18132: *3* << define mouseReleaseEvent >> (QMinibufferWrapper)
1225 def mouseReleaseEvent(event, self=self):
1226 """Override QLineEdit.mouseReleaseEvent.
1228 Simulate alt-x if we are not in an input state.
1229 """
1230 assert isinstance(self, QMinibufferWrapper), self
1231 assert isinstance(self.widget, QtWidgets.QLineEdit), self.widget
1232 c, k = self.c, self.c.k
1233 if not k.state.kind:
1234 # c.widgetWantsFocusNow(w) # Doesn't work.
1235 event2 = g.app.gui.create_key_event(c, w=c.frame.body.wrapper)
1236 k.fullCommand(event2)
1237 # c.outerUpdate() # Doesn't work.
1239 #@-<< define mouseReleaseEvent >>
1241 w.mouseReleaseEvent = mouseReleaseEvent
1243 def setStyleClass(self, style_class):
1244 self.widget.setProperty('style_class', style_class)
1245 #
1246 # to get the appearance to change because of a property
1247 # change, unlike a focus or hover change, we need to
1248 # re-apply the stylesheet. But re-applying at the top level
1249 # is too CPU hungry, so apply just to this widget instead.
1250 # It may lag a bit when the style's edited, but the new top
1251 # level sheet will get pushed down quite frequently.
1252 self.widget.setStyleSheet(self.c.frame.top.styleSheet())
1254 def setSelectionRange(self, i, j, insert=None, s=None):
1255 QLineEditWrapper.setSelectionRange(self, i, j, insert, s)
1256 insert = j if insert is None else insert
1257 if self.widget:
1258 self.widget._sel_and_insert = (i, j, insert)
1259#@+node:ekr.20110605121601.18103: ** class QScintillaWrapper(QTextMixin)
1260class QScintillaWrapper(QTextMixin):
1261 """
1262 A wrapper for QsciScintilla supporting the high-level interface.
1264 This widget will likely always be less capable the QTextEditWrapper.
1265 To do:
1266 - Fix all Scintilla unit-test failures.
1267 - Add support for all scintilla lexers.
1268 """
1269 #@+others
1270 #@+node:ekr.20110605121601.18105: *3* qsciw.ctor
1271 def __init__(self, widget, c, name=None):
1272 """Ctor for the QScintillaWrapper class."""
1273 super().__init__(c)
1274 self.baseClassName = 'QScintillaWrapper'
1275 self.c = c
1276 self.name = name
1277 self.useScintilla = True
1278 self.widget = widget
1279 # Complete the init.
1280 self.set_config()
1281 # Set the signal.
1282 g.app.gui.setFilter(c, widget, self, tag=name)
1283 #@+node:ekr.20110605121601.18106: *3* qsciw.set_config
1284 def set_config(self):
1285 """Set QScintillaWrapper configuration options."""
1286 c, w = self.c, self.widget
1287 n = c.config.getInt('qt-scintilla-zoom-in')
1288 if n not in (None, 1, 0):
1289 w.zoomIn(n)
1290 w.setUtf8(True) # Important.
1291 if 1:
1292 w.setBraceMatching(2) # Sloppy
1293 else:
1294 w.setBraceMatching(0) # wrapper.flashCharacter creates big problems.
1295 if 0:
1296 w.setMarginWidth(1, 40)
1297 w.setMarginLineNumbers(1, True)
1298 w.setIndentationWidth(4)
1299 w.setIndentationsUseTabs(False)
1300 w.setAutoIndent(True)
1301 #@+node:ekr.20110605121601.18107: *3* qsciw.WidgetAPI
1302 #@+node:ekr.20140901062324.18593: *4* qsciw.delete
1303 def delete(self, i, j=None):
1304 """Delete s[i:j]"""
1305 w = self.widget
1306 i = self.toPythonIndex(i)
1307 if j is None:
1308 j = i + 1
1309 j = self.toPythonIndex(j)
1310 self.setSelectionRange(i, j)
1311 try:
1312 self.changingText = True # Disable onTextChanged
1313 w.replaceSelectedText('')
1314 finally:
1315 self.changingText = False
1316 #@+node:ekr.20140901062324.18594: *4* qsciw.flashCharacter (disabled)
1317 def flashCharacter(self, i, bg='white', fg='red', flashes=2, delay=50):
1318 """Flash the character at position i."""
1319 if 0: # This causes a lot of problems: Better to use Scintilla matching.
1320 # This causes problems during unit tests:
1321 # The selection point isn't restored in time.
1322 if g.unitTesting:
1323 return
1324 #@+others
1325 #@+node:ekr.20140902084950.18635: *5* after
1326 def after(func, delay=delay):
1327 """Run func after the given delay."""
1328 QtCore.QTimer.singleShot(delay, func)
1329 #@+node:ekr.20140902084950.18636: *5* addFlashCallback
1330 def addFlashCallback(self=self):
1331 i = self.flashIndex
1332 w = self.widget
1333 self.setSelectionRange(i, i + 1)
1334 if self.flashBg:
1335 w.setSelectionBackgroundColor(QtGui.QColor(self.flashBg))
1336 if self.flashFg:
1337 w.setSelectionForegroundColor(QtGui.QColor(self.flashFg))
1338 self.flashCount -= 1
1339 after(removeFlashCallback)
1340 #@+node:ekr.20140902084950.18637: *5* removeFlashCallback
1341 def removeFlashCallback(self=self):
1342 """Remove the extra selections."""
1343 self.setInsertPoint(self.flashIndex)
1344 w = self.widget
1345 if self.flashCount > 0:
1346 after(addFlashCallback)
1347 else:
1348 w.resetSelectionBackgroundColor()
1349 self.setInsertPoint(self.flashIndex1)
1350 w.setFocus()
1351 #@-others
1352 # Numbered color names don't work in Ubuntu 8.10, so...
1353 if bg and bg[-1].isdigit() and bg[0] != '#':
1354 bg = bg[:-1]
1355 if fg and fg[-1].isdigit() and fg[0] != '#':
1356 fg = fg[:-1]
1357 # w = self.widget # A QsciScintilla widget.
1358 self.flashCount = flashes
1359 self.flashIndex1 = self.getInsertPoint()
1360 self.flashIndex = self.toPythonIndex(i)
1361 self.flashBg = None if bg.lower() == 'same' else bg
1362 self.flashFg = None if fg.lower() == 'same' else fg
1363 addFlashCallback()
1364 #@+node:ekr.20140901062324.18595: *4* qsciw.get
1365 def get(self, i, j=None):
1366 # Fix the following two bugs by using vanilla code:
1367 # https://bugs.launchpad.net/leo-editor/+bug/979142
1368 # https://bugs.launchpad.net/leo-editor/+bug/971166
1369 s = self.getAllText()
1370 i = self.toPythonIndex(i)
1371 j = self.toPythonIndex(j)
1372 return s[i:j]
1373 #@+node:ekr.20110605121601.18108: *4* qsciw.getAllText
1374 def getAllText(self):
1375 """Get all text from a QsciScintilla widget."""
1376 w = self.widget
1377 return w.text()
1378 #@+node:ekr.20110605121601.18109: *4* qsciw.getInsertPoint
1379 def getInsertPoint(self):
1380 """Get the insertion point from a QsciScintilla widget."""
1381 w = self.widget
1382 i = int(w.SendScintilla(w.SCI_GETCURRENTPOS))
1383 return i
1384 #@+node:ekr.20110605121601.18110: *4* qsciw.getSelectionRange
1385 def getSelectionRange(self, sort=True):
1386 """Get the selection range from a QsciScintilla widget."""
1387 w = self.widget
1388 i = int(w.SendScintilla(w.SCI_GETCURRENTPOS))
1389 j = int(w.SendScintilla(w.SCI_GETANCHOR))
1390 if sort and i > j:
1391 i, j = j, i
1392 return i, j
1393 #@+node:ekr.20140901062324.18599: *4* qsciw.getX/YScrollPosition (to do)
1394 def getXScrollPosition(self):
1395 # w = self.widget
1396 return 0 # Not ready yet.
1398 def getYScrollPosition(self):
1399 # w = self.widget
1400 return 0 # Not ready yet.
1401 #@+node:ekr.20110605121601.18111: *4* qsciw.hasSelection
1402 def hasSelection(self):
1403 """Return True if a QsciScintilla widget has a selection range."""
1404 return self.widget.hasSelectedText()
1405 #@+node:ekr.20140901062324.18601: *4* qsciw.insert
1406 def insert(self, i, s):
1407 """Insert s at position i."""
1408 w = self.widget
1409 i = self.toPythonIndex(i)
1410 w.SendScintilla(w.SCI_SETSEL, i, i)
1411 w.SendScintilla(w.SCI_ADDTEXT, len(s), g.toEncodedString(s))
1412 i += len(s)
1413 w.SendScintilla(w.SCI_SETSEL, i, i)
1414 return i
1415 #@+node:ekr.20140901062324.18603: *4* qsciw.linesPerPage
1416 def linesPerPage(self):
1417 """Return the number of lines presently visible."""
1418 # Not used in Leo's core. Not tested.
1419 w = self.widget
1420 return int(w.SendScintilla(w.SCI_LINESONSCREEN))
1421 #@+node:ekr.20140901062324.18604: *4* qsciw.scrollDelegate (maybe)
1422 if 0: # Not yet.
1424 def scrollDelegate(self, kind):
1425 """
1426 Scroll a QTextEdit up or down one page.
1427 direction is in ('down-line','down-page','up-line','up-page')
1428 """
1429 c = self.c
1430 w = self.widget
1431 vScroll = w.verticalScrollBar()
1432 h = w.size().height()
1433 lineSpacing = w.fontMetrics().lineSpacing()
1434 n = h / lineSpacing
1435 n = max(2, n - 3)
1436 if kind == 'down-half-page':
1437 delta = n / 2
1438 elif kind == 'down-line':
1439 delta = 1
1440 elif kind == 'down-page':
1441 delta = n
1442 elif kind == 'up-half-page':
1443 delta = -n / 2
1444 elif kind == 'up-line':
1445 delta = -1
1446 elif kind == 'up-page':
1447 delta = -n
1448 else:
1449 delta = 0
1450 g.trace('bad kind:', kind)
1451 val = vScroll.value()
1452 vScroll.setValue(val + (delta * lineSpacing))
1453 c.bodyWantsFocus()
1454 #@+node:ekr.20110605121601.18112: *4* qsciw.see
1455 def see(self, i):
1456 """Ensure insert point i is visible in a QsciScintilla widget."""
1457 # Ok for now. Using SCI_SETYCARETPOLICY might be better.
1458 w = self.widget
1459 s = self.getAllText()
1460 i = self.toPythonIndex(i)
1461 row, col = g.convertPythonIndexToRowCol(s, i)
1462 w.ensureLineVisible(row)
1463 #@+node:ekr.20110605121601.18113: *4* qsciw.setAllText
1464 def setAllText(self, s):
1465 """Set the text of a QScintilla widget."""
1466 w = self.widget
1467 assert isinstance(w, Qsci.QsciScintilla), w
1468 w.setText(s)
1469 # w.update()
1470 #@+node:ekr.20110605121601.18114: *4* qsciw.setInsertPoint
1471 def setInsertPoint(self, i, s=None):
1472 """Set the insertion point in a QsciScintilla widget."""
1473 w = self.widget
1474 i = self.toPythonIndex(i)
1475 # w.SendScintilla(w.SCI_SETCURRENTPOS,i)
1476 # w.SendScintilla(w.SCI_SETANCHOR,i)
1477 w.SendScintilla(w.SCI_SETSEL, i, i)
1478 #@+node:ekr.20110605121601.18115: *4* qsciw.setSelectionRange
1479 def setSelectionRange(self, i, j, insert=None, s=None):
1480 """Set the selection range in a QsciScintilla widget."""
1481 w = self.widget
1482 i = self.toPythonIndex(i)
1483 j = self.toPythonIndex(j)
1484 insert = j if insert is None else self.toPythonIndex(insert)
1485 if insert >= i:
1486 w.SendScintilla(w.SCI_SETSEL, i, j)
1487 else:
1488 w.SendScintilla(w.SCI_SETSEL, j, i)
1489 #@+node:ekr.20140901062324.18609: *4* qsciw.setX/YScrollPosition (to do)
1490 def setXScrollPosition(self, pos):
1491 """Set the position of the horizontal scrollbar."""
1493 def setYScrollPosition(self, pos):
1494 """Set the position of the vertical scrollbar."""
1495 #@-others
1496#@+node:ekr.20110605121601.18071: ** class QTextEditWrapper(QTextMixin)
1497class QTextEditWrapper(QTextMixin):
1498 """A wrapper for a QTextEdit/QTextBrowser supporting the high-level interface."""
1499 #@+others
1500 #@+node:ekr.20110605121601.18073: *3* qtew.ctor & helpers
1501 def __init__(self, widget, name, c=None):
1502 """Ctor for QTextEditWrapper class. widget is a QTextEdit/QTextBrowser."""
1503 super().__init__(c)
1504 # Make sure all ivars are set.
1505 self.baseClassName = 'QTextEditWrapper'
1506 self.c = c
1507 self.name = name
1508 self.widget = widget
1509 self.useScintilla = False
1510 # Complete the init.
1511 if c and widget:
1512 self.widget.setUndoRedoEnabled(False)
1513 self.set_config()
1514 self.set_signals()
1516 #@+node:ekr.20110605121601.18076: *4* qtew.set_config
1517 def set_config(self):
1518 """Set configuration options for QTextEdit."""
1519 w = self.widget
1520 w.setWordWrapMode(WrapMode.NoWrap)
1521 # tab stop in pixels - no config for this (yet)
1522 if isQt6:
1523 w.setTabStopDistance(24)
1524 else:
1525 w.setTabStopWidth(24)
1526 #@+node:ekr.20140901062324.18566: *4* qtew.set_signals (should be distributed?)
1527 def set_signals(self):
1528 """Set up signals."""
1529 c, name = self.c, self.name
1530 if name in ('body', 'rendering-pane-wrapper') or name.startswith('head'):
1531 # Hook up qt events.
1532 g.app.gui.setFilter(c, self.widget, self, tag=name)
1533 if name == 'body':
1534 w = self.widget
1535 w.textChanged.connect(self.onTextChanged)
1536 w.cursorPositionChanged.connect(self.onCursorPositionChanged)
1537 if name in ('body', 'log'):
1538 # Monkey patch the event handler.
1539 #@+others
1540 #@+node:ekr.20140901062324.18565: *5* mouseReleaseEvent (monkey-patch) QTextEditWrapper
1541 def mouseReleaseEvent(event, self=self):
1542 """
1543 Monkey patch for self.widget (QTextEditWrapper) mouseReleaseEvent.
1544 """
1545 assert isinstance(self, QTextEditWrapper), self
1546 assert isinstance(self.widget, QtWidgets.QTextEdit), self.widget
1547 QtWidgets.QTextEdit.mouseReleaseEvent(self.widget, event)
1548 # Call the base class.
1549 c = self.c
1550 setattr(event, 'c', c)
1551 # Open the url on a control-click.
1552 if KeyboardModifier.ControlModifier & event.modifiers():
1553 g.openUrlOnClick(event)
1554 else:
1555 if name == 'body':
1556 c.p.v.insertSpot = c.frame.body.wrapper.getInsertPoint()
1557 g.doHook("bodyclick2", c=c, p=c.p, v=c.p)
1558 # Do *not* change the focus! This would rip focus away from tab panes.
1559 c.k.keyboardQuit(setFocus=False)
1560 #@-others
1561 self.widget.mouseReleaseEvent = mouseReleaseEvent
1562 #@+node:ekr.20200312052821.1: *3* qtew.repr
1563 def __repr__(self):
1564 # Add a leading space to align with StringTextWrapper.
1565 return f" <QTextEditWrapper: {id(self)} {self.name}>"
1567 __str__ = __repr__
1568 #@+node:ekr.20110605121601.18078: *3* qtew.High-level interface
1569 # These are all widget-dependent
1570 #@+node:ekr.20110605121601.18079: *4* qtew.delete (avoid call to setAllText)
1571 def delete(self, i, j=None):
1572 """QTextEditWrapper."""
1573 w = self.widget
1574 i = self.toPythonIndex(i)
1575 if j is None:
1576 j = i + 1
1577 j = self.toPythonIndex(j)
1578 if i > j:
1579 i, j = j, i
1580 sb = w.verticalScrollBar()
1581 pos = sb.sliderPosition()
1582 cursor = w.textCursor()
1583 try:
1584 self.changingText = True # Disable onTextChanged
1585 old_i, old_j = self.getSelectionRange()
1586 if i == old_i and j == old_j:
1587 # Work around an apparent bug in cursor.movePosition.
1588 cursor.removeSelectedText()
1589 elif i == j:
1590 pass
1591 else:
1592 cursor.setPosition(i)
1593 moveCount = abs(j - i)
1594 cursor.movePosition(MoveOperation.Right, MoveMode.KeepAnchor, moveCount)
1595 w.setTextCursor(cursor) # Bug fix: 2010/01/27
1596 cursor.removeSelectedText()
1597 finally:
1598 self.changingText = False
1599 sb.setSliderPosition(pos)
1600 #@+node:ekr.20110605121601.18080: *4* qtew.flashCharacter
1601 def flashCharacter(self, i, bg='white', fg='red', flashes=3, delay=75):
1602 """QTextEditWrapper."""
1603 # numbered color names don't work in Ubuntu 8.10, so...
1604 if bg[-1].isdigit() and bg[0] != '#':
1605 bg = bg[:-1]
1606 if fg[-1].isdigit() and fg[0] != '#':
1607 fg = fg[:-1]
1608 # This might causes problems during unit tests.
1609 # The selection point isn't restored in time.
1610 if g.unitTesting:
1611 return
1612 w = self.widget # A QTextEdit.
1613 # Remember highlighted line:
1614 last_selections = w.extraSelections()
1616 def after(func):
1617 QtCore.QTimer.singleShot(delay, func)
1619 def addFlashCallback(self=self, w=w):
1620 i = self.flashIndex
1621 cursor = w.textCursor() # Must be the widget's cursor.
1622 cursor.setPosition(i)
1623 cursor.movePosition(MoveOperation.Right, MoveMode.KeepAnchor, 1)
1624 extra = w.ExtraSelection()
1625 extra.cursor = cursor
1626 if self.flashBg:
1627 extra.format.setBackground(QtGui.QColor(self.flashBg))
1628 if self.flashFg:
1629 extra.format.setForeground(QtGui.QColor(self.flashFg))
1630 self.extraSelList = last_selections[:]
1631 self.extraSelList.append(extra) # must be last
1632 w.setExtraSelections(self.extraSelList)
1633 self.flashCount -= 1
1634 after(removeFlashCallback)
1636 def removeFlashCallback(self=self, w=w):
1637 w.setExtraSelections(last_selections)
1638 if self.flashCount > 0:
1639 after(addFlashCallback)
1640 else:
1641 w.setFocus()
1643 self.flashCount = flashes
1644 self.flashIndex = i
1645 self.flashBg = None if bg.lower() == 'same' else bg
1646 self.flashFg = None if fg.lower() == 'same' else fg
1647 addFlashCallback()
1649 #@+node:ekr.20110605121601.18081: *4* qtew.getAllText
1650 def getAllText(self):
1651 """QTextEditWrapper."""
1652 w = self.widget
1653 return w.toPlainText()
1654 #@+node:ekr.20110605121601.18082: *4* qtew.getInsertPoint
1655 def getInsertPoint(self):
1656 """QTextEditWrapper."""
1657 return self.widget.textCursor().position()
1658 #@+node:ekr.20110605121601.18083: *4* qtew.getSelectionRange
1659 def getSelectionRange(self, sort=True):
1660 """QTextEditWrapper."""
1661 w = self.widget
1662 tc = w.textCursor()
1663 i, j = tc.selectionStart(), tc.selectionEnd()
1664 return i, j
1665 #@+node:ekr.20110605121601.18084: *4* qtew.getX/YScrollPosition
1666 # **Important**: There is a Qt bug here: the scrollbar position
1667 # is valid only if cursor is visible. Otherwise the *reported*
1668 # scrollbar position will be such that the cursor *is* visible.
1670 def getXScrollPosition(self):
1671 """QTextEditWrapper: Get the horizontal scrollbar position."""
1672 w = self.widget
1673 sb = w.horizontalScrollBar()
1674 pos = sb.sliderPosition()
1675 return pos
1677 def getYScrollPosition(self):
1678 """QTextEditWrapper: Get the vertical scrollbar position."""
1679 w = self.widget
1680 sb = w.verticalScrollBar()
1681 pos = sb.sliderPosition()
1682 return pos
1683 #@+node:ekr.20110605121601.18085: *4* qtew.hasSelection
1684 def hasSelection(self):
1685 """QTextEditWrapper."""
1686 return self.widget.textCursor().hasSelection()
1687 #@+node:ekr.20110605121601.18089: *4* qtew.insert (avoid call to setAllText)
1688 def insert(self, i, s):
1689 """QTextEditWrapper."""
1690 w = self.widget
1691 i = self.toPythonIndex(i)
1692 cursor = w.textCursor()
1693 try:
1694 self.changingText = True # Disable onTextChanged.
1695 cursor.setPosition(i)
1696 cursor.insertText(s)
1697 w.setTextCursor(cursor) # Bug fix: 2010/01/27
1698 finally:
1699 self.changingText = False
1700 #@+node:ekr.20110605121601.18077: *4* qtew.leoMoveCursorHelper & helper
1701 def leoMoveCursorHelper(self, kind, extend=False, linesPerPage=15):
1702 """QTextEditWrapper."""
1703 w = self.widget
1704 d = {
1705 'begin-line': MoveOperation.StartOfLine, # Was start-line
1706 'down': MoveOperation.Down,
1707 'end': MoveOperation.End,
1708 'end-line': MoveOperation.EndOfLine, # Not used.
1709 'exchange': True, # Dummy.
1710 'home': MoveOperation.Start,
1711 'left': MoveOperation.Left,
1712 'page-down': MoveOperation.Down,
1713 'page-up': MoveOperation.Up,
1714 'right': MoveOperation.Right,
1715 'up': MoveOperation.Up,
1716 }
1717 kind = kind.lower()
1718 op = d.get(kind)
1719 mode = MoveMode.KeepAnchor if extend else MoveMode.MoveAnchor
1720 if not op:
1721 g.trace(f"can not happen: bad kind: {kind}")
1722 return
1723 if kind in ('page-down', 'page-up'):
1724 self.pageUpDown(op, mode)
1725 elif kind == 'exchange': # exchange-point-and-mark
1726 cursor = w.textCursor()
1727 anchor = cursor.anchor()
1728 pos = cursor.position()
1729 cursor.setPosition(pos, MoveOperation.MoveAnchor)
1730 cursor.setPosition(anchor, MoveOperation.KeepAnchor)
1731 w.setTextCursor(cursor)
1732 else:
1733 if not extend:
1734 # Fix an annoyance. Make sure to clear the selection.
1735 cursor = w.textCursor()
1736 cursor.clearSelection()
1737 w.setTextCursor(cursor)
1738 w.moveCursor(op, mode)
1739 self.seeInsertPoint()
1740 self.rememberSelectionAndScroll()
1741 # #218.
1742 cursor = w.textCursor()
1743 sel = cursor.selection().toPlainText()
1744 if sel and hasattr(g.app.gui, 'setClipboardSelection'):
1745 g.app.gui.setClipboardSelection(sel)
1746 self.c.frame.updateStatusLine()
1747 #@+node:btheado.20120129145543.8180: *5* qtew.pageUpDown
1748 def pageUpDown(self, op, moveMode):
1749 """
1750 The QTextEdit PageUp/PageDown functionality seems to be "baked-in"
1751 and not externally accessible. Since Leo has its own keyhandling
1752 functionality, this code emulates the QTextEdit paging. This is a
1753 straight port of the C++ code found in the pageUpDown method of
1754 gui/widgets/qtextedit.cpp.
1755 """
1756 control = self.widget
1757 cursor = control.textCursor()
1758 moved = False
1759 lastY = control.cursorRect(cursor).top()
1760 distance = 0
1761 # move using movePosition to keep the cursor's x
1762 while True:
1763 y = control.cursorRect(cursor).top()
1764 distance += abs(y - lastY)
1765 lastY = y
1766 moved = cursor.movePosition(op, moveMode)
1767 if (not moved or distance >= control.height()):
1768 break
1769 sb = control.verticalScrollBar()
1770 if moved:
1771 if op == MoveOperation.Up:
1772 cursor.movePosition(MoveOperation.Down, moveMode)
1773 sb.triggerAction(SliderAction.SliderPageStepSub)
1774 else:
1775 cursor.movePosition(MoveOperation.Up, moveMode)
1776 sb.triggerAction(SliderAction.SliderPageStepAdd)
1777 control.setTextCursor(cursor)
1778 #@+node:ekr.20110605121601.18087: *4* qtew.linesPerPage
1779 def linesPerPage(self):
1780 """QTextEditWrapper."""
1781 # Not used in Leo's core.
1782 w = self.widget
1783 h = w.size().height()
1784 lineSpacing = w.fontMetrics().lineSpacing()
1785 n = h / lineSpacing
1786 return n
1787 #@+node:ekr.20110605121601.18088: *4* qtew.scrollDelegate
1788 def scrollDelegate(self, kind):
1789 """
1790 Scroll a QTextEdit up or down one page.
1791 direction is in ('down-line','down-page','up-line','up-page')
1792 """
1793 c = self.c
1794 w = self.widget
1795 vScroll = w.verticalScrollBar()
1796 h = w.size().height()
1797 lineSpacing = w.fontMetrics().lineSpacing()
1798 n = h / lineSpacing
1799 n = max(2, n - 3)
1800 if kind == 'down-half-page':
1801 delta = n / 2
1802 elif kind == 'down-line':
1803 delta = 1
1804 elif kind == 'down-page':
1805 delta = n
1806 elif kind == 'up-half-page':
1807 delta = -n / 2
1808 elif kind == 'up-line':
1809 delta = -1
1810 elif kind == 'up-page':
1811 delta = -n
1812 else:
1813 delta = 0
1814 g.trace('bad kind:', kind)
1815 val = vScroll.value()
1816 vScroll.setValue(val + (delta * lineSpacing))
1817 c.bodyWantsFocus()
1818 #@+node:ekr.20110605121601.18090: *4* qtew.see & seeInsertPoint
1819 def see(self, see_i):
1820 """Scroll so that position see_i is visible."""
1821 w = self.widget
1822 tc = w.textCursor()
1823 # Put see_i in range.
1824 s = self.getAllText()
1825 see_i = max(0, min(see_i, len(s)))
1826 # Remember the old cursor
1827 old_cursor = QtGui.QTextCursor(tc)
1828 # Scroll so that see_i is visible.
1829 tc.setPosition(see_i)
1830 w.setTextCursor(tc)
1831 w.ensureCursorVisible()
1832 # Restore the old cursor
1833 w.setTextCursor(old_cursor)
1835 def seeInsertPoint(self):
1836 """Make sure the insert point is visible."""
1837 self.widget.ensureCursorVisible()
1838 #@+node:ekr.20110605121601.18092: *4* qtew.setAllText
1839 def setAllText(self, s):
1840 """Set the text of body pane."""
1841 w = self.widget
1842 try:
1843 self.changingText = True # Disable onTextChanged.
1844 w.setReadOnly(False)
1845 w.setPlainText(s)
1846 finally:
1847 self.changingText = False
1848 #@+node:ekr.20110605121601.18095: *4* qtew.setInsertPoint
1849 def setInsertPoint(self, i, s=None):
1850 # Fix bug 981849: incorrect body content shown.
1851 # Use the more careful code in setSelectionRange.
1852 self.setSelectionRange(i=i, j=i, insert=i, s=s)
1853 #@+node:ekr.20110605121601.18096: *4* qtew.setSelectionRange
1854 def setSelectionRange(self, i, j, insert=None, s=None):
1855 """Set the selection range and the insert point."""
1856 #
1857 # Part 1
1858 w = self.widget
1859 i = self.toPythonIndex(i)
1860 j = self.toPythonIndex(j)
1861 if s is None:
1862 s = self.getAllText()
1863 n = len(s)
1864 i = max(0, min(i, n))
1865 j = max(0, min(j, n))
1866 if insert is None:
1867 ins = max(i, j)
1868 else:
1869 ins = self.toPythonIndex(insert)
1870 ins = max(0, min(ins, n))
1871 #
1872 # Part 2:
1873 # 2010/02/02: Use only tc.setPosition here.
1874 # Using tc.movePosition doesn't work.
1875 tc = w.textCursor()
1876 if i == j:
1877 tc.setPosition(i)
1878 elif ins == j:
1879 # Put the insert point at j
1880 tc.setPosition(i)
1881 tc.setPosition(j, MoveMode.KeepAnchor)
1882 elif ins == i:
1883 # Put the insert point at i
1884 tc.setPosition(j)
1885 tc.setPosition(i, MoveMode.KeepAnchor)
1886 else:
1887 # 2014/08/21: It doesn't seem possible to put the insert point somewhere else!
1888 tc.setPosition(j)
1889 tc.setPosition(i, MoveMode.KeepAnchor)
1890 w.setTextCursor(tc)
1891 # #218.
1892 if hasattr(g.app.gui, 'setClipboardSelection'):
1893 if s[i:j]:
1894 g.app.gui.setClipboardSelection(s[i:j])
1895 #
1896 # Remember the values for v.restoreCursorAndScroll.
1897 v = self.c.p.v # Always accurate.
1898 v.insertSpot = ins
1899 if i > j:
1900 i, j = j, i
1901 assert i <= j
1902 v.selectionStart = i
1903 v.selectionLength = j - i
1904 v.scrollBarSpot = w.verticalScrollBar().value()
1905 #@+node:ekr.20141103061944.40: *4* qtew.setXScrollPosition
1906 def setXScrollPosition(self, pos):
1907 """Set the position of the horizonatl scrollbar."""
1908 if pos is not None:
1909 w = self.widget
1910 sb = w.horizontalScrollBar()
1911 sb.setSliderPosition(pos)
1912 #@+node:ekr.20110605121601.18098: *4* qtew.setYScrollPosition
1913 def setYScrollPosition(self, pos):
1914 """Set the vertical scrollbar position."""
1915 if pos is not None:
1916 w = self.widget
1917 sb = w.verticalScrollBar()
1918 sb.setSliderPosition(pos)
1919 #@+node:ekr.20110605121601.18100: *4* qtew.toPythonIndex
1920 def toPythonIndex(self, index, s=None):
1921 """This is much faster than versions using g.toPythonIndex."""
1922 w = self
1923 te = self.widget
1924 if index is None:
1925 return 0
1926 if isinstance(index, int):
1927 return index
1928 if index == '1.0':
1929 return 0
1930 if index == 'end':
1931 return w.getLastPosition()
1932 doc = te.document()
1933 data = index.split('.')
1934 if len(data) == 2:
1935 row, col = data
1936 row, col = int(row), int(col)
1937 bl = doc.findBlockByNumber(row - 1)
1938 return bl.position() + col
1939 g.trace(f"bad string index: {index}")
1940 return 0
1941 #@+node:ekr.20110605121601.18101: *4* qtew.toPythonIndexRowCol
1942 def toPythonIndexRowCol(self, index):
1943 w = self
1944 if index == '1.0':
1945 return 0, 0, 0
1946 if index == 'end':
1947 index = w.getLastPosition()
1948 te = self.widget
1949 doc = te.document()
1950 i = w.toPythonIndex(index)
1951 bl = doc.findBlock(i)
1952 row = bl.blockNumber()
1953 col = i - bl.position()
1954 return i, row, col
1955 #@-others
1956#@-others
1958#@@language python
1959#@@tabwidth -4
1960#@@pagewidth 70
1961#@-leo