File: treeoutput.py

package info (click to toggle)
treeline 3.1.5-1.1
  • links: PTS
  • area: main
  • in suites: trixie
  • size: 6,508 kB
  • sloc: python: 20,489; javascript: 998; makefile: 54
file content (462 lines) | stat: -rw-r--r-- 17,597 bytes parent folder | download | duplicates (3)
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
#!/usr/bin/env python3

#******************************************************************************
# treeoutput.py, provides classes for output to views, html and printing
#
# TreeLine, an information storage program
# Copyright (C) 2018, Douglas W. Bell
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License, either Version 2 or any later
# version.  This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY.  See the included LICENSE file for details.
#******************************************************************************

import re
import itertools
from PyQt5.QtGui import QTextDocument
import globalref

_linkRe = re.compile(r'<a [^>]*href="#(.*?)"[^>]*>.*?</a>', re.I | re.S)


class OutputItem:
    """Class to store output for a single node.

    Stores text lines and original indent level.
    """
    def __init__(self, spot, level):
        """Convert the spot's node into an output item.

        Create a blank item if spot is None.

        Arguments:
            spot -- the tree spot to convert
            level -- the node's original indent level
        """
        if spot:
            node = spot.nodeRef
            nodeFormat = node.formatRef
            if not nodeFormat.useTables:
                self.textLines = [line + '<br />' for line in
                                  node.output(spotRef=spot)]
            else:
                self.textLines = node.output(keepBlanks=True, spotRef=spot)
            if not self.textLines:
                self.textLines = ['']
            self.addSpace = nodeFormat.spaceBetween
            self.siblingPrefix = nodeFormat.siblingPrefix
            self.siblingSuffix = nodeFormat.siblingSuffix
            if nodeFormat.useBullets and self.textLines:
                # remove <br /> extra space for bullets
                self.textLines[-1] = self.textLines[-1][:-6]
            self.uId = node.uId
        else:
            self.textLines = ['']
            self.addSpace = False
            self.siblingPrefix = ''
            self.siblingSuffix = ''
            self.uId = None
        self.level = level
        # following variables used by printdata only:
        self.height = 0
        self.pageNum = 0
        self.columnNum = 0
        self.pagePos = 0
        self.doc = None
        self.parentItem = None
        self.lastChildItem = None

    def duplicate(self):
        """Return an independent copy of this OutputItem.
        """
        item = OutputItem(None, 0)
        item.textLines = self.textLines[:]
        item.addSpace = self.addSpace
        item.siblingPrefix = self.siblingPrefix
        item.siblingSuffix = self.siblingSuffix
        item.uId = self.uId
        item.level = self.level
        item.height = self.height
        item.pageNum = self.pageNum
        item.columnNum = self.columnNum
        item.pagePos = self.pagePos
        item.doc = None
        item.parentItem = self.parentItem
        item.lastChildItem = self.lastChildItem
        return item

    def addIndent(self, prevLevel, nextLevel):
        """Add <div> tags to define indent levels in the output.

        Arguments:
            prevLevel -- the level of the previous item in the list
            nextLevel -- the level of the next item in the list
        """
        for num in range(self.level - prevLevel):
            self.textLines[0] = '<div>' + self.textLines[0]
        for num in range(self.level - nextLevel):
            self.textLines[-1] += '</div>'

    def addAbsoluteIndent(self, pixels):
        """Add tags for an individual indentation.

        Removes the <br /> tag from the last line to avoid excess space,
        since <div> starts a new line.
        The Qt output view does not fully support nested <div> tags.
        Arguments:
            pixels -- the amount to indent
        """
        self.textLines[0] = ('<div style="margin-left: {0}">{1}'.
                             format(pixels * self.level, self.textLines[0]))
        if not self.siblingPrefix and self.textLines[-1].endswith('<br />'):
            self.textLines[-1] = self.textLines[-1][:-6]
        self.textLines[-1] += '</div>'

    def addSiblingPrefix(self):
        """Add the sibling prefix before this output.
        """
        if self.siblingPrefix:
            self.textLines[0] = self.siblingPrefix + self.textLines[0]

    def addSiblingSuffix(self):
        """Add the sibling suffix after this output.
        """
        if self.siblingSuffix:
            self.textLines[-1] += self.siblingSuffix

    def addAnchor(self):
        """Add a link anchor to this item.
        """
        self.textLines[0] = '<a id="{0}" />{1}'.format(self.uId,
                                                       self.textLines[0])

    def intLinkIds(self):
        """Return a set of uIDs from any internal links in this item.
        """
        linkIds = set()
        for line in self.textLines:
            startPos = 0
            while True:
                match = _linkRe.search(line, startPos)
                if not match:
                    break
                uId = match.group(1)
                if uId:
                    linkIds.add(uId)
                startPos = match.start(1)
        return linkIds

    def numLines(self):
        """Return the number of text lines in the item.
        """
        return len(self.textLines)

    def equalPrefix(self, otherItem):
        """Return True if sibling prefixes and suffixes are equal.

        Arguments:
            otherItem -- the item to compare
        """
        return (self.siblingPrefix == otherItem.siblingPrefix and
                self.siblingSuffix == otherItem.siblingSuffix)

    def setDocHeight(self, paintDevice, width, printFont,
                          replaceDoc=False):
        """Set the height of this item for use in printer output.

        Creates an output document if not already created.
        Arguments:
            paintDevice -- the printer or other device for settings
            width -- the width available for the output text
            printFont -- the default font for the document
            replaceDoc -- if true, re-create the text document
        """
        if not self.doc or replaceDoc:
            self.doc = QTextDocument()
            lines = '\n'.join(self.textLines)
            if lines.endswith('<br />'):
                # remove trailing <br /> tag to avoid excess space
                lines = lines[:-6]
            self.doc.setHtml(lines)
            self.doc.setDefaultFont(printFont)
            frameFormat = self.doc.rootFrame().frameFormat()
            frameFormat.setBorder(0)
            frameFormat.setMargin(0)
            frameFormat.setPadding(0)
            self.doc.rootFrame().setFrameFormat(frameFormat)
        layout = self.doc.documentLayout()
        layout.setPaintDevice(paintDevice)
        self.doc.setTextWidth(width)
        self.height = layout.documentSize().height()

    def splitDocHeight(self, initHeight, maxHeight, paintDevice, width,
                            printFont):
        """Split this item into two items and return them.

        The first item will fit into initHeight if practical.
        Splits at line endings if posible.
        Arguments:
            initHeight -- the preferred height of the first page
            maxheight -- the max height of any pages
            paintDevice -- the printer or other device for settings
            width -- the width available for the output text
            printFont -- the default font for the document
        """
        newItem = self.duplicate()
        fullHeight = self.height
        lines = '\n'.join(self.textLines)
        allLines = [line + '<br />' for line in lines.split('<br />')]
        self.textLines = []
        prevHeight = 0
        for line in allLines:
            self.textLines.append(line)
            self.setDocHeight(paintDevice, width, printFont, True)
            if ((prevHeight and self.height > initHeight and
                 fullHeight - prevHeight > maxHeight) or
                (prevHeight and self.height > maxHeight)):
                self.textLines = self.textLines[:-1]
                self.setDocHeight(paintDevice, width, printFont, True)
                newItem.textLines = allLines[len(self.textLines):]
                newItem.setDocHeight(paintDevice, width, printFont, True)
                return (self, newItem)
            if self.height > maxHeight:
                break
            prevHeight = self.height
        # no line ending breaks found
        text = ' \n'.join(allLines)
        allWords = [word + ' ' for word in text.split(' ')]
        newWords = []
        prevHeight = 0
        for word in allWords:
            if word.strip() == '<img':
                break
            newWords.append(word)
            self.textLines = [''.join(newWords)]
            self.setDocHeight(paintDevice, width, printFont, True)
            if ((prevHeight and self.height > initHeight and
                 fullHeight - prevHeight < maxHeight) or
                (prevHeight and self.height > maxHeight)):
                self.textLines = [''.join(newWords[:-1])]
                self.setDocHeight(paintDevice, width, printFont, True)
                newItem.textLines = [''.join(allWords[len(newWords):])]
                newItem.setDocHeight(paintDevice, width, printFont, True)
                return (self, newItem)
            if self.height > maxHeight:
                break
            prevHeight = self.height
        newItem.setDocHeight(paintDevice, width, printFont, True)
        return (newItem, None)   # fail to split


