Virgil Dupras avatar Virgil Dupras committed b586c0e

[#349 state:fixed] Added split printing to Qt.

Comments (0)

Files changed (8)

core/gui/account_view.py

 from .entry_table import EntryTable
 from .account_balance_graph import AccountBalanceGraph
 from .account_flow_graph import AccountFlowGraph
+from .transaction_print import EntryPrint
 
 class AccountView(BaseView):
     VIEW_TYPE = PaneType.Account
     PRINT_TITLE_FORMAT = tr('{account_name}\nEntries from {start_date} to {end_date}')
+    PRINT_VIEW_CLASS = EntryPrint
     INVALIDATING_MESSAGES = MESSAGES_DOCUMENT_CHANGED | {'filter_applied',
         'date_range_changed', 'transactions_selected', 'area_visibility_changed'}
     
 from hscommon.gui.base import GUIObject
 from hscommon.gui.selectable_list import GUISelectableList
 
+from .print_view import PrintView
+
 class DocumentNotificationsMixin:
     def account_added(self):
         pass
     #
     
     VIEW_TYPE = -1
+    PRINT_VIEW_CLASS = PrintView
     
     def __init__(self, mainwindow):
         Repeater.__init__(self, mainwindow)

core/gui/general_ledger_view.py

 from ..const import PaneType
 from .base import BaseView, MESSAGES_DOCUMENT_CHANGED
 from .general_ledger_table import GeneralLedgerTable
+from .transaction_print import EntryPrint
 
 class GeneralLedgerView(BaseView):
     VIEW_TYPE = PaneType.GeneralLedger
     PRINT_TITLE_FORMAT = tr('General Ledger from {start_date} to {end_date}')
+    PRINT_VIEW_CLASS = EntryPrint
     INVALIDATING_MESSAGES = MESSAGES_DOCUMENT_CHANGED | {'filter_applied', 'date_range_changed',
         'transactions_selected'}
     

core/gui/print_view.py

 # which should be included with this package. The terms are also available at 
 # http://www.hardcoded.net/licenses/bsd_license
 
-from .account_view import AccountView
+from ..const import PaneType
 
 class PrintView:
     def __init__(self, parent):
     def title(self):
         if not hasattr(self.parent, 'PRINT_TITLE_FORMAT'):
             return ''
-        if isinstance(self.parent, AccountView):
+        if self.parent.VIEW_TYPE == PaneType.Account:
             account_name = self.parent.account.name
         title_format = self.parent.PRINT_TITLE_FORMAT
         start_date = self.app.format_date(self.document.date_range.start)

core/gui/transaction_print.py

 # which should be included with this package. The terms are also available at 
 # http://www.hardcoded.net/licenses/bsd_license
 
+from collections import namedtuple
+
 from hscommon.trans import tr
 from .print_view import PrintView
 
+SplitValues = namedtuple('SplitValues', 'account memo amount')
+
 class TransactionPrintBase(PrintView):
     #--- Virtual
     def _get_splits_at_row(self, row_index):
         splits = self._get_splits_at_row(row_index)
         split = splits[split_row_index]
         account_name = split.account.name if split.account is not None else tr('Unassigned')
-        return [account_name, split.memo, self.document.format_amount(split.amount)]
+        return SplitValues(account_name, split.memo, self.document.format_amount(split.amount))
     
 
 class TransactionPrint(TransactionPrintBase):

core/gui/transaction_view.py

 from .base import BaseView, MESSAGES_DOCUMENT_CHANGED
 from .filter_bar import FilterBar
 from .transaction_table import TransactionTable
+from .transaction_print import TransactionPrint
 
 class TransactionView(BaseView):
     VIEW_TYPE = PaneType.Transaction
     PRINT_TITLE_FORMAT = tr('Transactions from {start_date} to {end_date}')
+    PRINT_VIEW_CLASS = TransactionPrint
     INVALIDATING_MESSAGES = MESSAGES_DOCUMENT_CHANGED | {'filter_applied', 'date_range_changed'}
     
     def __init__(self, mainwindow):

qt/print_/__init__.py

 from PyQt4.QtCore import QRect
 from PyQt4.QtGui import QPainter
 
-from core.gui.print_view import PrintView as PrintViewModel
 from .layout import LayoutPage, LayoutViewElement
 from .item_view import (ItemViewLayoutElement, ItemViewPrintStats, TablePrintDatasource,
     TreePrintDatasource)
     def __init__(self, printer, baseView):
         self.document = baseView.model.document
         self.app = self.document.app
-        self.model = PrintViewModel(baseView.model)
+        self.model = baseView.model.PRINT_VIEW_CLASS(baseView.model)
         self.title = self.model.title
         self.printer = printer
         self.pageSize = printer.pageRect().size()
             self.layoutPages.append(page)
     
     def fitTable(self, table):
-        self._fitItemView(TablePrintDatasource(table))
+        self._fitItemView(TablePrintDatasource(self.model, table))
     
     def fitTree(self, tree):
-        self._fitItemView(TreePrintDatasource(tree))
+        self._fitItemView(TreePrintDatasource(self.model, tree))
     
     def render(self):
         painter = QPainter(self.printer)

qt/print_/item_view.py

 from .layout import LayoutElement
 
 CELL_MARGIN = 2
+SPLIT_XOFFSET = 50
+SPLIT_XPADDING = 4
 
 def applyMargin(rect, margin):
     result = QRect(rect)
     # If we want to print tables and trees with a good code re-use, we've got to abstract away the
     # fact that trees have nodes and stuff and start treating it as rows and columns, with some
     # cells having more indentation than others. That's what this class is about.:
+    def __init__(self, printViewModel, baseFont):
+        self.printViewModel = printViewModel # From core.gui.transaction_print
+        self._rowFont = QFont(baseFont)
+        self._splitFont = QFont(baseFont)
+        self._splitFont.setPointSize(self._splitFont.pointSize()-2)
+        self._headerFont = QFont(self._rowFont)
+        self._headerFont.setBold(True)
+    
     def columnCount(self):
         """Returns the number of columns *to print*."""
         raise NotImplementedError()
         """Returns the number of rows *to print*."""
         raise NotImplementedError()
     
+    def splitCount(self, rowIndex):
+        return self.printViewModel.split_count_at_row(rowIndex)
+    
+    def splitValues(self, rowIndex, splitIndex):
+        return self.printViewModel.split_values(rowIndex, splitIndex)
+    
     def data(self, rowIndex, colIndex, role):
         """Returns model data for the index at the *printable* (rowIndex, colIndex) cell."""
         raise NotImplementedError()
         return 0
     
     def rowFont(self):
-        raise NotImplementedError()
+        return self._rowFont
+    
+    def splitFont(self):
+        return self._splitFont
     
     def headerFont(self):
-        raise NotImplementedError()
+        return self._headerFont
     
 
 class TablePrintDatasource(ItemPrintDatasource):
-    def __init__(self, table):
-        ItemPrintDatasource.__init__(self)
+    def __init__(self, printViewModel, table):
+        ItemPrintDatasource.__init__(self, printViewModel, baseFont=table.view.font())
         self.table = table
         self.columns = [c for c in table.model.columns.ordered_columns if c.visible]
-        self._rowFont = QFont(table.view.font())
-        self._headerFont = QFont(self._rowFont)
-        self._headerFont.setBold(True)
     
     def columnCount(self):
         return len(self.columns)
     def columnAtIndex(self, colIndex):
         return self.columns[colIndex]
     
-    def rowFont(self):
-        return self._rowFont
-    
-    def headerFont(self):
-        return self._headerFont
-    
 
 class TreePrintDatasource(ItemPrintDatasource):
-    def __init__(self, tree):
-        ItemPrintDatasource.__init__(self)
+    def __init__(self, printViewModel, tree):
+        ItemPrintDatasource.__init__(self, printViewModel, tree.view.font())
         self.tree = tree
         self.columns = [c for c in tree.model.columns.ordered_columns if c.visible]
-        self._rowFont = QFont(tree.view.font())
-        self._headerFont = QFont(self._rowFont)
-        self._headerFont.setBold(True)
         
         self._mapRows()
     
             result += indentationOffset
         return result
     
-    def rowFont(self):
-        return self._rowFont
-    
-    def headerFont(self):
-        return self._headerFont
-    
+
+ColumnStats = namedtuple('ColumnStats', 'index col avgWidth maxWidth minWidth')
+RowStats = namedtuple('RowStats', 'index height splitCount')
 
 class ItemViewLayoutElement(LayoutElement):
     def __init__(self, ds, stats, width, startRow):
         # We start with a minimal rect (`width` and enough height to fit the header and 1 row),
-        # and then we let the element be placed. Afterwards, we can know what will be the endRow
-        # value (so that the ViewPrinter can know if we need another page).
+        # and then we let the element be placed. Afterwards, we can know how many rows we can fit
+        # (so that the ViewPrinter can know if we need another page).
         height = stats.headerHeight + stats.rowHeight
         rect = QRect(QPoint(), QSize(width, height)) 
         LayoutElement.__init__(self, rect)
         self.stats = stats
         self.startRow = startRow
         self.endRow = startRow
+        self.rowStats = []
     
     def placed(self):
         height = self.rect.height()
         height -= self.stats.headerHeight
-        rowToFit = height // self.stats.rowHeight
-        self.endRow = min(self.startRow+rowToFit-1, self.ds.rowCount()-1)
-        rowCount = self.endRow - self.startRow + 1
+        self.rowStats = []
+        rowIndex = self.startRow
+        cumulHeight = 0
+        while rowIndex < self.ds.rowCount():
+            splitCount = self.ds.splitCount(rowIndex)
+            rowHeight = self.stats.rowHeight
+            if splitCount > 2:
+                rowHeight += self.stats.splitHeight * splitCount
+            cumulHeight += rowHeight
+            if cumulHeight > height:
+                break
+            self.rowStats.append(RowStats(rowIndex, rowHeight, splitCount))
+            rowIndex += 1
+        self.endRow = self.startRow + len(self.rowStats) - 1
+        totalHeight = sum(rs.height for rs in self.rowStats)
         # If it's the last page, we want to adjust the height of the rect so it's possible to fit
         # stuff under it.
-        self.rect.setHeight(self.stats.headerHeight+(self.stats.rowHeight*rowCount))
+        self.rect.setHeight(self.stats.headerHeight+totalHeight)
     
-    def renderCell(self, painter, rowIndex, colIndex, itemRect):
+    def renderCell(self, painter, rowStats, colStats, itemRect):
+        rowIndex = rowStats.index
+        colIndex = colStats.index if colStats is not None else 0
         extraFlags = nonone(self.ds.data(rowIndex, colIndex, EXTRA_ROLE), 0)
         pixmap = self.ds.data(rowIndex, colIndex, Qt.DecorationRole)
         if pixmap:
             bgbrush = self.ds.data(rowIndex, colIndex, Qt.BackgroundRole)
             if bgbrush is not None:
                 painter.fillRect(itemRect, bgbrush)
-            text = self.ds.data(rowIndex, colIndex, Qt.DisplayRole)
+            if rowStats.splitCount > 2 and colStats.col.name in {'from', 'to', 'transfer'}:
+                text = '--split--'
+            else:
+                text = self.ds.data(rowIndex, colIndex, Qt.DisplayRole)
             if text:
                 alignment = self.ds.data(rowIndex, colIndex, Qt.TextAlignmentRole)
                 if not alignment:
                 p2.setY(p2.y()-3)
                 painter.drawLine(p1, p2)
     
+    def renderSplit(self, painter, rowStats, splitsRect):
+        painter.setFont(self.ds.splitFont())
+        splitValues = [self.ds.splitValues(rowStats.index, i) for i in range(rowStats.splitCount)]
+        colWidths = [0, 0, 0]
+        for sv in splitValues:
+            colWidths[0] = max(colWidths[0], self.stats.splitFM.width(sv.account))
+            colWidths[1] = max(colWidths[1], self.stats.splitFM.width(sv.memo))
+            colWidths[2] = max(colWidths[2], self.stats.splitFM.width(sv.amount))
+        top = splitsRect.top()
+        for sv in splitValues:
+            rect = QRect(splitsRect.left(), top, colWidths[0], self.stats.splitHeight)
+            painter.drawText(rect, Qt.AlignLeft, sv.account)
+            rect.setLeft(rect.right() + SPLIT_XPADDING)
+            rect.setWidth(colWidths[1])
+            painter.drawText(rect, Qt.AlignLeft, sv.memo)
+            rect.setLeft(rect.right() + SPLIT_XPADDING)
+            rect.setWidth(colWidths[2])
+            painter.drawText(rect, Qt.AlignRight, sv.amount)
+            top += self.stats.splitHeight
+    
     def render(self, painter):
         # We used to re-use itemDelegate() for cell drawing, but it turned out to me more
         # complex than anything (with margins being too wide and all...)
         columnWidths = self.stats.columnWidths(self.rect.width())
         rowHeight = self.stats.rowHeight
         headerHeight = self.stats.headerHeight
-        startRow = self.startRow
         left = self.rect.left()
+        top = self.rect.top()
         painter.save()
         painter.setFont(self.ds.headerFont())
         for colStats, colWidth in zip(self.stats.columns, columnWidths):
             col = colStats.col
-            headerRect = QRect(left, self.rect.top(), colWidth, headerHeight)
+            headerRect = QRect(left, top, colWidth, headerHeight)
             headerRect = applyMargin(headerRect, CELL_MARGIN)
             painter.drawText(headerRect, col.alignment, col.display)
             left += colWidth
+        top += headerHeight
         painter.restore()
         painter.drawLine(self.rect.left(), self.rect.top()+headerHeight, self.rect.right(), self.rect.top()+headerHeight)
         painter.save()
         painter.setFont(self.ds.rowFont())
-        for rowIndex in range(startRow, self.endRow+1):
-            top = self.rect.top() + rowHeight + ((rowIndex - startRow) * rowHeight)
+        for rs in self.rowStats:
+            rowIndex = rs.index
             left = self.rect.left()
             extraRole = nonone(self.ds.data(rowIndex, 0, EXTRA_ROLE), 0)
             rowIsSpanning = extraRole & EXTRA_SPAN_ALL_COLUMNS
             if rowIsSpanning:
                 itemRect = QRect(left, top, self.rect.width(), rowHeight)
-                self.renderCell(painter, rowIndex, 0, itemRect)
+                self.renderCell(painter, rs, None, itemRect)
             else:
-                for colIndex, colWidth in enumerate(columnWidths):
-                    indentation = self.ds.indentation(rowIndex, colIndex)
+                for colStats, colWidth in zip(self.stats.columns, columnWidths):
+                    indentation = self.ds.indentation(rowIndex, colStats.index)
                     itemRect = QRect(left+indentation, top, colWidth, rowHeight)
                     itemRect = applyMargin(itemRect, CELL_MARGIN)
-                    self.renderCell(painter, rowIndex, colIndex, itemRect)
+                    self.renderCell(painter, rs, colStats, itemRect)
                     left += colWidth
+                if rs.splitCount > 2:
+                    splitsRect = QRect(self.rect.left()+SPLIT_XOFFSET, top+rowHeight,
+                        self.rect.width(), rs.height-rowHeight)
+                    self.renderSplit(painter, rs, splitsRect)
+            top += rs.height
         painter.restore()
     
 
 class ItemViewPrintStats:
     def __init__(self, ds):
-        ColumnStats = namedtuple('ColumnStats', 'index col avgWidth maxWidth minWidth')
         rowFM = QFontMetrics(ds.rowFont())
+        self.splitFM = QFontMetrics(ds.splitFont())
         headerFM = QFontMetrics(ds.headerFont())
         self.rowHeight = rowFM.height() + CELL_MARGIN * 2
+        self.splitHeight = self.splitFM.height() + CELL_MARGIN * 2
         self.headerHeight = headerFM.height() + CELL_MARGIN * 2
         spannedRowIndexes = set()
         for rowIndex in range(ds.rowCount()):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.