Coverage for C:\leo.repo\leo-editor\leo\plugins\qt_text.py: 24%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.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 # A flag for k.masterKeyHandler and isTextWrapper.
99 self.supportsHighLevelInterface = True
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_fg': '', 'last_bg': '',
519 'last_hl_color': hl_color
520 }
521 #@+node:ekr.20110605121601.18007: *3* lqtb. __repr__ & __str__
522 def __repr__(self):
523 return f"(LeoQTextBrowser) {id(self)}"
525 __str__ = __repr__
526 #@+node:ekr.20110605121601.18008: *3* lqtb.Auto completion
527 #@+node:ekr.20110605121601.18009: *4* class LeoQListWidget(QListWidget)
528 class LeoQListWidget(QtWidgets.QListWidget): # type:ignore
529 #@+others
530 #@+node:ekr.20110605121601.18010: *5* lqlw.ctor
531 def __init__(self, c):
532 """ctor for LeoQListWidget class"""
533 super().__init__()
534 self.setWindowFlags(WindowType.Popup | self.windowFlags())
535 # Inject the ivars
536 # A LeoQTextBrowser, a subclass of QtWidgets.QTextBrowser.
537 self.leo_w = c.frame.body.wrapper.widget
538 self.leo_c = c
539 # A weird hack.
540 self.leo_geom_set = False # When true, self.geom returns global coords!
541 self.itemClicked.connect(self.select_callback)
542 #@+node:ekr.20110605121601.18011: *5* lqlw.closeEvent
543 def closeEvent(self, event):
544 """Kill completion and close the window."""
545 self.leo_c.k.autoCompleter.abort()
546 #@+node:ekr.20110605121601.18012: *5* lqlw.end_completer
547 def end_completer(self):
548 """End completion."""
549 c = self.leo_c
550 c.in_qt_dialog = False
551 # This is important: it clears the autocompletion state.
552 c.k.keyboardQuit()
553 c.bodyWantsFocusNow()
554 try:
555 self.deleteLater()
556 except RuntimeError:
557 # Avoid bug 1338773: Autocompleter error
558 pass
559 #@+node:ekr.20141024170936.7: *5* lqlw.get_selection
560 def get_selection(self):
561 """Return the presently selected item's text."""
562 return self.currentItem().text()
563 #@+node:ekr.20110605121601.18013: *5* lqlw.keyPressEvent
564 def keyPressEvent(self, event):
565 """Handle a key event from QListWidget."""
566 c = self.leo_c
567 w = c.frame.body.wrapper
568 key = event.key()
569 if event.modifiers() != Modifier.NoModifier and not event.text():
570 # A modifier key on it's own.
571 pass
572 elif key in (Key.Key_Up, Key.Key_Down):
573 QtWidgets.QListWidget.keyPressEvent(self, event)
574 elif key == Key.Key_Tab:
575 self.tab_callback()
576 elif key in (Key.Key_Enter, Key.Key_Return):
577 self.select_callback()
578 else:
579 # Pass all other keys to the autocompleter via the event filter.
580 w.ev_filter.eventFilter(obj=self, event=event)
581 #@+node:ekr.20110605121601.18014: *5* lqlw.select_callback
582 def select_callback(self):
583 """
584 Called when user selects an item in the QListWidget.
585 """
586 c = self.leo_c
587 p = c.p
588 w = c.k.autoCompleter.w or c.frame.body.wrapper
589 oldSel = w.getSelectionRange()
590 oldText = w.getAllText()
591 # Replace the tail of the prefix with the completion.
592 completion = self.currentItem().text()
593 prefix = c.k.autoCompleter.get_autocompleter_prefix()
594 parts = prefix.split('.')
595 if len(parts) > 1:
596 tail = parts[-1]
597 else:
598 tail = prefix
599 if tail != completion:
600 j = w.getInsertPoint()
601 i = j - len(tail)
602 w.delete(i, j)
603 w.insert(i, completion)
604 j = i + len(completion)
605 c.setChanged()
606 w.setInsertPoint(j)
607 c.undoer.doTyping(p, 'Typing', oldText,
608 newText=w.getAllText(),
609 newInsert=w.getInsertPoint(),
610 newSel=w.getSelectionRange(),
611 oldSel=oldSel,
612 )
613 self.end_completer()
614 #@+node:tbrown.20111011094944.27031: *5* lqlw.tab_callback
615 def tab_callback(self):
616 """Called when user hits tab on an item in the QListWidget."""
617 c = self.leo_c
618 w = c.k.autoCompleter.w or c.frame.body.wrapper # 2014/09/19
619 if w is None:
620 return
621 # Replace the tail of the prefix with the completion.
622 prefix = c.k.autoCompleter.get_autocompleter_prefix()
623 parts = prefix.split('.')
624 if len(parts) < 2:
625 return
626 i = j = w.getInsertPoint()
627 s = w.getAllText()
628 while (0 <= i < len(s) and s[i] != '.'):
629 i -= 1
630 i += 1
631 if j > i:
632 w.delete(i, j)
633 w.setInsertPoint(i)
634 c.k.autoCompleter.compute_completion_list()
635 #@+node:ekr.20110605121601.18015: *5* lqlw.set_position
636 def set_position(self, c):
637 """Set the position of the QListWidget."""
639 def glob(obj, pt):
640 """Convert pt from obj's local coordinates to global coordinates."""
641 return obj.mapToGlobal(pt)
643 w = self.leo_w
644 vp = self.viewport()
645 r = w.cursorRect()
646 geom = self.geometry() # In viewport coordinates.
647 gr_topLeft = glob(w, r.topLeft())
648 # As a workaround to the Qt setGeometry bug,
649 # The window is destroyed instead of being hidden.
650 if self.leo_geom_set:
651 g.trace('Error: leo_geom_set')
652 return
653 # This code illustrates the Qt bug...
654 # if self.leo_geom_set:
655 # # Unbelievable: geom is now in *global* coords.
656 # gg_topLeft = geom.topLeft()
657 # else:
658 # # Per documentation, geom in local (viewport) coords.
659 # gg_topLeft = glob(vp,geom.topLeft())
660 gg_topLeft = glob(vp, geom.topLeft())
661 delta_x = gr_topLeft.x() - gg_topLeft.x()
662 delta_y = gr_topLeft.y() - gg_topLeft.y()
663 # These offset are reasonable. Perhaps they should depend on font size.
664 x_offset, y_offset = 10, 60
665 # Compute the new geometry, setting the size by hand.
666 geom2_topLeft = QtCore.QPoint(
667 geom.x() + delta_x + x_offset,
668 geom.y() + delta_y + y_offset)
669 geom2_size = QtCore.QSize(400, 100)
670 geom2 = QtCore.QRect(geom2_topLeft, geom2_size)
671 # These tests fail once offsets are added.
672 if x_offset == 0 and y_offset == 0:
673 if self.leo_geom_set:
674 if geom2.topLeft() != glob(w, r.topLeft()):
675 g.trace(
676 f"Error: geom.topLeft: {geom2.topLeft()}, "
677 f"geom2.topLeft: {glob(w, r.topLeft())}")
678 else:
679 if glob(vp, geom2.topLeft()) != glob(w, r.topLeft()):
680 g.trace(
681 f"Error 2: geom.topLeft: {glob(vp, geom2.topLeft())}, "
682 f"geom2.topLeft: {glob(w, r.topLeft())}")
683 self.setGeometry(geom2)
684 self.leo_geom_set = True
685 #@+node:ekr.20110605121601.18016: *5* lqlw.show_completions
686 def show_completions(self, aList):
687 """Set the QListView contents to aList."""
688 self.clear()
689 self.addItems(aList)
690 self.setCurrentRow(0)
691 self.activateWindow()
692 self.setFocus()
693 #@-others
694 #@+node:ekr.20110605121601.18017: *4* lqtb.lqtb.init_completer
695 def init_completer(self, options):
696 """Connect a QCompleter."""
697 c = self.leo_c
698 self.leo_qc = qc = self.LeoQListWidget(c)
699 # Move the window near the body pane's cursor.
700 qc.set_position(c)
701 # Show the initial completions.
702 c.in_qt_dialog = True
703 qc.show()
704 qc.activateWindow()
705 c.widgetWantsFocusNow(qc)
706 qc.show_completions(options)
707 return qc
708 #@+node:ekr.20110605121601.18018: *4* lqtb.redirections to LeoQListWidget
709 def end_completer(self):
710 if hasattr(self, 'leo_qc'):
711 self.leo_qc.end_completer()
712 delattr(self, 'leo_qc')
714 def show_completions(self, aList):
715 if hasattr(self, 'leo_qc'):
716 self.leo_qc.show_completions(aList)
717 #@+node:tom.20210827230127.1: *3* lqtb Highlight Current Line
718 #@+node:tom.20210827225119.3: *4* lqtb.parse_css
719 #@@language python
720 @staticmethod
721 def parse_css(css_string, clas=''):
722 """Extract colors from a css stylesheet string.
724 This is an extremely simple-minded function. It assumes
725 that no quotation marks are being used, and that the
726 first block in braces with the name clas is the controlling
727 css for our widget.
729 Returns a tuple of strings (color, background).
730 """
731 # Get first block with name matching "clas'
732 block = css_string.split(clas, 1)
733 block = block[1].split('{', 1)
734 block = block[1].split('}', 1)
736 # Split into styles separated by ";"
737 styles = block[0].split(';')
739 # Split into fields separated by ":"
740 fields = [style.split(':') for style in styles if style.strip()]
742 # Only get fields whose names are "color" and "background"
743 color = bg = ''
744 for style, val in fields:
745 style = style.strip()
746 if style == 'color':
747 color = val.strip()
748 elif style == 'background':
749 bg = val.strip()
750 if color and bg:
751 break
752 return color, bg
754 #@+node:tom.20210827225119.4: *4* lqtb.assign_bg
755 #@@language python
756 @staticmethod
757 def assign_bg(fg):
758 """If fg or bg colors are missing, assign
759 reasonable values. Can happen with incorrectly
760 constructed themes, or no-theme color schemes.
762 Intended to be called when bg color is missing.
764 RETURNS
765 a QColor object for the background color
766 """
767 if not fg:
768 fg = 'black' # QTextEdit default
769 bg = 'white' # QTextEdit default
770 if fg == 'black':
771 bg = 'white' # QTextEdit default
772 else:
773 fg_color = QColor(fg)
774 h, s, v, a = fg_color.getHsv()
775 if v < 128: # dark foreground
776 bg = 'white'
777 else:
778 bg = 'black'
779 return QColor(bg)
780 #@+node:tom.20210827225119.5: *4* lqtb.calc_hl
781 #@@language python
782 @staticmethod
783 def calc_hl(bg_color):
784 """Return the line highlight color.
786 ARGUMENT
787 bg_color -- a QColor object for the background color
789 RETURNS
790 a QColor object for the highlight color
791 """
792 h, s, v, a = bg_color.getHsv()
794 if v < 40:
795 v = 60
796 bg_color.setHsv(h, s, v, a)
797 elif v > 240:
798 v = 220
799 bg_color.setHsv(h, s, v, a)
800 elif v < 128:
801 bg_color = bg_color.lighter(130)
802 else:
803 bg_color = bg_color.darker(130)
805 return bg_color
806 #@+node:tom.20210827225119.2: *4* lqtb.highlightCurrentLine
807 #@@language python
808 def highlightCurrentLine(self):
809 """Highlight cursor line."""
810 c = self.leo_c
811 params = self.hiliter_params
812 editor = c.frame.body.wrapper.widget
814 if not c.config.getBool('highlight-body-line', True):
815 editor.setExtraSelections([])
816 return
818 curs = editor.textCursor()
819 blocknum = curs.blockNumber()
821 # Some cursor movements don't change the line: ignore them
822 # if blocknum == params['lastblock'] and blocknum > 0:
823 # return
825 if blocknum == 0: # invalid position
826 blocknum = 1
827 params['lastblock'] = blocknum
829 hl_color = params['last_hl_color']
831 #@+<< Recalculate Color >>
832 #@+node:tom.20210909124441.1: *5* << Recalculate Color >>
833 config_setting = c.config.getString('line-highlight-color') \
834 or ''
835 config_setting = (config_setting.replace("'", '')
836 .replace('"', '').lower()
837 .replace('none', ''))
839 last_color_setting = params['last_color_setting']
840 config_setting_changed = config_setting != last_color_setting
842 if config_setting:
843 if config_setting_changed:
844 hl_color = QColor(config_setting)
845 params['last_hl_color'] = hl_color
846 params['last_color_setting'] = config_setting
847 else:
848 hl_color = params['last_hl_color']
849 else:
850 # Get current colors from the body editor widget
851 wrapper = c.frame.body.wrapper
852 w = wrapper.widget
853 pallete = w.viewport().palette()
854 fg_hex = pallete.text().color().rgb()
855 bg_hex = pallete.window().color().rgb()
856 fg = f'#{fg_hex:x}'
857 bg = f'#{bg_hex:x}'
859 if (params['last_fg'] != fg or params['last_bg'] != bg):
860 bg_color = QColor(bg) if bg else self.assign_bg(fg)
861 hl_color = self.calc_hl(bg_color)
862 #g.trace(f'fg: {fg}, bg: {bg}, hl_color: {hl_color.name()}')
863 params['last_hl_color'] = hl_color
864 params['last_fg'] = fg
865 params['last_bg'] = bg
866 #@-<< Recalculate Color >>
867 #@+<< Apply Highlight >>
868 #@+node:tom.20210909124551.1: *5* << Apply Highlight >>
869 # Based on code from
870 # https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
872 selection = editor.ExtraSelection()
873 selection.format.setBackground(hl_color)
874 selection.format.setProperty(FullWidthSelection, True)
875 selection.cursor = curs
876 selection.cursor.clearSelection()
878 editor.setExtraSelections([selection])
879 #@-<< Apply Highlight >>
880 #@+node:tom.20210905130804.1: *4* Add Help Menu Item
881 # Add entry to Help menu
882 new_entry = ('@item', 'help-for-&highlight-current-line', '')
884 if g.app.config:
885 for item in g.app.config.menusList:
886 if 'Help' in item[0]:
887 for entry in item[1]:
888 if entry[0].lower() == '@menu &open help topics':
889 menu_items = entry[1]
890 menu_items.append(new_entry)
891 menu_items.sort()
892 break
893 #@+node:ekr.20141103061944.31: *3* lqtb.get/setXScrollPosition
894 def getXScrollPosition(self):
895 """Get the horizontal scrollbar position."""
896 w = self
897 sb = w.horizontalScrollBar()
898 pos = sb.sliderPosition()
899 return pos
901 def setXScrollPosition(self, pos):
902 """Set the position of the horizontal scrollbar."""
903 if pos is not None:
904 w = self
905 sb = w.horizontalScrollBar()
906 sb.setSliderPosition(pos)
907 #@+node:ekr.20111002125540.7021: *3* lqtb.get/setYScrollPosition
908 def getYScrollPosition(self):
909 """Get the vertical scrollbar position."""
910 w = self
911 sb = w.verticalScrollBar()
912 pos = sb.sliderPosition()
913 return pos
915 def setYScrollPosition(self, pos):
916 """Set the position of the vertical scrollbar."""
917 w = self
918 if pos is None:
919 pos = 0
920 sb = w.verticalScrollBar()
921 sb.setSliderPosition(pos)
922 #@+node:ekr.20110605121601.18019: *3* lqtb.leo_dumpButton
923 def leo_dumpButton(self, event, tag):
924 button = event.button()
925 table = (
926 (MouseButton.NoButton, 'no button'),
927 (MouseButton.LeftButton, 'left-button'),
928 (MouseButton.RightButton, 'right-button'),
929 (MouseButton.MiddleButton, 'middle-button'),
930 )
931 for val, s in table:
932 if button == val:
933 kind = s
934 break
935 else:
936 kind = f"unknown: {repr(button)}"
937 return kind
938 #@+node:ekr.20200304130514.1: *3* lqtb.onContextMenu
939 def onContextMenu(self, point):
940 """LeoQTextBrowser: Callback for customContextMenuRequested events."""
941 # #1286.
942 c, w = self.leo_c, self
943 g.app.gui.onContextMenu(c, w, point)
944 #@+node:ekr.20120925061642.13506: *3* lqtb.onSliderChanged
945 def onSliderChanged(self, arg):
946 """Handle a Qt onSliderChanged event."""
947 c = self.leo_c
948 p = c.p
949 # Careful: changing nodes changes the scrollbars.
950 if hasattr(c.frame.tree, 'tree_select_lockout'):
951 if c.frame.tree.tree_select_lockout:
952 return
953 # Only scrolling in the body pane should set v.scrollBarSpot.
954 if not c.frame.body or self != c.frame.body.wrapper.widget:
955 return
956 if p:
957 p.v.scrollBarSpot = arg
958 #@+node:ekr.20201204172235.1: *3* lqtb.paintEvent
959 leo_cursor_width = 0
961 leo_vim_mode = None
963 def paintEvent(self, event):
964 """
965 LeoQTextBrowser.paintEvent.
967 New in Leo 6.4: Draw a box around the cursor in command mode.
968 This is as close as possible to vim's look.
969 """
970 c, vc, w = self.leo_c, self.leo_c.vimCommands, self
971 #
972 # First, call the base class paintEvent.
973 QtWidgets.QTextBrowser.paintEvent(self, event)
975 def set_cursor_width(width):
976 """Set the cursor width, but only if necessary."""
977 if self.leo_cursor_width != width:
978 self.leo_cursor_width = width
979 w.setCursorWidth(width)
981 #
982 # Are we in vim mode?
983 if self.leo_vim_mode is None:
984 self.leo_vim_mode = c.config.getBool('vim-mode', default=False)
985 #
986 # Are we in command mode?
987 if self.leo_vim_mode:
988 in_command = vc and vc.state == 'normal' # vim mode.
989 else:
990 in_command = c.k.unboundKeyAction == 'command' # vim emulation.
991 #
992 # Draw the box only in command mode, when w is the body pane, with focus.
993 if (
994 not in_command
995 or w != c.frame.body.widget
996 or w != g.app.gui.get_focus()
997 ):
998 set_cursor_width(c.config.getInt('qt-cursor-width') or 1)
999 return
1000 #
1001 # Set the width of the cursor.
1002 font = w.currentFont()
1003 cursor_width = QtGui.QFontMetrics(font).averageCharWidth()
1004 set_cursor_width(cursor_width)
1005 #
1006 # Draw a box around the cursor.
1007 qp = QtGui.QPainter()
1008 qp.begin(self.viewport())
1009 qp.drawRect(w.cursorRect())
1010 qp.end()
1011 #@+node:tbrown.20130411145310.18855: *3* lqtb.wheelEvent
1012 def wheelEvent(self, event):
1013 """Handle a wheel event."""
1014 if KeyboardModifier.ControlModifier & event.modifiers():
1015 d = {'c': self.leo_c}
1016 try: # Qt5 or later.
1017 point = event.angleDelta()
1018 delta = point.y() or point.x()
1019 except AttributeError:
1020 delta = event.delta() # Qt4.
1021 if delta < 0:
1022 zoom_out(d)
1023 else:
1024 zoom_in(d)
1025 event.accept()
1026 return
1027 QtWidgets.QTextBrowser.wheelEvent(self, event)
1028 #@-others
1029#@+node:ekr.20150403094706.2: ** class NumberBar(QFrame)
1030class NumberBar(QtWidgets.QFrame): # type:ignore
1031 #@+others
1032 #@+node:ekr.20150403094706.3: *3* NumberBar.__init__
1033 def __init__(self, c, e, *args):
1034 """Ctor for NumberBar class."""
1035 super().__init__(*args)
1036 self.c = c
1037 self.edit = e # A QTextEdit.
1038 self.d = e.document() # A QTextDocument.
1039 self.fm = self.fontMetrics() # A QFontMetrics
1040 self.image = QtGui.QImage(g.app.gui.getImageImage(
1041 g.os_path_finalize_join(g.app.loadDir,
1042 '..', 'Icons', 'Tango', '16x16', 'actions', 'stop.png')))
1043 self.highest_line = 0 # The highest line that is currently visibile.
1044 # Set the name to gutter so that the QFrame#gutter style sheet applies.
1045 self.offsets = []
1046 self.setObjectName('gutter')
1047 self.reloadSettings()
1048 #@+node:ekr.20181005093003.1: *3* NumberBar.reloadSettings
1049 def reloadSettings(self):
1050 c = self.c
1051 c.registerReloadSettings(self)
1052 # Extra width for column.
1053 self.w_adjust = c.config.getInt('gutter-w-adjust') or 12
1054 # The y offset of the first line of the gutter.
1055 self.y_adjust = c.config.getInt('gutter-y-adjust') or 10
1056 #@+node:ekr.20181005085507.1: *3* NumberBar.mousePressEvent
1057 def mousePressEvent(self, event):
1059 c = self.c
1061 def find_line(y):
1062 n, last_y = 0, 0
1063 for n, y2 in self.offsets:
1064 if last_y <= y < y2:
1065 return n
1066 last_y = y2
1067 return n if self.offsets else 0
1069 xdb = getattr(g.app, 'xdb', None)
1070 if not xdb:
1071 return
1072 path = xdb.canonic(g.fullPath(c, c.p))
1073 if not path:
1074 return
1075 n = find_line(event.y())
1076 if not xdb.checkline(path, n):
1077 g.trace('FAIL checkline', path, n)
1078 return
1079 if xdb.has_breakpoint(path, n):
1080 xdb.qc.put(f"clear {path}:{n}")
1081 else:
1082 xdb.qc.put(f"b {path}:{n}")
1083 #@+node:ekr.20150403094706.5: *3* NumberBar.update
1084 def update(self, *args):
1085 """
1086 Updates the number bar to display the current set of numbers.
1087 Also, adjusts the width of the number bar if necessary.
1088 """
1089 # w_adjust is used to compensate for the current line being bold.
1090 # Always allocate room for 2 columns
1091 #width = self.fm.width(str(max(1000, self.highest_line))) + self.w_adjust
1092 if isQt6:
1093 width = self.fm.boundingRect(str(max(1000, self.highest_line))).width()
1094 else:
1095 width = self.fm.width(str(max(1000, self.highest_line))) + self.w_adjust
1096 if self.width() != width:
1097 self.setFixedWidth(width)
1098 QtWidgets.QWidget.update(self, *args)
1099 #@+node:ekr.20150403094706.6: *3* NumberBar.paintEvent
1100 def paintEvent(self, event):
1101 """
1102 Enhance QFrame.paintEvent.
1103 Paint all visible text blocks in the editor's document.
1104 """
1105 e = self.edit
1106 d = self.d
1107 layout = d.documentLayout()
1108 # Compute constants.
1109 current_block = d.findBlock(e.textCursor().position())
1110 scroll_y = e.verticalScrollBar().value()
1111 page_bottom = scroll_y + e.viewport().height()
1112 # Paint each visible block.
1113 painter = QtGui.QPainter(self)
1114 block = d.begin()
1115 n = i = 0
1116 c = self.c
1117 translation = c.user_dict.get('line_number_translation', [])
1118 self.offsets = []
1119 while block.isValid():
1120 i = translation[n] if n < len(translation) else n + 1
1121 n += 1
1122 top_left = layout.blockBoundingRect(block).topLeft()
1123 if top_left.y() > page_bottom:
1124 break # Outside the visible area.
1125 bold = block == current_block
1126 self.paintBlock(bold, i, painter, top_left, scroll_y)
1127 block = block.next()
1128 self.highest_line = i
1129 painter.end()
1130 QtWidgets.QWidget.paintEvent(self, event)
1131 # Propagate the event.
1132 #@+node:ekr.20150403094706.7: *3* NumberBar.paintBlock
1133 def paintBlock(self, bold, n, painter, top_left, scroll_y):
1134 """Paint n, right justified in the line number field."""
1135 c = self.c
1136 if bold:
1137 self.setBold(painter, True)
1138 s = str(n)
1139 pad = max(4, len(str(self.highest_line))) - len(s)
1140 s = ' ' * pad + s
1141 # x = self.width() - self.fm.width(s) - self.w_adjust
1142 x = 0
1143 y = round(top_left.y()) - scroll_y + self.fm.ascent() + self.y_adjust
1144 self.offsets.append((n, y),)
1145 painter.drawText(x, y, s)
1146 if bold:
1147 self.setBold(painter, False)
1148 xdb = getattr(g.app, 'xdb', None)
1149 if not xdb:
1150 return
1151 if not xdb.has_breakpoints():
1152 return
1153 path = g.fullPath(c, c.p)
1154 if xdb.has_breakpoint(path, n):
1155 target_r = QtCore.QRect(
1156 self.fm.width(s) + 16,
1157 top_left.y() + self.y_adjust - 2,
1158 16.0, 16.0)
1159 if self.image:
1160 source_r = QtCore.QRect(0.0, 0.0, 16.0, 16.0)
1161 painter.drawImage(target_r, self.image, source_r)
1162 else:
1163 painter.drawEllipse(target_r)
1164 #@+node:ekr.20150403094706.8: *3* NumberBar.setBold
1165 def setBold(self, painter, flag):
1166 """Set or clear bold facing in the painter, depending on flag."""
1167 font = painter.font()
1168 font.setBold(flag)
1169 painter.setFont(font)
1170 #@-others
1171#@+node:ekr.20140901141402.18700: ** class PlainTextWrapper(QTextMixin)
1172class PlainTextWrapper(QTextMixin):
1173 """A Qt class for use by the find code."""
1175 def __init__(self, widget):
1176 """Ctor for the PlainTextWrapper class."""
1177 super().__init__()
1178 self.widget = widget
1179#@+node:ekr.20110605121601.18116: ** class QHeadlineWrapper (QLineEditWrapper)
1180class QHeadlineWrapper(QLineEditWrapper):
1181 """
1182 A wrapper class for QLineEdit widgets in QTreeWidget's.
1183 This class just redefines the check method.
1184 """
1185 #@+others
1186 #@+node:ekr.20110605121601.18117: *3* qhw.Birth
1187 def __init__(self, c, item, name, widget):
1188 """The ctor for the QHeadlineWrapper class."""
1189 assert isinstance(widget, QtWidgets.QLineEdit), widget
1190 super().__init__(widget, name, c)
1191 # Set ivars.
1192 self.c = c
1193 self.item = item
1194 self.name = name
1195 self.permanent = False # Warn the minibuffer that we can go away.
1196 self.widget = widget
1197 # Set the signal.
1198 g.app.gui.setFilter(c, self.widget, self, tag=name)
1200 def __repr__(self):
1201 return f"QHeadlineWrapper: {id(self)}"
1202 #@+node:ekr.20110605121601.18119: *3* qhw.check
1203 def check(self):
1204 """Return True if the tree item exists and it's edit widget exists."""
1205 tree = self.c.frame.tree
1206 try:
1207 e = tree.treeWidget.itemWidget(self.item, 0)
1208 except RuntimeError:
1209 return False
1210 valid = tree.isValidItem(self.item)
1211 result = valid and e == self.widget
1212 return result
1213 #@-others
1214#@+node:ekr.20110605121601.18131: ** class QMinibufferWrapper (QLineEditWrapper)
1215class QMinibufferWrapper(QLineEditWrapper):
1217 def __init__(self, c):
1218 """Ctor for QMinibufferWrapper class."""
1219 self.c = c
1220 w = c.frame.top.lineEdit # QLineEdit
1221 super().__init__(widget=w, name='minibuffer', c=c)
1222 assert self.widget
1223 g.app.gui.setFilter(c, w, self, tag='minibuffer')
1224 # Monkey-patch the event handlers
1225 #@+<< define mouseReleaseEvent >>
1226 #@+node:ekr.20110605121601.18132: *3* << define mouseReleaseEvent >> (QMinibufferWrapper)
1227 def mouseReleaseEvent(event, self=self):
1228 """Override QLineEdit.mouseReleaseEvent.
1230 Simulate alt-x if we are not in an input state.
1231 """
1232 assert isinstance(self, QMinibufferWrapper), self
1233 assert isinstance(self.widget, QtWidgets.QLineEdit), self.widget
1234 c, k = self.c, self.c.k
1235 if not k.state.kind:
1236 # c.widgetWantsFocusNow(w) # Doesn't work.
1237 event2 = g.app.gui.create_key_event(c, w=c.frame.body.wrapper)
1238 k.fullCommand(event2)
1239 # c.outerUpdate() # Doesn't work.
1241 #@-<< define mouseReleaseEvent >>
1243 w.mouseReleaseEvent = mouseReleaseEvent
1245 def setStyleClass(self, style_class):
1246 self.widget.setProperty('style_class', style_class)
1247 #
1248 # to get the appearance to change because of a property
1249 # change, unlike a focus or hover change, we need to
1250 # re-apply the stylesheet. But re-applying at the top level
1251 # is too CPU hungry, so apply just to this widget instead.
1252 # It may lag a bit when the style's edited, but the new top
1253 # level sheet will get pushed down quite frequently.
1254 self.widget.setStyleSheet(self.c.frame.top.styleSheet())
1256 def setSelectionRange(self, i, j, insert=None, s=None):
1257 QLineEditWrapper.setSelectionRange(self, i, j, insert, s)
1258 insert = j if insert is None else insert
1259 if self.widget:
1260 self.widget._sel_and_insert = (i, j, insert)
1261#@+node:ekr.20110605121601.18103: ** class QScintillaWrapper(QTextMixin)
1262class QScintillaWrapper(QTextMixin):
1263 """
1264 A wrapper for QsciScintilla supporting the high-level interface.
1266 This widget will likely always be less capable the QTextEditWrapper.
1267 To do:
1268 - Fix all Scintilla unit-test failures.
1269 - Add support for all scintilla lexers.
1270 """
1271 #@+others
1272 #@+node:ekr.20110605121601.18105: *3* qsciw.ctor
1273 def __init__(self, widget, c, name=None):
1274 """Ctor for the QScintillaWrapper class."""
1275 super().__init__(c)
1276 self.baseClassName = 'QScintillaWrapper'
1277 self.c = c
1278 self.name = name
1279 self.useScintilla = True
1280 self.widget = widget
1281 # Complete the init.
1282 self.set_config()
1283 # Set the signal.
1284 g.app.gui.setFilter(c, widget, self, tag=name)
1285 #@+node:ekr.20110605121601.18106: *3* qsciw.set_config
1286 def set_config(self):
1287 """Set QScintillaWrapper configuration options."""
1288 c, w = self.c, self.widget
1289 n = c.config.getInt('qt-scintilla-zoom-in')
1290 if n not in (None, 1, 0):
1291 w.zoomIn(n)
1292 w.setUtf8(True) # Important.
1293 if 1:
1294 w.setBraceMatching(2) # Sloppy
1295 else:
1296 w.setBraceMatching(0) # wrapper.flashCharacter creates big problems.
1297 if 0:
1298 w.setMarginWidth(1, 40)
1299 w.setMarginLineNumbers(1, True)
1300 w.setIndentationWidth(4)
1301 w.setIndentationsUseTabs(False)
1302 w.setAutoIndent(True)
1303 #@+node:ekr.20110605121601.18107: *3* qsciw.WidgetAPI
1304 #@+node:ekr.20140901062324.18593: *4* qsciw.delete
1305 def delete(self, i, j=None):
1306 """Delete s[i:j]"""
1307 w = self.widget
1308 i = self.toPythonIndex(i)
1309 if j is None:
1310 j = i + 1
1311 j = self.toPythonIndex(j)
1312 self.setSelectionRange(i, j)
1313 try:
1314 self.changingText = True # Disable onTextChanged
1315 w.replaceSelectedText('')
1316 finally:
1317 self.changingText = False
1318 #@+node:ekr.20140901062324.18594: *4* qsciw.flashCharacter (disabled)
1319 def flashCharacter(self, i, bg='white', fg='red', flashes=2, delay=50):
1320 """Flash the character at position i."""
1321 if 0: # This causes a lot of problems: Better to use Scintilla matching.
1322 # This causes problems during unit tests:
1323 # The selection point isn't restored in time.
1324 if g.unitTesting:
1325 return
1326 #@+others
1327 #@+node:ekr.20140902084950.18635: *5* after
1328 def after(func, delay=delay):
1329 """Run func after the given delay."""
1330 QtCore.QTimer.singleShot(delay, func)
1331 #@+node:ekr.20140902084950.18636: *5* addFlashCallback
1332 def addFlashCallback(self=self):
1333 i = self.flashIndex
1334 w = self.widget
1335 self.setSelectionRange(i, i + 1)
1336 if self.flashBg:
1337 w.setSelectionBackgroundColor(QtGui.QColor(self.flashBg))
1338 if self.flashFg:
1339 w.setSelectionForegroundColor(QtGui.QColor(self.flashFg))
1340 self.flashCount -= 1
1341 after(removeFlashCallback)
1342 #@+node:ekr.20140902084950.18637: *5* removeFlashCallback
1343 def removeFlashCallback(self=self):
1344 """Remove the extra selections."""
1345 self.setInsertPoint(self.flashIndex)
1346 w = self.widget
1347 if self.flashCount > 0:
1348 after(addFlashCallback)
1349 else:
1350 w.resetSelectionBackgroundColor()
1351 self.setInsertPoint(self.flashIndex1)
1352 w.setFocus()
1353 #@-others
1354 # Numbered color names don't work in Ubuntu 8.10, so...
1355 if bg and bg[-1].isdigit() and bg[0] != '#':
1356 bg = bg[:-1]
1357 if fg and fg[-1].isdigit() and fg[0] != '#':
1358 fg = fg[:-1]
1359 # w = self.widget # A QsciScintilla widget.
1360 self.flashCount = flashes
1361 self.flashIndex1 = self.getInsertPoint()
1362 self.flashIndex = self.toPythonIndex(i)
1363 self.flashBg = None if bg.lower() == 'same' else bg
1364 self.flashFg = None if fg.lower() == 'same' else fg
1365 addFlashCallback()
1366 #@+node:ekr.20140901062324.18595: *4* qsciw.get
1367 def get(self, i, j=None):
1368 # Fix the following two bugs by using vanilla code:
1369 # https://bugs.launchpad.net/leo-editor/+bug/979142
1370 # https://bugs.launchpad.net/leo-editor/+bug/971166
1371 s = self.getAllText()
1372 i = self.toPythonIndex(i)
1373 j = self.toPythonIndex(j)
1374 return s[i:j]
1375 #@+node:ekr.20110605121601.18108: *4* qsciw.getAllText
1376 def getAllText(self):
1377 """Get all text from a QsciScintilla widget."""
1378 w = self.widget
1379 return w.text()
1380 #@+node:ekr.20110605121601.18109: *4* qsciw.getInsertPoint
1381 def getInsertPoint(self):
1382 """Get the insertion point from a QsciScintilla widget."""
1383 w = self.widget
1384 i = int(w.SendScintilla(w.SCI_GETCURRENTPOS))
1385 return i
1386 #@+node:ekr.20110605121601.18110: *4* qsciw.getSelectionRange
1387 def getSelectionRange(self, sort=True):
1388 """Get the selection range from a QsciScintilla widget."""
1389 w = self.widget
1390 i = int(w.SendScintilla(w.SCI_GETCURRENTPOS))
1391 j = int(w.SendScintilla(w.SCI_GETANCHOR))
1392 if sort and i > j:
1393 i, j = j, i
1394 return i, j
1395 #@+node:ekr.20140901062324.18599: *4* qsciw.getX/YScrollPosition (to do)
1396 def getXScrollPosition(self):
1397 # w = self.widget
1398 return 0 # Not ready yet.
1400 def getYScrollPosition(self):
1401 # w = self.widget
1402 return 0 # Not ready yet.
1403 #@+node:ekr.20110605121601.18111: *4* qsciw.hasSelection
1404 def hasSelection(self):
1405 """Return True if a QsciScintilla widget has a selection range."""
1406 return self.widget.hasSelectedText()
1407 #@+node:ekr.20140901062324.18601: *4* qsciw.insert
1408 def insert(self, i, s):
1409 """Insert s at position i."""
1410 w = self.widget
1411 i = self.toPythonIndex(i)
1412 w.SendScintilla(w.SCI_SETSEL, i, i)
1413 w.SendScintilla(w.SCI_ADDTEXT, len(s), g.toEncodedString(s))
1414 i += len(s)
1415 w.SendScintilla(w.SCI_SETSEL, i, i)
1416 return i
1417 #@+node:ekr.20140901062324.18603: *4* qsciw.linesPerPage
1418 def linesPerPage(self):
1419 """Return the number of lines presently visible."""
1420 # Not used in Leo's core. Not tested.
1421 w = self.widget
1422 return int(w.SendScintilla(w.SCI_LINESONSCREEN))
1423 #@+node:ekr.20140901062324.18604: *4* qsciw.scrollDelegate (maybe)
1424 if 0: # Not yet.
1426 def scrollDelegate(self, kind):
1427 """
1428 Scroll a QTextEdit up or down one page.
1429 direction is in ('down-line','down-page','up-line','up-page')
1430 """
1431 c = self.c
1432 w = self.widget
1433 vScroll = w.verticalScrollBar()
1434 h = w.size().height()
1435 lineSpacing = w.fontMetrics().lineSpacing()
1436 n = h / lineSpacing
1437 n = max(2, n - 3)
1438 if kind == 'down-half-page':
1439 delta = n / 2
1440 elif kind == 'down-line':
1441 delta = 1
1442 elif kind == 'down-page':
1443 delta = n
1444 elif kind == 'up-half-page':
1445 delta = -n / 2
1446 elif kind == 'up-line':
1447 delta = -1
1448 elif kind == 'up-page':
1449 delta = -n
1450 else:
1451 delta = 0
1452 g.trace('bad kind:', kind)
1453 val = vScroll.value()
1454 vScroll.setValue(val + (delta * lineSpacing))
1455 c.bodyWantsFocus()
1456 #@+node:ekr.20110605121601.18112: *4* qsciw.see
1457 def see(self, i):
1458 """Ensure insert point i is visible in a QsciScintilla widget."""
1459 # Ok for now. Using SCI_SETYCARETPOLICY might be better.
1460 w = self.widget
1461 s = self.getAllText()
1462 i = self.toPythonIndex(i)
1463 row, col = g.convertPythonIndexToRowCol(s, i)
1464 w.ensureLineVisible(row)
1465 #@+node:ekr.20110605121601.18113: *4* qsciw.setAllText
1466 def setAllText(self, s):
1467 """Set the text of a QScintilla widget."""
1468 w = self.widget
1469 assert isinstance(w, Qsci.QsciScintilla), w
1470 w.setText(s)
1471 # w.update()
1472 #@+node:ekr.20110605121601.18114: *4* qsciw.setInsertPoint
1473 def setInsertPoint(self, i, s=None):
1474 """Set the insertion point in a QsciScintilla widget."""
1475 w = self.widget
1476 i = self.toPythonIndex(i)
1477 # w.SendScintilla(w.SCI_SETCURRENTPOS,i)
1478 # w.SendScintilla(w.SCI_SETANCHOR,i)
1479 w.SendScintilla(w.SCI_SETSEL, i, i)
1480 #@+node:ekr.20110605121601.18115: *4* qsciw.setSelectionRange
1481 def setSelectionRange(self, i, j, insert=None, s=None):
1482 """Set the selection range in a QsciScintilla widget."""
1483 w = self.widget
1484 i = self.toPythonIndex(i)
1485 j = self.toPythonIndex(j)
1486 insert = j if insert is None else self.toPythonIndex(insert)
1487 if insert >= i:
1488 w.SendScintilla(w.SCI_SETSEL, i, j)
1489 else:
1490 w.SendScintilla(w.SCI_SETSEL, j, i)
1491 #@+node:ekr.20140901062324.18609: *4* qsciw.setX/YScrollPosition (to do)
1492 def setXScrollPosition(self, pos):
1493 """Set the position of the horizontal scrollbar."""
1495 def setYScrollPosition(self, pos):
1496 """Set the position of the vertical scrollbar."""
1497 #@-others
1498#@+node:ekr.20110605121601.18071: ** class QTextEditWrapper(QTextMixin)
1499class QTextEditWrapper(QTextMixin):
1500 """A wrapper for a QTextEdit/QTextBrowser supporting the high-level interface."""
1501 #@+others
1502 #@+node:ekr.20110605121601.18073: *3* qtew.ctor & helpers
1503 def __init__(self, widget, name, c=None):
1504 """Ctor for QTextEditWrapper class. widget is a QTextEdit/QTextBrowser."""
1505 super().__init__(c)
1506 # Make sure all ivars are set.
1507 self.baseClassName = 'QTextEditWrapper'
1508 self.c = c
1509 self.name = name
1510 self.widget = widget
1511 self.useScintilla = False
1512 # Complete the init.
1513 if c and widget:
1514 self.widget.setUndoRedoEnabled(False)
1515 self.set_config()
1516 self.set_signals()
1518 #@+node:ekr.20110605121601.18076: *4* qtew.set_config
1519 def set_config(self):
1520 """Set configuration options for QTextEdit."""
1521 w = self.widget
1522 w.setWordWrapMode(WrapMode.NoWrap)
1523 # tab stop in pixels - no config for this (yet)
1524 if isQt6:
1525 w.setTabStopDistance(24)
1526 else:
1527 w.setTabStopWidth(24)
1528 #@+node:ekr.20140901062324.18566: *4* qtew.set_signals (should be distributed?)
1529 def set_signals(self):
1530 """Set up signals."""
1531 c, name = self.c, self.name
1532 if name in ('body', 'rendering-pane-wrapper') or name.startswith('head'):
1533 # Hook up qt events.
1534 g.app.gui.setFilter(c, self.widget, self, tag=name)
1535 if name == 'body':
1536 w = self.widget
1537 w.textChanged.connect(self.onTextChanged)
1538 w.cursorPositionChanged.connect(self.onCursorPositionChanged)
1539 if name in ('body', 'log'):
1540 # Monkey patch the event handler.
1541 #@+others
1542 #@+node:ekr.20140901062324.18565: *5* mouseReleaseEvent (monkey-patch) QTextEditWrapper
1543 def mouseReleaseEvent(event, self=self):
1544 """
1545 Monkey patch for self.widget (QTextEditWrapper) mouseReleaseEvent.
1546 """
1547 assert isinstance(self, QTextEditWrapper), self
1548 assert isinstance(self.widget, QtWidgets.QTextEdit), self.widget
1549 QtWidgets.QTextEdit.mouseReleaseEvent(self.widget, event)
1550 # Call the base class.
1551 c = self.c
1552 setattr(event, 'c', c)
1553 # Open the url on a control-click.
1554 if KeyboardModifier.ControlModifier & event.modifiers():
1555 g.openUrlOnClick(event)
1556 else:
1557 if name == 'body':
1558 c.p.v.insertSpot = c.frame.body.wrapper.getInsertPoint()
1559 g.doHook("bodyclick2", c=c, p=c.p, v=c.p)
1560 # Do *not* change the focus! This would rip focus away from tab panes.
1561 c.k.keyboardQuit(setFocus=False)
1562 #@-others
1563 self.widget.mouseReleaseEvent = mouseReleaseEvent
1564 #@+node:ekr.20200312052821.1: *3* qtew.repr
1565 def __repr__(self):
1566 # Add a leading space to align with StringTextWrapper.
1567 return f" <QTextEditWrapper: {id(self)} {self.name}>"
1569 __str__ = __repr__
1570 #@+node:ekr.20110605121601.18078: *3* qtew.High-level interface
1571 # These are all widget-dependent
1572 #@+node:ekr.20110605121601.18079: *4* qtew.delete (avoid call to setAllText)
1573 def delete(self, i, j=None):
1574 """QTextEditWrapper."""
1575 w = self.widget
1576 i = self.toPythonIndex(i)
1577 if j is None:
1578 j = i + 1
1579 j = self.toPythonIndex(j)
1580 if i > j:
1581 i, j = j, i
1582 sb = w.verticalScrollBar()
1583 pos = sb.sliderPosition()
1584 cursor = w.textCursor()
1585 try:
1586 self.changingText = True # Disable onTextChanged
1587 old_i, old_j = self.getSelectionRange()
1588 if i == old_i and j == old_j:
1589 # Work around an apparent bug in cursor.movePosition.
1590 cursor.removeSelectedText()
1591 elif i == j:
1592 pass
1593 else:
1594 cursor.setPosition(i)
1595 moveCount = abs(j - i)
1596 cursor.movePosition(MoveOperation.Right, MoveMode.KeepAnchor, moveCount)
1597 w.setTextCursor(cursor) # Bug fix: 2010/01/27
1598 cursor.removeSelectedText()
1599 finally:
1600 self.changingText = False
1601 sb.setSliderPosition(pos)
1602 #@+node:ekr.20110605121601.18080: *4* qtew.flashCharacter
1603 def flashCharacter(self, i, bg='white', fg='red', flashes=3, delay=75):
1604 """QTextEditWrapper."""
1605 # numbered color names don't work in Ubuntu 8.10, so...
1606 if bg[-1].isdigit() and bg[0] != '#':
1607 bg = bg[:-1]
1608 if fg[-1].isdigit() and fg[0] != '#':
1609 fg = fg[:-1]
1610 # This might causes problems during unit tests.
1611 # The selection point isn't restored in time.
1612 if g.unitTesting:
1613 return
1614 w = self.widget # A QTextEdit.
1615 # Remember highlighted line:
1616 last_selections = w.extraSelections()
1618 def after(func):
1619 QtCore.QTimer.singleShot(delay, func)
1621 def addFlashCallback(self=self, w=w):
1622 i = self.flashIndex
1623 cursor = w.textCursor() # Must be the widget's cursor.
1624 cursor.setPosition(i)
1625 cursor.movePosition(MoveOperation.Right, MoveMode.KeepAnchor, 1)
1626 extra = w.ExtraSelection()
1627 extra.cursor = cursor
1628 if self.flashBg:
1629 extra.format.setBackground(QtGui.QColor(self.flashBg))
1630 if self.flashFg:
1631 extra.format.setForeground(QtGui.QColor(self.flashFg))
1632 self.extraSelList = last_selections[:]
1633 self.extraSelList.append(extra) # must be last
1634 w.setExtraSelections(self.extraSelList)
1635 self.flashCount -= 1
1636 after(removeFlashCallback)
1638 def removeFlashCallback(self=self, w=w):
1639 w.setExtraSelections(last_selections)
1640 if self.flashCount > 0:
1641 after(addFlashCallback)
1642 else:
1643 w.setFocus()
1645 self.flashCount = flashes
1646 self.flashIndex = i
1647 self.flashBg = None if bg.lower() == 'same' else bg
1648 self.flashFg = None if fg.lower() == 'same' else fg
1649 addFlashCallback()
1651 #@+node:ekr.20110605121601.18081: *4* qtew.getAllText
1652 def getAllText(self):
1653 """QTextEditWrapper."""
1654 w = self.widget
1655 return w.toPlainText()
1656 #@+node:ekr.20110605121601.18082: *4* qtew.getInsertPoint
1657 def getInsertPoint(self):
1658 """QTextEditWrapper."""
1659 return self.widget.textCursor().position()
1660 #@+node:ekr.20110605121601.18083: *4* qtew.getSelectionRange
1661 def getSelectionRange(self, sort=True):
1662 """QTextEditWrapper."""
1663 w = self.widget
1664 tc = w.textCursor()
1665 i, j = tc.selectionStart(), tc.selectionEnd()
1666 return i, j
1667 #@+node:ekr.20110605121601.18084: *4* qtew.getX/YScrollPosition
1668 # **Important**: There is a Qt bug here: the scrollbar position
1669 # is valid only if cursor is visible. Otherwise the *reported*
1670 # scrollbar position will be such that the cursor *is* visible.
1672 def getXScrollPosition(self):
1673 """QTextEditWrapper: Get the horizontal scrollbar position."""
1674 w = self.widget
1675 sb = w.horizontalScrollBar()
1676 pos = sb.sliderPosition()
1677 return pos
1679 def getYScrollPosition(self):
1680 """QTextEditWrapper: Get the vertical scrollbar position."""
1681 w = self.widget
1682 sb = w.verticalScrollBar()
1683 pos = sb.sliderPosition()
1684 return pos
1685 #@+node:ekr.20110605121601.18085: *4* qtew.hasSelection
1686 def hasSelection(self):
1687 """QTextEditWrapper."""
1688 return self.widget.textCursor().hasSelection()
1689 #@+node:ekr.20110605121601.18089: *4* qtew.insert (avoid call to setAllText)
1690 def insert(self, i, s):
1691 """QTextEditWrapper."""
1692 w = self.widget
1693 i = self.toPythonIndex(i)
1694 cursor = w.textCursor()
1695 try:
1696 self.changingText = True # Disable onTextChanged.
1697 cursor.setPosition(i)
1698 cursor.insertText(s)
1699 w.setTextCursor(cursor) # Bug fix: 2010/01/27
1700 finally:
1701 self.changingText = False
1702 #@+node:ekr.20110605121601.18077: *4* qtew.leoMoveCursorHelper & helper
1703 def leoMoveCursorHelper(self, kind, extend=False, linesPerPage=15):
1704 """QTextEditWrapper."""
1705 w = self.widget
1706 d = {
1707 'begin-line': MoveOperation.StartOfLine, # Was start-line
1708 'down': MoveOperation.Down,
1709 'end': MoveOperation.End,
1710 'end-line': MoveOperation.EndOfLine, # Not used.
1711 'exchange': True, # Dummy.
1712 'home': MoveOperation.Start,
1713 'left': MoveOperation.Left,
1714 'page-down': MoveOperation.Down,
1715 'page-up': MoveOperation.Up,
1716 'right': MoveOperation.Right,
1717 'up': MoveOperation.Up,
1718 }
1719 kind = kind.lower()
1720 op = d.get(kind)
1721 mode = MoveMode.KeepAnchor if extend else MoveMode.MoveAnchor
1722 if not op:
1723 g.trace(f"can not happen: bad kind: {kind}")
1724 return
1725 if kind in ('page-down', 'page-up'):
1726 self.pageUpDown(op, mode)
1727 elif kind == 'exchange': # exchange-point-and-mark
1728 cursor = w.textCursor()
1729 anchor = cursor.anchor()
1730 pos = cursor.position()
1731 cursor.setPosition(pos, MoveOperation.MoveAnchor)
1732 cursor.setPosition(anchor, MoveOperation.KeepAnchor)
1733 w.setTextCursor(cursor)
1734 else:
1735 if not extend:
1736 # Fix an annoyance. Make sure to clear the selection.
1737 cursor = w.textCursor()
1738 cursor.clearSelection()
1739 w.setTextCursor(cursor)
1740 w.moveCursor(op, mode)
1741 self.seeInsertPoint()
1742 self.rememberSelectionAndScroll()
1743 # #218.
1744 cursor = w.textCursor()
1745 sel = cursor.selection().toPlainText()
1746 if sel and hasattr(g.app.gui, 'setClipboardSelection'):
1747 g.app.gui.setClipboardSelection(sel)
1748 self.c.frame.updateStatusLine()
1749 #@+node:btheado.20120129145543.8180: *5* qtew.pageUpDown
1750 def pageUpDown(self, op, moveMode):
1751 """
1752 The QTextEdit PageUp/PageDown functionality seems to be "baked-in"
1753 and not externally accessible. Since Leo has its own keyhandling
1754 functionality, this code emulates the QTextEdit paging. This is a
1755 straight port of the C++ code found in the pageUpDown method of
1756 gui/widgets/qtextedit.cpp.
1757 """
1758 control = self.widget
1759 cursor = control.textCursor()
1760 moved = False
1761 lastY = control.cursorRect(cursor).top()
1762 distance = 0
1763 # move using movePosition to keep the cursor's x
1764 while True:
1765 y = control.cursorRect(cursor).top()
1766 distance += abs(y - lastY)
1767 lastY = y
1768 moved = cursor.movePosition(op, moveMode)
1769 if (not moved or distance >= control.height()):
1770 break
1771 sb = control.verticalScrollBar()
1772 if moved:
1773 if op == MoveOperation.Up:
1774 cursor.movePosition(MoveOperation.Down, moveMode)
1775 sb.triggerAction(SliderAction.SliderPageStepSub)
1776 else:
1777 cursor.movePosition(MoveOperation.Up, moveMode)
1778 sb.triggerAction(SliderAction.SliderPageStepAdd)
1779 control.setTextCursor(cursor)
1780 #@+node:ekr.20110605121601.18087: *4* qtew.linesPerPage
1781 def linesPerPage(self):
1782 """QTextEditWrapper."""
1783 # Not used in Leo's core.
1784 w = self.widget
1785 h = w.size().height()
1786 lineSpacing = w.fontMetrics().lineSpacing()
1787 n = h / lineSpacing
1788 return n
1789 #@+node:ekr.20110605121601.18088: *4* qtew.scrollDelegate
1790 def scrollDelegate(self, kind):
1791 """
1792 Scroll a QTextEdit up or down one page.
1793 direction is in ('down-line','down-page','up-line','up-page')
1794 """
1795 c = self.c
1796 w = self.widget
1797 vScroll = w.verticalScrollBar()
1798 h = w.size().height()
1799 lineSpacing = w.fontMetrics().lineSpacing()
1800 n = h / lineSpacing
1801 n = max(2, n - 3)
1802 if kind == 'down-half-page':
1803 delta = n / 2
1804 elif kind == 'down-line':
1805 delta = 1
1806 elif kind == 'down-page':
1807 delta = n
1808 elif kind == 'up-half-page':
1809 delta = -n / 2
1810 elif kind == 'up-line':
1811 delta = -1
1812 elif kind == 'up-page':
1813 delta = -n
1814 else:
1815 delta = 0
1816 g.trace('bad kind:', kind)
1817 val = vScroll.value()
1818 vScroll.setValue(val + (delta * lineSpacing))
1819 c.bodyWantsFocus()
1820 #@+node:ekr.20110605121601.18090: *4* qtew.see & seeInsertPoint
1821 def see(self, see_i):
1822 """Scroll so that position see_i is visible."""
1823 w = self.widget
1824 tc = w.textCursor()
1825 # Put see_i in range.
1826 s = self.getAllText()
1827 see_i = max(0, min(see_i, len(s)))
1828 # Remember the old cursor
1829 old_cursor = QtGui.QTextCursor(tc)
1830 # Scroll so that see_i is visible.
1831 tc.setPosition(see_i)
1832 w.setTextCursor(tc)
1833 w.ensureCursorVisible()
1834 # Restore the old cursor
1835 w.setTextCursor(old_cursor)
1837 def seeInsertPoint(self):
1838 """Make sure the insert point is visible."""
1839 self.widget.ensureCursorVisible()
1840 #@+node:ekr.20110605121601.18092: *4* qtew.setAllText
1841 def setAllText(self, s):
1842 """Set the text of body pane."""
1843 w = self.widget
1844 try:
1845 self.changingText = True # Disable onTextChanged.
1846 w.setReadOnly(False)
1847 w.setPlainText(s)
1848 finally:
1849 self.changingText = False
1850 #@+node:ekr.20110605121601.18095: *4* qtew.setInsertPoint
1851 def setInsertPoint(self, i, s=None):
1852 # Fix bug 981849: incorrect body content shown.
1853 # Use the more careful code in setSelectionRange.
1854 self.setSelectionRange(i=i, j=i, insert=i, s=s)
1855 #@+node:ekr.20110605121601.18096: *4* qtew.setSelectionRange
1856 def setSelectionRange(self, i, j, insert=None, s=None):
1857 """Set the selection range and the insert point."""
1858 #
1859 # Part 1
1860 w = self.widget
1861 i = self.toPythonIndex(i)
1862 j = self.toPythonIndex(j)
1863 if s is None:
1864 s = self.getAllText()
1865 n = len(s)
1866 i = max(0, min(i, n))
1867 j = max(0, min(j, n))
1868 if insert is None:
1869 ins = max(i, j)
1870 else:
1871 ins = self.toPythonIndex(insert)
1872 ins = max(0, min(ins, n))
1873 #
1874 # Part 2:
1875 # 2010/02/02: Use only tc.setPosition here.
1876 # Using tc.movePosition doesn't work.
1877 tc = w.textCursor()
1878 if i == j:
1879 tc.setPosition(i)
1880 elif ins == j:
1881 # Put the insert point at j
1882 tc.setPosition(i)
1883 tc.setPosition(j, MoveMode.KeepAnchor)
1884 elif ins == i:
1885 # Put the insert point at i
1886 tc.setPosition(j)
1887 tc.setPosition(i, MoveMode.KeepAnchor)
1888 else:
1889 # 2014/08/21: It doesn't seem possible to put the insert point somewhere else!
1890 tc.setPosition(j)
1891 tc.setPosition(i, MoveMode.KeepAnchor)
1892 w.setTextCursor(tc)
1893 # #218.
1894 if hasattr(g.app.gui, 'setClipboardSelection'):
1895 if s[i:j]:
1896 g.app.gui.setClipboardSelection(s[i:j])
1897 #
1898 # Remember the values for v.restoreCursorAndScroll.
1899 v = self.c.p.v # Always accurate.
1900 v.insertSpot = ins
1901 if i > j:
1902 i, j = j, i
1903 assert i <= j
1904 v.selectionStart = i
1905 v.selectionLength = j - i
1906 v.scrollBarSpot = w.verticalScrollBar().value()
1907 #@+node:ekr.20141103061944.40: *4* qtew.setXScrollPosition
1908 def setXScrollPosition(self, pos):
1909 """Set the position of the horizonatl scrollbar."""
1910 if pos is not None:
1911 w = self.widget
1912 sb = w.horizontalScrollBar()
1913 sb.setSliderPosition(pos)
1914 #@+node:ekr.20110605121601.18098: *4* qtew.setYScrollPosition
1915 def setYScrollPosition(self, pos):
1916 """Set the vertical scrollbar position."""
1917 if pos is not None:
1918 w = self.widget
1919 sb = w.verticalScrollBar()
1920 sb.setSliderPosition(pos)
1921 #@+node:ekr.20110605121601.18100: *4* qtew.toPythonIndex
1922 def toPythonIndex(self, index, s=None):
1923 """This is much faster than versions using g.toPythonIndex."""
1924 w = self
1925 te = self.widget
1926 if index is None:
1927 return 0
1928 if isinstance(index, int):
1929 return index
1930 if index == '1.0':
1931 return 0
1932 if index == 'end':
1933 return w.getLastPosition()
1934 doc = te.document()
1935 data = index.split('.')
1936 if len(data) == 2:
1937 row, col = data
1938 row, col = int(row), int(col)
1939 bl = doc.findBlockByNumber(row - 1)
1940 return bl.position() + col
1941 g.trace(f"bad string index: {index}")
1942 return 0
1943 #@+node:ekr.20110605121601.18101: *4* qtew.toPythonIndexRowCol
1944 def toPythonIndexRowCol(self, index):
1945 w = self
1946 if index == '1.0':
1947 return 0, 0, 0
1948 if index == 'end':
1949 index = w.getLastPosition()
1950 te = self.widget
1951 doc = te.document()
1952 i = w.toPythonIndex(index)
1953 bl = doc.findBlock(i)
1954 row = bl.blockNumber()
1955 col = i - bl.position()
1956 return i, row, col
1957 #@-others
1958#@-others
1960#@@language python
1961#@@tabwidth -4
1962#@@pagewidth 70
1963#@-leo