# 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 . """ 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)