1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
|
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
#
# Copyright (c) 2008 - 2014 by Wilbert Berendsen
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# See http://www.gnu.org/licenses/ for more information.
"""
Manages marked lines (bookmarks) for a Document.
A mark is simply a QTextCursor that maintains its position in the document.
There are different types (categories) of marks, listed in the module-global
types variable. Currently the available types are 'mark' (a normal mark)
and 'error' (marking a line containing an error).
"""
import bisect
import json
from PyQt5.QtGui import QTextCursor
import metainfo
import signals
import plugin
types = (
'mark',
'error',
)
metainfo.define('bookmarks', json.dumps(None))
def bookmarks(document):
"""Returns the Bookmarks instance for the document."""
return Bookmarks.instance(document)
class Bookmarks(plugin.DocumentPlugin):
"""Manages bookmarks (marked lines) for a Document.
The marks are stored in the metainfo for the Document.
"""
marksChanged = signals.Signal()
def __init__(self, document):
"""Creates the Bookmarks instance."""
document.loaded.connect(self.load)
document.saved.connect(self.save)
document.closed.connect(self.save)
self.load() # initializes self._marks
def marks(self, type=None):
"""Returns marks (QTextCursor instances).
If type is specified (one of the names in the module-global types variable),
the list of marks of that type is returned.
If type is None, a dictionary listing all types mapped to lists of marks
is returned.
"""
return self._marks[type] if type else self._marks
def setMark(self, linenum, type):
"""Marks the given line number with a mark of the given type."""
nums = [mark.blockNumber() for mark in self._marks[type]]
if linenum in nums:
return
index = bisect.bisect_left(nums, linenum)
mark = QTextCursor(self.document().findBlockByNumber(linenum))
try:
# only available in very recent PyQt5 versions
mark.setKeepPositionOnInsert(True)
except AttributeError:
pass
self._marks[type].insert(index, mark)
self.marksChanged()
def unsetMark(self, linenum, type):
"""Removes a mark of the given type on the given line."""
nums = [mark.blockNumber() for mark in self._marks[type]]
if linenum in nums:
# remove double occurrences
while True:
index = bisect.bisect_left(nums, linenum)
del self._marks[type][index]
del nums[index]
if linenum not in nums:
break
self.marksChanged()
def toggleMark(self, linenum, type):
"""Toggles the mark of the given type on the given line."""
nums = [mark.blockNumber() for mark in self._marks[type]]
index = bisect.bisect_left(nums, linenum)
if linenum in nums:
# remove double occurrences
while True:
del self._marks[type][index]
del nums[index]
if linenum not in nums:
break
index = bisect.bisect_left(nums, linenum)
else:
mark = QTextCursor(self.document().findBlockByNumber(linenum))
try:
# only available in very recent PyQt5 versions
mark.setKeepPositionOnInsert(True)
except AttributeError:
pass
self._marks[type].insert(index, mark)
self.marksChanged()
def hasMark(self, linenum, type=None):
"""Returns True if the line has a mark (of the given type if specified) else False."""
for type in types if type is None else (type,):
for mark in self._marks[type]:
if mark.blockNumber() == linenum:
return True
return False
def clear(self, type=None):
"""Removes all marks, or only all marks of the given type. if specified."""
if type is None:
for type in types:
self._marks[type] = []
else:
self._marks[type] = []
self.marksChanged()
def nextMark(self, cursor, type=None):
"""Finds the first mark after the cursor (of the type if specified)."""
if type is None:
marks = []
for type in types:
marks += self._marks[type]
# sort the marks on line number
marks.sort(key=lambda mark: mark.blockNumber())
else:
marks = self._marks[type]
nums = [mark.blockNumber() for mark in marks]
index = bisect.bisect_right(nums, cursor.blockNumber())
if index < len(nums):
return QTextCursor(marks[index].block())
def previousMark(self, cursor, type=None):
"""Finds the first mark before the cursor (of the type if specified)."""
if type is None:
marks = []
for type in types:
marks += self._marks[type]
# sort the marks on line number
marks.sort(key=lambda mark: mark.blockNumber())
else:
marks = self._marks[type]
nums = [mark.blockNumber() for mark in marks]
index = bisect.bisect_left(nums, cursor.blockNumber())
if index > 0:
return QTextCursor(marks[index-1].block())
def load(self):
"""Loads the marks from the metainfo."""
self._marks = dict((type, []) for type in types)
marks = metainfo.info(self.document()).bookmarks
try:
d = json.loads(marks) or {}
except ValueError:
return # No JSON object could be decoded
for type in types:
self._marks[type] = [QTextCursor(self.document().findBlockByNumber(num)) for num in d.get(type, [])]
self.marksChanged()
def save(self):
"""Saves the marks to the metainfo."""
d = {}
for type in types:
d[type] = lines = []
for mark in self._marks[type]:
linenum = mark.blockNumber()
if linenum not in lines:
lines.append(linenum)
metainfo.info(self.document()).bookmarks = json.dumps(d)
|