1#!/usr/bin/env python3
2
3#******************************************************************************
4# printdialogs.py, provides print preview and print settings dialogs
5#
6# TreeLine, an information storage program
7# Copyright (C) 2018, Douglas W. Bell
8#
9# This is free software; you can redistribute it and/or modify it under the
10# terms of the GNU General Public License, either Version 2 or any later
11# version.  This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY.  See the included LICENSE file for details.
13#******************************************************************************
14
15import re
16import collections
17from PyQt5.QtCore import (QMarginsF, QPoint, QRect, QSize, QSizeF, Qt,
18                          pyqtSignal)
19from PyQt5.QtGui import (QFontDatabase, QFontInfo, QFontMetrics, QIntValidator,
20                         QPageLayout, QPageSize)
21from PyQt5.QtWidgets import (QAbstractItemView, QAction, QButtonGroup,
22                             QCheckBox, QComboBox, QDialog, QDoubleSpinBox,
23                             QGridLayout, QGroupBox, QHBoxLayout, QLabel,
24                             QLineEdit, QListWidget, QMenu, QMessageBox,
25                             QPushButton, QRadioButton, QSpinBox, QTabWidget,
26                             QToolBar, QVBoxLayout, QWidget)
27from PyQt5.QtPrintSupport import (QPrintPreviewWidget, QPrinter, QPrinterInfo)
28import printdata
29import configdialog
30import treeformats
31import undo
32import globalref
33
34
35class PrintPreviewDialog(QDialog):
36    """Dialog for print previews.
37
38    Similar to QPrintPreviewDialog but calls a custom page setup dialog.
39    """
40    def __init__(self, printData, parent=None):
41        """Create the print preview dialog.
42
43        Arguments:
44            printData -- the PrintData object
45            parent -- the parent window
46        """
47        super().__init__(parent)
48        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
49                            Qt.WindowCloseButtonHint)
50        self.setWindowTitle(_('Print Preview'))
51        self.printData = printData
52        topLayout = QVBoxLayout(self)
53        self.setLayout(topLayout)
54
55        toolBar = QToolBar(self)
56        topLayout.addWidget(toolBar)
57
58        self.previewWidget = QPrintPreviewWidget(printData.printer, self)
59        topLayout.addWidget(self.previewWidget)
60        self.previewWidget.previewChanged.connect(self.updateControls)
61
62        self.zoomWidthAct = QAction(_('Fit Width'), self, checkable=True)
63        icon = globalref.toolIcons.getIcon('printpreviewzoomwidth')
64        if icon:
65            self.zoomWidthAct.setIcon(icon)
66        self.zoomWidthAct.triggered.connect(self.zoomWidth)
67        toolBar.addAction(self.zoomWidthAct)
68
69        self.zoomAllAct = QAction(_('Fit Page'), self, checkable=True)
70        icon = globalref.toolIcons.getIcon('printpreviewzoomall')
71        if icon:
72            self.zoomAllAct.setIcon(icon)
73        self.zoomAllAct.triggered.connect(self.zoomAll)
74        toolBar.addAction(self.zoomAllAct)
75        toolBar.addSeparator()
76
77        self.zoomCombo = QComboBox(self)
78        self.zoomCombo.setEditable(True)
79        self.zoomCombo.setInsertPolicy(QComboBox.NoInsert)
80        self.zoomCombo.addItems(['  12%', '  25%', '  50%', '  75%', ' 100%',
81                                 ' 125%', ' 150%', ' 200%', ' 400%', ' 800%'])
82        self.zoomCombo.currentIndexChanged[str].connect(self.zoomToValue)
83        self.zoomCombo.lineEdit().returnPressed.connect(self.zoomToValue)
84        toolBar.addWidget(self.zoomCombo)
85
86        zoomInAct = QAction(_('Zoom In'), self)
87        icon = globalref.toolIcons.getIcon('printpreviewzoomin')
88        if icon:
89            zoomInAct.setIcon(icon)
90        zoomInAct.triggered.connect(self.zoomIn)
91        toolBar.addAction(zoomInAct)
92
93        zoomOutAct = QAction(_('Zoom Out'), self)
94        icon = globalref.toolIcons.getIcon('printpreviewzoomout')
95        if icon:
96            zoomOutAct.setIcon(icon)
97        zoomOutAct.triggered.connect(self.zoomOut)
98        toolBar.addAction(zoomOutAct)
99        toolBar.addSeparator()
100
101        self.previousAct = QAction(_('Previous Page'), self)
102        icon = globalref.toolIcons.getIcon('printpreviewprevious')
103        if icon:
104            self.previousAct.setIcon(icon)
105        self.previousAct.triggered.connect(self.previousPage)
106        toolBar.addAction(self.previousAct)
107
108        self.pageNumEdit = QLineEdit(self)
109        self.pageNumEdit.setAlignment(Qt.AlignRight |
110                                      Qt.AlignVCenter)
111        width = QFontMetrics(self.pageNumEdit.font()).width('0000')
112        self.pageNumEdit.setMaximumWidth(width)
113        self.pageNumEdit.returnPressed.connect(self.setPageNum)
114        toolBar.addWidget(self.pageNumEdit)
115
116        self.maxPageLabel = QLabel(' / 000 ', self)
117        toolBar.addWidget(self.maxPageLabel)
118
119        self.nextAct = QAction(_('Next Page'), self)
120        icon = globalref.toolIcons.getIcon('printpreviewnext')
121        if icon:
122            self.nextAct.setIcon(icon)
123        self.nextAct.triggered.connect(self.nextPage)
124        toolBar.addAction(self.nextAct)
125        toolBar.addSeparator()
126
127        self.onePageAct = QAction(_('Single Page'), self, checkable=True)
128        icon = globalref.toolIcons.getIcon('printpreviewsingle')
129        if icon:
130            self.onePageAct.setIcon(icon)
131        self.onePageAct.triggered.connect(self.previewWidget.
132                                          setSinglePageViewMode)
133        toolBar.addAction(self.onePageAct)
134
135        self.twoPageAct = QAction(_('Facing Pages'), self,
136                                        checkable=True)
137        icon = globalref.toolIcons.getIcon('printpreviewdouble')
138        if icon:
139            self.twoPageAct.setIcon(icon)
140        self.twoPageAct.triggered.connect(self.previewWidget.
141                                          setFacingPagesViewMode)
142        toolBar.addAction(self.twoPageAct)
143        toolBar.addSeparator()
144
145        pageSetupAct = QAction(_('Print Setup'), self)
146        icon = globalref.toolIcons.getIcon('fileprintsetup')
147        if icon:
148            pageSetupAct.setIcon(icon)
149        pageSetupAct.triggered.connect(self.printSetup)
150        toolBar.addAction(pageSetupAct)
151
152        filePrintAct = QAction(_('Print'), self)
153        icon = globalref.toolIcons.getIcon('fileprint')
154        if icon:
155            filePrintAct.setIcon(icon)
156        filePrintAct.triggered.connect(self.filePrint)
157        toolBar.addAction(filePrintAct)
158
159    def updateControls(self):
160        """Update control availability and status based on a change signal.
161        """
162        self.zoomWidthAct.setChecked(self.previewWidget.zoomMode() ==
163                                     QPrintPreviewWidget.FitToWidth)
164        self.zoomAllAct.setChecked(self.previewWidget.zoomMode() ==
165                                   QPrintPreviewWidget.FitInView)
166        zoom = self.previewWidget.zoomFactor() * 100
167        self.zoomCombo.setEditText('{0:4.0f}%'.format(zoom))
168        self.previousAct.setEnabled(self.previewWidget.currentPage() > 1)
169        self.nextAct.setEnabled(self.previewWidget.currentPage() <
170                                self.previewWidget.pageCount())
171        self.pageNumEdit.setText(str(self.previewWidget.currentPage()))
172        self.maxPageLabel.setText(' / {0} '.format(self.previewWidget.
173                                                   pageCount()))
174        self.onePageAct.setChecked(self.previewWidget.viewMode() ==
175                                   QPrintPreviewWidget.SinglePageView)
176        self.twoPageAct.setChecked(self.previewWidget.viewMode() ==
177                                   QPrintPreviewWidget.FacingPagesView)
178
179    def zoomWidth(self, checked=True):
180        """Set the fit to width zoom mode if checked.
181
182        Arguments:
183            checked -- set this mode if True
184        """
185        if checked:
186            self.previewWidget.setZoomMode(QPrintPreviewWidget.
187                                           FitToWidth)
188        else:
189            self.previewWidget.setZoomMode(QPrintPreviewWidget.
190                                           CustomZoom)
191        self.updateControls()
192
193    def zoomAll(self, checked=True):
194        """Set the fit in view zoom mode if checked.
195
196        Arguments:
197            checked -- set this mode if True
198        """
199        if checked:
200            self.previewWidget.setZoomMode(QPrintPreviewWidget.FitInView)
201        else:
202            self.previewWidget.setZoomMode(QPrintPreviewWidget.
203                                           CustomZoom)
204        self.updateControls()
205
206    def zoomToValue(self, factorStr=''):
207        """Zoom to the given combo box string value.
208
209        Arguments:
210            factorStr -- the zoom factor as a string, often with a % suffix
211        """
212        if not factorStr:
213            factorStr = self.zoomCombo.lineEdit().text()
214        try:
215            factor = float(factorStr.strip(' %')) / 100
216            self.previewWidget.setZoomFactor(factor)
217        except ValueError:
218            pass
219        self.updateControls()
220
221    def zoomIn(self):
222        """Increase the zoom level by an increment.
223        """
224        self.previewWidget.zoomIn()
225        self.updateControls()
226
227    def zoomOut(self):
228        """Decrease the zoom level by an increment.
229        """
230        self.previewWidget.zoomOut()
231        self.updateControls()
232
233    def previousPage(self):
234        """Go to the previous page of the preview.
235        """
236        self.previewWidget.setCurrentPage(self.previewWidget.currentPage() - 1)
237        self.updateControls()
238
239    def nextPage(self):
240        """Go to the next page of the preview.
241        """
242        self.previewWidget.setCurrentPage(self.previewWidget.currentPage() + 1)
243        self.updateControls()
244
245    def setPageNum(self):
246        """Go to a page number from the line editor based on a signal.
247        """
248        try:
249            self.previewWidget.setCurrentPage(int(self.pageNumEdit.text()))
250        except ValueError:
251            pass
252        self.updateControls()
253
254    def printSetup(self):
255        """Show a dialog to set margins, page size and other printing options.
256        """
257        setupDialog = PrintSetupDialog(self.printData, False, self)
258        if setupDialog.exec_() == QDialog.Accepted:
259            self.printData.setupData()
260            self.previewWidget.updatePreview()
261
262    def filePrint(self):
263        """Show dialog and print tree output based on current options.
264        """
265        self.close()
266        if self.printData.printer.outputFormat() == QPrinter.NativeFormat:
267            self.printData.filePrint()
268        else:
269            self.printData.filePrintPdf()
270
271    def sizeHint(self):
272        """Return a larger default height.
273        """
274        size = super().sizeHint()
275        size.setHeight(600)
276        return size
277
278    def restoreDialogGeom(self):
279        """Restore dialog window geometry from history options.
280        """
281        rect = QRect(globalref.histOptions['PrintPrevXPos'],
282                            globalref.histOptions['PrintPrevYPos'],
283                            globalref.histOptions['PrintPrevXSize'],
284                            globalref.histOptions['PrintPrevYSize'])
285        if rect.height() and rect.width():
286            self.setGeometry(rect)
287
288    def saveDialogGeom(self):
289        """Savedialog window geometry to history options.
290        """
291        globalref.histOptions.changeValue('PrintPrevXSize', self.width())
292        globalref.histOptions.changeValue('PrintPrevYSize', self.height())
293        globalref.histOptions.changeValue('PrintPrevXPos', self.geometry().x())
294        globalref.histOptions.changeValue('PrintPrevYPos', self.geometry().y())
295
296    def closeEvent(self, event):
297        """Save dialog geometry at close.
298
299        Arguments:
300            event -- the close event
301        """
302        if globalref.genOptions['SaveWindowGeom']:
303            self.saveDialogGeom()
304
305
306class PrintSetupDialog(QDialog):
307    """Base dialog for setting the print configuration.
308
309    Pushes most options to the PrintData class.
310    """
311    def __init__(self, printData, showExtraButtons=True, parent=None):
312        """Create the printing setup dialog.
313
314        Arguments:
315            printData -- a reference to the PrintData class
316            showExtraButtons -- add print preview and print shortcut buttons
317            parent -- the parent window
318        """
319        super().__init__(parent)
320        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
321                            Qt.WindowCloseButtonHint)
322        self.setWindowTitle(_('Printing Setup'))
323        self.printData = printData
324
325        topLayout = QVBoxLayout(self)
326        self.setLayout(topLayout)
327
328        tabs = QTabWidget()
329        topLayout.addWidget(tabs)
330        generalPage = GeneralPage(self.printData)
331        tabs.addTab(generalPage, _('&General Options'))
332        pageSetupPage = PageSetupPage(self.printData,
333                                      generalPage.currentPrinterName)
334        tabs.addTab(pageSetupPage, _('Page &Setup'))
335        fontPage = FontPage(self.printData)
336        tabs.addTab(fontPage, _('&Font Selection'))
337        headerPage = HeaderPage(self.printData)
338        tabs.addTab(headerPage, _('&Header/Footer'))
339        generalPage.printerChanged.connect(pageSetupPage.changePrinter)
340        self.tabPages = [generalPage, pageSetupPage, fontPage, headerPage]
341
342        ctrlLayout = QHBoxLayout()
343        topLayout.addLayout(ctrlLayout)
344        ctrlLayout.addStretch()
345        if showExtraButtons:
346            previewButton =  QPushButton(_('Print Pre&view...'))
347            ctrlLayout.addWidget(previewButton)
348            previewButton.clicked.connect(self.preview)
349            printButton = QPushButton(_('&Print...'))
350            ctrlLayout.addWidget(printButton)
351            printButton.clicked.connect(self.quickPrint)
352        okButton = QPushButton(_('&OK'))
353        ctrlLayout.addWidget(okButton)
354        okButton.clicked.connect(self.accept)
355        cancelButton = QPushButton(_('&Cancel'))
356        ctrlLayout.addWidget(cancelButton)
357        cancelButton.clicked.connect(self.reject)
358
359    def quickPrint(self):
360        """Accept this dialog and go to print dialog.
361        """
362        self.accept()
363        if self.printData.printer.outputFormat() == QPrinter.NativeFormat:
364            self.printData.filePrint()
365        else:
366            self.printData.filePrintPdf()
367
368    def preview(self):
369        """Accept this dialog and go to print preview dialog.
370        """
371        self.accept()
372        self.printData.printPreview()
373
374    def accept(self):
375        """Store results before closing dialog.
376        """
377        if not self.tabPages[1].checkValid():
378            QMessageBox.warning(self, 'TreeLine',
379                                _('Error:  Page size or margins are invalid'))
380            return
381        changed = False
382        control = self.printData.localControl
383        undoObj = undo.StateSettingUndo(control.structure.undoList,
384                                        self.printData.fileData,
385                                        self.printData.readData)
386        for page in self.tabPages:
387            if page.saveChanges():
388                changed = True
389        if changed:
390            self.printData.adjustSpacing()
391            control.setModified()
392        else:
393            control.structure.undoList.removeLastUndo(undoObj)
394        super().accept()
395
396
397_pdfPrinterName = _('TreeLine PDF Printer')
398
399
400class GeneralPage(QWidget):
401    """Dialog page for misc. print options.
402    """
403    printerChanged = pyqtSignal(str)
404    def __init__(self, printData, parent=None):
405        """Create the general settings page.
406
407        Arguments:
408            printData -- a reference to the PrintData class
409            parent -- the parent dialog
410        """
411        super().__init__(parent)
412        self.printData = printData
413        self.printerList = QPrinterInfo.availablePrinterNames()
414        self.printerList.insert(0, _pdfPrinterName)
415        self.currentPrinterName = self.printData.printer.printerName()
416        if not self.currentPrinterName:
417            self.currentPrinterName = _pdfPrinterName
418
419        topLayout = QHBoxLayout(self)
420        self.setLayout(topLayout)
421        leftLayout = QVBoxLayout()
422        topLayout.addLayout(leftLayout)
423
424        whatGroupBox = QGroupBox(_('What to print'))
425        leftLayout.addWidget(whatGroupBox)
426        whatLayout = QVBoxLayout(whatGroupBox)
427        self.whatButtons = QButtonGroup(self)
428        treeButton = QRadioButton(_('&Entire tree'))
429        self.whatButtons.addButton(treeButton, printdata.PrintScope.entireTree)
430        whatLayout.addWidget(treeButton)
431        branchButton = QRadioButton(_('Selected &branches'))
432        self.whatButtons.addButton(branchButton,
433                                   printdata.PrintScope.selectBranch)
434        whatLayout.addWidget(branchButton)
435        nodeButton = QRadioButton(_('Selected &nodes'))
436        self.whatButtons.addButton(nodeButton, printdata.PrintScope.selectNode)
437        whatLayout.addWidget(nodeButton)
438        self.whatButtons.button(self.printData.printWhat).setChecked(True)
439        self.whatButtons.buttonClicked.connect(self.updateCmdAvail)
440
441        includeBox = QGroupBox(_('Included Nodes'))
442        leftLayout.addWidget(includeBox)
443        includeLayout = QVBoxLayout(includeBox)
444        self.rootButton = QCheckBox(_('&Include root node'))
445        includeLayout.addWidget(self.rootButton)
446        self.rootButton.setChecked(self.printData.includeRoot)
447        self.openOnlyButton = QCheckBox(_('Onl&y open node children'))
448        includeLayout.addWidget(self.openOnlyButton)
449        self.openOnlyButton.setChecked(self.printData.openOnly)
450        leftLayout.addStretch()
451
452        rightLayout = QVBoxLayout()
453        topLayout.addLayout(rightLayout)
454
455        printerBox = QGroupBox(_('Select &Printer'))
456        rightLayout.addWidget(printerBox)
457        printerLayout = QVBoxLayout(printerBox)
458        printerCombo = QComboBox()
459        printerLayout.addWidget(printerCombo)
460        printerCombo.addItems(self.printerList)
461        printerCombo.setCurrentIndex(self.printerList.index(self.
462                                                           currentPrinterName))
463        printerCombo.currentIndexChanged.connect(self.changePrinter)
464
465        featureBox = QGroupBox(_('Features'))
466        rightLayout.addWidget(featureBox)
467        featureLayout = QVBoxLayout(featureBox)
468        self.linesButton = QCheckBox(_('&Draw lines to children'))
469        featureLayout.addWidget(self.linesButton)
470        self.linesButton.setChecked(self.printData.drawLines)
471        self.widowButton = QCheckBox(_('&Keep first child with parent'))
472        featureLayout.addWidget(self.widowButton)
473        self.widowButton.setChecked(self.printData.widowControl)
474
475        indentBox = QGroupBox(_('Indent'))
476        rightLayout.addWidget(indentBox)
477        indentLayout = QHBoxLayout(indentBox)
478        indentLabel = QLabel(_('Indent Offse&t\n(line height units)'))
479        indentLayout.addWidget(indentLabel)
480        self.indentSpin =  QDoubleSpinBox()
481        indentLayout.addWidget(self.indentSpin)
482        indentLabel.setBuddy(self.indentSpin)
483        self.indentSpin.setMinimum(0.5)
484        self.indentSpin.setSingleStep(0.5)
485        self.indentSpin.setDecimals(1)
486        self.indentSpin.setValue(self.printData.indentFactor)
487        rightLayout.addStretch()
488
489        self.updateCmdAvail()
490
491    def updateCmdAvail(self):
492        """Update options available based on print what settings.
493        """
494        if self.whatButtons.checkedId() == printdata.PrintScope.selectNode:
495            self.rootButton.setChecked(True)
496            self.rootButton.setEnabled(False)
497            self.openOnlyButton.setChecked(False)
498            self.openOnlyButton.setEnabled(False)
499        else:
500            self.rootButton.setEnabled(True)
501            self.openOnlyButton.setEnabled(True)
502
503    def changePrinter(self, printerNum):
504        """Change the current printer based on a combo box signal.
505
506        Arguments:
507            printerNum -- the printer number from the combo box
508        """
509        self.currentPrinterName = self.printerList[printerNum]
510        self.printerChanged.emit(self.currentPrinterName)
511
512    def saveChanges(self):
513        """Update print data with current dialog settings.
514
515        Return True if saved settings have changed, False otherwise.
516        """
517        self.printData.printWhat = self.whatButtons.checkedId()
518        self.printData.includeRoot = self.rootButton.isChecked()
519        self.printData.openOnly = self.openOnlyButton.isChecked()
520        if self.currentPrinterName != _pdfPrinterName:
521            self.printData.printer.setPrinterName(self.currentPrinterName)
522        else:
523            self.printData.printer.setPrinterName('')
524        changed = False
525        if self.printData.drawLines != self.linesButton.isChecked():
526            self.printData.drawLines = self.linesButton.isChecked()
527            changed = True
528        if self.printData.widowControl != self.widowButton.isChecked():
529            self.printData.widowControl = self.widowButton.isChecked()
530            changed = True
531        if self.printData.indentFactor != self.indentSpin.value():
532            self.printData.indentFactor = self.indentSpin.value()
533            changed = True
534        return changed
535
536
537_paperSizes = collections.OrderedDict([('Letter', _('Letter (8.5 x 11 in.)')),
538                                       ('Legal', _('Legal (8.5 x 14 in.)'),),
539                                       ('Tabloid', _('Tabloid (11 x 17 in.)')),
540                                       ('A3', _('A3 (279 x 420 mm)')),
541                                       ('A4', _('A4 (210 x 297 mm)')),
542                                       ('A5', _('A5 (148 x 210 mm)')),
543                                       ('Custom', _('Custom Size'))])
544_units = collections.OrderedDict([('in', _('Inches (in)')),
545                                  ('mm', _('Millimeters (mm)')),
546                                  ('cm', _('Centimeters (cm)'))])
547_unitValues = {'in': 1.0, 'cm': 2.54, 'mm': 25.4}
548_unitDecimals = {'in': 2, 'cm': 1, 'mm': 0}
549
550class PageSetupPage(QWidget):
551    """Dialog page for page setup options.
552    """
553    def __init__(self, printData, currentPrinterName, parent=None):
554        """Create the page setup settings page.
555
556        Arguments:
557            printData -- a reference to the PrintData class
558            currentPrinterName -- the selected printer for validation
559            parent -- the parent dialog
560        """
561        super().__init__(parent)
562        self.printData = printData
563        self.currentPrinterName = currentPrinterName
564
565        topLayout = QHBoxLayout(self)
566        self.setLayout(topLayout)
567        leftLayout = QVBoxLayout()
568        topLayout.addLayout(leftLayout)
569
570        unitsBox = QGroupBox(_('&Units'))
571        leftLayout.addWidget(unitsBox)
572        unitsLayout = QVBoxLayout(unitsBox)
573        unitsCombo = QComboBox()
574        unitsLayout.addWidget(unitsCombo)
575        unitsCombo.addItems(list(_units.values()))
576        self.currentUnit = globalref.miscOptions['PrintUnits']
577        if self.currentUnit not in _units:
578            self.currentUnit = 'in'
579        unitsCombo.setCurrentIndex(list(_units.keys()).index(self.currentUnit))
580        unitsCombo.currentIndexChanged.connect(self.changeUnits)
581
582        paperSizeBox = QGroupBox(_('Paper &Size'))
583        leftLayout.addWidget(paperSizeBox)
584        paperSizeLayout = QGridLayout(paperSizeBox)
585        spacing = paperSizeLayout.spacing()
586        paperSizeLayout.setVerticalSpacing(0)
587        paperSizeLayout.setRowMinimumHeight(1, spacing)
588        paperSizeCombo = QComboBox()
589        paperSizeLayout.addWidget(paperSizeCombo, 0, 0, 1, 2)
590        paperSizeCombo.addItems(list(_paperSizes.values()))
591        self.currentPaperSize = self.printData.paperSizeName()
592        if self.currentPaperSize not in _paperSizes:
593            self.currentPaperSize = 'Custom'
594        paperSizeCombo.setCurrentIndex(list(_paperSizes.keys()).
595                                       index(self.currentPaperSize))
596        paperSizeCombo.currentIndexChanged.connect(self.changePaper)
597        widthLabel = QLabel(_('&Width:'))
598        paperSizeLayout.addWidget(widthLabel, 2, 0)
599        self.paperWidthSpin = UnitSpinBox(self.currentUnit)
600        paperSizeLayout.addWidget(self.paperWidthSpin, 3, 0)
601        widthLabel.setBuddy(self.paperWidthSpin)
602        paperWidth, paperHeight = self.printData.roundedPaperSize()
603        self.paperWidthSpin.setInchValue(paperWidth)
604        heightlabel = QLabel(_('Height:'))
605        paperSizeLayout.addWidget(heightlabel, 2, 1)
606        self.paperHeightSpin = UnitSpinBox(self.currentUnit)
607        paperSizeLayout.addWidget(self.paperHeightSpin, 3, 1)
608        heightlabel.setBuddy(self.paperHeightSpin)
609        self.paperHeightSpin.setInchValue(paperHeight)
610        if self.currentPaperSize != 'Custom':
611            self.paperWidthSpin.setEnabled(False)
612            self.paperHeightSpin.setEnabled(False)
613
614        orientbox = QGroupBox(_('Orientation'))
615        leftLayout.addWidget(orientbox)
616        orientLayout = QVBoxLayout(orientbox)
617        portraitButton = QRadioButton(_('Portra&it'))
618        orientLayout.addWidget(portraitButton)
619        landscapeButton = QRadioButton(_('Lan&dscape'))
620        orientLayout.addWidget(landscapeButton)
621        self.portraitOrient = (self.printData.pageLayout.orientation() ==
622                               QPageLayout.Portrait)
623        if self.portraitOrient:
624            portraitButton.setChecked(True)
625        else:
626            landscapeButton.setChecked(True)
627        portraitButton.toggled.connect(self.changeOrient)
628
629        rightLayout = QVBoxLayout()
630        topLayout.addLayout(rightLayout)
631
632        marginsBox = QGroupBox(_('Margins'))
633        rightLayout.addWidget(marginsBox)
634        marginsLayout = QGridLayout(marginsBox)
635        spacing = marginsLayout.spacing()
636        marginsLayout.setVerticalSpacing(0)
637        marginsLayout.setRowMinimumHeight(2, spacing)
638        marginsLayout.setRowMinimumHeight(5, spacing)
639        leftLabel = QLabel(_('&Left:'))
640        marginsLayout.addWidget(leftLabel, 3, 0)
641        leftMarginSpin = UnitSpinBox(self.currentUnit)
642        marginsLayout.addWidget(leftMarginSpin, 4, 0)
643        leftLabel.setBuddy(leftMarginSpin)
644        topLabel = QLabel(_('&Top:'))
645        marginsLayout.addWidget(topLabel, 0, 1)
646        topMarginSpin = UnitSpinBox(self.currentUnit)
647        marginsLayout.addWidget(topMarginSpin, 1, 1)
648        topLabel.setBuddy(topMarginSpin)
649        rightLabel = QLabel(_('&Right:'))
650        marginsLayout.addWidget(rightLabel, 3, 2)
651        rightMarginSpin = UnitSpinBox(self.currentUnit)
652        marginsLayout.addWidget(rightMarginSpin, 4, 2)
653        rightLabel.setBuddy(rightMarginSpin)
654        bottomLabel = QLabel(_('&Bottom:'))
655        marginsLayout.addWidget(bottomLabel, 6, 1)
656        bottomMarginSpin = UnitSpinBox(self.currentUnit)
657        marginsLayout.addWidget(bottomMarginSpin, 7, 1)
658        bottomLabel.setBuddy(bottomMarginSpin)
659        self.marginControls = (leftMarginSpin, topMarginSpin, rightMarginSpin,
660                               bottomMarginSpin)
661        for control, value in zip(self.marginControls,
662                                  self.printData.roundedMargins()):
663            control.setInchValue(value)
664        headerLabel = QLabel(_('He&ader:'))
665        marginsLayout.addWidget(headerLabel, 0, 2)
666        self.headerMarginSpin = UnitSpinBox(self.currentUnit)
667        marginsLayout.addWidget(self.headerMarginSpin, 1, 2)
668        headerLabel.setBuddy(self.headerMarginSpin)
669        self.headerMarginSpin.setInchValue(self.printData.headerMargin)
670        footerLabel = QLabel(_('Foot&er:'))
671        marginsLayout.addWidget(footerLabel, 6, 2)
672        self.footerMarginSpin = UnitSpinBox(self.currentUnit)
673        marginsLayout.addWidget(self.footerMarginSpin, 7, 2)
674        footerLabel.setBuddy(self.footerMarginSpin)
675        self.footerMarginSpin.setInchValue(self.printData.footerMargin)
676
677        columnsBox = QGroupBox(_('Columns'))
678        rightLayout.addWidget(columnsBox)
679        columnLayout = QGridLayout(columnsBox)
680        numLabel = QLabel(_('&Number of columns'))
681        columnLayout.addWidget(numLabel, 0, 0)
682        self.columnSpin = QSpinBox()
683        columnLayout.addWidget(self.columnSpin, 0, 1)
684        numLabel.setBuddy(self.columnSpin)
685        self.columnSpin.setMinimum(1)
686        self.columnSpin.setMaximum(9)
687        self.columnSpin.setValue(self.printData.numColumns)
688        spaceLabel = QLabel(_('Space between colu&mns'))
689        columnLayout.addWidget(spaceLabel, 1, 0)
690        self.columnSpaceSpin = UnitSpinBox(self.currentUnit)
691        columnLayout.addWidget(self.columnSpaceSpin, 1, 1)
692        spaceLabel.setBuddy(self.columnSpaceSpin)
693        self.columnSpaceSpin.setInchValue(self.printData.columnSpacing)
694
695    def changePrinter(self, newPrinterName):
696        """Change the currently selected printer.
697
698        Arguments:
699            newPrinterName -- new printer selection
700        """
701        self.currentPrinterName = newPrinterName
702
703    def changeUnits(self, unitNum):
704        """Change the current unit and update conversions based on a signal.
705
706        Arguments:
707            unitNum -- the unit index number from the combobox
708        """
709        oldUnit = self.currentUnit
710        self.currentUnit = list(_units.keys())[unitNum]
711        self.paperWidthSpin.changeUnit(self.currentUnit)
712        self.paperHeightSpin.changeUnit(self.currentUnit)
713        for control in self.marginControls:
714            control.changeUnit(self.currentUnit)
715        self.headerMarginSpin.changeUnit(self.currentUnit)
716        self.footerMarginSpin.changeUnit(self.currentUnit)
717        self.columnSpaceSpin.changeUnit(self.currentUnit)
718
719    def changePaper(self, paperNum):
720        """Change the current paper size based on a signal.
721
722        Arguments:
723            paperNum -- the paper size index number from the combobox
724        """
725        self.currentPaperSize = list(_paperSizes.keys())[paperNum]
726        if self.currentPaperSize != 'Custom':
727            tempPrinter = QPrinter()
728            pageLayout = tempPrinter.pageLayout()
729            pageLayout.setPageSize(QPageSize(getattr(QPageSize,
730                                                     self.currentPaperSize)))
731            if not self.portraitOrient:
732                pageLayout.setOrientation(QPageLayout.Landscape)
733            paperSize = pageLayout.fullRect(QPageLayout.Inch)
734            self.paperWidthSpin.setInchValue(round(paperSize.width(), 2))
735            self.paperHeightSpin.setInchValue(round(paperSize.height(), 2))
736        self.paperWidthSpin.setEnabled(self.currentPaperSize == 'Custom')
737        self.paperHeightSpin.setEnabled(self.currentPaperSize == 'Custom')
738
739    def changeOrient(self, isPortrait):
740        """Change the orientation based on a signal.
741
742        Arguments:
743            isPortrait -- true if portrait orientation is selected
744        """
745        self.portraitOrient = isPortrait
746        width = self.paperWidthSpin.inchValue
747        height = self.paperHeightSpin.inchValue
748        if (self.portraitOrient and width > height) or (not self.portraitOrient
749                                                        and width < height):
750            self.paperWidthSpin.setInchValue(height)
751            self.paperHeightSpin.setInchValue(width)
752
753    def checkValid(self):
754        """Return True if the current page size and margins appear to be valid.
755        """
756        pageWidth = self.paperWidthSpin.inchValue
757        pageHeight = self.paperHeightSpin.inchValue
758        if pageWidth <= 0 or pageHeight <= 0:
759            return False
760        margins = tuple(control.inchValue for control in self.marginControls)
761        if (margins[0] + margins[2] >= pageWidth or
762            margins[1] + margins[3] >= pageHeight):
763            return False
764        return True
765
766    def saveChanges(self):
767        """Update print data with current dialog settings.
768
769        Return True if saved settings have changed, False otherwise.
770        """
771        if self.currentUnit != globalref.miscOptions['PrintUnits']:
772            globalref.miscOptions.changeValue('PrintUnits', self.currentUnit)
773            globalref.miscOptions.writeFile()
774        changed = False
775        pageLayout = self.printData.pageLayout
776        if self.currentPaperSize != 'Custom':
777            size = getattr(QPageSize, self.currentPaperSize)
778            if size != pageLayout.pageSize().id():
779                pageLayout.setPageSize(QPageSize(size))
780                changed = True
781        else:
782            size = (self.paperWidthSpin.inchValue,
783                    self.paperHeightSpin.inchValue)
784            if size != self.printData.roundedPaperSize():
785                pageLayout.setPageSize(QPageSize(QSizeF(*size),
786                                                 QPageSize.Inch))
787                changed = True
788        orient = (QPageLayout.Portrait if self.portraitOrient else
789                  QPageLayout.Landscape)
790        if orient != pageLayout.orientation():
791            pageLayout.setOrientation(orient)
792            changed = True
793        margins = tuple(control.inchValue for control in self.marginControls)
794        if margins != self.printData.roundedMargins():
795            pageLayout.setMargins(QMarginsF(*margins))
796            changed = True
797        if self.printData.headerMargin != self.headerMarginSpin.inchValue:
798            self.printData.headerMargin = self.headerMarginSpin.inchValue
799            changed = True
800        if self.printData.footerMargin != self.footerMarginSpin.inchValue:
801            self.printData.footerMargin = self.footerMarginSpin.inchValue
802            changed = True
803        if self.printData.numColumns != self.columnSpin.value():
804            self.printData.numColumns = self.columnSpin.value()
805            changed = True
806        if self.printData.columnSpacing != self.columnSpaceSpin.inchValue:
807            self.printData.columnSpacing = self.columnSpaceSpin.inchValue
808            changed = True
809        return changed
810
811
812class UnitSpinBox(QDoubleSpinBox):
813    """Spin box with unit suffix that can convert the units of its contents.
814
815    Stores the value at full precision to avoid round-trip rounding errors.
816    """
817    def __init__(self, unit, parent=None):
818        """Create the unit spin box.
819
820        Arguments:
821            unit -- the original unit (abbreviated string)
822            parent -- the parent dialog if given
823        """
824        super().__init__(parent)
825        self.unit = unit
826        self.inchValue = 0.0
827        self.setupUnit()
828        self.valueChanged.connect(self.changeValue)
829
830    def setupUnit(self):
831        """Set the suffix, decimal places and maximum based on the unit.
832        """
833        self.blockSignals(True)
834        self.setSuffix(' {0}'.format(self.unit))
835        decPlaces = _unitDecimals[self.unit]
836        self.setDecimals(decPlaces)
837        # set maximum to 5 digits total
838        self.setMaximum((10**5 - 1) / 10**decPlaces)
839        self.blockSignals(False)
840
841    def changeUnit(self, unit):
842        """Change current unit.
843
844        Arguments:
845            unit -- the new unit (abbreviated string)
846        """
847        self.unit = unit
848        self.setupUnit()
849        self.setInchValue(self.inchValue)
850
851    def setInchValue(self, inchValue):
852        """Set box to given value, converted to current unit.
853
854        Arguments:
855            inchValue -- the value to set in inches
856        """
857        self.inchValue = inchValue
858        value = self.inchValue * _unitValues[self.unit]
859        self.blockSignals(True)
860        self.setValue(value)
861        self.blockSignals(False)
862        if value < 4:
863            self.setSingleStep(0.1)
864        elif value > 50:
865            self.setSingleStep(10)
866        else:
867            self.setSingleStep(1)
868
869    def changeValue(self):
870        """Change the stored inch value based on a signal.
871        """
872        self.inchValue = round(self.value() / _unitValues[self.unit], 2)
873
874
875class SmallListWidget(QListWidget):
876    """ListWidget with a smaller size hint.
877    """
878    def __init__(self, parent=None):
879        """Initialize the widget.
880
881        Arguments:
882            parent -- the parent, if given
883        """
884        super().__init__(parent)
885
886    def sizeHint(self):
887        """Return smaller width.
888        """
889        itemHeight = self.visualItemRect(self.item(0)).height()
890        return QSize(100, itemHeight * 3)
891
892
893class FontPage(QWidget):
894    """Font selection print option dialog page.
895    """
896    def __init__(self, printData, defaultLabel='', parent=None):
897        """Create the font settings page.
898
899        Arguments:
900            printData -- a reference to the PrintData class
901            defaultLabel -- default font label if given, o/w TreeLine output
902            parent -- the parent dialog
903        """
904        super().__init__(parent)
905        self.printData = printData
906        self.currentFont = self.printData.mainFont
907
908        topLayout = QVBoxLayout(self)
909        self.setLayout(topLayout)
910        defaultBox = QGroupBox(_('Default Font'))
911        topLayout.addWidget(defaultBox)
912        defaultLayout = QVBoxLayout(defaultBox)
913        if not defaultLabel:
914            defaultLabel = _('&Use TreeLine output view font')
915        self.defaultCheck = QCheckBox(defaultLabel)
916        defaultLayout.addWidget(self.defaultCheck)
917        self.defaultCheck.setChecked(self.printData.useDefaultFont)
918        self.defaultCheck.clicked.connect(self.setFontSelectAvail)
919
920        self.fontBox = QGroupBox(_('Select Font'))
921        topLayout.addWidget(self.fontBox)
922        fontLayout = QGridLayout(self.fontBox)
923        spacing = fontLayout.spacing()
924        fontLayout.setSpacing(0)
925
926        label = QLabel(_('&Font'))
927        fontLayout.addWidget(label, 0, 0)
928        label.setIndent(2)
929        self.familyEdit = QLineEdit()
930        fontLayout.addWidget(self.familyEdit, 1, 0)
931        self.familyEdit.setReadOnly(True)
932        self.familyList = SmallListWidget()
933        fontLayout.addWidget(self.familyList, 2, 0)
934        label.setBuddy(self.familyList)
935        self.familyEdit.setFocusProxy(self.familyList)
936        fontLayout.setColumnMinimumWidth(1, spacing)
937        families = [family for family in QFontDatabase().families()]
938        families.sort(key=str.lower)
939        self.familyList.addItems(families)
940        self.familyList.currentItemChanged.connect(self.updateFamily)
941
942        label = QLabel(_('Font st&yle'))
943        fontLayout.addWidget(label, 0, 2)
944        label.setIndent(2)
945        self.styleEdit = QLineEdit()
946        fontLayout.addWidget(self.styleEdit, 1, 2)
947        self.styleEdit.setReadOnly(True)
948        self.styleList = SmallListWidget()
949        fontLayout.addWidget(self.styleList, 2, 2)
950        label.setBuddy(self.styleList)
951        self.styleEdit.setFocusProxy(self.styleList)
952        fontLayout.setColumnMinimumWidth(3, spacing)
953        self.styleList.currentItemChanged.connect(self.updateStyle)
954
955        label = QLabel(_('Si&ze'))
956        fontLayout.addWidget(label, 0, 4)
957        label.setIndent(2)
958        self.sizeEdit = QLineEdit()
959        fontLayout.addWidget(self.sizeEdit, 1, 4)
960        self.sizeEdit.setFocusPolicy(Qt.ClickFocus)
961        validator = QIntValidator(1, 512, self)
962        self.sizeEdit.setValidator(validator)
963        self.sizeList = SmallListWidget()
964        fontLayout.addWidget(self.sizeList, 2, 4)
965        label.setBuddy(self.sizeList)
966        self.sizeList.currentItemChanged.connect(self.updateSize)
967
968        fontLayout.setColumnStretch(0, 30)
969        fontLayout.setColumnStretch(2, 25)
970        fontLayout.setColumnStretch(4, 10)
971
972        sampleBox = QGroupBox(_('Sample'))
973        topLayout.addWidget(sampleBox)
974        sampleLayout = QVBoxLayout(sampleBox)
975        self.sampleEdit = QLineEdit()
976        sampleLayout.addWidget(self.sampleEdit)
977        self.sampleEdit.setAlignment(Qt.AlignCenter)
978        self.sampleEdit.setText(_('AaBbCcDdEeFfGg...TtUuVvWvXxYyZz'))
979        self.sampleEdit.setFixedHeight(self.sampleEdit.sizeHint().height() * 2)
980
981        self.setFontSelectAvail()
982
983    def setFontSelectAvail(self):
984        """Disable font selection if default font is checked.
985
986        Also set the controls with the current or default fonts.
987        """
988        if self.defaultCheck.isChecked():
989            font = self.readFont()
990            if font:
991                self.currentFont = font
992            self.setFont(self.printData.defaultFont)
993            self.fontBox.setEnabled(False)
994        else:
995            self.setFont(self.currentFont)
996            self.fontBox.setEnabled(True)
997
998    def setFont(self, font):
999        """Set the font selector to the given font.
1000
1001        Arguments:
1002            font -- the QFont to set.
1003        """
1004        fontInfo = QFontInfo(font)
1005        family = fontInfo.family()
1006        matches = self.familyList.findItems(family, Qt.MatchExactly)
1007        if matches:
1008            self.familyList.setCurrentItem(matches[0])
1009            self.familyList.scrollToItem(matches[0],
1010                                         QAbstractItemView.PositionAtTop)
1011        style = QFontDatabase().styleString(fontInfo)
1012        matches = self.styleList.findItems(style, Qt.MatchExactly)
1013        if matches:
1014            self.styleList.setCurrentItem(matches[0])
1015            self.styleList.scrollToItem(matches[0])
1016        else:
1017            self.styleList.setCurrentRow(0)
1018            self.styleList.scrollToItem(self.styleList.currentItem())
1019        size = repr(fontInfo.pointSize())
1020        matches = self.sizeList.findItems(size, Qt.MatchExactly)
1021        if matches:
1022            self.sizeList.setCurrentItem(matches[0])
1023            self.sizeList.scrollToItem(matches[0])
1024
1025    def updateFamily(self, currentItem, previousItem):
1026        """Update the family edit box and adjust the style and size options.
1027
1028        Arguments:
1029            currentItem -- the new list widget family item
1030            previousItem -- the previous list widget item
1031        """
1032        family = currentItem.text()
1033        self.familyEdit.setText(family)
1034        if self.familyEdit.hasFocus():
1035            self.familyEdit.selectAll()
1036        prevStyle = self.styleEdit.text()
1037        prevSize = self.sizeEdit.text()
1038        fontDb = QFontDatabase()
1039        styles = [style for style in fontDb.styles(family)]
1040        self.styleList.clear()
1041        self.styleList.addItems(styles)
1042        if prevStyle:
1043            try:
1044                num = styles.index(prevStyle)
1045            except ValueError:
1046                num = 0
1047            self.styleList.setCurrentRow(num)
1048            self.styleList.scrollToItem(self.styleList.currentItem())
1049        sizes = [repr(size) for size in fontDb.pointSizes(family)]
1050        self.sizeList.clear()
1051        self.sizeList.addItems(sizes)
1052        if prevSize:
1053            try:
1054                num = sizes.index(prevSize)
1055            except ValueError:
1056                num = 0
1057            self.sizeList.setCurrentRow(num)
1058            self.sizeList.scrollToItem(self.sizeList.currentItem())
1059            self.updateSample()
1060
1061    def updateStyle(self, currentItem, previousItem):
1062        """Update the style edit box.
1063
1064        Arguments:
1065            currentItem -- the new list widget style item
1066            previousItem -- the previous list widget item
1067        """
1068        if currentItem:
1069            style = currentItem.text()
1070            self.styleEdit.setText(style)
1071            if self.styleEdit.hasFocus():
1072                self.styleEdit.selectAll()
1073            self.updateSample()
1074
1075    def updateSize(self, currentItem, previousItem):
1076        """Update the size edit box.
1077
1078        Arguments:
1079            currentItem -- the new list widget size item
1080            previousItem -- the previous list widget item
1081        """
1082        if currentItem:
1083            size = currentItem.text()
1084            self.sizeEdit.setText(size)
1085            if self.sizeEdit.hasFocus():
1086                self.sizeEdit.selectAll()
1087            self.updateSample()
1088
1089    def updateSample(self):
1090        """Update the font sample edit font.
1091        """
1092        font = self.readFont()
1093        if font:
1094            self.sampleEdit.setFont(font)
1095
1096    def readFont(self):
1097        """Return the selected font or None.
1098        """
1099        family = self.familyEdit.text()
1100        style = self.styleEdit.text()
1101        size = self.sizeEdit.text()
1102        if family and style and size:
1103            return QFontDatabase().font(family, style, int(size))
1104        return None
1105
1106    def saveChanges(self):
1107        """Update print data with current dialog settings.
1108
1109        Return True if saved settings have changed, False otherwise.
1110        """
1111        if self.defaultCheck.isChecked():
1112            if not self.printData.useDefaultFont:
1113                self.printData.useDefaultFont = True
1114                self.printData.mainFont = self.printData.defaultFont
1115                return True
1116        else:
1117            font = self.readFont()
1118            if font and (self.printData.useDefaultFont or
1119                         font != self.printData.mainFont):
1120                self.printData.useDefaultFont = False
1121                self.printData.mainFont = font
1122                return True
1123        return False
1124
1125
1126_headerNames = (_('&Header Left'), _('Header C&enter'), _('Header &Right'))
1127_footerNames = (_('Footer &Left'), _('Footer Ce&nter'), _('Footer Righ&t'))
1128
1129class HeaderPage(QWidget):
1130    """Header/footer print option dialog page.
1131    """
1132    def __init__(self, printData, parent=None):
1133        """Create the header/footer settings page.
1134
1135        Arguments:
1136            printData -- a reference to the PrintData class
1137            parent -- the parent dialog
1138        """
1139        super().__init__(parent)
1140        self.printData = printData
1141        self.focusedEditor = None
1142
1143        topLayout = QGridLayout(self)
1144        fieldBox = QGroupBox(_('Fiel&ds'))
1145        topLayout.addWidget(fieldBox, 0, 0, 3, 1)
1146        fieldLayout = QVBoxLayout(fieldBox)
1147        self.fieldListWidget = FieldListWidget()
1148        fieldLayout.addWidget(self.fieldListWidget)
1149        fieldFormatButton = QPushButton(_('Field For&mat'))
1150        fieldLayout.addWidget(fieldFormatButton)
1151        fieldFormatButton.clicked.connect(self.showFieldFormatDialog)
1152
1153        self.addFieldButton = QPushButton('>>')
1154        topLayout.addWidget(self.addFieldButton, 0, 1)
1155        self.addFieldButton.setMaximumWidth(self.addFieldButton.sizeHint().
1156                                            height())
1157        self.addFieldButton.clicked.connect(self.addField)
1158
1159        self.delFieldButton = QPushButton('<<')
1160        topLayout.addWidget(self.delFieldButton, 1, 1)
1161        self.delFieldButton.setMaximumWidth(self.delFieldButton.sizeHint().
1162                                            height())
1163        self.delFieldButton.clicked.connect(self.delField)
1164
1165        headerFooterBox = QGroupBox(_('Header and Footer'))
1166        topLayout.addWidget(headerFooterBox, 0, 2, 2, 1)
1167        headerFooterLayout = QGridLayout(headerFooterBox)
1168        spacing = headerFooterLayout.spacing()
1169        headerFooterLayout.setVerticalSpacing(0)
1170        headerFooterLayout.setRowMinimumHeight(2, spacing)
1171
1172        self.headerEdits = self.addLineEdits(_headerNames, headerFooterLayout,
1173                                             0)
1174        self.footerEdits = self.addLineEdits(_footerNames, headerFooterLayout,
1175                                             3)
1176        self.loadContent()
1177
1178    def addLineEdits(self, names, layout, startRow):
1179        """Add line edits for header or footer.
1180
1181        Return a list of line edits added to the top layout.
1182        Arguments:
1183            names -- a list of label names
1184            layout -- the grid layout t use
1185            startRow -- the initial row number
1186        """
1187        lineEdits = []
1188        for num, name in enumerate(names):
1189            label = QLabel(name)
1190            layout.addWidget(label, startRow, num)
1191            lineEdit = configdialog.TitleEdit()
1192            layout.addWidget(lineEdit, startRow + 1, num)
1193            label.setBuddy(lineEdit)
1194            lineEdit.cursorPositionChanged.connect(self.setControlAvailability)
1195            lineEdit.focusIn.connect(self.setCurrentEditor)
1196            lineEdits.append(lineEdit)
1197        return lineEdits
1198
1199    def loadContent(self):
1200        """Load field names and header/footer text into the controls.
1201        """
1202        self.fieldListWidget.addItems(self.printData.localControl.structure.
1203                                      treeFormats.fileInfoFormat.fieldNames())
1204        self.fieldListWidget.setCurrentRow(0)
1205        for text, lineEdit in zip(splitHeaderFooter(self.printData.headerText),
1206                                  self.headerEdits):
1207            lineEdit.blockSignals(True)
1208            lineEdit.setText(text)
1209            lineEdit.blockSignals(False)
1210        for text, lineEdit in zip(splitHeaderFooter(self.printData.footerText),
1211                                  self.footerEdits):
1212            lineEdit.blockSignals(True)
1213            lineEdit.setText(text)
1214            lineEdit.blockSignals(False)
1215        self.focusedEditor = self.headerEdits[0]
1216        self.headerEdits[0].setFocus()
1217        self.setControlAvailability()
1218
1219    def setControlAvailability(self):
1220        """Set controls available based on text cursor movements.
1221        """
1222        cursorInField = self.isCursorInField()
1223        self.addFieldButton.setEnabled(cursorInField == None)
1224        self.delFieldButton.setEnabled(cursorInField == True)
1225
1226    def setCurrentEditor(self, sender):
1227        """Set focusedEditor based on editor focus change signal.
1228
1229        Arguments:
1230            sender -- the line editor to focus
1231        """
1232        self.focusedEditor = sender
1233        self.setControlAvailability()
1234
1235    def isCursorInField(self, selectField=False):
1236        """Return True if a field pattern encloses the cursor/selection.
1237
1238        Return False if the selection overlaps a field.
1239        Return None if there is no field at the cursor.
1240        Arguments:
1241            selectField -- select the entire field pattern if True.
1242        """
1243        cursorPos = self.focusedEditor.cursorPosition()
1244        selectStart = self.focusedEditor.selectionStart()
1245        if selectStart < 0:
1246            selectStart = cursorPos
1247        elif selectStart == cursorPos:   # backward selection
1248            cursorPos += len(self.focusedEditor.selectedText())
1249        textLine = self.focusedEditor.text()
1250        for match in configdialog.fieldPattern.finditer(textLine):
1251            start = (match.start() if match.start() < selectStart < match.end()
1252                     else None)
1253            end = (match.end() if match.start() < cursorPos < match.end()
1254                   else None)
1255            if start != None and end != None:
1256                if selectField:
1257                    self.focusedEditor.setSelection(start, end - start)
1258                return True
1259            if start != None or end != None:
1260                return False
1261        return None
1262
1263    def addField(self):
1264        """Add selected field to cursor pos in current line editor.
1265        """
1266        fieldName = self.fieldListWidget.currentItem().text()
1267        self.focusedEditor.insert('{{*!{0}*}}'.format(fieldName))
1268        self.focusedEditor.setFocus()
1269
1270    def delField(self):
1271        """Remove field from cursor pos in current line editor.
1272        """
1273        if self.isCursorInField(True):
1274            self.focusedEditor.insert('')
1275            self.focusedEditor.setFocus()
1276
1277    def showFieldFormatDialog(self):
1278        """Show thw dialog used to set file info field formats.
1279        """
1280        fileInfoFormat = (self.printData.localControl.structure.treeFormats.
1281                          fileInfoFormat)
1282        fieldName = self.fieldListWidget.currentItem().text()
1283        field = fileInfoFormat.fieldDict[fieldName]
1284        dialog = HeaderFieldFormatDialog(field, self.printData.localControl,
1285                                         self)
1286        dialog.exec_()
1287
1288    def saveChanges(self):
1289        """Update print data with current dialog settings.
1290
1291        Return True if saved settings have changed, False otherwise.
1292        """
1293        changed = False
1294        headerList = [lineEdit.text().replace('/', r'\/') for lineEdit in
1295                      self.headerEdits]
1296        while len(headerList) > 1 and not headerList[-1]:
1297            del headerList[-1]
1298        text = '/'.join(headerList)
1299        if self.printData.headerText != text:
1300            self.printData.headerText = text
1301            changed = True
1302        footerList = [lineEdit.text().replace('/', r'\/') for lineEdit in
1303                      self.footerEdits]
1304        while len(footerList) > 1 and not footerList[-1]:
1305            del footerList[-1]
1306        text = '/'.join(footerList)
1307        if self.printData.footerText != text:
1308            self.printData.footerText = text
1309            changed = True
1310        return changed
1311
1312
1313class FieldListWidget(QListWidget):
1314    """List widget for fields with smaller width size hint.
1315    """
1316    def __init__(self, parent=None):
1317        """Create the list widget.
1318
1319        Arguments:
1320            parent -- the parent dialog
1321        """
1322        super().__init__(parent)
1323
1324    def sizeHint(self):
1325        """Return a size with a smaller width.
1326        """
1327        return QSize(120, 100)
1328
1329
1330class HeaderFieldFormatDialog(QDialog):
1331    """Dialog to modify file info field formats used in headers and footers.
1332    """
1333    def __init__(self, field, localControl, parent=None):
1334        """Create the field format dialog.
1335
1336        Arguments:
1337            field -- the field to be modified
1338            localControl -- a ref to the control to save changes and undo
1339        """
1340        super().__init__(parent)
1341        self.field = field
1342        self.localControl = localControl
1343
1344        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
1345                            Qt.WindowCloseButtonHint)
1346        self.setWindowTitle(_('Field Format for "{0}"').format(field.name))
1347        topLayout = QVBoxLayout(self)
1348        self.setLayout(topLayout)
1349
1350        self.formatBox = QGroupBox(_('Output &Format'))
1351        topLayout.addWidget(self.formatBox)
1352        formatLayout = QHBoxLayout(self.formatBox)
1353        self.formatEdit = QLineEdit()
1354        formatLayout.addWidget(self.formatEdit)
1355        self.helpButton = QPushButton(_('Format &Help'))
1356        formatLayout.addWidget(self.helpButton)
1357        self.helpButton.clicked.connect(self.formatHelp)
1358
1359        extraBox = QGroupBox(_('Extra Text'))
1360        topLayout.addWidget(extraBox)
1361        extraLayout = QVBoxLayout(extraBox)
1362        spacing = extraLayout.spacing()
1363        extraLayout.setSpacing(0)
1364        prefixLabel = QLabel(_('&Prefix'))
1365        extraLayout.addWidget(prefixLabel)
1366        self.prefixEdit = QLineEdit()
1367        extraLayout.addWidget(self.prefixEdit)
1368        prefixLabel.setBuddy(self.prefixEdit)
1369        extraLayout.addSpacing(spacing)
1370        suffixLabel = QLabel(_('&Suffix'))
1371        extraLayout.addWidget(suffixLabel)
1372        self.suffixEdit = QLineEdit()
1373        extraLayout.addWidget(self.suffixEdit)
1374        suffixLabel.setBuddy(self.suffixEdit)
1375
1376        ctrlLayout = QHBoxLayout()
1377        topLayout.addLayout(ctrlLayout)
1378        ctrlLayout.addStretch()
1379        okButton = QPushButton(_('&OK'))
1380        ctrlLayout.addWidget(okButton)
1381        okButton.clicked.connect(self.accept)
1382        cancelButton = QPushButton(_('&Cancel'))
1383        ctrlLayout.addWidget(cancelButton)
1384        cancelButton.clicked.connect(self.reject)
1385
1386        self.prefixEdit.setText(self.field.prefix)
1387        self.suffixEdit.setText(self.field.suffix)
1388        self.formatEdit.setText(self.field.format)
1389
1390        self.formatBox.setEnabled(self.field.defaultFormat != '')
1391
1392    def formatHelp(self):
1393        """Provide a format help menu based on a button signal.
1394        """
1395        menu = QMenu(self)
1396        self.formatHelpDict = {}
1397        for descript, key in self.field.getFormatHelpMenuList():
1398            if descript:
1399                self.formatHelpDict[descript] = key
1400                menu.addAction(descript)
1401            else:
1402                menu.addSeparator()
1403        menu.popup(self.helpButton.
1404                   mapToGlobal(QPoint(0, self.helpButton.height())))
1405        menu.triggered.connect(self.insertFormat)
1406
1407    def insertFormat(self, action):
1408        """Insert format text from help menu into edit box.
1409
1410        Arguments:
1411            action -- the action from the help menu
1412        """
1413        self.formatEdit.insert(self.formatHelpDict[action.text()])
1414
1415    def accept(self):
1416        """Set changes after OK is hit"""
1417        prefix = self.prefixEdit.text()
1418        suffix = self.suffixEdit.text()
1419        format = self.formatEdit.text()
1420        if (self.field.prefix != prefix or self.field.suffix != suffix or
1421            self.field.format != format):
1422            undo.FormatUndo(self.localControl.structure.undoList,
1423                            self.localControl.structure.treeFormats,
1424                            treeformats.TreeFormats())
1425            self.field.prefix = prefix
1426            self.field.suffix = suffix
1427            self.field.format = format
1428            self.localControl.setModified()
1429        super().accept()
1430
1431
1432_headerSplitRe = re.compile(r'(?<!\\)/')
1433
1434def splitHeaderFooter(combinedText):
1435    """Return a list of header/footer parts from the text, separated by "/".
1436
1437    Backslash escapes avoid splits.
1438    Arguments:
1439        combinedText -- the text to split
1440    """
1441    textList = _headerSplitRe.split(combinedText)
1442    return [text.replace(r'\/', '/') for text in textList]
1443