File: C:/Users/fred/anaconda3/Lib/site-packages/spyder/plugins/editor/panels/scrollflag.py
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""
This module contains the Scroll Flag panel
"""
# Standard library imports
import sys
from math import ceil
# Third party imports
from qtpy.QtCore import QSize, Qt, QTimer
from qtpy.QtGui import QPainter, QColor, QCursor
from qtpy.QtWidgets import (QStyle, QStyleOptionSlider, QApplication)
# Local imports
from spyder.api.panel import Panel
from spyder.plugins.completion.api import DiagnosticSeverity
from spyder.plugins.editor.utils.editor import is_block_safe
REFRESH_RATE = 1000
# Maximum number of flags to print in a file
MAX_FLAGS = 1000
class ScrollFlagArea(Panel):
"""Source code editor's scroll flag area"""
WIDTH = 24 if sys.platform == 'darwin' else 12
FLAGS_DX = 4
FLAGS_DY = 2
def __init__(self):
Panel.__init__(self)
self.setAttribute(Qt.WA_OpaquePaintEvent)
self.scrollable = True
self.setMouseTracking(True)
# Define some attributes to be used for unit testing.
self._unit_testing = False
self._range_indicator_is_visible = False
self._alt_key_is_down = False
self._slider_range_color = QColor(Qt.gray)
self._slider_range_color.setAlphaF(.85)
self._slider_range_brush = QColor(Qt.gray)
self._slider_range_brush.setAlphaF(.5)
self._update_list_timer = QTimer(self)
self._update_list_timer.setSingleShot(True)
self._update_list_timer.timeout.connect(self.update_flags)
# Dictionary with flag lists
self._dict_flag_list = {}
def on_install(self, editor):
"""Manages install setup of the pane."""
super().on_install(editor)
# Define permanent Qt colors that are needed for painting the flags
# and the slider range.
self._facecolors = {
'warning': QColor(editor.warning_color),
'error': QColor(editor.error_color),
'todo': QColor(editor.todo_color),
'breakpoint': QColor(editor.breakpoint_color),
'occurrence': QColor(editor.occurrence_color),
'found_results': QColor(editor.found_results_color)
}
self._edgecolors = {key: color.darker(120) for
key, color in self._facecolors.items()}
# Signals
editor.sig_focus_changed.connect(self.update)
editor.sig_key_pressed.connect(self.keyPressEvent)
editor.sig_key_released.connect(self.keyReleaseEvent)
editor.sig_alt_left_mouse_pressed.connect(self.mousePressEvent)
editor.sig_alt_mouse_moved.connect(self.mouseMoveEvent)
editor.sig_leave_out.connect(self.update)
editor.sig_flags_changed.connect(self.delayed_update_flags)
editor.sig_theme_colors_changed.connect(self.update_flag_colors)
@property
def slider(self):
"""This property holds whether the vertical scrollbar is visible."""
return self.editor.verticalScrollBar().isVisible()
def closeEvent(self, event):
self._update_list_timer.stop()
super().closeEvent(event)
def sizeHint(self):
"""Override Qt method"""
return QSize(self.WIDTH, 0)
def update_flag_colors(self, color_dict):
"""
Update the permanent Qt colors that are used for painting the flags
and the slider range with the new colors defined in the given dict.
"""
for name, color in color_dict.items():
self._facecolors[name] = QColor(color)
self._edgecolors[name] = self._facecolors[name].darker(120)
def delayed_update_flags(self):
"""
This function is called every time a flag is changed.
There is no need of updating the flags thousands of time by second,
as it is quite resources-heavy. This limits the calls to REFRESH_RATE.
"""
if self._update_list_timer.isActive():
return
self._update_list_timer.start(REFRESH_RATE)
def update_flags(self):
"""
Update flags list.
This parses the entire file, which can take a lot of time for
large files. Save all the flags in lists for painting during
paint events.
"""
self._dict_flag_list = {
'error': [],
'warning': [],
'todo': [],
'breakpoint': [],
}
editor = self.editor
block = editor.document().firstBlock()
while block.isValid():
# Parse all lines in the file looking for something to flag.
data = block.userData()
if data:
if data.code_analysis:
# Paint the errors and warnings
for _, _, severity, _ in data.code_analysis:
if severity == DiagnosticSeverity.ERROR:
flag_type = 'error'
break
else:
flag_type = 'warning'
elif data.todo:
flag_type = 'todo'
elif data.breakpoint:
flag_type = 'breakpoint'
else:
flag_type = None
if flag_type is not None:
self._dict_flag_list[flag_type].append(block)
block = block.next()
self.update()
def paintEvent(self, event):
"""
Override Qt method.
Painting the scroll flag area
There is two cases:
- The scroll bar is moving, in which case paint all flags.
- The scroll bar is not moving, only paint flags corresponding
to visible lines.
"""
# The area in which the slider handle of the scrollbar may move.
groove_rect = self.get_scrollbar_groove_rect()
# This is necessary to catch a possible error when the scrollbar
# has zero height.
# Fixes spyder-ide/spyder#21600
try:
# The scrollbar's scale factor ratio between pixel span height and
# value span height
scale_factor = (
groove_rect.height() / self.get_scrollbar_value_height()
)
except ZeroDivisionError:
scale_factor = 1
# The vertical offset of the scroll flag area relative to the
# top of the text editor.
offset = groove_rect.y()
# Note that we calculate the pixel metrics required to draw the flags
# here instead of using the convenience methods of the ScrollFlagArea
# for performance reason.
rect_x = ceil(self.FLAGS_DX / 2)
rect_w = self.WIDTH - self.FLAGS_DX
rect_h = self.FLAGS_DY
# Fill the whole painting area
painter = QPainter(self)
painter.fillRect(event.rect(), self.editor.sideareas_color)
editor = self.editor
# Define compute_flag_ypos to position the flags:
# Paint flags for the entire document
last_line = editor.document().lastBlock().firstLineNumber()
# The 0.5 offset is used to align the flags with the center of
# their corresponding text edit block before scaling.
first_y_pos = self.value_to_position(
0.5, scale_factor, offset) - self.FLAGS_DY / 2
last_y_pos = self.value_to_position(
last_line + 0.5, scale_factor, offset) - self.FLAGS_DY / 2
# Compute the height of a line and of a flag in lines.
line_height = last_y_pos - first_y_pos
if line_height > 0:
flag_height_lines = rect_h * last_line / line_height
else:
flag_height_lines = 0
# All the lists of block numbers for flags
dict_flag_lists = {
"occurrence": editor.occurrences,
"found_results": editor.found_results
}
dict_flag_lists.update(self._dict_flag_list)
# The ability to reverse dictionaries was added in Python 3.8.
# Fixes spyder-ide/spyder#21286
if sys.version_info[:2] > (3, 7):
# This is necessary to paint find matches above errors and
# warnings.
# See spyder-ide/spyder#20970
dict_flag_lists_iter = reversed(dict_flag_lists)
else:
dict_flag_lists_iter = dict_flag_lists
for flag_type in dict_flag_lists_iter:
painter.setBrush(self._facecolors[flag_type])
painter.setPen(self._edgecolors[flag_type])
if editor.verticalScrollBar().maximum() == 0:
# No scroll
for block in dict_flag_lists[flag_type]:
if not is_block_safe(block):
continue
geometry = editor.blockBoundingGeometry(block)
rect_y = ceil(
geometry.y() +
geometry.height() / 2 +
rect_h / 2
)
painter.drawRect(rect_x, rect_y, rect_w, rect_h)
elif last_line == 0:
# Only one line
for block in dict_flag_lists[flag_type]:
if not is_block_safe(block):
continue
rect_y = ceil(first_y_pos)
painter.drawRect(rect_x, rect_y, rect_w, rect_h)
else:
# Many lines
if len(dict_flag_lists[flag_type]) < MAX_FLAGS:
# If the file is too long, do not freeze the editor
next_line = 0
for block in dict_flag_lists[flag_type]:
if not is_block_safe(block):
continue
block_line = block.firstLineNumber()
# block_line = -1 if invalid
if block_line < next_line:
# Don't print flags on top of flags
continue
next_line = block_line + flag_height_lines / 2
frac = block_line / last_line
rect_y = ceil(first_y_pos + frac * line_height)
painter.drawRect(rect_x, rect_y, rect_w, rect_h)
# Paint the slider range
if not self._unit_testing:
alt = QApplication.queryKeyboardModifiers() & Qt.AltModifier
else:
alt = self._alt_key_is_down
if self.slider:
cursor_pos = self.mapFromGlobal(QCursor().pos())
is_over_self = self.rect().contains(cursor_pos)
is_over_editor = editor.rect().contains(
editor.mapFromGlobal(QCursor().pos()))
# We use QRect.contains instead of QWidget.underMouse method to
# determined if the cursor is over the editor or the flag scrollbar
# because the later gives a wrong result when a mouse button
# is pressed.
if is_over_self or (alt and is_over_editor):
painter.setPen(self._slider_range_color)
painter.setBrush(self._slider_range_brush)
x, y, width, height = self.make_slider_range(
cursor_pos, scale_factor, offset, groove_rect)
painter.drawRect(x, y, width, height)
self._range_indicator_is_visible = True
else:
self._range_indicator_is_visible = False
def enterEvent(self, event):
"""Override Qt method"""
self.update()
def leaveEvent(self, event):
"""Override Qt method"""
self.update()
def mouseMoveEvent(self, event):
"""Override Qt method"""
self.update()
def mousePressEvent(self, event):
"""Override Qt method"""
if self.slider and event.button() == Qt.LeftButton:
vsb = self.editor.verticalScrollBar()
value = self.position_to_value(event.pos().y())
vsb.setValue(int(value-vsb.pageStep()/2))
def keyReleaseEvent(self, event):
"""Override Qt method."""
if event.key() == Qt.Key_Alt:
self._alt_key_is_down = False
self.update()
def keyPressEvent(self, event):
"""Override Qt method"""
if event.key() == Qt.Key_Alt:
self._alt_key_is_down = True
self.update()
def get_vertical_offset(self):
"""
Return the vertical offset of the scroll flag area relative to the
top of the text editor.
"""
groove_rect = self.get_scrollbar_groove_rect()
return groove_rect.y()
def get_slider_min_height(self):
"""
Return the minimum height of the slider range based on that set for
the scroll bar's slider.
"""
return QApplication.instance().style().pixelMetric(
QStyle.PM_ScrollBarSliderMin)
def get_scrollbar_groove_rect(self):
"""Return the area in which the slider handle may move."""
vsb = self.editor.verticalScrollBar()
style = QApplication.instance().style()
opt = QStyleOptionSlider()
vsb.initStyleOption(opt)
# Get the area in which the slider handle may move.
groove_rect = style.subControlRect(
QStyle.CC_ScrollBar, opt, QStyle.SC_ScrollBarGroove, self)
return groove_rect
def get_scrollbar_position_height(self):
"""Return the pixel span height of the scrollbar area in which
the slider handle may move"""
groove_rect = self.get_scrollbar_groove_rect()
return float(groove_rect.height())
def get_scrollbar_value_height(self):
"""Return the value span height of the scrollbar"""
vsb = self.editor.verticalScrollBar()
return vsb.maximum() - vsb.minimum() + vsb.pageStep()
def get_scale_factor(self):
"""Return scrollbar's scale factor:
ratio between pixel span height and value span height"""
return (self.get_scrollbar_position_height() /
self.get_scrollbar_value_height())
def value_to_position(self, y, scale_factor, offset):
"""Convert value to position in pixels"""
vsb = self.editor.verticalScrollBar()
return int((y - vsb.minimum()) * scale_factor + offset)
def position_to_value(self, y):
"""Convert position in pixels to value"""
vsb = self.editor.verticalScrollBar()
offset = self.get_vertical_offset()
return vsb.minimum() + max([0, (y - offset) / self.get_scale_factor()])
def make_slider_range(self, cursor_pos, scale_factor, offset, groove_rect):
"""
Return the slider x and y positions and the slider width and height.
"""
# The slider range indicator position follows the mouse vertical
# position while its height corresponds to the part of the file that
# is currently visible on screen.
vsb = self.editor.verticalScrollBar()
slider_height = self.value_to_position(
vsb.pageStep(), scale_factor, offset) - offset
slider_height = max(slider_height, self.get_slider_min_height())
# Calculate the minimum and maximum y-value to constraint the slider
# range indicator position to the height span of the scrollbar area
# where the slider may move.
min_ypos = offset
max_ypos = groove_rect.height() + offset - slider_height
# Determine the bounded y-position of the slider rect.
slider_y = max(min_ypos, min(max_ypos,
ceil(cursor_pos.y()-slider_height/2)))
return 1, slider_y, self.WIDTH - 2, slider_height
def wheelEvent(self, event):
"""Override Qt method"""
self.editor.wheelEvent(event)
def set_enabled(self, state):
"""Toggle scroll flag area visibility"""
self.enabled = state
self.setVisible(state)