Staging
v0.8.1
v0.8.1
https://foss.heptapod.net/mercurial/hgview
Tip revision: 96077f41e4d14ee3531ee599068a306415c14300 authored by Philippe Pepiot on 06 September 2019, 09:02:14 UTC
[pkg] version 1.12.0
[pkg] version 1.12.0
Tip revision: 96077f4
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 Qt4 usefull widgets.
"""
from mercurial import pycompat
from PyQt4.QtGui import QTableView, QFontMetrics, QHeaderView, QLineEdit, \
QPalette, QPainter, QSizePolicy, QFrame
from PyQt4.QtCore import QTimer, pyqtSignal, Qt
from PyQt4.Qsci import QsciScintilla as qsci
from hgviewlib.qt4 import icon as geticon
from hgviewlib.qt4.styleditemdelegate import StyledItemDelegate
from hgviewlib.qt4.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().setResizeMode(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 = 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)