Staging
v0.5.0
https://foss.heptapod.net/mercurial/hgview
Raw File
Tip revision: 1850a087c1b6cb026470c738b2c7f82c6fada06d authored by Mads Kiilerich on 04 April 2020, 02:27:44 UTC
qt5: renaming of references to hgviewlib/qt5, finishing up the Qt5 port
Tip revision: 1850a08
widgets.py
# Copyright (c) 2013 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Generic Qt5 usefull widgets.
"""

from mercurial import pycompat

from PyQt5.QtGui import QFontMetrics, QPalette, QPainter
from PyQt5.QtWidgets import QTableView, QHeaderView, QLineEdit, \
     QSizePolicy, QFrame
from PyQt5.QtCore import QTimer, pyqtSignal, Qt
from PyQt5.Qsci import QsciScintilla as qsci

from hgviewlib.qt5 import icon as geticon
from hgviewlib.qt5.styleditemdelegate import StyledItemDelegate
from hgviewlib.qt5.lexers import get_lexer

class StyledTableView(QTableView):
    """An Abstract QTableView with some Columns rendered with css. """

    def __init__(self, parent=None):
        super(StyledTableView, self).__init__(parent)
        self.standard_delegate = self.itemDelegate()
        self.styled_item_delegate = StyledItemDelegate(self)

    def setModel(self, model):
        super(StyledTableView, self).setModel(model)
        self._reset_delegate()
        model.layoutChanged.connect(self._reset_delegate)

    def _reset_delegate(self):
        # Model column layout has changed so we need to move
        # our column delegate to correct location
        model = self.model()
        if not model:
            return
        for index in range(model.columnCount()):
            if self.is_styled_column(index):
                self.setItemDelegateForColumn(index, self.styled_item_delegate)
            else:
                self.setItemDelegateForColumn(index, self.standard_delegate)

    def is_styled_column(self, index):
        """ Return True if the column at ``index`` is rendered with style. """
        raise NotImplementedError()


class SmartResizeTableView(QTableView):
    """A smart header resizable table."""
    def __init__(self, parent=None):
        super(SmartResizeTableView, self).__init__(parent)
        self._autoresize = True
        self.horizontalHeader().sectionResized[int, int, int].connect(
            self.disableAutoResize)

    def enableAutoResize(self, *args):
        self._autoresize =  True

    def disableAutoResize(self, *args):
        self._autoresize =  False
        QTimer.singleShot(100, lambda: self.enableAutoResize())

    def resizeEvent(self, event):
        # we catch this event to resize smartly tables' columns
        super(SmartResizeTableView, self).resizeEvent(event)
        if self._autoresize:
            self.resizeColumns()

    def resizeColumns(self, *args):
        # resize columns the smart way: the column holding Log
        # is resized according to the total widget size.
        model = self.model()
        if not model:
            return
        col1_width = self.viewport().width()
        fontm = QFontMetrics(self.font())
        tot_stretch = 0.0
        for index in range(model.columnCount()):
            stretch = self.get_column_stretch(index)
            if stretch is not None:
                tot_stretch += stretch
                continue
            width = model.maxWidthValueForColumn(index)
            if width is not None:
                width = fontm.width(width + 'W')
                self.setColumnWidth(index, width)
            else:
                width = self.sizeHintForColumn(index)
                self.setColumnWidth(index, width)
            col1_width -= self.columnWidth(index)
        col1_width = max(col1_width, 100)
        for index in range(model.columnCount()):
            stretch = self.get_column_stretch(index)
            if stretch is not None:
                width = stretch // tot_stretch
                self.setColumnWidth(index, col1_width * width)

    def setModel(self, model):
        super(SmartResizeTableView, self).setModel(model)
        col = col = list(model._columns).index('Log')
        self.horizontalHeader().setSectionResizeMode(col, QHeaderView.Stretch | QHeaderView.Interactive)

    def get_column_stretch(self, index):
        raise NotImplementedError


class QueryLineEdit(QLineEdit):
    """Special LineEdit class with visual marks for the revset query status"""

    text_edited_no_blank = pyqtSignal(str)

    FORGROUNDS = {'normal':Qt.color1,
                  'valid':Qt.color1,
                  'failed':Qt.darkRed,
                  'query':Qt.darkGray}

    ICONS = {'valid':'valid', 'query':'loading'}

    def __init__(self, parent):
        self._parent = parent
        self._status = None # one of the keys of self.FORGROUNDS and self.ICONS
        super(QueryLineEdit, self).__init__(parent)
        self.setTextMargins(0,0,-16,0)
        self.textEdited.connect(self.on_text_edited)
        self.previous_text = ''

    def set_status(self, status=None):
        self._status = status
        color = self.FORGROUNDS.get(status, None)
        if color is not None:
            palette = self.palette()
            palette.setColor(QPalette.Text, color)
            self.setPalette(palette)

    def get_status(self):
        return self._status

    status = property(get_status, set_status, None, "query status")

    def paintEvent(self, event):
        super(QueryLineEdit, self).paintEvent(event)
        icn = geticon(self.ICONS.get(self._status))
        if icn is None:
            return
        painter = QPainter(self)
        icn.paint(painter, self.width() - 18, (self.height() - 18) // 2, 16, 16)

    def on_text_edited(self):
        current_text = pycompat.unicode(self.text()).strip()
        if current_text == self.previous_text:
            return
        self.previous_text = current_text
        self.text_edited_no_blank.emit(current_text)


class Annotator(qsci):
    # we use a QScintilla for the annotator cause it makes
    # it much easier to keep the text area and the annotator sync
    # (same font rendering etc). However, it have the drawback of making much
    # more difficult to implement things like QTextBrowser.anchorClicked, which
    # would have been nice to directly go to the annotated revision...
    def __init__(self, textarea, parent=None):
        super(Annotator, self).__init__(parent)

        self.setFrameStyle(0)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setReadOnly(True)
        self.sizePolicy().setControlType(QSizePolicy.Slider)
        self.setMinimumWidth(20)
        self.setMaximumWidth(40) # XXX TODO make this computed
        self.setFont(textarea.font())
        self.setMarginWidth(0, '')
        self.setMarginWidth(1, '')

        self.SendScintilla(qsci.SCI_SETCURSOR, 2)
        self.SendScintilla(qsci.SCI_SETCARETSTYLE, 0)

        # used to set a background color for every annotating rev
        N = 32
        self.markers = []
        for i in range(N):
            marker = self.markerDefine(qsci.Background)
            color = 0x7FFF00 + (i-N/2)*256/N*256*256 - i*256/N*256 + i*256/N
            self.SendScintilla(qsci.SCI_MARKERSETBACK, marker, color)
            self.markers.append(marker)

        textarea.verticalScrollBar().valueChanged[int].connect(
                self.verticalScrollBar().setValue)

    def set_line_ticks(self, ticks):
        """Specify line ticks instead of line numbers.

        A background color is automatically added.
        A new background color is used when the tick changes.
        """
        self.setText('\n'.join(ticks))
        uniq_ticks = list(sorted(set(ticks)))
        for i, tick in enumerate(ticks):
            idx = uniq_ticks.index(tick)
            self.markerAdd(i, self.markers[idx % len(self.markers)])


class SourceViewer(qsci):

    def __init__(self, *args, **kwargs):
        super(SourceViewer, self).__init__(*args, **kwargs)
        self.setUtf8(True)
        self.setFrameStyle(0)
        self.setFrameShape(QFrame.NoFrame)

        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.setReadOnly(True)

        self.SendScintilla(qsci.SCI_SETSELEOLFILLED, True)
        self.SendScintilla(qsci.SCI_SETCARETSTYLE, 0)

        # margin 1 is used for line numbers
        self.setMarginLineNumbers(1, True)
        self.setMarginWidth(1, '000')

        self.SendScintilla(qsci.SCI_INDICSETSTYLE, 8, qsci.INDIC_ROUNDBOX)
        self.SendScintilla(qsci.SCI_INDICSETUNDER, 8, True)
        self.SendScintilla(qsci.SCI_INDICSETFORE, 8, 0xBBFFFF)
        self.SendScintilla(qsci.SCI_INDICSETSTYLE, 9, qsci.INDIC_ROUNDBOX)
        self.SendScintilla(qsci.SCI_INDICSETUNDER, 9, True)
        self.SendScintilla(qsci.SCI_INDICSETFORE, 9, 0x58A8FF)

        # hide margin 0 (markers)
        self.SendScintilla(qsci.SCI_SETMARGINTYPEN, 0, 0)
        self.SendScintilla(qsci.SCI_SETMARGINWIDTHN, 0, 0)
        # setup margin 1 for line numbers only
        self.SendScintilla(qsci.SCI_SETMARGINTYPEN, 1, 1)
        self.SendScintilla(qsci.SCI_SETMARGINWIDTHN, 1, 20)
        self.SendScintilla(qsci.SCI_SETMARGINMASKN, 1, 0)

        # define markers for colorize zones of diff
        self.markerplus = self.markerDefine(qsci.Background)
        self.SendScintilla(qsci.SCI_MARKERSETBACK, self.markerplus, 0xB0FFA0)
        self.markerminus = self.markerDefine(qsci.Background)
        self.SendScintilla(qsci.SCI_MARKERSETBACK, self.markerminus, 0xA0A0FF)
        self.markertriangle = self.markerDefine(qsci.Background)
        self.SendScintilla(qsci.SCI_MARKERSETBACK, self.markertriangle, 0xFFA0A0)

    def clear_highlights(self):
        n = self.length()
        self.SendScintilla(qsci.SCI_SETINDICATORCURRENT, 8) # highlight
        self.SendScintilla(qsci.SCI_INDICATORCLEARRANGE, 0, n)
        self.SendScintilla(qsci.SCI_SETINDICATORCURRENT, 9) # current found
                                                            # occurrence
        self.SendScintilla(qsci.SCI_INDICATORCLEARRANGE, 0, n)

    def highlight_current_search_string(self, pos, text):
        line = self.SendScintilla(qsci.SCI_LINEFROMPOSITION, pos)
        self.ensureLineVisible(line)
        self.SendScintilla(qsci.SCI_SETINDICATORCURRENT, 9)
        self.SendScintilla(qsci.SCI_INDICATORCLEARRANGE, 0, pos)
        self.SendScintilla(qsci.SCI_INDICATORFILLRANGE, pos, len(text))

    def search_and_highlight_string(self, text):
        data = pycompat.unicode(self.text())
        self.SendScintilla(qsci.SCI_SETINDICATORCURRENT, 8)
        pos = [data.find(text)]
        n = len(text)
        while pos[-1] > -1:
            self.SendScintilla(qsci.SCI_INDICATORFILLRANGE, pos[-1], n)
            pos.append(data.find(text, pos[-1]+1))
        return [x for x in pos if x > -1]

    def set_text(self, filename, data, flag=None, cfg=None):
        lexer = get_lexer(filename, data, flag, cfg)
        if lexer:
            # lexer.setFont(self.font())
            self.setLexer(lexer)
        nlines = data.count('\n')
        self.setMarginWidth(1, str(nlines)+'00')
        self.setText(data)
back to top