class OutputGroup(list):
    """A list of OutputItems that takes TreeNodes as input.

    Modifies the output text for use in views, html and printing.
    """
    def __init__(self, spotList, includeRoot=True, includeDescend=False,
                 openOnly=False):
        """Convert the node iter list into a list of output items.

        Arguments:
            spotList -- a list of spots to convert to output
            includeRoot -- if True, include the nodes in nodeList
            includeDescend -- if True, include children, grandchildren, etc.
            openOnly -- if true, ignore collapsed children in the main treeView
        """
        super().__init__()
        for spot in spotList:
            level = -1
            if includeRoot:
                level = 0
                self.append(OutputItem(spot, level))
            if includeDescend:
                self.addChildren(spot, level, openOnly)

    def addChildren(self, spot, level, openOnly=False):
        """Recursively add OutputItems for descendants of the given spot.

        Arguments:
            spot -- the parent tree spot
            level -- the parent node's original indent level
        """
        treeView = globalref.mainControl.activeControl.activeWindow.treeView
        if not openOnly or treeView.isSpotExpanded(spot):
            for child in spot.childSpots():
                self.append(OutputItem(child, level + 1))
                self.addChildren(child, level + 1, openOnly)

    def addIndents(self):
        """Add nested <div> elements to define indentations in the output.
        """
        prevLevel = 0
        for item, nextItem in itertools.zip_longest(self, self[1:]):
            try:
                nextLevel = nextItem.level
            except AttributeError:
                nextLevel = 0
            item.addIndent(prevLevel, nextLevel)
            prevLevel = item.level

    def addAbsoluteIndents(self, pixels=20):
        """Add tags for individual indentation on each node.

        The Qt output view does not fully support nested <div> tags.
        Arguments:
            pixels -- the amount to indent
        """
        for item in self:
            item.addAbsoluteIndent(pixels)

    def addBlanksBetween(self):
        """Add blank lines between nodes based on node format's spaceBetween.
        """
        for item, nextItem in zip(self, self[1:]):
            if item.addSpace or nextItem.addSpace:
                item.textLines[-1] += '<br />'

    def addAnchors(self, extraLevels=0):
        """Add anchors to items that are targets and to low level items.

        Arguments:
            extraLevels -- force adding anchors if level < this
        """
        linkIds = set()
        for item in self:
            linkIds.update(item.intLinkIds())
        for item in self:
            if item.uId in linkIds or item.level < extraLevels:
                item.addAnchor()

    def hasPrefixes(self):
        """Return True if sibling prefixes or suffixes are found.
        """
        return bool([item for item in self if item.siblingPrefix or
                     item.siblingSuffix])

    def addSiblingPrefixes(self):
        """Add sibling prefixes and suffixes for each node.
        """
        if not self.hasPrefixes():
            return
        addPrefix = True
        for item, nextItem in itertools.zip_longest(self, self[1:]):
            if addPrefix:
                item.addSiblingPrefix()
            if (not nextItem or item.level != nextItem.level or
                not item.equalPrefix(nextItem)):
                item.addSiblingSuffix()
                addPrefix = True
            else:
                addPrefix = False

    def combineAllSiblings(self):
        """Group all sibling items with the same prefix into single items.

        Also add sibling prefixes and suffixes and spaces in between.
        """
        newItems = []
        prevItem = None
        for item in self:
            if prevItem:
                if item.level == prevItem.level and item.equalPrefix(prevItem):
                    if item.addSpace or prevItem.addSpace:
                        prevItem.textLines[-1] += '<br />'
                    prevItem.textLines.extend(item.textLines)
                else:
                    prevItem.addSiblingSuffix()
                    newItems.append(prevItem)
                    item.addSiblingPrefix()
                    prevItem = item
            else:
                item.addSiblingPrefix()
                prevItem = item
        prevItem.addSiblingSuffix()
        newItems.append(prevItem)
        self[:] = newItems

    def combineLines(self, addSpacing=True, addPrefixes=True):
        """Return an OutputItem including all of the text from all items.

        Arguments:
            addPrefixes -- if True, add sibling prefix and suffix to result
            addSpacing -- if True, add spacing between items with addSpace True
        """
        comboItem = self[0].duplicate()
        for item in self[1:]:
            if item.addSpace:
                comboItem.textLines[-1] += '<br />'
            comboItem.textLines.extend(item.textLines)
        if addPrefixes:
            comboItem.addSiblingPrefix()
            comboItem.addSiblingSuffix()
        return comboItem

    def splitColumns(self, numColumns):
        """Split output into even length columns using number of lines.

       Return a list with a group for each column.
       Arguments:
           numColumns - the number of columns to split
       """
        if numColumns < 2:
            return [self]
        groupList = []
        if len(self) <= numColumns:
            for item in self:
                groupList.append(OutputGroup([]))
                groupList[-1].append(item)
            return groupList
        numEach = len(self) // numColumns
        for colNum in range(numColumns - 1):
            groupList.append(OutputGroup([]))
            groupList[-1].extend(self[colNum * numEach :
                                      (colNum + 1) * numEach])
        groupList.append(OutputGroup([]))
        groupList[-1].extend(self[(numColumns - 1) * numEach : ])
        numChanges = 1
        while numChanges:
            numChanges = 0
            for colNum in range(numColumns - 1):
                if (groupList[colNum].totalNumLines() > groupList[colNum + 1].
                    totalNumLines() + groupList[colNum][-1].numLines()):
                    groupList[colNum + 1].insert(0, groupList[colNum][-1])
                    del groupList[colNum][-1]
                    numChanges += 1
                if (groupList[colNum].totalNumLines() +
                    groupList[colNum + 1][0].numLines() <=
                    groupList[colNum + 1].totalNumLines()):
                    groupList[colNum].append(groupList[colNum + 1][0])
                    del groupList[colNum + 1][0]
                    numChanges += 1
        return groupList

    def getLines(self):
        """Return the full list of text lines from this group.
        """
        if not self:
            return []
        lines = []
        for item in self:
            lines.extend(item.textLines)
        return lines

    def totalNumLines(self):
        """Return the total number of lines of all items in this container.
        """
        return sum([item.numLines() for item in self])

    def loadFamilyRefs(self):
        """Set parentItem and lastChildItem for all items.

        Used by the printdata class.
        """
        recentParents = [None]
        for item in self:
            if item.level > 0:
                item.parentItem = recentParents[item.level - 1]
                item.parentItem.lastChildItem = item
            try:
                recentParents[item.level] = item
            except IndexError:
                recentParents.append(item)