mirror of
https://github.com/olivierkes/manuskript.git
synced 2024-06-01 18:49:38 +12:00
4a9937f041
The lambda function will keep a reference to the scrollbar python object preventing it from getting destroyed when the QScrollbar is destroyed. This causes the underlying QT widget to be freed while the python object still exists, therefore the timer itself doesn't get stopped/cleaned, so the timer will get called and cause a crash with : "RuntimeError: Wrapped C/C++ object of type myScrollbar has been deleted" To reproduce, press F11 repeatedly while scrolling.
390 lines
14 KiB
Python
390 lines
14 KiB
Python
#!/usr/bin/env python
|
|
# --!-- coding: utf8 --!--
|
|
import os
|
|
|
|
from PyQt5.QtCore import Qt, QSize, QPoint, QRect, QEvent, QTimer
|
|
from PyQt5.QtGui import QFontMetrics, QColor, QBrush, QPalette, QPainter, QPixmap
|
|
from PyQt5.QtGui import QIcon
|
|
from PyQt5.QtWidgets import QFrame, QWidget, QPushButton, qApp, QStyle, QComboBox, QLabel, QScrollBar, \
|
|
QStyleOptionSlider, QHBoxLayout, QVBoxLayout, QMenu, QAction
|
|
|
|
# Spell checker support
|
|
from manuskript import settings
|
|
from manuskript.enums import Outline
|
|
from manuskript.functions import allPaths, drawProgress
|
|
from manuskript.ui.editors.locker import locker
|
|
from manuskript.ui.editors.themes import findThemePath, generateTheme, setThemeEditorDatas
|
|
from manuskript.ui.editors.themes import loadThemeDatas
|
|
from manuskript.ui.views.MDEditView import MDEditView
|
|
|
|
try:
|
|
import enchant
|
|
except ImportError:
|
|
enchant = None
|
|
|
|
|
|
class fullScreenEditor(QWidget):
|
|
def __init__(self, index, parent=None):
|
|
QWidget.__init__(self, parent)
|
|
self._background = None
|
|
self._index = index
|
|
self._theme = findThemePath(settings.fullScreenTheme)
|
|
self._themeDatas = loadThemeDatas(self._theme)
|
|
self.setMouseTracking(True)
|
|
self._geometries = {}
|
|
|
|
# Text editor
|
|
self.editor = MDEditView(self,
|
|
index=index,
|
|
spellcheck=settings.spellcheck,
|
|
highlighting=True,
|
|
dict=settings.dict)
|
|
self.editor.setFrameStyle(QFrame.NoFrame)
|
|
self.editor.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
self.editor.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
self.editor.installEventFilter(self)
|
|
self.editor.setMouseTracking(True)
|
|
self.editor.setVerticalScrollBar(myScrollBar())
|
|
self.scrollBar = self.editor.verticalScrollBar()
|
|
self.scrollBar.setParent(self)
|
|
|
|
# Top Panel
|
|
self.topPanel = myPanel(parent=self)
|
|
# self.topPanel.layout().addStretch(1)
|
|
|
|
# Spell checking
|
|
if enchant:
|
|
self.btnSpellCheck = QPushButton(self)
|
|
self.btnSpellCheck.setFlat(True)
|
|
self.btnSpellCheck.setIcon(QIcon.fromTheme("tools-check-spelling"))
|
|
self.btnSpellCheck.setCheckable(True)
|
|
self.btnSpellCheck.setChecked(self.editor.spellcheck)
|
|
self.btnSpellCheck.toggled.connect(self.editor.toggleSpellcheck)
|
|
self.topPanel.layout().addWidget(self.btnSpellCheck)
|
|
|
|
self.topPanel.layout().addStretch(1)
|
|
|
|
# Close
|
|
self.btnClose = QPushButton(self)
|
|
self.btnClose.setIcon(qApp.style().standardIcon(QStyle.SP_DialogCloseButton))
|
|
self.btnClose.clicked.connect(self.close)
|
|
self.btnClose.setFlat(True)
|
|
self.topPanel.layout().addWidget(self.btnClose)
|
|
|
|
# Left Panel
|
|
self._locked = False
|
|
self.leftPanel = myPanel(vertical=True, parent=self)
|
|
self.locker = locker(self)
|
|
self.locker.lockChanged.connect(self.setLocked)
|
|
self.leftPanel.layout().addWidget(self.locker)
|
|
|
|
# Bottom Panel
|
|
self.bottomPanel = myPanel(parent=self)
|
|
|
|
self.bottomPanel.layout().addSpacing(24)
|
|
self.lstThemes = QComboBox(self)
|
|
self.lstThemes.setAttribute(Qt.WA_TranslucentBackground)
|
|
paths = allPaths("resources/themes")
|
|
for p in paths:
|
|
lst = [i for i in os.listdir(p) if os.path.splitext(i)[1] == ".theme"]
|
|
for t in lst:
|
|
themeIni = os.path.join(p, t)
|
|
name = loadThemeDatas(themeIni)["Name"]
|
|
# self.lstThemes.addItem(os.path.splitext(t)[0])
|
|
self.lstThemes.addItem(name)
|
|
self.lstThemes.setItemData(self.lstThemes.count()-1, os.path.splitext(t)[0])
|
|
|
|
self.lstThemes.setCurrentIndex(self.lstThemes.findData(settings.fullScreenTheme))
|
|
# self.lstThemes.setCurrentText(settings.fullScreenTheme)
|
|
self.lstThemes.currentTextChanged.connect(self.setTheme)
|
|
self.lstThemes.setMaximumSize(QSize(300, QFontMetrics(qApp.font()).height()))
|
|
self.bottomPanel.layout().addWidget(QLabel(self.tr("Theme:"), self))
|
|
self.bottomPanel.layout().addWidget(self.lstThemes)
|
|
self.bottomPanel.layout().addStretch(1)
|
|
|
|
self.lblProgress = QLabel(self)
|
|
self.lblProgress.setMaximumSize(QSize(200, 14))
|
|
self.lblProgress.setMinimumSize(QSize(100, 14))
|
|
self.lblWC = QLabel(self)
|
|
self.bottomPanel.layout().addWidget(self.lblWC)
|
|
self.bottomPanel.layout().addWidget(self.lblProgress)
|
|
self.updateStatusBar()
|
|
|
|
self.bottomPanel.layout().addSpacing(24)
|
|
|
|
# Connection
|
|
self._index.model().dataChanged.connect(self.dataChanged)
|
|
|
|
# self.updateTheme()
|
|
self.showFullScreen()
|
|
# self.showMaximized()
|
|
# self.show()
|
|
|
|
def __del__(self):
|
|
# print("Leaving fullScreenEditor via Destructor event", flush=True)
|
|
self.showNormal()
|
|
self.close()
|
|
|
|
def setLocked(self, val):
|
|
self._locked = val
|
|
self.btnClose.setVisible(not val)
|
|
|
|
def setTheme(self, themeName):
|
|
themeName = self.lstThemes.currentData()
|
|
settings.fullScreenTheme = themeName
|
|
self._theme = findThemePath(themeName)
|
|
self._themeDatas = loadThemeDatas(self._theme)
|
|
self.updateTheme()
|
|
|
|
def updateTheme(self):
|
|
# Reinit stored geometries for hiding widgets
|
|
self._geometries = {}
|
|
rect = self.geometry()
|
|
self._background = generateTheme(self._themeDatas, rect)
|
|
|
|
setThemeEditorDatas(self.editor, self._themeDatas, self._background, rect)
|
|
|
|
# Colors
|
|
if self._themeDatas["Foreground/Color"] == self._themeDatas["Background/Color"] or \
|
|
self._themeDatas["Foreground/Opacity"] < 5:
|
|
self._bgcolor = QColor(self._themeDatas["Text/Color"])
|
|
self._fgcolor = QColor(self._themeDatas["Background/Color"])
|
|
else:
|
|
self._bgcolor = QColor(self._themeDatas["Foreground/Color"])
|
|
self._bgcolor.setAlpha(self._themeDatas["Foreground/Opacity"] * 255 / 100)
|
|
self._fgcolor = QColor(self._themeDatas["Text/Color"])
|
|
if self._themeDatas["Text/Color"] == self._themeDatas["Foreground/Color"]:
|
|
self._fgcolor = QColor(self._themeDatas["Background/Color"])
|
|
|
|
# ScrollBar
|
|
r = self.editor.geometry()
|
|
w = qApp.style().pixelMetric(QStyle.PM_ScrollBarExtent)
|
|
r.setWidth(w)
|
|
r.moveRight(rect.right() - rect.left())
|
|
self.scrollBar.setGeometry(r)
|
|
# self.scrollBar.setVisible(False)
|
|
self.hideWidget(self.scrollBar)
|
|
p = self.scrollBar.palette()
|
|
b = QBrush(self._background.copy(self.scrollBar.geometry()))
|
|
p.setBrush(QPalette.Base, b)
|
|
self.scrollBar.setPalette(p)
|
|
|
|
self.scrollBar.setColor(self._bgcolor)
|
|
|
|
# Left Panel
|
|
r = self.locker.geometry()
|
|
r.moveTopLeft(QPoint(
|
|
0,
|
|
self.geometry().height() / 2 - r.height() / 2
|
|
))
|
|
self.leftPanel.setGeometry(r)
|
|
self.hideWidget(self.leftPanel)
|
|
self.leftPanel.setColor(self._bgcolor)
|
|
|
|
# Top / Bottom Panels
|
|
r = QRect(0, 0, 0, 24)
|
|
r.setWidth(rect.width())
|
|
# r.moveLeft(rect.center().x() - r.width() / 2)
|
|
self.topPanel.setGeometry(r)
|
|
# self.topPanel.setVisible(False)
|
|
self.hideWidget(self.topPanel)
|
|
r.moveBottom(rect.bottom() - rect.top())
|
|
self.bottomPanel.setGeometry(r)
|
|
# self.bottomPanel.setVisible(False)
|
|
self.hideWidget(self.bottomPanel)
|
|
self.topPanel.setColor(self._bgcolor)
|
|
self.bottomPanel.setColor(self._bgcolor)
|
|
|
|
# Lst theme
|
|
# p = self.lstThemes.palette()
|
|
p = self.palette()
|
|
p.setBrush(QPalette.Button, self._bgcolor)
|
|
p.setBrush(QPalette.ButtonText, self._fgcolor)
|
|
p.setBrush(QPalette.WindowText, self._fgcolor)
|
|
|
|
for panel in (self.bottomPanel, self.topPanel):
|
|
for i in range(panel.layout().count()):
|
|
item = panel.layout().itemAt(i)
|
|
if item.widget():
|
|
item.widget().setPalette(p)
|
|
# self.lstThemes.setPalette(p)
|
|
# self.lblWC.setPalette(p)
|
|
|
|
self.update()
|
|
self.editor.centerCursor()
|
|
|
|
def paintEvent(self, event):
|
|
if self._background:
|
|
painter = QPainter(self)
|
|
painter.setClipRegion(event.region())
|
|
painter.drawPixmap(event.rect(), self._background, event.rect())
|
|
painter.end()
|
|
|
|
def resizeEvent(self, event):
|
|
self.updateTheme()
|
|
|
|
def keyPressEvent(self, event):
|
|
if event.key() in [Qt.Key_Escape, Qt.Key_F11] and \
|
|
not self._locked:
|
|
# print("Leaving fullScreenEditor via keyPressEvent", flush=True)
|
|
self.showNormal()
|
|
self.close()
|
|
else:
|
|
QWidget.keyPressEvent(self, event)
|
|
|
|
def mouseMoveEvent(self, event):
|
|
r = self.geometry()
|
|
|
|
for w in [self.scrollBar, self.topPanel,
|
|
self.bottomPanel, self.leftPanel]:
|
|
# w.setVisible(w.geometry().contains(event.pos()))
|
|
if self._geometries[w].contains(event.pos()):
|
|
self.showWidget(w)
|
|
else:
|
|
self.hideWidget(w)
|
|
|
|
def hideWidget(self, widget):
|
|
if widget not in self._geometries:
|
|
self._geometries[widget] = widget.geometry()
|
|
|
|
if hasattr(widget, "_autoHide") and not widget._autoHide:
|
|
return
|
|
|
|
# Hides widget in the bottom right corner
|
|
widget.move(self.geometry().bottomRight() + QPoint(1, 1))
|
|
|
|
def showWidget(self, widget):
|
|
if widget in self._geometries:
|
|
widget.move(self._geometries[widget].topLeft())
|
|
|
|
def eventFilter(self, obj, event):
|
|
if obj == self.editor and event.type() == QEvent.Enter:
|
|
for w in [self.scrollBar, self.topPanel,
|
|
self.bottomPanel, self.leftPanel]:
|
|
# w.setVisible(False)
|
|
self.hideWidget(w)
|
|
return QWidget.eventFilter(self, obj, event)
|
|
|
|
def dataChanged(self, topLeft, bottomRight):
|
|
# This is called sometimes after self has been destroyed. Don't know why.
|
|
if not self or not self._index:
|
|
return
|
|
if topLeft.row() <= self._index.row() <= bottomRight.row():
|
|
self.updateStatusBar()
|
|
|
|
def updateStatusBar(self):
|
|
if self._index:
|
|
item = self._index.internalPointer()
|
|
|
|
wc = item.data(Outline.wordCount)
|
|
goal = item.data(Outline.goal)
|
|
pg = item.data(Outline.goalPercentage)
|
|
|
|
if goal:
|
|
rect = self.lblProgress.geometry()
|
|
rect = QRect(QPoint(0, 0), rect.size())
|
|
self.px = QPixmap(rect.size())
|
|
self.px.fill(Qt.transparent)
|
|
p = QPainter(self.px)
|
|
drawProgress(p, rect, pg, 2)
|
|
p.end()
|
|
self.lblProgress.setPixmap(self.px)
|
|
self.lblWC.setText(self.tr("{} words / {}").format(wc, goal))
|
|
else:
|
|
self.lblProgress.hide()
|
|
self.lblWC.setText(self.tr("{} words").format(wc))
|
|
|
|
self.locker.setWordCount(wc)
|
|
# If there's a goal, then we update the locker target's number of word accordingly
|
|
# (also if there is a word count, we deduce it.
|
|
if goal and not self.locker.isLocked():
|
|
if wc and goal - wc > 0:
|
|
self.locker.spnWordTarget.setValue(goal - wc)
|
|
elif not wc:
|
|
self.locker.spnWordTarget.setValue(goal)
|
|
|
|
|
|
class myScrollBar(QScrollBar):
|
|
def __init__(self, color=Qt.white, parent=None):
|
|
QScrollBar.__init__(self, parent)
|
|
self._color = color
|
|
# self.setAttribute(Qt.WA_TranslucentBackground)
|
|
self.timer = QTimer()
|
|
self.timer.setInterval(500)
|
|
self.timer.setSingleShot(True)
|
|
self.timer.timeout.connect(self.hide)
|
|
self.valueChanged.connect(lambda v: self.timer.start())
|
|
self.valueChanged.connect(lambda: self.parent().showWidget(self))
|
|
self.rangeChanged.connect(self.rangeHasChanged)
|
|
|
|
def hide(self):
|
|
self.parent().hideWidget(self)
|
|
|
|
def setColor(self, color):
|
|
self._color = color
|
|
|
|
def rangeHasChanged(self, min, max):
|
|
"""
|
|
Adds viewport height to scrollbar max so that we can center cursor
|
|
on screen.
|
|
"""
|
|
if settings.textEditor["alwaysCenter"]:
|
|
self.blockSignals(True)
|
|
self.setMaximum(max + self.parent().height())
|
|
self.blockSignals(False)
|
|
|
|
def paintEvent(self, event):
|
|
opt = QStyleOptionSlider()
|
|
self.initStyleOption(opt)
|
|
style = qApp.style()
|
|
painter = QPainter(self)
|
|
|
|
# Background (Necessary with Qt 5.2 it seems, not with 5.4)
|
|
# painter.save()
|
|
# painter.setPen(Qt.NoPen)
|
|
# painter.setBrush(self.palette().brush(QPalette.Base))
|
|
# painter.drawRect(event.rect())
|
|
# painter.restore()
|
|
|
|
# slider
|
|
r = style.subControlRect(style.CC_ScrollBar, opt, style.SC_ScrollBarSlider)
|
|
painter.fillRect(r, self._color)
|
|
painter.end()
|
|
|
|
|
|
class myPanel(QWidget):
|
|
def __init__(self, color=Qt.white, vertical=False, parent=None):
|
|
QWidget.__init__(self, parent)
|
|
self._color = color
|
|
self.show()
|
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
|
self._autoHide = True
|
|
|
|
if not vertical:
|
|
self.setLayout(QHBoxLayout())
|
|
else:
|
|
self.setLayout(QVBoxLayout())
|
|
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
|
|
def setColor(self, color):
|
|
self._color = color
|
|
|
|
def paintEvent(self, event):
|
|
r = event.rect()
|
|
painter = QPainter(self)
|
|
painter.fillRect(r, self._color)
|
|
|
|
def setAutoHide(self, value):
|
|
self._autoHide = value
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if event.button() == Qt.RightButton:
|
|
m = QMenu()
|
|
a = QAction(self.tr("Auto-hide"), m)
|
|
a.setCheckable(True)
|
|
a.setChecked(self._autoHide)
|
|
a.toggled.connect(self.setAutoHide)
|
|
m.addAction(a)
|
|
m.popup(self.mapToGlobal(event.pos()))
|
|
self._m = m
|