1#!/usr/bin/env python3
2
3#****************************************************************************
4# fontset.py, provides storage/retrieval and a dialog for custom fonts
5#
6# ConvertAll, a units conversion program
7# Copyright (C) 2019, 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 WITTHOUT ANY WARRANTY.  See the included LICENSE file for details.
13#*****************************************************************************
14
15from PyQt5.QtCore import (QSize, Qt)
16from PyQt5.QtGui import (QFontDatabase, QFontInfo, QIntValidator)
17from PyQt5.QtWidgets import (QAbstractItemView, QCheckBox, QDialog,
18                             QGridLayout, QGroupBox, QHBoxLayout, QLabel,
19                             QLineEdit, QListWidget, QPushButton, QVBoxLayout)
20
21
22class CustomFontDialog(QDialog):
23    """Dialog for selecting a custom font.
24    """
25    def __init__(self, sysFont, currentFont=None, parent=None):
26        """Create a font customization dialog.
27        """
28        super().__init__(parent)
29        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
30                            Qt.WindowCloseButtonHint)
31        self.setWindowTitle(_('Customize Font'))
32        self.sysFont = sysFont
33        self.currentFont = currentFont
34
35        topLayout = QVBoxLayout(self)
36        self.setLayout(topLayout)
37        defaultBox = QGroupBox(_('Default Font'))
38        topLayout.addWidget(defaultBox)
39        defaultLayout = QVBoxLayout(defaultBox)
40        self.defaultCheck = QCheckBox(_('&Use system default font'))
41        defaultLayout.addWidget(self.defaultCheck)
42        self.defaultCheck.setChecked(self.currentFont == None)
43        self.defaultCheck.clicked.connect(self.setFontSelectAvail)
44
45        self.fontBox = QGroupBox(_('Select Font'))
46        topLayout.addWidget(self.fontBox)
47        fontLayout = QGridLayout(self.fontBox)
48        spacing = fontLayout.spacing()
49        fontLayout.setSpacing(0)
50
51        label = QLabel(_('&Font'))
52        fontLayout.addWidget(label, 0, 0)
53        label.setIndent(2)
54        self.familyEdit = QLineEdit()
55        fontLayout.addWidget(self.familyEdit, 1, 0)
56        self.familyEdit.setReadOnly(True)
57        self.familyList = SmallListWidget()
58        fontLayout.addWidget(self.familyList, 2, 0)
59        label.setBuddy(self.familyList)
60        self.familyEdit.setFocusProxy(self.familyList)
61        fontLayout.setColumnMinimumWidth(1, spacing)
62        families = [family for family in QFontDatabase().families()]
63        families.sort(key=str.lower)
64        self.familyList.addItems(families)
65        self.familyList.currentItemChanged.connect(self.updateFamily)
66
67        label = QLabel(_('Font st&yle'))
68        fontLayout.addWidget(label, 0, 2)
69        label.setIndent(2)
70        self.styleEdit = QLineEdit()
71        fontLayout.addWidget(self.styleEdit, 1, 2)
72        self.styleEdit.setReadOnly(True)
73        self.styleList = SmallListWidget()
74        fontLayout.addWidget(self.styleList, 2, 2)
75        label.setBuddy(self.styleList)
76        self.styleEdit.setFocusProxy(self.styleList)
77        fontLayout.setColumnMinimumWidth(3, spacing)
78        self.styleList.currentItemChanged.connect(self.updateStyle)
79
80        label = QLabel(_('Si&ze'))
81        fontLayout.addWidget(label, 0, 4)
82        label.setIndent(2)
83        self.sizeEdit = QLineEdit()
84        fontLayout.addWidget(self.sizeEdit, 1, 4)
85        self.sizeEdit.setFocusPolicy(Qt.ClickFocus)
86        validator = QIntValidator(1, 512, self)
87        self.sizeEdit.setValidator(validator)
88        self.sizeList = SmallListWidget()
89        fontLayout.addWidget(self.sizeList, 2, 4)
90        label.setBuddy(self.sizeList)
91        self.sizeList.currentItemChanged.connect(self.updateSize)
92
93        fontLayout.setColumnStretch(0, 30)
94        fontLayout.setColumnStretch(2, 25)
95        fontLayout.setColumnStretch(4, 10)
96
97        sampleBox = QGroupBox(_('Sample'))
98        topLayout.addWidget(sampleBox)
99        sampleLayout = QVBoxLayout(sampleBox)
100        self.sampleEdit = QLineEdit()
101        sampleLayout.addWidget(self.sampleEdit)
102        self.sampleEdit.setAlignment(Qt.AlignCenter)
103        self.sampleEdit.setText(_('AaBbCcDdEeFfGg...TtUuVvWvXxYyZz'))
104        self.sampleEdit.setFixedHeight(self.sampleEdit.sizeHint().height() * 2)
105
106        ctrlLayout = QHBoxLayout()
107        topLayout.addLayout(ctrlLayout)
108        ctrlLayout.addStretch()
109        self.okButton = QPushButton(_('&OK'))
110        ctrlLayout.addWidget(self.okButton)
111        self.okButton.clicked.connect(self.accept)
112        cancelButton = QPushButton(_('&Cancel'))
113        ctrlLayout.addWidget(cancelButton)
114        cancelButton.clicked.connect(self.reject)
115
116        self.setFontSelectAvail()
117
118    def setFontSelectAvail(self):
119        """Disable font selection if default font is checked.
120
121        Also set the controls with the current or default fonts.
122        """
123        if self.currentFont and not self.defaultCheck.isChecked():
124            self.setFont(self.currentFont)
125        else:
126            self.setFont(self.sysFont)
127        self.fontBox.setEnabled(not self.defaultCheck.isChecked())
128
129    def setFont(self, font):
130        """Set the font selector to the given font.
131
132        Arguments:
133            font -- the QFont to set.
134        """
135        fontInfo = QFontInfo(font)
136        family = fontInfo.family()
137        matches = self.familyList.findItems(family, Qt.MatchExactly)
138        if matches:
139            self.familyList.setCurrentItem(matches[0])
140            self.familyList.scrollToItem(matches[0],
141                                         QAbstractItemView.PositionAtTop)
142        style = QFontDatabase().styleString(fontInfo)
143        matches = self.styleList.findItems(style, Qt.MatchExactly)
144        if matches:
145            self.styleList.setCurrentItem(matches[0])
146            self.styleList.scrollToItem(matches[0])
147        else:
148            self.styleList.setCurrentRow(0)
149            self.styleList.scrollToItem(self.styleList.currentItem())
150        size = repr(fontInfo.pointSize())
151        matches = self.sizeList.findItems(size, Qt.MatchExactly)
152        if matches:
153            self.sizeList.setCurrentItem(matches[0])
154            self.sizeList.scrollToItem(matches[0])
155
156    def updateFamily(self, currentItem, previousItem):
157        """Update the family edit box and adjust the style and size options.
158
159        Arguments:
160            currentItem -- the new list widget family item
161            previousItem -- the previous list widget item
162        """
163        family = currentItem.text()
164        self.familyEdit.setText(family)
165        if self.familyEdit.hasFocus():
166            self.familyEdit.selectAll()
167        prevStyle = self.styleEdit.text()
168        prevSize = self.sizeEdit.text()
169        fontDb = QFontDatabase()
170        styles = [style for style in fontDb.styles(family)]
171        self.styleList.clear()
172        self.styleList.addItems(styles)
173        if prevStyle:
174            try:
175                num = styles.index(prevStyle)
176            except ValueError:
177                num = 0
178            self.styleList.setCurrentRow(num)
179            self.styleList.scrollToItem(self.styleList.currentItem())
180        sizes = [repr(size) for size in fontDb.pointSizes(family)]
181        self.sizeList.clear()
182        self.sizeList.addItems(sizes)
183        if prevSize:
184            try:
185                num = sizes.index(prevSize)
186            except ValueError:
187                num = 0
188            self.sizeList.setCurrentRow(num)
189            self.sizeList.scrollToItem(self.sizeList.currentItem())
190            self.updateSample()
191
192    def updateStyle(self, currentItem, previousItem):
193        """Update the style edit box.
194
195        Arguments:
196            currentItem -- the new list widget style item
197            previousItem -- the previous list widget item
198        """
199        if currentItem:
200            style = currentItem.text()
201            self.styleEdit.setText(style)
202            if self.styleEdit.hasFocus():
203                self.styleEdit.selectAll()
204            self.updateSample()
205
206    def updateSize(self, currentItem, previousItem):
207        """Update the size edit box.
208
209        Arguments:
210            currentItem -- the new list widget size item
211            previousItem -- the previous list widget item
212        """
213        if currentItem:
214            size = currentItem.text()
215            self.sizeEdit.setText(size)
216            if self.sizeEdit.hasFocus():
217                self.sizeEdit.selectAll()
218            self.updateSample()
219
220    def updateSample(self):
221        """Update the font sample edit font.
222        """
223        font = self.readFont()
224        if font:
225            self.sampleEdit.setFont(font)
226
227    def readFont(self):
228        """Return the selected font or None.
229        """
230        family = self.familyEdit.text()
231        style = self.styleEdit.text()
232        size = self.sizeEdit.text()
233        if family and style and size:
234            return QFontDatabase().font(family, style, int(size))
235        return None
236
237    def resultingFont(self):
238        """Return the selected font or None if system font.
239        """
240        if self.defaultCheck.isChecked():
241            return None
242        return self.readFont()
243
244
245class SmallListWidget(QListWidget):
246    """ListWidget with a smaller size hint.
247    """
248    def __init__(self, parent=None):
249        """Initialize the widget.
250
251        Arguments:
252            parent -- the parent, if given
253        """
254        super().__init__(parent)
255
256    def sizeHint(self):
257        """Return smaller width.
258        """
259        itemHeight = self.visualItemRect(self.item(0)).height()
260        return QSize(100, itemHeight * 6)
261