def __init__(self): super().__init__() # don't pass parent because of mem problem # Because some fonts have much more space at top and bottom, we use ascent instead # of height, and add it with a small number. magic = int(4 * scaleRatio) self.title_h = max(font.title_m.ascent(), font.datetime_m.ascent()) + magic self.titleArea_h = self.title_h + 4 self.text_h = font.text_m.lineSpacing() * settings['Main'].getint( 'previewLines') self.tagPath_h = font.default_m.ascent() + magic self.tag_h = self.tagPath_h + 4 self.dt_w = font.datetime_m.width( datetimeTrans('2000-01-01 00:00')) + 40 self.all_h = None # updated in sizeHint before each item being painting # doc is used to draw text(diary's body) self.doc = NTextDocument() self.doc.setDefaultFont(font.text) self.doc.setUndoRedoEnabled(False) self.doc.setDocumentMargin(0) self.doc.documentLayout().setPaintDevice( qApp.desktop()) # refer actual list will cause segfault # setup colors self.c_text = Qt.black self.c_bg = QColor(255, 236, 176) self.c_border = QColor(214, 172, 41) self.c_inActBg = QColor(255, 236, 176, 40) self.c_gray = QColor(93, 73, 57)
def __init__(self, parent=None, lines=None, **kwargs): super().__init__(parent, **kwargs) self._lines = self._heightHint = None self.doc = NTextDocument(self) self.doc.setDocumentMargin(0) self.doc.setUndoRedoEnabled(False) self.setLines(lines if lines else 4) self.doc.documentLayout().setPaintDevice(self) # make difference on high DPI
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._doc = NTextDocument(self) self.setDocument(self._doc) # remove highlight color's alpha to avoid alpha loss in copy&paste. # NTextDocument should use this color too. hl, bg = self.HlColor, self.palette().base().color() fac = hl.alpha() / 255 self.HlColor = QColor(round(hl.red()*fac + bg.red()*(1-fac)), round(hl.green()*fac + bg.green()*(1-fac)), round(hl.blue()*fac + bg.blue()*(1-fac))) self.autoIndent = False # used by tab indent shortcut if QLocale().language() in (QLocale.Chinese, QLocale.Japanese): self._indent = ' ' # 2 full width spaces else: self._indent = ' ' # 4 spaces # setup format menu onHLAct = lambda: super(NTextEdit, self).setHL(self.hlAct.isChecked()) onBDAct = lambda: super(NTextEdit, self).setBD(self.bdAct.isChecked()) onSOAct = lambda: super(NTextEdit, self).setSO(self.soAct.isChecked()) onULAct = lambda: super(NTextEdit, self).setUL(self.ulAct.isChecked()) onItaAct = lambda: super(NTextEdit, self).setIta(self.itaAct.isChecked()) self.fmtMenu = QMenu(self.tr('Format'), self) # shortcuts of format actions only used to display shortcut-hint in menu self.hlAct = QAction(makeQIcon(':/menu/highlight.png'), self.tr('Highlight'), self, triggered=onHLAct, shortcut=QKeySequence('Ctrl+H')) self.bdAct = QAction(makeQIcon(':/menu/bold.png'), self.tr('Bold'), self, triggered=onBDAct, shortcut=QKeySequence.Bold) self.soAct = QAction(makeQIcon(':/menu/strikeout.png'), self.tr('Strike out'), self, triggered=onSOAct, shortcut=QKeySequence('Ctrl+T')) self.ulAct = QAction(makeQIcon(':/menu/underline.png'), self.tr('Underline'), self, triggered=onULAct, shortcut=QKeySequence.Underline) self.itaAct = QAction(makeQIcon(':/menu/italic.png'), self.tr('Italic'), self, triggered=onItaAct, shortcut=QKeySequence.Italic) self.clrAct = QAction(self.tr('Clear format'), self, shortcut=QKeySequence('Ctrl+D'), triggered=self.clearFormat) self.acts = (self.hlAct, self.bdAct, self.soAct, self.ulAct, self.itaAct) # excluding uncheckable clrAct for a in self.acts: self.fmtMenu.addAction(a) a.setCheckable(True) self.fmtMenu.addSeparator() self.addAction(self.clrAct) self.fmtMenu.addAction(self.clrAct) self.key2act = { Qt.Key_H: self.hlAct, Qt.Key_B: self.bdAct, Qt.Key_T: self.soAct, Qt.Key_U: self.ulAct, Qt.Key_I: self.itaAct}
def test_overlap(self): test_str = 'This is something, string, string.\nparagraph, paragraph!\n method?' test_fmt = [(0, 2, 1), (0, 2, 2), (0, 10, 3), (5, 15, 4), (34, 3, 5)] # output formats may be duplicated, because Qt store format this way true_result = [ (0, 2, 1), (0, 2, 2), (0, 2, 3), (2, 3, 3), (5, 5, 3), # from (0, 10, 3), broken into three parts (5, 5, 4), (10, 10, 4), # from (5, 15, 4) (35, 2, 5) # 34 is \n ] doc = NTextDocument() doc.setText(test_str, test_fmt) result = NTextDocument.getFormats(doc) self.assertEqual(true_result, result)
def __init__(self): super().__init__() # don't pass parent because of mem problem # Because some fonts have much more space at top and bottom, we use ascent instead # of height, and add it with a small number. magic = int(4 * scaleRatio) self.title_h = max(font.title_m.ascent(), font.datetime_m.ascent()) + magic self.titleArea_h = self.title_h + 4 self.text_h = font.text_m.lineSpacing() * settings['Main'].getint('previewLines') self.tagPath_h = font.default_m.ascent() + magic self.tag_h = self.tagPath_h + 4 self.dt_w = font.datetime_m.width(datetimeTrans('2000-01-01 00:00')) + 40 self.all_h = None # updated in sizeHint before each item being painting # doc is used to draw text(diary's body) self.doc = NTextDocument() self.doc.setDefaultFont(font.text) self.doc.setUndoRedoEnabled(False) self.doc.setDocumentMargin(0) self.doc.documentLayout().setPaintDevice(qApp.desktop()) # refer actual list will cause segfault # setup colors self.c_text = Qt.black self.c_bg = QColor(255, 236, 176) self.c_border = QColor(214, 172, 41) self.c_inActBg = QColor(255, 236, 176, 40) self.c_gray = QColor(93, 73, 57)
class NTextEdit(QTextEdit, TextFormatter): """The widget used to edit diary contents in Editor window.""" # spaces that auto-indent can recognize SPACE_KINDS = (' ', '\u3000') # full width space U+3000 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._doc = NTextDocument(self) self.setDocument(self._doc) # remove highlight color's alpha to avoid alpha loss in copy&paste. # NTextDocument should use this color too. hl, bg = self.HlColor, self.palette().base().color() fac = hl.alpha() / 255 self.HlColor = QColor(round(hl.red()*fac + bg.red()*(1-fac)), round(hl.green()*fac + bg.green()*(1-fac)), round(hl.blue()*fac + bg.blue()*(1-fac))) self.autoIndent = False # used by tab indent shortcut if QLocale().language() in (QLocale.Chinese, QLocale.Japanese): self._indent = ' ' # 2 full width spaces else: self._indent = ' ' # 4 spaces # setup format menu onHLAct = lambda: super(NTextEdit, self).setHL(self.hlAct.isChecked()) onBDAct = lambda: super(NTextEdit, self).setBD(self.bdAct.isChecked()) onSOAct = lambda: super(NTextEdit, self).setSO(self.soAct.isChecked()) onULAct = lambda: super(NTextEdit, self).setUL(self.ulAct.isChecked()) onItaAct = lambda: super(NTextEdit, self).setIta(self.itaAct.isChecked()) self.fmtMenu = QMenu(self.tr('Format'), self) # shortcuts of format actions only used to display shortcut-hint in menu self.hlAct = QAction(makeQIcon(':/menu/highlight.png'), self.tr('Highlight'), self, triggered=onHLAct, shortcut=QKeySequence('Ctrl+H')) self.bdAct = QAction(makeQIcon(':/menu/bold.png'), self.tr('Bold'), self, triggered=onBDAct, shortcut=QKeySequence.Bold) self.soAct = QAction(makeQIcon(':/menu/strikeout.png'), self.tr('Strike out'), self, triggered=onSOAct, shortcut=QKeySequence('Ctrl+T')) self.ulAct = QAction(makeQIcon(':/menu/underline.png'), self.tr('Underline'), self, triggered=onULAct, shortcut=QKeySequence.Underline) self.itaAct = QAction(makeQIcon(':/menu/italic.png'), self.tr('Italic'), self, triggered=onItaAct, shortcut=QKeySequence.Italic) self.clrAct = QAction(self.tr('Clear format'), self, shortcut=QKeySequence('Ctrl+D'), triggered=self.clearFormat) self.acts = (self.hlAct, self.bdAct, self.soAct, self.ulAct, self.itaAct) # excluding uncheckable clrAct for a in self.acts: self.fmtMenu.addAction(a) a.setCheckable(True) self.fmtMenu.addSeparator() self.addAction(self.clrAct) self.fmtMenu.addAction(self.clrAct) self.key2act = { Qt.Key_H: self.hlAct, Qt.Key_B: self.bdAct, Qt.Key_T: self.soAct, Qt.Key_U: self.ulAct, Qt.Key_I: self.itaAct} def contextMenuEvent(self, event): menu = self.createStandardContextMenu() setStdEditMenuIcons(menu) if not self.isReadOnly(): if self.textCursor().hasSelection(): self._setFmtActs() self.fmtMenu.setEnabled(True) else: self.fmtMenu.setEnabled(False) before = menu.actions()[2] menu.insertSeparator(before) menu.insertMenu(before, self.fmtMenu) menu.exec_(event.globalPos()) menu.deleteLater() def keyPressEvent(self, event): if self.isReadOnly(): return super().keyPressEvent(event) if event.modifiers() == Qt.ControlModifier and event.key() in self.key2act: # set actions before calling format methods self._setFmtActs() self.key2act[event.key()].trigger() elif event.key() == Qt.Key_Tab: # will not receive event if tabChangesFocus is True self.textCursor().insertText(self._indent) elif event.key() == Qt.Key_Return and self.autoIndent: # auto-indent support para = self.textCursor().block().text() if len(para) > 0 and para[0] in NTextEdit.SPACE_KINDS: space, spaceCount = para[0], 1 for c in para[1:]: if c != space: break spaceCount += 1 super().keyPressEvent(event) self.textCursor().insertText(space * spaceCount) else: super().keyPressEvent(event) else: super().keyPressEvent(event) def insertFromMimeData(self, source): """Disable some unsupported types""" self.insertHtml(source.html() or source.text()) def setRichText(self, text, formats): self._doc.setHlColor(self.HlColor) self._doc.setText(text, formats) def setAutoIndent(self, enabled): assert isinstance(enabled, (bool, int)) self.autoIndent = enabled def getRichText(self): # self.document() will return QTextDocument, not NTextDocument return self.toPlainText(), self._doc.getFormats() def _setFmtActs(self): """Check formats in current selection and check or uncheck actions""" fmts = [QTextFormat.BackgroundBrush, QTextFormat.FontWeight, QTextFormat.FontStrikeOut, QTextFormat.TextUnderlineStyle, QTextFormat.FontItalic] cur = self.textCursor() start, end = cur.anchor(), cur.position() if start > end: start, end = end, start results = [True] * 5 for pos in range(end, start, -1): cur.setPosition(pos) charFmt = cur.charFormat() for i, f in enumerate(fmts): if results[i] and not charFmt.hasProperty(f): results[i] = False if not any(results): break for i, c in enumerate(results): self.acts[i].setChecked(c) def clearFormat(self): fmt = QTextCharFormat() self.textCursor().setCharFormat(fmt)
class NDocumentLabel(QFrame): """Simple widget to draw QTextDocument. sizeHint will always related to fixed number of lines set. If font fallback happen, it may look bad.""" def __init__(self, parent=None, lines=None, **kwargs): super().__init__(parent, **kwargs) self._lines = self._heightHint = None self.doc = NTextDocument(self) self.doc.setDocumentMargin(0) self.doc.setUndoRedoEnabled(False) self.setLines(lines if lines else 4) self.doc.documentLayout().setPaintDevice(self) # make difference on high DPI def setFont(self, f): self.doc.setDefaultFont(f) super().setFont(f) self.setLines(self._lines) # refresh size hint def setText(self, text, formats): self.doc.setText(text, formats) # delete exceed lines here using QTextCursor will slow down def setLines(self, lines): self._lines = lines self.doc.setText('\n' * (lines - 1), None) self._heightHint = int(self.doc.size().height()) self.updateGeometry() def paintEvent(self, event): painter = QPainter(self) rect = self.contentsRect() painter.translate(rect.topLeft()) rect.moveTo(0, 0) # become clip rect self.doc.drawContentsPalette(painter, rect, self.palette()) def resizeEvent(self, event): self.doc.setTextWidth(self.contentsRect().width()) super().resizeEvent(event) def sizeHint(self): __, top, __, bottom = self.getContentsMargins() return QSize(-1, self._heightHint + top + bottom)
class DiaryListDelegate(QStyledItemDelegate): """ItemDelegate of old theme 'one-pixel-rect' for DiaryList, Using 'traditional' painting method compared to colorful theme.""" def __init__(self): super().__init__() # don't pass parent because of mem problem # Because some fonts have much more space at top and bottom, we use ascent instead # of height, and add it with a small number. magic = int(4 * scaleRatio) self.title_h = max(font.title_m.ascent(), font.datetime_m.ascent()) + magic self.titleArea_h = self.title_h + 4 self.text_h = font.text_m.lineSpacing() * settings['Main'].getint('previewLines') self.tagPath_h = font.default_m.ascent() + magic self.tag_h = self.tagPath_h + 4 self.dt_w = font.datetime_m.width(datetimeTrans('2000-01-01 00:00')) + 40 self.all_h = None # updated in sizeHint before each item being painting # doc is used to draw text(diary's body) self.doc = NTextDocument() self.doc.setDefaultFont(font.text) self.doc.setUndoRedoEnabled(False) self.doc.setDocumentMargin(0) self.doc.documentLayout().setPaintDevice(qApp.desktop()) # refer actual list will cause segfault # setup colors self.c_text = Qt.black self.c_bg = QColor(255, 236, 176) self.c_border = QColor(214, 172, 41) self.c_inActBg = QColor(255, 236, 176, 40) self.c_gray = QColor(93, 73, 57) def paint(self, painter, option, index): x, y, w = option.rect.x(), option.rect.y(), option.rect.width()-1 row = index.row() dt, text, title, tags, formats = (index.sibling(row, i).data() for i in range(1, 6)) selected = bool(option.state & QStyle.State_Selected) active = bool(option.state & QStyle.State_Active) # draw border and background painter.setPen(self.c_border) painter.setBrush(self.c_bg if selected and active else self.c_inActBg) painter.drawRect(x+1, y, w-2, self.all_h) # outer border if selected: # draw inner border pen = QPen() pen.setStyle(Qt.DashLine) pen.setColor(self.c_gray) painter.setPen(pen) painter.drawRect(x+2, y+1, w-4, self.all_h-2) # draw datetime and title painter.setPen(self.c_gray) painter.drawLine(x+10, y+self.titleArea_h, x+w-10, y+self.titleArea_h) painter.setPen(self.c_text) painter.setFont(font.datetime) painter.drawText(x+14, y+self.titleArea_h-self.title_h, self.dt_w, self.title_h, Qt.AlignVCenter, datetimeTrans(dt)) if title: painter.setFont(font.title) title_w = w - self.dt_w - 13 title = font.title_m.elidedText(title, Qt.ElideRight, title_w) painter.drawText(x+self.dt_w, y+self.titleArea_h-self.title_h, title_w, self.title_h, Qt.AlignVCenter | Qt.AlignRight, title) # draw text self.doc.setText(text, formats) self.doc.setTextWidth(w-26) painter.translate(x+14, y+self.titleArea_h+2) self.doc.drawContentsColor(painter, QRect(0, 0, w-26, self.text_h), self.c_text) painter.resetTransform() # draw tags if tags: painter.setPen(self.c_gray) painter.setFont(font.default) painter.translate(x + 15, y+self.titleArea_h+6+self.text_h) real_x, max_x = x+15, w-10 for t in tags.split(): oneTag_w = font.default_m.width(t) + 4 real_x += oneTag_w + 15 if real_x > max_x: break tagPath = QPainterPath() tagPath.moveTo(8, 0) tagPath.lineTo(8+oneTag_w, 0) tagPath.lineTo(8+oneTag_w, self.tagPath_h) tagPath.lineTo(8, self.tagPath_h) tagPath.lineTo(0, self.tagPath_h/2) tagPath.closeSubpath() painter.drawPath(tagPath) painter.drawText(8, 0, oneTag_w, self.tagPath_h, Qt.AlignCenter, t) painter.translate(oneTag_w+15, 0) # translate by offset else: painter.resetTransform() return # draw ellipsis if too many tags painter.setPen(Qt.DotLine) painter.drawLine(-4, self.tagPath_h/2, 2, self.tagPath_h/2) painter.resetTransform() def sizeHint(self, option, index): tag_h = self.tag_h if index.sibling(index.row(), 4).data() else 0 self.all_h = self.titleArea_h + 2 + self.text_h + tag_h + 6 return QSize(-1, self.all_h+3) # 3 is spacing between entries
class DiaryListDelegate(QStyledItemDelegate): """ItemDelegate of old theme 'one-pixel-rect' for DiaryList, Using 'traditional' painting method compared to colorful theme.""" def __init__(self): super().__init__() # don't pass parent because of mem problem # Because some fonts have much more space at top and bottom, we use ascent instead # of height, and add it with a small number. magic = int(4 * scaleRatio) self.title_h = max(font.title_m.ascent(), font.datetime_m.ascent()) + magic self.titleArea_h = self.title_h + 4 self.text_h = font.text_m.lineSpacing() * settings['Main'].getint( 'previewLines') self.tagPath_h = font.default_m.ascent() + magic self.tag_h = self.tagPath_h + 4 self.dt_w = font.datetime_m.width( datetimeTrans('2000-01-01 00:00')) + 40 self.all_h = None # updated in sizeHint before each item being painting # doc is used to draw text(diary's body) self.doc = NTextDocument() self.doc.setDefaultFont(font.text) self.doc.setUndoRedoEnabled(False) self.doc.setDocumentMargin(0) self.doc.documentLayout().setPaintDevice( qApp.desktop()) # refer actual list will cause segfault # setup colors self.c_text = Qt.black self.c_bg = QColor(255, 236, 176) self.c_border = QColor(214, 172, 41) self.c_inActBg = QColor(255, 236, 176, 40) self.c_gray = QColor(93, 73, 57) def paint(self, painter, option, index): x, y, w = option.rect.x(), option.rect.y(), option.rect.width() - 1 row = index.row() dt, text, title, tags, formats = (index.sibling(row, i).data() for i in range(1, 6)) selected = bool(option.state & QStyle.State_Selected) active = bool(option.state & QStyle.State_Active) # draw border and background painter.setPen(self.c_border) painter.setBrush(self.c_bg if selected and active else self.c_inActBg) painter.drawRect(x + 1, y, w - 2, self.all_h) # outer border if selected: # draw inner border pen = QPen() pen.setStyle(Qt.DashLine) pen.setColor(self.c_gray) painter.setPen(pen) painter.drawRect(x + 2, y + 1, w - 4, self.all_h - 2) # draw datetime and title painter.setPen(self.c_gray) painter.drawLine(x + 10, y + self.titleArea_h, x + w - 10, y + self.titleArea_h) painter.setPen(self.c_text) painter.setFont(font.datetime) painter.drawText(x + 14, y + self.titleArea_h - self.title_h, self.dt_w, self.title_h, Qt.AlignVCenter, datetimeTrans(dt)) if title: painter.setFont(font.title) title_w = w - self.dt_w - 13 title = font.title_m.elidedText(title, Qt.ElideRight, title_w) painter.drawText(x + self.dt_w, y + self.titleArea_h - self.title_h, title_w, self.title_h, Qt.AlignVCenter | Qt.AlignRight, title) # draw text self.doc.setText(text, formats) self.doc.setTextWidth(w - 26) painter.translate(x + 14, y + self.titleArea_h + 2) self.doc.drawContentsColor(painter, QRect(0, 0, w - 26, self.text_h), self.c_text) painter.resetTransform() # draw tags if tags: painter.setPen(self.c_gray) painter.setFont(font.default) painter.translate(x + 15, y + self.titleArea_h + 6 + self.text_h) real_x, max_x = x + 15, w - 10 for t in tags.split(): oneTag_w = font.default_m.width(t) + 4 real_x += oneTag_w + 15 if real_x > max_x: break tagPath = QPainterPath() tagPath.moveTo(8, 0) tagPath.lineTo(8 + oneTag_w, 0) tagPath.lineTo(8 + oneTag_w, self.tagPath_h) tagPath.lineTo(8, self.tagPath_h) tagPath.lineTo(0, self.tagPath_h / 2) tagPath.closeSubpath() painter.drawPath(tagPath) painter.drawText(8, 0, oneTag_w, self.tagPath_h, Qt.AlignCenter, t) painter.translate(oneTag_w + 15, 0) # translate by offset else: painter.resetTransform() return # draw ellipsis if too many tags painter.setPen(Qt.DotLine) painter.drawLine(-4, self.tagPath_h / 2, 2, self.tagPath_h / 2) painter.resetTransform() def sizeHint(self, option, index): tag_h = self.tag_h if index.sibling(index.row(), 4).data() else 0 self.all_h = self.titleArea_h + 2 + self.text_h + tag_h + 6 return QSize(-1, self.all_h + 3) # 3 is spacing between entries