/', '')
267 | if not os.path.isabs(iconPrefixDir):
268 | iconPrefixDir = os.path.join(prefixDir, iconPrefixDir)
269 | iconBuildDir = os.path.join(buildRoot, iconPrefixDir[1:])
270 | print(' Copying icon files to {0}'.format(iconBuildDir))
271 | copyDir('icons', iconBuildDir)
272 | # update icon location in main python script
273 | replaceLine(os.path.join(pythonBuildDir, '{0}.py'.format(progName)),
274 | 'iconPath = None',
275 | 'iconPath = \'{0}\' # modified by install script\n'
276 | .format(iconPrefixDir))
277 | if os.path.isfile(os.path.join('icons', progName + '-icon.svg')):
278 | svgIconPrefixDir = os.path.join(prefixDir, 'share', 'icons',
279 | 'hicolor', 'scalable', 'apps')
280 | svgIconBuildDir = os.path.join(buildRoot, svgIconPrefixDir[1:])
281 | print(' Copying app icon files to {0}'.format(svgIconBuildDir))
282 | if not os.path.isdir(svgIconBuildDir):
283 | os.makedirs(svgIconBuildDir)
284 | shutil.copy2(os.path.join('icons', progName + '-icon.svg'),
285 | svgIconBuildDir)
286 | if os.path.isfile(progName + '.desktop'):
287 | desktopPrefixDir = os.path.join(prefixDir, 'share', 'applications')
288 | desktopBuildDir = os.path.join(buildRoot, desktopPrefixDir[1:])
289 | print(' Copying desktop file to {0}'.format(desktopBuildDir))
290 | if not os.path.isdir(desktopBuildDir):
291 | os.makedirs(desktopBuildDir)
292 | shutil.copy2(progName + '.desktop', desktopBuildDir)
293 |
294 | if os.path.isdir('source'):
295 | createWrapper(pythonPrefixDir, progName)
296 | binBuildDir = os.path.join(buildRoot, prefixDir[1:], 'bin')
297 | print(' Copying executable file "{0}" to {1}'
298 | .format(progName, binBuildDir))
299 | if not os.path.isdir(binBuildDir):
300 | os.makedirs(binBuildDir)
301 | shutil.copy2(progName, binBuildDir)
302 | compileall.compile_dir(pythonBuildDir, ddir=prefixDir)
303 | cleanSource()
304 | print('Install complete.')
305 |
306 |
307 | if __name__ == '__main__':
308 | main()
309 |
--------------------------------------------------------------------------------
/source/bases.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # bases.py, provides conversions of number bases and fractions
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 |
15 | import math
16 | from PyQt5.QtCore import Qt, QRegularExpression
17 | from PyQt5.QtGui import QRegularExpressionValidator
18 | from PyQt5.QtWidgets import (QApplication, QCheckBox, QDialog, QHBoxLayout,
19 | QLabel, QLineEdit, QMessageBox, QPushButton,
20 | QSpinBox, QTreeWidget, QTreeWidgetItem,
21 | QVBoxLayout)
22 | import numedit
23 |
24 |
25 | class BasesDialog(QDialog):
26 | """A dialog for conversion of number bases.
27 | """
28 | def __init__(self, parent=None):
29 | super().__init__(parent)
30 | self.setAttribute(Qt.WA_QuitOnClose, False)
31 | self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
32 | Qt.WindowSystemMenuHint)
33 | self.setWindowTitle(_('Base Conversions'))
34 | self.value = 0
35 | self.numBits = 32
36 | self.twosComplement = False
37 | layout = QVBoxLayout(self)
38 | layout.setSpacing(0)
39 | decimalLabel = QLabel(_('&Decmal'))
40 | layout.addWidget(decimalLabel)
41 | decimalEdit = QLineEdit()
42 | decimalLabel.setBuddy(decimalEdit)
43 | decimalEdit.base = 10
44 | decRegEx = QRegularExpression('[-0-9]*')
45 | decimalEdit.setValidator(QRegularExpressionValidator(decRegEx))
46 | layout.addWidget(decimalEdit)
47 | layout.addSpacing(8)
48 | hexLabel = QLabel(_('&Hex'))
49 | layout.addWidget(hexLabel)
50 | hexEdit = QLineEdit()
51 | hexLabel.setBuddy(hexEdit)
52 | hexEdit.base = 16
53 | hexRegEx = QRegularExpression('[-0-9a-fA-F]*')
54 | hexEdit.setValidator(QRegularExpressionValidator(hexRegEx))
55 | layout.addWidget(hexEdit)
56 | layout.addSpacing(8)
57 | octalLabel = QLabel(_('&Octal'))
58 | layout.addWidget(octalLabel)
59 | octalEdit = QLineEdit()
60 | octalLabel.setBuddy(octalEdit)
61 | octalEdit.base = 8
62 | octRegEx = QRegularExpression('[-0-7]*')
63 | octalEdit.setValidator(QRegularExpressionValidator(octRegEx))
64 | layout.addWidget(octalEdit)
65 | layout.addSpacing(8)
66 | binaryLabel = QLabel(_('&Binary'))
67 | layout.addWidget(binaryLabel)
68 | binaryEdit = QLineEdit()
69 | binaryLabel.setBuddy(binaryEdit)
70 | binaryEdit.base = 2
71 | binRegEx = QRegularExpression('[-01]*')
72 | binaryEdit.setValidator(QRegularExpressionValidator(binRegEx))
73 | layout.addWidget(binaryEdit)
74 | layout.addSpacing(8)
75 | self.bitsButton = QPushButton('')
76 | self.setButtonLabel()
77 | layout.addWidget(self.bitsButton)
78 | self.bitsButton.clicked.connect(self.changeBitSettings)
79 | layout.addSpacing(8)
80 | closeButton = QPushButton(_('&Close'))
81 | layout.addWidget(closeButton)
82 | closeButton.clicked.connect(self.close)
83 | self.editors = (decimalEdit, hexEdit, octalEdit, binaryEdit)
84 | for editor in self.editors:
85 | editor.textEdited.connect(self.updateValue)
86 |
87 | def updateValue(self):
88 | """Update the current number base and then the other editors.
89 | """
90 | activeEditor = self.focusWidget()
91 | text = activeEditor.text()
92 | if text:
93 | try:
94 | self.value = baseNum(text, activeEditor.base, self.numBits,
95 | self.twosComplement)
96 | except ValueError:
97 | QMessageBox.warning(self, 'ConvertAll', _('Number overflow'))
98 | activeEditor = None
99 | else:
100 | self.value = 0
101 | for editor in self.editors:
102 | if editor is not activeEditor:
103 | editor.setText(baseNumStr(self.value, editor.base,
104 | self.numBits, self.twosComplement))
105 |
106 | def changeBitSettings(self):
107 | """Show the dialog to update bit settings.
108 | """
109 | dlg = QDialog(self)
110 | dlg.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
111 | Qt.WindowSystemMenuHint)
112 | dlg.setWindowTitle(_('Bit Settings'))
113 | topLayout = QVBoxLayout(dlg)
114 | dlg.setLayout(topLayout)
115 | bitLayout = QHBoxLayout()
116 | topLayout.addLayout(bitLayout)
117 | bitSizeBox = QSpinBox(dlg)
118 | bitSizeBox.setMinimum(1)
119 | bitSizeBox.setMaximum(256)
120 | bitSizeBox.setSingleStep(16)
121 | bitSizeBox.setValue(self.numBits)
122 | bitLayout.addWidget(bitSizeBox)
123 | label = QLabel(_('&bit overflow limit'), dlg)
124 | label.setBuddy(bitSizeBox)
125 | bitLayout.addWidget(label)
126 | twoCompBox = QCheckBox(_("&Use two's complement\n"
127 | "for negative numbers"), dlg)
128 | twoCompBox.setChecked(self.twosComplement)
129 | topLayout.addWidget(twoCompBox)
130 |
131 | ctrlLayout = QHBoxLayout()
132 | topLayout.addLayout(ctrlLayout)
133 | ctrlLayout.addStretch(0)
134 | okButton = QPushButton(_('&OK'), dlg)
135 | ctrlLayout.addWidget(okButton)
136 | okButton.clicked.connect(dlg.accept)
137 | cancelButton = QPushButton(_('&Cancel'), dlg)
138 | ctrlLayout.addWidget(cancelButton)
139 | cancelButton.clicked.connect(dlg.reject)
140 | if dlg.exec_() == QDialog.Accepted:
141 | self.numBits = bitSizeBox.value()
142 | self.twosComplement = twoCompBox.isChecked()
143 | self.setButtonLabel()
144 |
145 | def setButtonLabel(self):
146 | """Set the text label on the bitsButton to match settings.
147 | """
148 | text = '{0} {1}, '.format(self.numBits, _('bit'))
149 | if self.twosComplement:
150 | text += _('&two\'s complement')
151 | else:
152 | text += _('no &two\'s complement')
153 | self.bitsButton.setText(text)
154 |
155 |
156 | class FractionDialog(QDialog):
157 | """A dialog for conversion of numbers into fractions.
158 | """
159 | def __init__(self, parent=None):
160 | super().__init__(parent)
161 | self.setAttribute(Qt.WA_QuitOnClose, False)
162 | self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
163 | Qt.WindowSystemMenuHint)
164 | self.setWindowTitle(_('Fraction Conversions'))
165 | layout = QVBoxLayout(self)
166 | layout.setSpacing(0)
167 | expLabel = QLabel(_('&Expression'))
168 | layout.addWidget(expLabel)
169 | horizLayout = QHBoxLayout()
170 | layout.addLayout(horizLayout)
171 | horizLayout.setSpacing(5)
172 | self.exprEdit = QLineEdit()
173 | expLabel.setBuddy(self.exprEdit)
174 | horizLayout.addWidget(self.exprEdit)
175 | self.exprEdit.setValidator(numedit.FloatExprValidator(self))
176 | self.exprEdit.returnPressed.connect(self.calcFractions)
177 | enterButton = QPushButton(_('E&nter'))
178 | horizLayout.addWidget(enterButton)
179 | enterButton.setAutoDefault(False)
180 | enterButton.clicked.connect(self.calcFractions)
181 | layout.addSpacing(10)
182 | self.resultView = QTreeWidget()
183 | self.resultView.setColumnCount(2)
184 | self.resultView.setHeaderLabels([_('Fraction'), _('Decimal')])
185 | layout.addWidget(self.resultView)
186 | layout.addSpacing(10)
187 | self.powerTwoCtrl = QCheckBox(_('Limit denominators to powers of two'))
188 | layout.addWidget(self.powerTwoCtrl)
189 | layout.addSpacing(10)
190 | closeButton = QPushButton(_('&Close'))
191 | layout.addWidget(closeButton)
192 | closeButton.setAutoDefault(False)
193 | closeButton.clicked.connect(self.close)
194 |
195 | def calcFractions(self):
196 | """Find fractions from the expression in the editor.
197 | """
198 | self.resultView.clear()
199 | text = self.exprEdit.text()
200 | try:
201 | num = float(text)
202 | except ValueError:
203 | try:
204 | num = float(eval(text))
205 | output = [_('Entry'), '{0}'.format(num)]
206 | self.resultView.addTopLevelItem(QTreeWidgetItem(output))
207 | except:
208 | QMessageBox.warning(self, 'ConvertAll',
209 | _('Invalid expresssion'))
210 | return
211 | QApplication.setOverrideCursor(Qt.WaitCursor)
212 | powerOfTwo = self.powerTwoCtrl.isChecked()
213 | for numer, denom in listFractions(num, powerOfTwo):
214 | output = ['{0}/{1}'.format(numer, denom),
215 | '{0}'.format(numer / denom)]
216 | self.resultView.addTopLevelItem(QTreeWidgetItem(output))
217 | QApplication.restoreOverrideCursor()
218 |
219 |
220 | def baseNumStr(number, base, numBits=32, twosComplement=False):
221 | """Return string of number in given base (2-16).
222 |
223 | Arguments:
224 | base -- the number base to convert to
225 | numBits -- the number of bits available for the result
226 | twosComplement -- if True, use two's complement for negative numbers
227 | """
228 | digits = '0123456789abcdef'
229 | number = int(round(number))
230 | result = ''
231 | sign = ''
232 | if number == 0:
233 | return '0'
234 | if twosComplement:
235 | if number >= 2**(numBits - 1) or \
236 | number < -2**(numBits - 1):
237 | return 'overflow'
238 | if number < 0:
239 | number = 2**numBits + number
240 | else:
241 | if number < 0:
242 | number = abs(number)
243 | sign = '-'
244 | if number >= 2**numBits:
245 | return 'overflow'
246 | while number:
247 | number, remainder = divmod(number, base)
248 | result = '{0}{1}'.format(digits[remainder], result)
249 | return '{0}{1}'.format(sign, result)
250 |
251 |
252 | def baseNum(numStr, base, numBits=32, twosComplement=False):
253 | """Convert number string to an integer using given base.
254 |
255 | Arguments:
256 | base -- the number base to convert from
257 | numBits -- the number of bits available for the numStr
258 | twosComplement -- if True, use two's complement for negative numbers
259 | """
260 | numStr = numStr.replace(' ', '')
261 | if numStr == '-':
262 | return 0
263 | num = int(numStr, base)
264 | if num >= 2**numBits:
265 | raise ValueError
266 | if base != 10 and twosComplement and num >= 2**(numBits - 1):
267 | num = num - 2**numBits
268 | return num
269 |
270 |
271 | def listFractions(decimal, powerOfTwo=False):
272 | """Return a list of numerator, denominator tuples.
273 |
274 | The tuples approximate the decimal, becoming more accurate.
275 | Arguments:
276 | decimal -- a real number to approximate as a fraction
277 | powerOfTwo -- if True, restrict the denominator to powers of 2
278 | """
279 | results = []
280 | if decimal == 0.0:
281 | return results
282 | denom = 2
283 | denomLimit = 10**9
284 | minOffset = 10**-10
285 | minDelta = denomLimit
286 | numer = round(decimal * denom)
287 | delta = abs(decimal - numer / denom)
288 | while denom < denomLimit:
289 | nextDenom = denom + 1 if not powerOfTwo else denom * 2
290 | nextNumer = round(decimal * nextDenom)
291 | nextDelta = abs(decimal - nextNumer / nextDenom)
292 | if numer != 0 and (delta == 0.0 or (delta < minDelta - minOffset and
293 | delta <= nextDelta)):
294 | results.append((numer, denom))
295 | if delta == 0.0:
296 | break
297 | minDelta = delta
298 | denom = nextDenom
299 | numer = nextNumer
300 | delta = nextDelta
301 | if results: # handle when first result is a whole num (2/2, 4/2, etc.)
302 | numer, denom = results[0]
303 | if denom == 2 and numer / denom == round(numer / denom):
304 | results[0] = (round(numer / denom), 1)
305 | return results
306 |
--------------------------------------------------------------------------------
/source/cmdline.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # cmdline.py, provides a class to read and execute command line arguments
5 | #
6 | # ConvertAll, a units conversion program
7 | # Copyright (C) 2015, 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 |
15 | import sys
16 | import re
17 | import option
18 | import optiondefaults
19 | import unitdata
20 | import unitgroup
21 |
22 | usage = [_('Usage:'),
23 | '',
24 | ' convertall [{0}]'.format(_('qt-options')),
25 | '',
26 | _('-or- (non-GUI):'),
27 | ' convertall [{0}] [{1}] {2} [{3}]'.format(_('options'),
28 | _('number'),
29 | _('from_unit'),
30 | _('to_unit')),
31 | '',
32 | _('-or- (non-GUI):'),
33 | ' convertall -i [{0}]'.format(_('options')),
34 | '',
35 | _('Units with spaces must be "quoted"'),
36 | '',
37 | _('Options:'),
38 | ' -d, --decimals={0:6} {1}'.format(_('num'),
39 | _('set number of decimals to show')),
40 | ' -f, --fixed-decimals {0}'.format(
41 | _('show set number of decimals, even if zeros')),
42 | ' -s, --sci-notation {0}'.format(
43 | _('show results in scientific notation')),
44 | ' -e, --eng-notation {0}'.format(
45 | _('show results in engineering notation')),
46 | ' -h, --help {0}'.format(
47 | _('display this message and exit')),
48 | ' -i, --interactive {0}'.format(
49 | _('interactive command line mode (non-GUI)')),
50 | ' -q, --quiet {0}'.format(
51 | _('convert without further prompts')),
52 | '']
53 |
54 | def parseArgs(opts, args):
55 | """Parse the command line and output conversion results.
56 | """
57 | options = option.Option('convertall', 20)
58 | options.loadAll(optiondefaults.defaultList)
59 | quiet = False
60 | dataTestMode = False
61 | for opt, arg in opts:
62 | if opt in ('-h', '--help'):
63 | printUsage()
64 | return
65 | if opt in ('-d', '--decimals'):
66 | try:
67 | decimals = int(arg)
68 | if 0 <= decimals <= unitgroup.UnitGroup.maxDecPlcs:
69 | options.changeData('DecimalPlaces', arg, False)
70 | except ValueError:
71 | pass
72 | elif opt in ('-f', '--fixed-decimals'):
73 | options.changeData('Notation', 'fixed', False)
74 | elif opt in ('-s', '--sci-notation'):
75 | options.changeData('Notation', 'scientific', False)
76 | elif opt in ('-e', '--eng-notation'):
77 | options.changeData('Notation', 'engineering', False)
78 | elif opt in ('-q', '--quiet'):
79 | quiet = True
80 | elif opt in ('-t', '--test'):
81 | dataTestMode = True
82 | data = unitdata.UnitData()
83 | try:
84 | data.readData()
85 | except unitdata.UnitDataError as text:
86 | print('Error in unit data - {0}'.format(text))
87 | sys.exit(1)
88 | if dataTestMode:
89 | unitDataTest(data, options)
90 | return
91 | numStr = '1.0'
92 | if args:
93 | numStr = args[0]
94 | try:
95 | float(numStr)
96 | del args[0]
97 | except (ValueError):
98 | numStr = '1.0'
99 | fromUnit = None
100 | try:
101 | fromUnit = getUnit(data, options, args.pop(0))
102 | except IndexError:
103 | pass
104 | if not fromUnit and quiet:
105 | return
106 | toUnit = None
107 | try:
108 | toUnit = getUnit(data, options, args[0])
109 | except IndexError:
110 | pass
111 | if not toUnit and quiet:
112 | return
113 | while True:
114 | while not fromUnit:
115 | text = _('Enter from unit -> ')
116 | fromText = input(text)
117 | if not fromText:
118 | return
119 | fromUnit = getUnit(data, options, fromText)
120 | while not toUnit:
121 | text = _('Enter to unit -> ')
122 | toText = input(text)
123 | if not toText:
124 | return
125 | toUnit = getUnit(data, options, toText)
126 | if fromUnit.categoryMatch(toUnit):
127 | badEntry = False
128 | while True:
129 | if not badEntry:
130 | newNumStr = fromUnit.convertStr(float(numStr), toUnit)
131 | print('{0} {1} = {2} {3}'.format(numStr,
132 | fromUnit.unitString(),
133 | newNumStr,
134 | toUnit.unitString()))
135 | if quiet:
136 | return
137 | badEntry = False
138 | text = _('Enter number, [n]ew, [r]everse or [q]uit -> ')
139 | rep = input(text)
140 | if not rep or rep[0] in ('q', 'Q'):
141 | return
142 | if rep[0] in ('r', 'R'):
143 | fromUnit, toUnit = toUnit, fromUnit
144 | elif rep[0] in ('n', 'N'):
145 | fromUnit = None
146 | toUnit = None
147 | numStr = '1.0'
148 | print()
149 | break
150 | else:
151 | try:
152 | float(rep)
153 | numStr = rep
154 | except ValueError:
155 | badEntry = True
156 | else:
157 | print(_('Units {0} and {1} are not compatible').
158 | format(fromUnit.unitString(), toUnit.unitString()))
159 | if quiet:
160 | return
161 | fromUnit = None
162 | toUnit = None
163 |
164 | def getUnit(data, options, text):
165 | """Create unit from text, check unit is valid,
166 | return reduced unit or None.
167 | """
168 | unit = unitgroup.UnitGroup(data, options)
169 | unit.update(text)
170 | if unit.groupValid():
171 | unit.reduceGroup()
172 | return unit
173 | print(_('{0} is not a valid unit').format(text))
174 | return None
175 |
176 | def printUsage():
177 | """Print usage text.
178 | """
179 | print('\n'.join(usage))
180 |
181 | def unitDataTest(data, options):
182 | """Run through a test of all units for consistent definitions,
183 | print results, return True if all pass.
184 | """
185 | badUnits = {}
186 | errorRegEx = re.compile(r'.*"(.*)"$')
187 | for unit in data.values():
188 | if not unit.unitValid():
189 | badUnits.setdefault(unit.name, []).append(unit.name)
190 | group = unitgroup.UnitGroup(data, options)
191 | group.replaceCurrent(unit)
192 | try:
193 | group.reduceGroup()
194 | except unitdata.UnitDataError as errorText:
195 | rootUnitName = errorRegEx.match(errorText).group(1)
196 | badUnits.setdefault(rootUnitName, []).append(unit.name)
197 | if not badUnits:
198 | print('All units pass tests')
199 | return True
200 | for key in sorted(badUnits.keys()):
201 | impacts = ', '.join(sorted(badUnits[key]))
202 | print('{0}\n Impacts: {1}\n'.format(key, impacts))
203 | return False
204 |
--------------------------------------------------------------------------------
/source/colorset.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # colorset.py, provides storage/retrieval and dialogs for system colors
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 |
15 | import enum
16 | from collections import OrderedDict
17 | from PyQt5.QtCore import pyqtSignal, Qt, QEvent, QObject
18 | from PyQt5.QtGui import QColor, QFontMetrics, QPalette, QPixmap
19 | from PyQt5.QtWidgets import (QApplication, QColorDialog, QComboBox, QDialog,
20 | QFrame, QGroupBox, QHBoxLayout, QLabel,
21 | QGridLayout, QPushButton, QVBoxLayout, qApp)
22 |
23 | roles = OrderedDict([('Window', _('Dialog background color')),
24 | ('WindowText', _('Dialog text color')),
25 | ('Base', _('Text widget background color')),
26 | ('Text', _('Text widget foreground color')),
27 | ('Highlight', _('Selected item background color')),
28 | ('HighlightedText', _('Selected item text color')),
29 | ('Button', _('Button background color')),
30 | ('ButtonText', _('Button text color')),
31 | ('Text-Disabled', _('Disabled text foreground color')),
32 | ('ButtonText-Disabled', _('Disabled button text color'))])
33 |
34 | ThemeSetting = enum.IntEnum('ThemeSetting', 'system dark custom')
35 |
36 | darkColors = {'Window': '#353535', 'WindowText': '#ffffff',
37 | 'Base': '#191919', 'Text': '#ffffff',
38 | 'Highlight': '#2a82da', 'HighlightedText': '#000000',
39 | 'Button': '#353535', 'ButtonText': '#ffffff',
40 | 'Text-Disabled': '#808080', 'ButtonText-Disabled': '#808080'}
41 |
42 | class ColorSet:
43 | """Stores color settings and provides dialogs for user changes.
44 | """
45 | def __init__(self, option):
46 | self.option = option
47 | self.sysPalette = QApplication.palette()
48 | self.colors = [Color(roleKey) for roleKey in roles.keys()]
49 | self.theme = ThemeSetting[self.option.strData('ColorTheme')]
50 | for color in self.colors:
51 | color.colorChanged.connect(self.setCustomTheme)
52 | color.setFromPalette(self.sysPalette)
53 | if self.theme == ThemeSetting.dark:
54 | color.setFromTheme(darkColors)
55 | elif self.theme == ThemeSetting.custom:
56 | color.setFromOption(self.option)
57 |
58 | def setAppColors(self):
59 | """Set application to current colors.
60 | """
61 | newPalette = QApplication.palette()
62 | for color in self.colors:
63 | color.updatePalette(newPalette)
64 | qApp.setPalette(newPalette)
65 |
66 |
67 | def showDialog(self, parent):
68 | """Show a dialog for user color changes.
69 |
70 | Return True if changes were made.
71 | """
72 | dialog = QDialog(parent)
73 | dialog.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
74 | Qt.WindowSystemMenuHint)
75 | dialog.setWindowTitle(_('Color Settings'))
76 | topLayout = QVBoxLayout(dialog)
77 | dialog.setLayout(topLayout)
78 | themeBox = QGroupBox(_('Color Theme'), dialog)
79 | topLayout.addWidget(themeBox)
80 | themeLayout = QVBoxLayout(themeBox)
81 | self.themeControl = QComboBox(dialog)
82 | self.themeControl.addItem(_('Default system theme'),
83 | ThemeSetting.system)
84 | self.themeControl.addItem(_('Dark theme'), ThemeSetting.dark)
85 | self.themeControl.addItem(_('Custom theme'), ThemeSetting.custom)
86 | self.themeControl.setCurrentIndex(self.themeControl.
87 | findData(self.theme))
88 | self.themeControl.currentIndexChanged.connect(self.updateThemeSetting)
89 | themeLayout.addWidget(self.themeControl)
90 | self.groupBox = QGroupBox(dialog)
91 | self.setBoxTitle()
92 | topLayout.addWidget(self.groupBox)
93 | gridLayout = QGridLayout(self.groupBox)
94 | row = 0
95 | for color in self.colors:
96 | gridLayout.addWidget(color.getLabel(), row, 0)
97 | gridLayout.addWidget(color.getSwatch(), row, 1)
98 | row += 1
99 | ctrlLayout = QHBoxLayout()
100 | topLayout.addLayout(ctrlLayout)
101 | ctrlLayout.addStretch(0)
102 | okButton = QPushButton(_('&OK'), dialog)
103 | ctrlLayout.addWidget(okButton)
104 | okButton.clicked.connect(dialog.accept)
105 | cancelButton = QPushButton(_('&Cancel'), dialog)
106 | ctrlLayout.addWidget(cancelButton)
107 | cancelButton.clicked.connect(dialog.reject)
108 | if dialog.exec_() == QDialog.Accepted:
109 | self.theme = ThemeSetting(self.themeControl.currentData())
110 | self.option.changeData('ColorTheme', self.theme.name, True)
111 | if self.theme == ThemeSetting.system:
112 | qApp.setPalette(self.sysPalette)
113 | else: # dark theme or custom
114 | if self.theme == ThemeSetting.custom:
115 | for color in self.colors:
116 | color.updateOption(self.option)
117 | self.setAppColors()
118 | else:
119 | for color in self.colors:
120 | color.setFromPalette(self.sysPalette)
121 | if self.theme == ThemeSetting.dark:
122 | color.setFromTheme(darkColors)
123 | elif self.theme == ThemeSetting.custom:
124 | color.setFromOption(self.option)
125 |
126 | def setBoxTitle(self):
127 | """Set title of group box to standard or custom.
128 | """
129 | if self.themeControl.currentData() == ThemeSetting.custom:
130 | title = _('Custom Colors')
131 | else:
132 | title = _('Theme Colors')
133 | self.groupBox.setTitle(title)
134 |
135 | def updateThemeSetting(self):
136 | """Update the colors based on a theme control change.
137 | """
138 | if self.themeControl.currentData() == ThemeSetting.system:
139 | for color in self.colors:
140 | color.setFromPalette(self.sysPalette)
141 | color.changeSwatchColor()
142 | elif self.themeControl.currentData() == ThemeSetting.dark:
143 | for color in self.colors:
144 | color.setFromTheme(darkColors)
145 | color.changeSwatchColor()
146 | else:
147 | for color in self.colors:
148 | color.setFromOption(self.option)
149 | color.changeSwatchColor()
150 | self.setBoxTitle()
151 |
152 | def setCustomTheme(self):
153 | """Set to custom theme setting after user color change.
154 | """
155 | if self.themeControl.currentData != ThemeSetting.custom:
156 | self.themeControl.blockSignals(True)
157 | self.themeControl.setCurrentIndex(2)
158 | self.themeControl.blockSignals(False)
159 | self.setBoxTitle()
160 |
161 |
162 | class Color(QObject):
163 | """Stores a single color setting for a role.
164 | """
165 | colorChanged = pyqtSignal()
166 | def __init__(self, roleKey, parent=None):
167 | super().__init__(parent)
168 | self.roleKey = roleKey
169 | if '-' in roleKey:
170 | roleStr, groupStr = roleKey.split('-')
171 | self.group = eval('QPalette.' + groupStr)
172 | else:
173 | roleStr = roleKey
174 | self.group = None
175 | self.role = eval('QPalette.' + roleStr)
176 | self.currentColor = None
177 | self.swatch = None
178 |
179 | def setFromPalette(self, palette):
180 | """Set the color based on the given palette.
181 | """
182 | if self.group:
183 | self.currentColor = palette.color(self.group, self.role)
184 | else:
185 | self.currentColor = palette.color(self.role)
186 |
187 | def setFromOption(self, option):
188 | """Set color based on the option setting.
189 | """
190 | colorStr = '#' + option.strData(self.roleKey + 'Color', True)
191 | color = QColor(colorStr)
192 | if color.isValid():
193 | self.currentColor = color
194 |
195 | def setFromTheme(self, theme):
196 | """Set color based on the given theme dictionary.
197 | """
198 | self.currentColor = QColor(theme[self.roleKey])
199 |
200 | def updateOption(self, option):
201 | """Set the option to the current color.
202 | """
203 | if self.currentColor:
204 | colorStr = self.currentColor.name().lstrip('#')
205 | option.changeData(self.roleKey + 'Color', colorStr, True)
206 |
207 | def updatePalette(self, palette):
208 | """Set the role in the given palette to the current color.
209 | """
210 | if self.group:
211 | palette.setColor(self.group, self.role, self.currentColor)
212 | else:
213 | palette.setColor(self.role, self.currentColor)
214 |
215 | def getLabel(self):
216 | """Return a label for this role in a dialog.
217 | """
218 | return QLabel(roles[self.roleKey])
219 |
220 | def getSwatch(self):
221 | """Return a label color swatch with the current color.
222 | """
223 | self.swatch = QLabel()
224 | self.changeSwatchColor()
225 | self.swatch.setFrameStyle(QFrame.Panel | QFrame.Raised)
226 | self.swatch.setLineWidth(3)
227 | self.swatch.installEventFilter(self)
228 | return self.swatch
229 |
230 | def changeSwatchColor(self):
231 | """Set swatch to currentColor.
232 | """
233 | height = QFontMetrics(self.swatch.font()).height()
234 | pixmap = QPixmap(3 * height, height)
235 | pixmap.fill(self.currentColor)
236 | self.swatch.setPixmap(pixmap)
237 |
238 | def eventFilter(self, obj, event):
239 | """Handle mouse clicks on swatches.
240 | """
241 | if obj == self.swatch and event.type() == QEvent.MouseButtonRelease:
242 | color = QColorDialog.getColor(self.currentColor,
243 | QApplication.activeWindow(),
244 | _('Select {0} color').
245 | format(self.roleKey))
246 | if color.isValid() and color != self.currentColor:
247 | self.currentColor = color
248 | self.changeSwatchColor()
249 | self.colorChanged.emit()
250 | return True
251 | return False
252 |
--------------------------------------------------------------------------------
/source/convertall.pro:
--------------------------------------------------------------------------------
1 | SOURCES = cmdline.py \
2 | convertall.py \
3 | convertdlg.py \
4 | helpview.py \
5 | icondict.py \
6 | numedit.py \
7 | optiondefaults.py \
8 | optiondlg.py \
9 | option.py \
10 | setup.py \
11 | unitatom.py \
12 | unitdata.py \
13 | unitedit.py \
14 | unitgroup.py \
15 | unitlistview.py
16 |
17 | TRANSLATIONS = convertall_es.ts \
18 | convertall_de.ts \
19 | convertall_fr.ts \
20 | convertall_it.ts \
21 | convertall_ru.ts \
22 | convertall_xx.ts
23 |
--------------------------------------------------------------------------------
/source/convertall.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | ****************************************************************************
4 | convertall.py, the main program file
5 |
6 | ConvertAll, a units conversion program
7 | Copyright (C) 2020, 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 | """
15 |
16 | __progname__ = 'ConvertAll'
17 | __version__ = '0.8.0'
18 | __author__ = 'Doug Bell'
19 |
20 | dataFilePath = None # modified by install script if required
21 | helpFilePath = None # modified by install script if required
22 | iconPath = None # modified by install script if required
23 | translationPath = 'translations'
24 | lang = ''
25 |
26 | import sys
27 | import os.path
28 | import locale
29 | import getopt
30 | import signal
31 | import builtins
32 | from PyQt5.QtCore import (QCoreApplication, QTranslator)
33 | from PyQt5.QtWidgets import QApplication
34 |
35 | def loadTranslator(fileName, app):
36 | """Load and install qt translator, return True if sucessful.
37 | """
38 | translator = QTranslator(app)
39 | modPath = os.path.abspath(sys.path[0])
40 | if modPath.endswith('.zip'): # for py2exe
41 | modPath = os.path.dirname(modPath)
42 | path = os.path.join(modPath, translationPath)
43 | result = translator.load(fileName, path)
44 | if not result:
45 | path = os.path.join(modPath, '..', translationPath)
46 | result = translator.load(fileName, path)
47 | if not result:
48 | path = os.path.join(modPath, '..', 'i18n', translationPath)
49 | result = translator.load(fileName, path)
50 | if result:
51 | QCoreApplication.installTranslator(translator)
52 | return True
53 | else:
54 | print('Warning: translation file "{0}" could not be loaded'.
55 | format(fileName))
56 | return False
57 |
58 | def setupTranslator(app):
59 | """Set language, load translators and setup translator function.
60 | """
61 | try:
62 | locale.setlocale(locale.LC_ALL, '')
63 | except locale.Error:
64 | pass
65 | global lang
66 | lang = os.environ.get('LC_MESSAGES', '')
67 | if not lang:
68 | lang = os.environ.get('LANG', '')
69 | if not lang:
70 | try:
71 | lang = locale.getdefaultlocale()[0]
72 | except ValueError:
73 | pass
74 | if not lang:
75 | lang = ''
76 | numTranslators = 0
77 | if lang and lang[:2] not in ['C', 'en']:
78 | numTranslators += loadTranslator('qt_{0}'.format(lang), app)
79 | numTranslators += loadTranslator('convertall_{0}'.format(lang), app)
80 |
81 | def translate(text, comment=''):
82 | """Translation function that sets context to calling module's
83 | filename.
84 | """
85 | try:
86 | frame = sys._getframe(1)
87 | fileName = frame.f_code.co_filename
88 | finally:
89 | del frame
90 | context = os.path.basename(os.path.splitext(fileName)[0])
91 | return QCoreApplication.translate(context, text, comment)
92 |
93 | def markNoTranslate(text, comment=''):
94 | return text
95 |
96 | if numTranslators:
97 | builtins._ = translate
98 | else:
99 | builtins._ = markNoTranslate
100 |
101 |
102 | def main():
103 | if len(sys.argv) > 1:
104 | try:
105 | opts, args = getopt.gnu_getopt(sys.argv, 'd:fhiqset',
106 | ['decimals=', 'fixed-decimals',
107 | 'help', 'interactive', 'quiet',
108 | 'sci-notation', 'eng-notation',
109 | 'test'])
110 | except getopt.GetoptError:
111 | # check that arguments aren't Qt GUI options
112 | if sys.argv[1][:3] not in ['-ba', '-bg', '-bt', '-bu', '-cm',
113 | '-di', '-do', '-fg', '-fn', '-fo',
114 | '-ge', '-gr', '-im', '-in', '-na',
115 | '-nc', '-no', '-re', '-se', '-st',
116 | '-sy', '-ti', '-vi', '-wi']:
117 | app = QCoreApplication(sys.argv)
118 | setupTranslator(app)
119 | import cmdline
120 | cmdline.printUsage()
121 | sys.exit(2)
122 | else:
123 | app = QCoreApplication(sys.argv)
124 | setupTranslator(app)
125 | import cmdline
126 | try:
127 | cmdline.parseArgs(opts, args[1:])
128 | except KeyboardInterrupt:
129 | pass
130 | return
131 | userStyle = '-style' in ' '.join(sys.argv)
132 | app = QApplication(sys.argv)
133 | setupTranslator(app) # must be before importing any convertall modules
134 | import convertdlg
135 | if not userStyle:
136 | QApplication.setStyle('fusion')
137 | win = convertdlg.ConvertDlg()
138 | win.show()
139 | signal.signal(signal.SIGINT, signal.SIG_IGN)
140 | app.exec_()
141 |
142 |
143 | if __name__ == '__main__':
144 | main()
145 |
--------------------------------------------------------------------------------
/source/convertall.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 |
3 | #******************************************************************************
4 | # convertall.spec, provides settings for use with PyInstaller
5 | #
6 | # Creates a standalone windows executable
7 | #
8 | # Run the build process by running the command 'pyinstaller convertall.spec'
9 | #
10 | # If everything works well you should find a 'dist/convertall' subdirectory
11 | # that contains the files needed to run the application
12 | #
13 | # ConvertAll, an information storage program
14 | # Copyright (C) 2019, Douglas W. Bell
15 | #
16 | # This is free software; you can redistribute it and/or modify it under the
17 | # terms of the GNU General Public License, either Version 2 or any later
18 | # version. This program is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY. See the included LICENSE file for details.
20 | #******************************************************************************
21 |
22 | block_cipher = None
23 |
24 | extraFiles = [('../data', 'data'),
25 | ('../doc', 'doc'),
26 | ('../icons', 'icons'),
27 | ('../source/*.py', 'source'),
28 | ('../source/*.pro', 'source'),
29 | ('../source/*.spec', 'source'),
30 | ('../translations', 'translations'),
31 | ('../win/*.*', '.')]
32 |
33 | a = Analysis(['convertall.py'],
34 | pathex=['C:\\git\\convertall\\devel\\source'],
35 | binaries=[],
36 | datas=extraFiles,
37 | hiddenimports=[],
38 | hookspath=[],
39 | runtime_hooks=[],
40 | excludes=[],
41 | win_no_prefer_redirects=False,
42 | win_private_assemblies=False,
43 | cipher=block_cipher,
44 | noarchive=False)
45 | pyz = PYZ(a.pure, a.zipped_data,
46 | cipher=block_cipher)
47 | exe = EXE(pyz,
48 | a.scripts,
49 | [],
50 | exclude_binaries=True,
51 | name='convertall',
52 | debug=False,
53 | bootloader_ignore_signals=False,
54 | strip=False,
55 | upx=True,
56 | console=False,
57 | icon='..\\win\\convertall.ico')
58 | a.binaries = a.binaries - TOC([('d3dcompiler_47.dll', None, None),
59 | ('libcrypto-1_1.dll', None, None),
60 | ('libeay32.dll', None, None),
61 | ('libglesv2.dll', None, None),
62 | ('libssl-1_1.dll', None, None),
63 | ('opengl32sw.dll', None, None),
64 | ('qt5dbus.dll', None, None),
65 | ('qt5network.dll', None, None),
66 | ('qt5qml.dll', None, None),
67 | ('qt5qmlmodels.dll', None, None),
68 | ('qt5quick.dll', None, None),
69 | ('qt5websockets.dll', None, None)])
70 | coll = COLLECT(exe,
71 | a.binaries,
72 | a.zipfiles,
73 | a.datas,
74 | strip=False,
75 | upx=True,
76 | name='convertall')
77 |
--------------------------------------------------------------------------------
/source/fontset.py:
--------------------------------------------------------------------------------
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 |
15 | from PyQt5.QtCore import (QSize, Qt)
16 | from PyQt5.QtGui import (QFontDatabase, QFontInfo, QIntValidator)
17 | from PyQt5.QtWidgets import (QAbstractItemView, QCheckBox, QDialog,
18 | QGridLayout, QGroupBox, QHBoxLayout, QLabel,
19 | QLineEdit, QListWidget, QPushButton, QVBoxLayout)
20 |
21 |
22 | class 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 |
245 | class 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 |
--------------------------------------------------------------------------------
/source/helpview.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # helpview.py, provides a window for viewing an html help file
5 | #
6 | # Copyright (C) 2016, Douglas W. Bell
7 | #
8 | # This is free software; you can redistribute it and/or modify it under the
9 | # terms of the GNU General Public License, either Version 2 or any later
10 | # version. This program is distributed in the hope that it will be useful,
11 | # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details.
12 | #*****************************************************************************
13 |
14 | import os.path
15 | import sys
16 | import webbrowser
17 | from PyQt5.QtCore import (QUrl, Qt)
18 | from PyQt5.QtGui import QTextDocument
19 | from PyQt5.QtWidgets import (QAction, QLabel, QLineEdit, QMainWindow, QMenu,
20 | QStatusBar, QTextBrowser)
21 |
22 |
23 | class HelpView(QMainWindow):
24 | """Main window for viewing an html help file.
25 | """
26 | def __init__(self, path, caption, icons, parent=None):
27 | """Helpview initialize with text.
28 | """
29 | QMainWindow.__init__(self, parent)
30 | self.setAttribute(Qt.WA_QuitOnClose, False)
31 | self.setWindowFlags(Qt.Window)
32 | self.setStatusBar(QStatusBar())
33 | self.textView = HelpViewer(self)
34 | self.setCentralWidget(self.textView)
35 | path = os.path.abspath(path)
36 | if sys.platform.startswith('win'):
37 | path = path.replace('\\', '/')
38 | self.textView.setSearchPaths([os.path.dirname(path)])
39 | self.textView.setSource(QUrl('file:///{0}'.format(path)))
40 | self.resize(520, 440)
41 | self.setWindowTitle(caption)
42 | tools = self.addToolBar('Tools')
43 | self.menu = QMenu(self.textView)
44 | self.textView.highlighted[str].connect(self.showLink)
45 |
46 | backAct = QAction(_('&Back'), self)
47 | backAct.setIcon(icons['helpback'])
48 | tools.addAction(backAct)
49 | self.menu.addAction(backAct)
50 | backAct.triggered.connect(self.textView.backward)
51 | backAct.setEnabled(False)
52 | self.textView.backwardAvailable.connect(backAct.setEnabled)
53 |
54 | forwardAct = QAction(_('&Forward'), self)
55 | forwardAct.setIcon(icons['helpforward'])
56 | tools.addAction(forwardAct)
57 | self.menu.addAction(forwardAct)
58 | forwardAct.triggered.connect(self.textView.forward)
59 | forwardAct.setEnabled(False)
60 | self.textView.forwardAvailable.connect(forwardAct.setEnabled)
61 |
62 | homeAct = QAction(_('&Home'), self)
63 | homeAct.setIcon(icons['helphome'])
64 | tools.addAction(homeAct)
65 | self.menu.addAction(homeAct)
66 | homeAct.triggered.connect(self.textView.home)
67 |
68 | tools.addSeparator()
69 | tools.addSeparator()
70 | findLabel = QLabel(' {0}: '.format(_('Find')), self)
71 | tools.addWidget(findLabel)
72 | self.findEdit = QLineEdit(self)
73 | tools.addWidget(self.findEdit)
74 | self.findEdit.textEdited.connect(self.findTextChanged)
75 | self.findEdit.returnPressed.connect(self.findNext)
76 |
77 | self.findPreviousAct = QAction(_('Find &Previous'), self)
78 | self.findPreviousAct.setIcon(icons['helpprevious'])
79 | tools.addAction(self.findPreviousAct)
80 | self.menu.addAction(self.findPreviousAct)
81 | self.findPreviousAct.triggered.connect(self.findPrevious)
82 | self.findPreviousAct.setEnabled(False)
83 |
84 | self.findNextAct = QAction(_('Find &Next'), self)
85 | self.findNextAct.setIcon(icons['helpnext'])
86 | tools.addAction(self.findNextAct)
87 | self.menu.addAction(self.findNextAct)
88 | self.findNextAct.triggered.connect(self.findNext)
89 | self.findNextAct.setEnabled(False)
90 |
91 | def showLink(self, text):
92 | """Send link text to the statusbar.
93 | """
94 | self.statusBar().showMessage(text)
95 |
96 | def findTextChanged(self, text):
97 | """Update find controls based on text in text edit.
98 | """
99 | self.findPreviousAct.setEnabled(len(text) > 0)
100 | self.findNextAct.setEnabled(len(text) > 0)
101 |
102 | def findPrevious(self):
103 | """Command to find the previous string.
104 | """
105 | if self.textView.find(self.findEdit.text(),
106 | QTextDocument.FindBackward):
107 | self.statusBar().clearMessage()
108 | else:
109 | self.statusBar().showMessage(_('Text string not found'))
110 |
111 | def findNext(self):
112 | """Command to find the next string.
113 | """
114 | if self.textView.find(self.findEdit.text()):
115 | self.statusBar().clearMessage()
116 | else:
117 | self.statusBar().showMessage(_('Text string not found'))
118 |
119 |
120 | class HelpViewer(QTextBrowser):
121 | """Shows an html help file.
122 | """
123 | def __init__(self, parent=None):
124 | QTextBrowser.__init__(self, parent)
125 |
126 | def setSource(self, url):
127 | """Called when user clicks on a URL.
128 | """
129 | name = url.toString()
130 | if name.startswith('http'):
131 | webbrowser.open(name, True)
132 | else:
133 | QTextBrowser.setSource(self, QUrl(name))
134 |
135 | def contextMenuEvent(self, event):
136 | """Init popup menu on right click"".
137 | """
138 | self.parentWidget().menu.exec_(event.globalPos())
139 |
--------------------------------------------------------------------------------
/source/icondict.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # icondict.py, provides a class to load and store icons
5 | #
6 | # Copyright (C) 2016, Douglas W. Bell
7 | #
8 | # This is free software; you can redistribute it and/or modify it under the
9 | # terms of the GNU General Public License, either Version 2 or any later
10 | # version. This program is distributed in the hope that it will be useful,
11 | # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details.
12 | #*****************************************************************************
13 |
14 | import os.path
15 | from PyQt5.QtGui import (QIcon, QPixmap)
16 |
17 | class IconDict(dict):
18 | """Stores icons by name, loads on demand.
19 | """
20 | iconExt = ['.png', '.bmp']
21 | def __init__(self):
22 | dict.__init__(self, {})
23 | self.pathList = []
24 |
25 | def addIconPath(self, potentialPaths):
26 | """Add first good path from potentialPaths.
27 | """
28 | for path in potentialPaths:
29 | try:
30 | for name in os.listdir(path):
31 | pixmap = QPixmap(os.path.join(path, name))
32 | if not pixmap.isNull():
33 | self.pathList.append(path)
34 | return
35 | except OSError:
36 | pass
37 |
38 | def __getitem__(self, name):
39 | """Return icon, loading if necessary.
40 | """
41 | try:
42 | return dict.__getitem__(self, name)
43 | except KeyError:
44 | icon = self.loadIcon(name)
45 | if not icon:
46 | raise
47 | return icon
48 |
49 | def loadAllIcons(self):
50 | """Load all icons available in self.pathList.
51 | """
52 | self.clear()
53 | for path in self.pathList:
54 | try:
55 | for name in os.listdir(path):
56 | pixmap = QPixmap(os.path.join(path, name))
57 | if not pixmap.isNull():
58 | name = os.path.splitext(name)[0]
59 | try:
60 | icon = self[name]
61 | except KeyError:
62 | icon = QIcon()
63 | self[name] = icon
64 | icon.addPixmap(pixmap)
65 | except OSError:
66 | pass
67 |
68 | def loadIcon(self, iconName):
69 | """Load icon from iconPath, add to dictionary and return the icon.
70 | """
71 | icon = QIcon()
72 | for path in self.pathList:
73 | for ext in IconDict.iconExt:
74 | fileName = iconName + ext
75 | pixmap = QPixmap(os.path.join(path, fileName))
76 | if not pixmap.isNull():
77 | icon.addPixmap(pixmap)
78 | if not icon.isNull():
79 | self[iconName] = icon
80 | return icon
81 | return None
82 |
--------------------------------------------------------------------------------
/source/numedit.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # numedit.py, provides a number entry editor
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 |
15 | import re
16 | import sys
17 | from PyQt5.QtCore import pyqtSignal
18 | from PyQt5.QtGui import QValidator
19 | from PyQt5.QtWidgets import (QLineEdit, QMessageBox)
20 | import unitdata
21 |
22 |
23 | class NumEdit(QLineEdit):
24 | """Number entry editor.
25 | """
26 | convertRqd = pyqtSignal()
27 | convertNum = pyqtSignal(str)
28 | gotFocus = pyqtSignal()
29 | def __init__(self, thisUnit, otherUnit, label, status, recentUnits,
30 | primary, parent=None):
31 | super().__init__(parent)
32 | self.thisUnit = thisUnit
33 | self.otherUnit = otherUnit
34 | self.label = label
35 | self.status = status
36 | self.recentUnits = recentUnits
37 | self.primary = primary
38 | self.onLeft = primary
39 | self.setValidator(FloatExprValidator(self))
40 | self.setText(self.thisUnit.formatNumStr(1.0))
41 | self.textEdited.connect(self.convert)
42 |
43 | def unitUpdate(self):
44 | """Update the editor and labels based on a unit change.
45 | """
46 | if self.thisUnit.groupValid():
47 | self.label.setText(self.thisUnit.unitString())
48 | if self.otherUnit.groupValid():
49 | try:
50 | self.thisUnit.reduceGroup()
51 | self.otherUnit.reduceGroup()
52 | except unitdata.UnitDataError as text:
53 | QMessageBox.warning(self, 'ConvertAll',
54 | _('Error in unit data - {0}').
55 | format(text))
56 | return
57 | if self.thisUnit.categoryMatch(self.otherUnit):
58 | self.status.setText(_('Converting...'))
59 | if self.primary:
60 | self.convert()
61 | else:
62 | self.convertRqd.emit()
63 | return
64 | if self.onLeft:
65 | self.status.setText(_('Units are not compatible '
66 | '({0} vs. {1})').
67 | format(self.thisUnit.compatStr(),
68 | self.otherUnit.compatStr()))
69 | else:
70 | self.status.setText(_('Units are not compatible '
71 | '({0} vs. {1})').
72 | format(self.otherUnit.compatStr(),
73 | self.thisUnit.compatStr()))
74 | else:
75 | self.status.setText(_('Set units'))
76 | else:
77 | self.status.setText(_('Set units'))
78 | self.label.setText(_('No Unit Set'))
79 | self.setEnabled(False)
80 | self.convertNum.emit('')
81 |
82 | def convert(self):
83 | """Do conversion with self primary.
84 | """
85 | self.primary = True
86 | self.setEnabled(True)
87 | if self.onLeft:
88 | self.recentUnits.addEntry(self.otherUnit.unitString())
89 | self.recentUnits.addEntry(self.thisUnit.unitString())
90 | else:
91 | self.recentUnits.addEntry(self.thisUnit.unitString())
92 | self.recentUnits.addEntry(self.otherUnit.unitString())
93 | try:
94 | num = float(eval(self.text()))
95 | except:
96 | self.convertNum.emit('')
97 | return
98 | try:
99 | numText = self.thisUnit.convertStr(num, self.otherUnit)
100 | self.convertNum.emit(numText)
101 | except unitdata.UnitDataError as text:
102 | QMessageBox.warning(self, 'ConvertAll',
103 | _('Error in unit data - {0}').
104 | format(text))
105 |
106 | def setNum(self, numText):
107 | """Set text based on conversion from other number editor.
108 | """
109 | if not numText:
110 | self.setEnabled(False)
111 | else:
112 | self.primary = False
113 | self.setEnabled(True)
114 | self.setText(numText)
115 |
116 | def focusInEvent(self, event):
117 | """Signal that this number editor received focus.
118 | """
119 | super().focusInEvent(event)
120 | self.gotFocus.emit()
121 |
122 |
123 | class FloatExprValidator(QValidator):
124 | """Validator for float python expressions typed into NumEdit.
125 | """
126 | invalidRe = re.compile(r'[^\d\.eE\+\-\*/\(\) ]')
127 | def __init__(self, parent):
128 | super().__init__(parent)
129 |
130 | def validate(self, inputStr, pos):
131 | """Check for valid characters in entry.
132 | """
133 | if FloatExprValidator.invalidRe.search(inputStr):
134 | return (QValidator.Invalid, inputStr, pos)
135 | return (QValidator.Acceptable, inputStr, pos)
136 |
--------------------------------------------------------------------------------
/source/option.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # option.py, provides classes to read and set user preferences
5 | #
6 | # Copyright (C) 2014, Douglas W. Bell
7 | #
8 | # This is free software; you can redistribute it and/or modify it under the
9 | # terms of the GNU General Public License, either Version 2 or any later
10 | # version. This program is distributed in the hope that it will be useful,
11 | # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details.
12 | #*****************************************************************************
13 |
14 | import sys
15 | import os.path
16 |
17 | class Option:
18 | """Stores and retrieves string options.
19 | """
20 | def __init__(self, baseFileName, keySpaces=20):
21 | self.path = ''
22 | if baseFileName:
23 | if sys.platform.startswith('win'):
24 | fileName = '{0}.ini'.format(baseFileName)
25 | userPath = os.environ.get('APPDATA', '')
26 | if userPath:
27 | userPath = os.path.join(userPath, 'bellz', baseFileName)
28 | else:
29 | fileName = '.{0}'.format(baseFileName)
30 | userPath = os.environ.get('HOME', '')
31 | self.path = os.path.join(userPath, fileName)
32 | if not os.path.exists(self.path):
33 | modPath = os.path.abspath(sys.path[0])
34 | if modPath.endswith('.zip') or modPath.endswith('.exe'):
35 | modPath = os.path.dirname(modPath) # for py2exe/cx_freeze
36 | self.path = os.path.join(modPath, fileName)
37 | if not os.access(self.path, os.W_OK):
38 | self.path = os.path.join(userPath, fileName)
39 | if not os.path.exists(userPath):
40 | try:
41 | os.makedirs(userPath)
42 | except OSError:
43 | print('Error - could not write to config dir')
44 | self.path = ''
45 | self.keySpaces = keySpaces
46 | self.dfltDict = {}
47 | self.userDict = {}
48 | self.dictList = (self.userDict, self.dfltDict)
49 | self.chgList = []
50 |
51 | def loadAll(self, defaultList):
52 | """Reads defaultList & file, writes file if required
53 | return true if file read.
54 | """
55 | self.loadSet(defaultList, self.dfltDict)
56 | if self.path:
57 | try:
58 | with open(self.path, 'r', encoding='utf-8') as f:
59 | self.loadSet(f.readlines(), self.userDict)
60 | return True
61 | except IOError:
62 | try:
63 | with open(self.path, 'w', encoding='utf-8') as f:
64 | f.writelines([line + '\n' for line in defaultList])
65 | except IOError:
66 | print('Error - could not write to config file', self.path)
67 | self.path = ''
68 | return False
69 |
70 | def loadSet(self, list, data):
71 | """Reads settings from list into dict.
72 | """
73 | for line in list:
74 | line = line.split('#', 1)[0].strip()
75 | if line:
76 | item = line.split(None, 1) + [''] # add value if blank
77 | data[item[0]] = item[1].strip()
78 |
79 | def addData(self, key, strData, storeChange=0):
80 | """Add new entry, add to write list if storeChange.
81 | """
82 | self.userDict[key] = strData
83 | if storeChange:
84 | self.chgList.append(key)
85 |
86 | def boolData(self, key):
87 | """Returns true or false from yes or no in option data.
88 | """
89 | for data in self.dictList:
90 | val = data.get(key)
91 | if val and val[0] in ('y', 'Y'):
92 | return True
93 | if val and val[0] in ('n', 'N'):
94 | return False
95 | print('Option error - bool key', key, 'is not valid')
96 | return False
97 |
98 | def numData(self, key, min=None, max=None):
99 | """Return float from option data.
100 | """
101 | for data in self.dictList:
102 | val = data.get(key)
103 | if val:
104 | try:
105 | num = float(val)
106 | if (min == None or num >= min) and \
107 | (max == None or num <= max):
108 | return num
109 | except ValueError:
110 | pass
111 | print('Option error - float key', key, 'is not valid')
112 | return 0
113 |
114 | def intData(self, key, min=None, max=None):
115 | """Return int from option data.
116 | """
117 | for data in self.dictList:
118 | val = data.get(key)
119 | if val:
120 | try:
121 | num = int(val)
122 | if (min == None or num >= min) and \
123 | (max == None or num <= max):
124 | return num
125 | except ValueError:
126 | pass
127 | print('Option error - int key', key, 'is not valid')
128 | return 0
129 |
130 | def strData(self, key, emptyOk=0):
131 | """Return string from option data.
132 | """
133 | for data in self.dictList:
134 | val = data.get(key)
135 | if val != None:
136 | if val or emptyOk:
137 | return val
138 | print('Option error - string key', key, 'is not valid')
139 | return ''
140 |
141 | def changeData(self, key, strData, storeChange):
142 | """Change entry, add to write list if storeChange
143 | Return true if changed.
144 | """
145 | for data in self.dictList:
146 | val = data.get(key)
147 | if val != None:
148 | if strData == val: # no change reqd
149 | return False
150 | self.userDict[key] = strData
151 | if storeChange:
152 | self.chgList.append(key)
153 | return True
154 | print('Option error - key', key, 'is not valid')
155 | return False
156 |
157 | def writeChanges(self):
158 | """Write any stored changes to the option file - rtn true on success.
159 | """
160 | if self.path and self.chgList:
161 | try:
162 | with open(self.path, 'r', encoding='utf-8') as f:
163 | fileList = f.readlines()
164 | for key in self.chgList[:]:
165 | hitList = [line for line in fileList if
166 | line.strip().split(None, 1)[:1] == [key]]
167 | if not hitList:
168 | hitList = [line for line in fileList if
169 | line.replace('#', ' ', 1).strip().
170 | split(None, 1)[:1] == [key]]
171 | if hitList:
172 | fileList[fileList.index(hitList[-1])] = '{0}{1}\n'.\
173 | format(key.ljust(self.keySpaces),
174 | self.userDict[key])
175 | self.chgList.remove(key)
176 | for key in self.chgList:
177 | fileList.append('{0}{1}\n'.format(key.ljust(self.keySpaces),
178 | self.userDict[key]))
179 | with open(self.path, 'w', encoding='utf-8') as f:
180 | f.writelines([line for line in fileList])
181 | return True
182 | except IOError:
183 | print('Error - could not write to config file', self.path)
184 | return False
185 |
--------------------------------------------------------------------------------
/source/optiondefaults.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # optiondefaults.py, provides defaults for program options
5 | #
6 | # ConvertAll, a units conversion program
7 | # Copyright (C) 2020, 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 |
15 |
16 | defaultList = [
17 | "# Options for the ConvertAll unit conversion program",
18 | "#",
19 | "# All options are set from within the program,",
20 | "# editing here is not recommended",
21 | "#",
22 | "ColorTheme system",
23 | "WindowColor ",
24 | "WindowTextColor ",
25 | "BaseColor ",
26 | "TextColor ",
27 | "HighlightColor ",
28 | "HighlightedTextColor ",
29 | "ButtonColor ",
30 | "ButtonTextColor ",
31 | "Text-DisabledColor ",
32 | "ButtonText-DisabledColor ",
33 | "GuiFont ",
34 | "#",
35 | "DecimalPlaces 8",
36 | "Notation general",
37 | "ShowOpButtons yes",
38 | "ShowUnitButtons yes",
39 | "RecentUnits 8",
40 | "LoadLastUnit no",
41 | "ShowStartupTip yes",
42 | "RemenberDlgPos yes",
43 | "#",
44 | "MainDlgXSize 0",
45 | "MainDlgYSize 0",
46 | "MainDlgXPos 0",
47 | "MainDlgYPos 0",
48 | "MainDlgTopMargin 0",
49 | "MainDlgOtherMargin 0",
50 | "#",
51 | "RecentUnit1 ",
52 | "RecentUnit2 ",
53 | "RecentUnit3 ",
54 | "RecentUnit4 ",
55 | "RecentUnit5 ",
56 | "RecentUnit6 ",
57 | "RecentUnit7 ",
58 | "RecentUnit8 "]
59 |
--------------------------------------------------------------------------------
/source/optiondlg.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # optiondlg.py, provides classes for option setting dialogs
5 | #
6 | # Copyright (C) 2016, Douglas W. Bell
7 | #
8 | # This is free software; you can redistribute it and/or modify it under the
9 | # terms of the GNU General Public License, either Version 2 or any later
10 | # version. This program is distributed in the hope that it will be useful,
11 | # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details.
12 | #*****************************************************************************
13 |
14 | import sys
15 | from PyQt5.QtCore import Qt
16 | from PyQt5.QtGui import (QDoubleValidator, QValidator)
17 | from PyQt5.QtWidgets import (QButtonGroup, QCheckBox, QDialog, QGridLayout,
18 | QGroupBox, QHBoxLayout, QLabel, QLineEdit,
19 | QPushButton, QRadioButton, QSpinBox, QVBoxLayout)
20 |
21 |
22 | class OptionDlg(QDialog):
23 | """Works with Option class to provide a dialog for pref/options.
24 | """
25 | def __init__(self, option, parent=None):
26 | QDialog.__init__(self, parent)
27 | self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
28 | Qt.WindowSystemMenuHint)
29 | self.option = option
30 |
31 | topLayout = QVBoxLayout(self)
32 | self.setLayout(topLayout)
33 | self.columnLayout = QHBoxLayout()
34 | topLayout.addLayout(self.columnLayout)
35 | self.gridLayout = QGridLayout()
36 | self.columnLayout.addLayout(self.gridLayout)
37 | self.oldLayout = self.gridLayout
38 |
39 | ctrlLayout = QHBoxLayout()
40 | topLayout.addLayout(ctrlLayout)
41 | ctrlLayout.addStretch(0)
42 | okButton = QPushButton(_('&OK'), self)
43 | ctrlLayout.addWidget(okButton)
44 | okButton.clicked.connect(self.accept)
45 | cancelButton = QPushButton(_('&Cancel'), self)
46 | ctrlLayout.addWidget(cancelButton)
47 | cancelButton.clicked.connect(self.reject)
48 | self.setWindowTitle(_('Preferences'))
49 | self.itemList = []
50 | self.curGroup = None
51 |
52 | def addItem(self, dlgItem, widget, label=None):
53 | """Add a control with optional label, called by OptionDlgItem.
54 | """
55 | row = self.gridLayout.rowCount()
56 | if label:
57 | self.gridLayout.addWidget(label, row, 0)
58 | self.gridLayout.addWidget(widget, row, 1)
59 | else:
60 | self.gridLayout.addWidget(widget, row, 0, 1, 2)
61 | self.itemList.append(dlgItem)
62 |
63 | def startGroupBox(self, title, intSpace=5):
64 | """Use a group box for next added items.
65 | """
66 | self.curGroup = QGroupBox(title, self)
67 | row = self.oldLayout.rowCount()
68 | self.oldLayout.addWidget(self.curGroup, row, 0, 1, 2)
69 | self.gridLayout = QGridLayout(self.curGroup)
70 | self.gridLayout.setVerticalSpacing(intSpace)
71 |
72 | def endGroupBox(self):
73 | """Cancel group box for next added items.
74 | """
75 | self.gridLayout = self.oldLayout
76 | self.curGroup = None
77 |
78 | def startNewColumn(self):
79 | """Cancel any group box and start a second column.
80 | """
81 | self.curGroup = None
82 | row = self.oldLayout.rowCount()
83 | self.gridLayout = QGridLayout()
84 | self.columnLayout.addLayout(self.gridLayout)
85 | self.oldLayout = self.gridLayout
86 |
87 | def parentGroup(self):
88 | """Return parent for new widgets.
89 | """
90 | if self.curGroup:
91 | return self.curGroup
92 | return self
93 |
94 | def accept(self):
95 | """Called by dialog when OK button pressed.
96 | """
97 | for item in self.itemList:
98 | item.updateData()
99 | QDialog.accept(self)
100 |
101 |
102 | class OptionDlgItem:
103 | """Base class for items to add to dialog.
104 | """
105 | def __init__(self, dlg, key, writeChg):
106 | self.dlg = dlg
107 | self.key = key
108 | self.writeChg = writeChg
109 | self.control = None
110 |
111 | def updateData(self):
112 | """Dummy update function.
113 | """
114 | pass
115 |
116 | class OptionDlgBool(OptionDlgItem):
117 | """Holds widget for bool checkbox.
118 | """
119 | def __init__(self, dlg, key, menuText, writeChg=True):
120 | OptionDlgItem.__init__(self, dlg, key, writeChg)
121 | self.control = QCheckBox(menuText, dlg.parentGroup())
122 | self.control.setChecked(dlg.option.boolData(key))
123 | dlg.addItem(self, self.control)
124 |
125 | def updateData(self):
126 | """Update Option class based on checkbox status.
127 | """
128 | if self.control.isChecked() != self.dlg.option.boolData(self.key):
129 | if self.control.isChecked():
130 | self.dlg.option.changeData(self.key, 'yes', self.writeChg)
131 | else:
132 | self.dlg.option.changeData(self.key, 'no', self.writeChg)
133 |
134 | class OptionDlgInt(OptionDlgItem):
135 | """Holds widget for int spinbox.
136 | """
137 | def __init__(self, dlg, key, menuText, min, max, writeChg=True, step=1,
138 | wrap=False, suffix=''):
139 | OptionDlgItem.__init__(self, dlg, key, writeChg)
140 | label = QLabel(menuText, dlg.parentGroup())
141 | self.control = QSpinBox(dlg.parentGroup())
142 | self.control.setMinimum(min)
143 | self.control.setMaximum(max)
144 | self.control.setSingleStep(step)
145 | self.control.setWrapping(wrap)
146 | self.control.setSuffix(suffix)
147 | self.control.setValue(dlg.option.intData(key, min, max))
148 | dlg.addItem(self, self.control, label)
149 |
150 | def updateData(self):
151 | """Update Option class based on spinbox status.
152 | """
153 | if self.control.value() != int(self.dlg.option.numData(self.key)):
154 | self.dlg.option.changeData(self.key, repr(self.control.value()),
155 | self.writeChg)
156 |
157 | class OptionDlgDbl(OptionDlgItem):
158 | """Holds widget for double line edit.
159 | """
160 | def __init__(self, dlg, key, menuText, min, max, writeChg=True):
161 | OptionDlgItem.__init__(self, dlg, key, writeChg)
162 | label = QLabel(menuText, dlg.parentGroup())
163 | self.control = QLineEdit(repr(dlg.option.numData(key, min, max)),
164 | dlg.parentGroup())
165 | valid = QDoubleValidator(min, max, 6, self.control)
166 | self.control.setValidator(valid)
167 | dlg.addItem(self, self.control, label)
168 |
169 | def updateData(self):
170 | """Update Option class based on edit status.
171 | """
172 | text = self.control.text()
173 | unusedPos = 0
174 | if self.control.validator().validate(text, unusedPos)[0] != \
175 | QValidator.Acceptable:
176 | return
177 | num = float(text)
178 | if num != self.dlg.option.numData(self.key):
179 | self.dlg.option.changeData(self.key, repr(num), self.writeChg)
180 |
181 | class OptionDlgStr(OptionDlgItem):
182 | """Holds widget for string line edit.
183 | """
184 | def __init__(self, dlg, key, menuText, writeChg=True):
185 | OptionDlgItem.__init__(self, dlg, key, writeChg)
186 | label = QLabel(menuText, dlg.parentGroup())
187 | self.control = QLineEdit(dlg.option.strData(key, True),
188 | dlg.parentGroup())
189 | dlg.addItem(self, self.control, label)
190 |
191 | def updateData(self):
192 | """Update Option class based on edit status.
193 | """
194 | newStr = self.control.text()
195 | if newStr != self.dlg.option.strData(self.key, True):
196 | self.dlg.option.changeData(self.key, newStr, self.writeChg)
197 |
198 | class OptionDlgRadio(OptionDlgItem):
199 | """Holds widget for exclusive radio button group.
200 | """
201 | def __init__(self, dlg, key, headText, textList, writeChg=True):
202 | # textList is list of tuples: optionText, labelText
203 | OptionDlgItem.__init__(self, dlg, key, writeChg)
204 | self.optionList = [x[0] for x in textList]
205 | buttonBox = QGroupBox(headText, dlg.parentGroup())
206 | self.control = QButtonGroup(buttonBox)
207 | layout = QVBoxLayout(buttonBox)
208 | buttonBox.setLayout(layout)
209 | optionSetting = dlg.option.strData(key)
210 | id = 0
211 | for optionText, labelText in textList:
212 | button = QRadioButton(labelText, buttonBox)
213 | layout.addWidget(button)
214 | self.control.addButton(button, id)
215 | id += 1
216 | if optionText == optionSetting:
217 | button.setChecked(True)
218 | dlg.addItem(self, buttonBox)
219 |
220 | def updateData(self):
221 | """Update Option class based on button status.
222 | """
223 | data = self.optionList[self.control.checkedId()]
224 | if data != self.dlg.option.strData(self.key):
225 | self.dlg.option.changeData(self.key, data, self.writeChg)
226 |
227 | class OptionDlgPush(OptionDlgItem):
228 | """Holds widget for extra misc. push button.
229 | """
230 | def __init__(self, dlg, text, cmd):
231 | OptionDlgItem.__init__(self, dlg, '', 0)
232 | self.control = QPushButton(text, dlg.parentGroup())
233 | self.control.clicked.connect(cmd)
234 | dlg.addItem(self, self.control)
235 |
--------------------------------------------------------------------------------
/source/recentunits.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # recentunits.py, provides a list of recently used units
5 | #
6 | # ConvertAll, a units conversion program
7 | # Copyright (C) 2014, 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 |
15 |
16 | class RecentUnits(list):
17 | """A list of recent unit combo names.
18 | """
19 | def __init__(self, options):
20 | list.__init__(self)
21 | self.options = options
22 | self.updateQuantity()
23 | self.loadList()
24 |
25 | def updateQuantity(self):
26 | """Update number of entries from options.
27 | """
28 | self.numEntries = self.options.intData('RecentUnits', 0, 99)
29 | del self[self.numEntries:]
30 |
31 | def loadList(self):
32 | """Load recent units from option file.
33 | """
34 | self[:] = []
35 | for num in range(self.numEntries):
36 | name = self.options.strData(self.optionTitle(num), True)
37 | if name:
38 | self.append(name)
39 |
40 | def writeList(self):
41 | """Write list of paths to options.
42 | """
43 | for num in range(self.numEntries):
44 | try:
45 | name = self[num]
46 | except IndexError:
47 | name = ''
48 | self.options.changeData(self.optionTitle(num), name, True)
49 | self.options.writeChanges()
50 |
51 | def addEntry(self, name):
52 | """Move name to start if found, otherwise add it.
53 | """
54 | try:
55 | self.remove(name)
56 | except ValueError:
57 | pass
58 | self.insert(0, name)
59 | del self[self.numEntries:]
60 |
61 | def optionTitle(self, num):
62 | """Return option key for the given nummber.
63 | """
64 | return 'RecentUnit{0}'.format(num + 1)
65 |
--------------------------------------------------------------------------------
/source/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # setup.py, provides a distutils script for use with cx_Freeze
5 | #
6 | # Creates a standalone windows executable
7 | #
8 | # Run the build process by running the command 'python setup.py build'
9 | #
10 | # If everything works well you should find a subdirectory in the build
11 | # subdirectory that contains the files needed to run the application
12 | #
13 | # ConvertAll, a units conversion program
14 | # Copyright (C) 2017, Douglas W. Bell
15 | #
16 | # This is free software; you can redistribute it and/or modify it under the
17 | # terms of the GNU General Public License, either Version 2 or any later
18 | # version. This program is distributed in the hope that it will be useful,
19 | # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details.
20 | #*****************************************************************************
21 |
22 | import sys
23 | from cx_Freeze import setup, Executable
24 | from convertall import __version__
25 |
26 | base = None
27 | if sys.platform == 'win32':
28 | base = 'Win32GUI'
29 |
30 | extraFiles = [('../data', 'data'), ('../doc', 'doc'), ('../icons', 'icons'),
31 | ('../source', 'source'), ('../translations', 'translations'),
32 | ('../win', '.')]
33 |
34 | setup(name = 'convertall',
35 | version = __version__,
36 | description = 'ConvertAll, a units conversion program',
37 | options = {'build_exe': {'includes': 'atexit',
38 | 'include_files': extraFiles,
39 | 'excludes': ['*.pyc'],
40 | 'zip_include_packages': ['*'],
41 | 'zip_exclude_packages': [],
42 | 'include_msvcr': True,
43 | 'build_exe': '../../convertall-0.7'}},
44 | executables = [Executable('convertall.py', base=base,
45 | icon='../win/convertall.ico')])
46 |
--------------------------------------------------------------------------------
/source/unitatom.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # unitatom.py, provides class to hold data on each available unit
5 | #
6 | # ConvertAll, a units conversion program
7 | # Copyright (C) 2017, 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 |
15 | import re
16 | import copy
17 | import unitdata
18 |
19 |
20 | class UnitDatum:
21 | """Reads and stores data for a single unit, without an exponent.
22 | """
23 | badOpRegEx = re.compile(r'[^\d\.eE\+\-\*/]')
24 | eqnRegEx = re.compile(r'\[(.*?)\](.*)')
25 | def __init__(self, dataStr):
26 | """Initialize with a string from the data file.
27 | """
28 | dataList = dataStr.split('#')
29 | unitList = dataList.pop(0).split('=', 1)
30 | self.name = unitList.pop(0).strip()
31 | self.equiv = ''
32 | self.factor = 1.0
33 | self.fromEqn = '' # used only for non-linear units
34 | self.toEqn = '' # used only for non-linear units
35 | if unitList:
36 | self.equiv = unitList[0].strip()
37 | if self.equiv[0] == '[': # used only for non-linear units
38 | try:
39 | self.equiv, self.fromEqn = (UnitDatum.eqnRegEx.
40 | match(self.equiv).groups())
41 | if ';' in self.fromEqn:
42 | self.fromEqn, self.toEqn = self.fromEqn.split(';', 1)
43 | self.toEqn = self.toEqn.strip()
44 | self.fromEqn = self.fromEqn.strip()
45 | except AttributeError:
46 | raise unitdata.UnitDataError(_('Bad equation for "{0}"').
47 | format(self.name))
48 | else: # split factor and equiv unit for linear
49 | parts = self.equiv.split(None, 1)
50 | if (len(parts) > 1 and
51 | UnitDatum.badOpRegEx.search(parts[0]) == None):
52 | # only allowed digits and operators
53 | try:
54 | self.factor = float(eval(parts[0]))
55 | self.equiv = parts[1]
56 | except:
57 | pass
58 | self.comments = [comm.strip() for comm in dataList]
59 | self.comments.extend([''] * (2 - len(self.comments)))
60 | self.keyWords = self.name.lower().split()
61 | self.viewLink = None
62 | self.typeName = ''
63 |
64 | def description(self):
65 | """Return name and 1st comment (usu. full name) if applicable.
66 | """
67 | if self.comments[0]:
68 | return '{0} ({1})'.format(self.name, self.comments[0])
69 | return self.name
70 |
71 | def columnText(self, colNum):
72 | """Return text for given column number in the list view.
73 | """
74 | if colNum == 0:
75 | return self.description()
76 | if colNum == 1:
77 | return self.typeName
78 | return self.comments[1]
79 |
80 | def partialMatch(self, wordList):
81 | """Return True if parts of name start with items from wordList.
82 | """
83 | for word in wordList:
84 | for key in self.keyWords:
85 | if key.startswith(word):
86 | return True
87 | return False
88 |
89 | def __lt__(self, other):
90 | """Less than comparison for sorting.
91 | """
92 | return self.name.lower() < other.name.lower()
93 |
94 | def __eq__(self, other):
95 | """Equality test.
96 | """
97 | return self.name.lower() == other.name.lower()
98 |
99 |
100 | class UnitAtom:
101 | """Stores a unit datum or a temporary name with an exponent.
102 | """
103 | invalidExp = 1000
104 | def __init__(self, name='', unitDatum = None):
105 | """Initialize with either a text name or a unitDatum.
106 | """
107 | self.datum = None
108 | self.unitName = name
109 | self.exp = 1
110 | self.partialExp = '' # starts with '^' for incomplete exp
111 | if unitDatum:
112 | self.datum = unitDatum
113 | self.unitName = unitDatum.name
114 |
115 | def unitValid(self):
116 | """Return True if unit and exponent are valid.
117 | """
118 | if (self.datum and self.datum.equiv and
119 | abs(self.exp) < UnitAtom.invalidExp):
120 | return True
121 | return False
122 |
123 | def unitText(self, absExp=False):
124 | """Return text for unit name with exponent or absolute value of exp.
125 | """
126 | exp = self.exp
127 | if absExp:
128 | exp = abs(self.exp)
129 | if self.partialExp:
130 | return '{0}{1}'.format(self.unitName, self.partialExp)
131 | if exp == 1:
132 | return self.unitName
133 | return '{0}^{1}'.format(self.unitName, exp)
134 |
135 | def __lt__(self, other):
136 | """Less than comparison for sorting.
137 | """
138 | return self.unitName.lower() < other.unitName.lower()
139 |
140 | def __eq__(self, other):
141 | """Equality test.
142 | """
143 | return self.unitName.lower() == other.unitName.lower()
144 |
--------------------------------------------------------------------------------
/source/unitdata.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # unitdata.py, reads unit data from file
5 | #
6 | # ConvertAll, a units conversion program
7 | # Copyright (C) 2016, 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 |
15 | import sys
16 | import os.path
17 | import collections
18 | try:
19 | from __main__ import dataFilePath, lang
20 | except ImportError:
21 | dataFilePath = None
22 | lang = ''
23 | import unitatom
24 |
25 |
26 | class UnitDataError(Exception):
27 | """General exception for unit data problems.
28 | """
29 | pass
30 |
31 |
32 | class UnitData(collections.OrderedDict):
33 | """Reads unit data nad stores in a dictionary based on unit name.
34 | """
35 | def __init__(self):
36 | super().__init__()
37 | self.typeList = []
38 |
39 | def findDataFile(self):
40 | """Search for data file, return line list or None.
41 | """
42 | modPath = os.path.abspath(sys.path[0])
43 | if modPath.endswith('.zip'): # for py2exe
44 | modPath = os.path.dirname(modPath)
45 | pathList = [dataFilePath, os.path.join(modPath, '../data/'),
46 | os.path.join(modPath, 'data/'), modPath]
47 | fileList = ['units.dat']
48 | if lang and lang != 'C':
49 | fileList[0:0] = ['units_{0}.dat'.format(lang),
50 | 'units_{0}.dat'.format(lang[:2])]
51 | for path in pathList:
52 | if path:
53 | for fileName in fileList:
54 | try:
55 | with open(os.path.join(path, fileName), 'r',
56 | encoding='utf-8') as f:
57 | lineList = f.readlines()
58 | return lineList
59 | except IOError:
60 | pass
61 | raise UnitDataError(_('Can not read "units.dat" file'))
62 |
63 | def readData(self):
64 | """Read all unit data from file, return number loaded.
65 | """
66 | lines = self.findDataFile()
67 | for i in range(len(lines) - 2, -1, -1): # join continuation lines
68 | if lines[i].rstrip().endswith('\\'):
69 | lines[i] = ''.join([lines[i].rstrip()[:-1], lines[i+1]])
70 | lines[i+1] = ''
71 | units = [unitatom.UnitDatum(line) for line in lines if
72 | line.split('#', 1)[0].strip()] # remove comment/empty lines
73 | typeText = ''
74 | for unit in units: # find & set headings
75 | if unit.name.startswith('['):
76 | typeText = unit.name[1:-1].strip()
77 | self.typeList.append(typeText)
78 | unit.typeName = typeText
79 | units = [unit for unit in units if unit.equiv] # keep valid units
80 | for unit in sorted(units):
81 | self[unit.name.lower().replace(' ', '')] = unit
82 | if len(self) < len(units):
83 | raise UnitDataError(_('Duplicate unit names found'))
84 | self.typeList.sort()
85 | return len(units)
86 |
87 | def sortUnits(self, colNum, ascend=True):
88 | """Sort units using key from given column.
89 | """
90 | unitDict = self.copy()
91 | self.clear()
92 | self.update(sorted(unitDict.items(),
93 | key=lambda u: u[1].columnText(colNum).lower(),
94 | reverse=not ascend))
95 |
96 | def partialMatches(self, text):
97 | """Return list of units with names starting with parts of text.
98 | """
99 | textList = text.lower().split()
100 | return [unit for unit in self.values() if unit.partialMatch(textList)]
101 |
102 | def findPartialMatch(self, text):
103 | """Return first partially matching unit or None.
104 | """
105 | text = text.lower().replace(' ', '')
106 | if not text:
107 | return None
108 | for name in self.keys():
109 | if name.startswith(text):
110 | return self[name]
111 | return None
112 |
--------------------------------------------------------------------------------
/source/unitedit.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # unitedit.py, provides a line edit for unit entry
5 | #
6 | # ConvertAll, a units conversion program
7 | # Copyright (C) 2016, 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 |
15 | from PyQt5.QtCore import (QEvent, Qt, pyqtSignal)
16 | from PyQt5.QtWidgets import (QLineEdit, QWidget)
17 |
18 |
19 | class UnitEdit(QLineEdit):
20 | """Text line editor for unit entry.
21 | """
22 | unitChanged = pyqtSignal()
23 | currentChanged = pyqtSignal(QWidget) # pass line edit for focus proxy
24 | keyPressed = pyqtSignal(int) # pass key to list view for some key presses
25 | gotFocus = pyqtSignal()
26 | def __init__(self, unitGroup, parent=None):
27 | super().__init__(parent)
28 | self.unitGroup = unitGroup
29 | self.activeEditor = False;
30 | self.textEdited.connect(self.updateGroup)
31 | self.cursorPositionChanged.connect(self.updateCurrentUnit)
32 |
33 | def unitUpdate(self):
34 | """Update text from unit group.
35 | """
36 | if not self.activeEditor:
37 | return
38 | newText = self.unitGroup.unitString()
39 | cursorPos = len(newText) - len(self.text()) + self.cursorPosition()
40 | if cursorPos < 0: # cursor set to same distance from right end
41 | cursorPos = 0
42 | self.blockSignals(True)
43 | self.setText(newText)
44 | self.setCursorPosition(cursorPos)
45 | self.blockSignals(False)
46 | self.unitChanged.emit()
47 |
48 | def updateGroup(self):
49 | """Update unit based on edit text change (except spacing change).
50 | """
51 | if (self.text().replace(' ', '') !=
52 | self.unitGroup.unitString().replace(' ', '')):
53 | self.unitGroup.update(self.text(), self.cursorPosition())
54 | self.currentChanged.emit(self) # update listView
55 | self.unitUpdate() # replace text with formatted text
56 |
57 | def updateCurrentUnit(self):
58 | """Change current unit based on cursor movement.
59 | """
60 | self.unitGroup.updateCurrentUnit(self.text(),
61 | self.cursorPosition())
62 | self.currentChanged.emit(self) # update listView
63 |
64 | def keyPressEvent(self, event):
65 | """Keys for return and up/down.
66 | """
67 | if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp,
68 | Qt.Key_PageDown, Qt.Key_Return, Qt.Key_Enter):
69 | self.keyPressed.emit(event.key())
70 | else:
71 | super().keyPressEvent(event)
72 |
73 | def event(self, event):
74 | """Catch tab press to complete unit.
75 | """
76 | if (event.type() == QEvent.KeyPress and
77 | event.key() == Qt.Key_Tab):
78 | # self.unitGroup.completePartial()
79 | self.currentChanged.emit(self) # update listView
80 | self.unitUpdate()
81 | return super().event(event)
82 |
83 | def setInactive(self):
84 | """Set inactive based on a signal from another editor.
85 | """
86 | self.activeEditor = False;
87 |
88 | def focusInEvent(self, event):
89 | """Signal that this unit editor received focus.
90 | """
91 | super().focusInEvent(event)
92 | if not self.activeEditor:
93 | self.activeEditor = True
94 | self.updateCurrentUnit()
95 | self.gotFocus.emit()
96 |
--------------------------------------------------------------------------------
/source/unitlistview.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #****************************************************************************
4 | # unitlistview.py, provides a list view of available units
5 | #
6 | # ConvertAll, a units conversion program
7 | # Copyright (C) 2016, 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 |
15 | from PyQt5.QtCore import (pyqtSignal, Qt, QItemSelectionModel)
16 | from PyQt5.QtGui import QPalette
17 | from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QTreeWidget,
18 | QTreeWidgetItem)
19 | import re
20 |
21 |
22 | class UnitListView(QTreeWidget):
23 | """ListView of units available.
24 | """
25 | unitChanged = pyqtSignal()
26 | haveCurrentUnit = pyqtSignal(bool, bool)
27 | # pass True if unitEdit active, then True if have unit text, o/w False
28 | def __init__(self, unitData, parent=None):
29 | super().__init__(parent)
30 | self.unitData = unitData
31 | self.highlightNum = 0
32 | self.typeFilter = ''
33 | self.setRootIsDecorated(False)
34 | self.setColumnCount(3)
35 | self.setHeaderLabels([_('Unit Name'), _('Unit Type'), _('Comments')])
36 | self.header().setStretchLastSection(False)
37 | self.header().setSortIndicatorShown(True)
38 | self.header().setSectionsClickable(True)
39 | self.header().setSortIndicator(0, Qt.AscendingOrder)
40 | self.header().sectionClicked.connect(self.changeSort)
41 | self.itemSelectionChanged.connect(self.replaceUnit)
42 | self.loadUnits()
43 |
44 | def loadUnits(self):
45 | """Load unit items.
46 | """
47 | self.clear()
48 | for unit in self.unitData.values():
49 | UnitListViewItem(unit, self)
50 | for col in range(3):
51 | self.resizeColumnToContents(col)
52 |
53 | def updateFiltering(self, focusProxy=None):
54 | """Update list after change to line editor.
55 | Set focus proxy to line editor if given (no change if None).
56 | """
57 | if focusProxy:
58 | self.setFocusProxy(focusProxy)
59 | if self.focusProxy():
60 | currentUnit = self.focusProxy().unitGroup.currentUnit()
61 | else:
62 | currentUnit = None
63 | self.blockSignals(True)
64 | self.clear()
65 | if currentUnit and currentUnit.unitName:
66 | for unit in self.unitData.partialMatches(currentUnit.unitName):
67 | if not self.typeFilter or unit.typeName == self.typeFilter:
68 | UnitListViewItem(unit, self)
69 | else:
70 | unit.viewLink = None
71 | if currentUnit.datum and currentUnit.datum.viewLink:
72 | self.setCurrentItem(currentUnit.datum.viewLink)
73 | self.highlightNum = self.indexOfTopLevelItem(currentUnit.
74 | datum.viewLink)
75 | else:
76 | for unit in self.unitData.values():
77 | if not self.typeFilter or unit.typeName == self.typeFilter:
78 | UnitListViewItem(unit, self)
79 | else:
80 | unit.viewLink = None
81 | if (not self.currentItem() and self.focusProxy() and
82 | self.topLevelItemCount()):
83 | self.setHighlight(0)
84 | self.blockSignals(False)
85 | self.haveCurrentUnit.emit(bool(self.focusProxy()),
86 | bool(currentUnit and currentUnit.unitName))
87 |
88 | def resetFiltering(self):
89 | """Clear the focus proxy and remove search filtering.
90 | """
91 | if self.focusProxy():
92 | self.setFocusProxy(None)
93 | self.updateFiltering()
94 |
95 | def replaceUnit(self):
96 | """Replace current unit in response to a selection change.
97 | """
98 | selectList = self.selectedItems()
99 | if selectList:
100 | selection = selectList[-1]
101 | if self.focusProxy():
102 | self.focusProxy().unitGroup.replaceCurrent(selection.unit)
103 | self.unitChanged.emit() # update unitEdit
104 | self.updateFiltering()
105 | else:
106 | self.setCurrentItem(None)
107 | self.setHighlight(self.indexOfTopLevelItem(selection))
108 |
109 | def addUnitText(self):
110 | """Add exponent or operator text from push button to unit group.
111 | Autocomplete a highlighted unit if not selected.
112 | """
113 | if self.focusProxy():
114 | button = self.sender()
115 | text = re.match(r'.*\((.*?)\)$', button.text()).group(1)
116 | if not self.selectedItems():
117 | item = self.topLevelItem(self.highlightNum)
118 | if item:
119 | self.setCurrentItem(item)
120 | if text.startswith('^'):
121 | self.focusProxy().unitGroup.changeExp(int(text[1:]))
122 | else:
123 | self.focusProxy().unitGroup.addOper(text == '*')
124 | self.updateFiltering()
125 | self.unitChanged.emit()
126 |
127 | def clearUnitText(self):
128 | """Remove all unit text.
129 | """
130 | if self.focusProxy():
131 | self.focusProxy().unitGroup.clearUnit()
132 | self.unitChanged.emit()
133 | self.updateFiltering()
134 |
135 | def setHighlight(self, num):
136 | """Set the item at row num to be highlighted.
137 | """
138 | self.clearHighlight()
139 | item = self.topLevelItem(num)
140 | if item:
141 | if [item] != self.selectedItems():
142 | pal = QApplication.palette(self)
143 | brush = pal.brush(QPalette.Highlight)
144 | for col in range(3):
145 | item.setForeground(col, brush)
146 | self.scrollToItem(item)
147 | self.highlightNum = num
148 |
149 | def clearHighlight(self):
150 | """Clear the highlight from currently highlighted item.
151 | """
152 | item = self.topLevelItem(self.highlightNum)
153 | if item and [item] != self.selectedItems():
154 | pal = QApplication.palette(self)
155 | brush = pal.brush(QPalette.Text)
156 | for col in range(3):
157 | item.setForeground(col, brush)
158 |
159 | def changeSort(self):
160 | """Change the sort order based on a header click.
161 | """
162 | colNum = self.header().sortIndicatorSection()
163 | order = self.header().sortIndicatorOrder() == Qt.AscendingOrder
164 | self.unitData.sortUnits(colNum, order)
165 | self.updateFiltering()
166 |
167 | def handleKeyPress(self, key):
168 | """Handle up/down, page up/down and enter key presses.
169 | """
170 | if key == Qt.Key_Up:
171 | pos = self.highlightNum - 1
172 | elif key == Qt.Key_Down:
173 | pos = self.highlightNum + 1
174 | elif key == Qt.Key_PageUp:
175 | ht = self.viewport().height()
176 | numVisible = (self.indexOfTopLevelItem(self.itemAt(0, ht)) -
177 | self.indexOfTopLevelItem(self.itemAt(0, 0)))
178 | pos = self.highlightNum - numVisible
179 | elif key == Qt.Key_PageDown:
180 | ht = self.viewport().height()
181 | numVisible = (self.indexOfTopLevelItem(self.itemAt(0, ht)) -
182 | self.indexOfTopLevelItem(self.itemAt(0, 0)))
183 | pos = self.highlightNum + numVisible
184 | elif key in (Qt.Key_Return, Qt.Key_Enter):
185 | item = self.topLevelItem(self.highlightNum)
186 | if item:
187 | self.setCurrentItem(item)
188 | return
189 | else:
190 | return
191 | if pos < 0:
192 | pos = 0
193 | if pos >= self.topLevelItemCount():
194 | pos = self.topLevelItemCount() - 1
195 | self.setHighlight(pos)
196 |
197 | def sizeHint(self):
198 | """Adjust width smaller.
199 | """
200 | size = super().sizeHint()
201 | size.setWidth(self.viewportSizeHint().width() + 5 +
202 | self.verticalScrollBar().sizeHint().width())
203 | return size
204 |
205 |
206 | class UnitListViewItem(QTreeWidgetItem):
207 | """Item in list view, references unit.
208 | """
209 | def __init__(self, unit, parent=None):
210 | super().__init__(parent)
211 | self.unit = unit
212 | unit.viewLink = self
213 | for colNum in range(3):
214 | self.setText(colNum, unit.columnText(colNum))
215 |
--------------------------------------------------------------------------------
/translations/convertall_ca.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/convertall_ca.qm
--------------------------------------------------------------------------------
/translations/convertall_de.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/convertall_de.qm
--------------------------------------------------------------------------------
/translations/convertall_es.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/convertall_es.qm
--------------------------------------------------------------------------------
/translations/convertall_fr.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/convertall_fr.qm
--------------------------------------------------------------------------------
/translations/convertall_ru.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/convertall_ru.qm
--------------------------------------------------------------------------------
/translations/convertall_sv.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/convertall_sv.qm
--------------------------------------------------------------------------------
/translations/qt_ca.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/qt_ca.qm
--------------------------------------------------------------------------------
/translations/qt_de.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/qt_de.qm
--------------------------------------------------------------------------------
/translations/qt_es.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/qt_es.qm
--------------------------------------------------------------------------------
/translations/qt_fr.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/qt_fr.qm
--------------------------------------------------------------------------------
/translations/qt_ru.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/qt_ru.qm
--------------------------------------------------------------------------------
/translations/qt_sv.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/qt_sv.qm
--------------------------------------------------------------------------------
/translations/working/README_it.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ConvertAll Leggimi
4 |
5 |
6 |
7 | File leggimi per ConvertAll
8 | Convertitore di unità
9 |
10 | Scritto da Doug Bell
11 | Version 0.5.0
12 | 23 Aprile 2010
13 |
14 |
15 | Contenuti
16 |
17 |
38 |
39 | Premessa
40 |
41 | Perchè scrivere un altro convertitore di unità se ce ne sono già in
42 | abbondanza? Perchè non ne ho mai trovato uno organizzato come
43 | volevo io.
44 |
45 | Con ConvertAll, si possono convertire oltre 500 unità di misura
46 | suddivise in classi. Da pollici in sistema metrico, da metri in libbre
47 | da Km in miglia nautiche. E moltissimo altro ancora.
48 |
49 | Poichè non sono nel business del software, ho voluto che questo programma
50 | fosse gratuito per tutti, tanto nella distribuzione quanto nella sua modifica, a patto che
51 | non sia mai integrato in programmi a pagamento. Se Vi piace il programma,
52 | sentitevi liberi di farlo conoscere agli altri. E fatemi sapere cosa ne pensate
53 | -Il mio indirizzo email è doug101 AT bellz DOT org
54 |
55 | Caratteristiche
56 |
57 |
58 |
59 | - L'unità di base per la conversione può essere digitata nei 2 modi (con
60 | completamento automatico o selezionandola dall'elenco).
61 |
62 | - Le unità possono essere selezionate utilizzando la relativa sigla o il nome completo.
63 |
64 | - Le unità possono essere combinate con il segno "*" and "/" .
65 |
66 | - Le unità possono essere elevate a potenza con il segno "^" .
67 |
68 | - Anche unità con scala non lineare, quali la temperatura, possono essere convertite.
69 |
70 | - Un elenco di unità può essere filtrata e ricercata
71 |
72 | - I valori numerici possono essere inseriti sia dal lato "From" che da quello "To",
73 | permettendo la conversione bidirezionale.
74 |
75 | - Espressioni matematiche di base possono essere inserite al posto dei numeri.
76 |
77 | - Opzioni di controllo della formattazione dei risultati numerici.
78 |
79 | - Il database include oltre 400 unità.
80 |
81 | - Il formato del file dati contenente le unità rende facile aggiungerne altre.
82 |
83 | - Le opzioni da linea di comando sono a disposizione per fare conversioni senza usare
84 | l'interfaccia grafica.
85 |
86 |
87 |
88 | Aspetti legali
89 |
90 | ConvertAll è un software gratuito; la ridistruzione e la modifica
91 | sono consentite nel rispetto della Pubblica Licenza GNU come specificato
92 | dalla Free Software Foundation; tanto nella 2° Versione della Licenza, quanto (a
93 | vostra discrezione) in quelle successive.
94 |
95 | Questo programma è distribuito nella speranza che sia utile, ma
96 | SENZA ALCUNA GARANZIA. Per ulteriori informazioni leggete il file LICENZA allegato
97 | al programma .
98 |
99 | Requisiti di sistema
100 |
101 | Linux
102 |
103 | ConvertAll richiede le seguenti librerie:
104 |
105 | - Qt (Versione 4.1 o superiore - visitare Trolltech per maggiori informazioni)
107 | - Python (Versione 2.3 o superiore)
108 | - PyQt (Version 4.0 o superiore - visitare Riverbank
110 | per maggiori informazioni)
111 |
112 |
113 | Queste librerie sono relativamente recenti - quindi i pacchetti potrebbero non essere disponibili
114 | per la vostra distributione. In questo caso, una versione precedente di ConvertAll
115 | (0.3.2), che depende ancora da librerie più datate, è tutt'ora disponibile.
116 |
117 | Windows
118 |
119 | Utilizzando i file forniti con la distribuzione binaria, ConvertAll
120 | dovrebbe funzionare su qualsiasi computer con Win 95, 98, NT, 2000, or XP.
121 |
122 | Installazione
123 |
124 | Linux
125 |
126 | Estrarre i sorgenti dal file tar di convertall, cambiare
127 | la ConvertAll directory da terminale. Per un'
128 | installazione di base, è sufficiente eseguire il seguente comando come root: python
129 | install.py
130 |
131 | Per visualizzare le opzioni di installazione, usare: python install.py -h
132 |
133 | Per installare ConvertAll con un differente prefisso (il default è
134 | /usr/local), usare: python install.py -p
135 | /prefix/path
136 |
137 | Windows
138 |
139 | Eseguire semplicemente il file di installazione scaricato
140 | (convertall-x.x.x-install.exe). Il programma verrà installato
141 | con le sue librerie, e creata l'associatione ai files e ai relativi
142 | Tasti di scelta rapida.
143 |
144 | Se si ha già installato precedentemente ConvertAll versione 0.3.0
145 | o superiore, si può optare per l'aggiornamento eseguendo
146 | convertall-x.x.x-upgrade.exe per aggiornare i files della
147 | precedente installazione.
148 |
149 | Se si desidera modificare il codice sorgente o scrivere il proprio programma PyQt
150 | per Windows, non si deve usare la procedura descritta sopra, ma piuttosto
151 | installare Python i binari PyQt. Ora estrarre il
153 | codice sorgente e i file dalla versione Linux (convertall tar file)
154 | in una directory a piacere ed eseguire il file convertall.py
155 | file.
156 |
157 | Utilizzo di ConvertAll
158 |
159 | Nozioni di base
160 |
161 | Digitare il nome di un unità nel campo "From Unit". Non appena è
162 | scritta, l'elenco a scorrimento visualizza i nomi di unità per cui
163 | c'è corrispondenza.Anche solo la sigla dell'unità permette di usare
164 | il tasto return (enter) per selezionare l'unità evidenziata dall'elenco.
165 | Naturalmente, cliccando con il mouse su una unità nella lista, la aggiungerà anche
166 | nella finestra "edit". È inoltre possibile utilizzare la freccia su e giù
167 | per selezionare le unità della lista.
168 |
169 | Ripetere la selezione dell'unità nel campo "to". E se,
170 | le unità sono compatibili, si vedrà,sotto, la finestra di modifica numerica
171 | divenire attiva. Può essere inserito un numero e nell'altra finestra verrà
172 | visualizzato il risultato.
173 |
174 | Unità combinate
175 |
176 | La vera forza di ConvertAll risiede nella sua capacità di combinare
177 | unità multiple. Basta digitare le unità con un '*' or a '/'
178 | tra loro. Questo permette l'inserimento di unità come "ft*lbf" or
179 | "mi/hr". Il simbolo '^' è usato come esponente, come ad esempio "ft^3" o
180 | "ft*lbm/sec^2". Esponenti negativi possono essere usati per unità come
181 | "sec^-1" (secondi alla meno uno) e simboli di moltiplicazioni e divisioni
182 | ("ft*sec^-2" becomes "ft/sec^2").
183 |
184 | Le multiplicazioni hanno la priorità sulle divisioni, così "m /
185 | sec * hr" vuol dire "m / (sec * hr)". Analogamente, "m / sec / hr" è lo
186 | stesso di "m * hr / sec" (ma fa meno confusione).
187 |
188 | Il pulsante sotto la lista unità ('X', '/', '^2', '^3') inserirà
189 | l'operatore dopo l'unità più vicina al cursore. Il tasto esponente
190 | emetterà l'esponente all'unità.
191 |
192 | Analogamente, cliccando sull'unità della lista si sostituisce in genere l'
193 | unità più vicina al cursore.
194 |
195 | Il pulsante "Cancella unità" sotto il pulsante operatore, può essere utilizzato per
196 | svuotare la finestra di modifica unità per consentire un nuovo inserimento.
197 |
198 | Tasti di scelta rapida
199 |
200 | Quando si digita il nome dell'unità, gli spazi sono ignorati, quindi possono essere omessi.
201 | Inoltre, è generalmente ignorata la forma plurale del nome dell'unità.
202 | Quando si digita un nome parziale dell'unità, esso è sottolineatonella lista,
203 | così che la pressione del tasto enter completerà il nome. Anche premendo il tasto TAB
204 | si completerà il nome e si passerà al successivo campo.
205 |
206 | Il numero da convertire può essere scritto sia nel campo "From" che in quello
207 | "To". La nota standard o quella scientifica può essere usata come
208 | espressione contenente i normali operatori matematici (+, -, *, /, **), anche le
209 | parentesi possono essere digitate.
210 |
211 | Ricerca di Unità
212 |
213 | La Ricerca di Unità si usa per filtrare le unità, digitando e/o ricercandole
214 | con una stringa di testo. Verrà visualizzata una lista separata in una nuova
215 | finestra. La lista sarà aggiornata in base al filtro e alla digitazione della stringa di
216 | ricerca.
217 |
218 | Il pulsante vicino al fondo della finestra di ricerca aggiunge l'unità selezionata all'
219 | unità nella finestra principale. Il pulsante "Replace" sostituisce l'unità combinata
220 | con la selezione. il pulsante "Insert" canmbia solo la parte della unità combinata che
221 | è attiva (nel cursore presente nella finestra
222 | principale).
223 |
224 | Opzioni
225 |
226 | Il pulsante "Opzioni" permette la modifica di alcune
227 | impostazioni. Questw sono automaticamente memorizzate, così che ConvertAll
228 | si riapra con le nuove modifiche.
229 |
230 | Le prime opzioni controllano la visualizzazione dei risultati numerici,
231 | compresi l'uso della nota scientifica e il numero degli spazi
232 | decimali. Siate cauti nel fissare il numero di cifre decimali
233 | al valore più basso, poichè può risultare la perdita della precisione nel risultato. Sei
234 | o più decimali sono consigliati (otto è il default).
235 |
236 | C'è un'opzione per nascondere i pulsanti di opzione del gestore di testo (x, /,
237 | ^2, ^3 e cancellare l'unità). Possono essere nascoste per salvare spazio e la
238 | tastiera usata per inserire l'operatore.
239 |
240 | I pulsanti sono inoltre inclusi nelle opzioni di dialogo per cambiare il colore
241 | campi di testo.
242 |
243 | Conversioni Non-Lineari
244 |
245 | Per la conversione di alcune unità non lineari, un esempio di esse
246 | include le scale Fahrenheit e Celsius delle temperature (a causa di un
247 | offset zero point) e all'American Wire Gauge (logaritmica). L'unità
248 | non-lineare è etichettata nella colonna dei commenti (presente
249 | alla destra della colonna "Type").
250 |
251 | Queste unità possono essere convertite solo quando non sono combinate con
252 | altre unità o non è presente l'esponente. Diversamente la conversione
253 | sarà senza significato.
254 |
255 | Command Line Usage
256 |
257 | La conversione può essere fatta da linea di comando (console Linux o DOS)
258 | senza usare l'interfaccia grafica. Digitare in ordine: il comando
259 | ("convertall"), il valore numerico, l'unità di partenza e quella di destinazione (separate da
260 | uno spazio) e poi invio per ottenere la conversione. I nomi di unità contenenti uno spazio devono essere
261 | dagli apici (le virgolette). In alternativa, per ricerere la richiesta del valore, digitare
262 | "convertall -i" da linea di comando.
263 |
264 | Terminata la conversione, ConvertAll richiederà un nuovo numero
265 | per una nuova conversione dello stesso tipo precedente. Premere "r" per invertire le unità della
266 | conversione or "q" per uscire.
267 |
268 | Per la lista dettagliata delle opzioni, digitare "convertall -h" da linea
269 | di comando.
270 |
271 | Cronologia delle revisioni
272 |
273 | La Cronologia completa delle revisioni si trova nella versione inglese del
274 | file ReadMe file.
275 |
276 | Domande, Commenti, Critiche?
277 |
278 | Per contatti, il mio indirizzo email è doug101 AT bellz DOT org, sono
279 | benvenuti commenti, suggerimenti e avvisi di bugs scoperti. E' possibile
280 | visitare periodicamente questo indirizzo per controllare la presenza di aggiornamenti. www.bellz.org
282 |
283 |
284 |
285 |
--------------------------------------------------------------------------------
/translations/working/README_xx.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ConvertAll ReadMe
4 |
5 |
6 |
7 | ReadMe file for ConvertAll
8 | a unit conversion program
9 |
10 | Written by Doug Bell
11 | Version 0.5.0
12 | April 23, 2010
13 |
14 |
15 | Contents
16 |
17 |
38 |
39 | Background
40 |
41 | Why write another unit converter? There are plenty of them out
42 | there. Well, I couldn't find one that worked quite the way I
43 | wanted.
44 |
45 | With ConvertAll, you can combine the units any way you want. If
46 | you want to convert from inches per decade, that's fine. Or from
47 | meter-pounds. Or from cubic nautical miles. The units don't have to
48 | make sense to anyone else.
49 |
50 | Since I'm not in the software business, I'm making this program
51 | free for anyone to use, distribute and modify, as long as it is not
52 | incorporated into any proprietary programs. If you like the software,
53 | feel free to let others know about it. And let me know what you think
54 | - my email address is doug101 AT bellz DOT org
55 |
56 | Features
57 |
58 |
59 |
60 | - The base units for conversion may be either typed (with
61 | auto-completion) or selected from a list.
62 |
63 | - Units may be selected using either an abbreviation or a full
64 | name.
65 |
66 | - Units may be combined with the "*" and "/" operators.
67 |
68 | - Units may be raised to powers with the "^" operator.
69 |
70 | - Units in the denominator may be grouped with parenthesis.
71 |
72 | - Units with non-linear scales, such as temperature, can also be
73 | converted.
74 |
75 | - A unit list may be filtered and searched.
76 |
77 | - Recently used unit combinations may be picked from a menu.
78 |
79 | - Numbers may be entered on either the "From" or the "To" units
80 | side, for conversions in both directions.
81 |
82 | - Basic mathematical expressions may be entered in place of
83 | numbers.
84 |
85 | - Options control the formatting of numerical results.
86 |
87 | - The unit data includes over 500 units.
88 |
89 | - The format of the unit data file makes it easy to add additional
90 | units.
91 |
92 | - Command line options are available to do conversions without the
93 | GUI.
94 |
95 |
96 |
97 | Legal Issues
98 |
99 | ConvertAll is free software; you can redistribute it and/or modify
100 | it under the terms of the GNU General Public License as published by
101 | the Free Software Foundation; either Version 2 of the License, or (at
102 | your option) any later version.
103 |
104 | This program is distributed in the hope that it will be useful, but
105 | WITHOUT ANY WARRANTY. See the LICENSE file provided with
106 | this program for more information.
107 |
108 | System Requirements
109 |
110 | Linux
111 |
112 | ConvertAll requires the following libraries:
113 |
114 | - Qt (Version 4.1 or higher - see Trolltech for more
116 | information)
117 | - Python (Version 2.3 or higher)
118 | - PyQt (Version 4.0 or higher - see Riverbank
120 | for more information)
121 |
122 |
123 | Windows
124 |
125 | Using the files provided in the binary distribution, ConvertAll
126 | should run on any computer running Win 95, 98, NT, 2000, XP, Vista, or
127 | 7.
128 |
129 | Installation
130 |
131 | Linux
132 |
133 | Extract the source files from the convertall tar file, then change to
134 | the ConvertAll directory in a terminal. For a basic
135 | installation, simply execute the following command as root: python
136 | install.py
137 |
138 | To see all install options, use: python install.py -h
139 |
140 | To install ConvertAll with a different prefix (the default is
141 | /usr/local), use: python install.py -p
142 | /prefix/path
143 |
144 | Windows
145 |
146 | Simply execute the downloaded installation file
147 | (convertall-x.x.x-install.exe). It will install the program
148 | with its libraries and optionally create file associations and
149 | shortcuts.
150 |
151 | If you wish to modify the source code or write your own PyQt programs
152 | for Windows, do not use the above procedure. Instead, you need to
153 | install Python and the binary for PyQt. Then extract the
155 | source code and data files from the Linux version (convertall tar file)
156 | to a directory of your choice and execute the convertall.py
157 | file.
158 |
159 | Using ConvertAll
160 |
161 | Basics
162 |
163 | Simply type a unit name in the "From Unit" edit window. As you
164 | type, the list below the window will scroll to show unit names which
165 | are close matches. Either type the complete unit abbreviation or unit
166 | name or hit the return key to use the unit highlighted in the list.
167 | Of course, clicking with the mouse on a unit in the list will also add
168 | the unit to the edit window. You may also use the up and down arrow
169 | keys to select nearby units from the list.
170 |
171 | Repeat the unit selection in the "To Unit" edit window. When done,
172 | if the units are compatible, you will see the numeric edit windows
173 | below the unit lists activate. A number may be entered into either
174 | numeric window and the other window will display the conversion
175 | result.
176 |
177 | Combining Units
178 |
179 | The real strength of ConvertAll lies in its ability to combine
180 | multiple units. Simply type the unit names with an '*' or a '/'
181 | between them. This allows the entry of units such as "ft*lbf" or
182 | "mi/hr". The '^' symbol may be used for exponents, such as "ft^3" or
183 | "ft*lbm/sec^2". Negative exponents are allowed for units such as
184 | "sec^-1" (per second), but may switch the multiplication or division
185 | symbol ("ft*sec^-2" becomes "ft/sec^2").
186 |
187 | Multiplication and division have the same precedence, so they are
188 | evaluated left-to-right. Parenthesis may also be used to group units in
189 | the denominator. So "m / sec / kg" can also be entered as "m / (sec *
190 | kg)". The version with parenthesis is probably less confusing.
191 |
192 | The buttons below the unit lists ('X', '/', '^2', '^3') will also
193 | place the operators after the unit nearest to the cursor. The
194 | exponent keys will replace the unit's exponent.
195 |
196 | Similarly, clicking on a unit from the list generally replaces the
197 | unit nearest the cursor.
198 |
199 | The "Clear Unit" button below the operator buttons may be used to
200 | empty the unit edit window to allow a new unit to be entered.
201 |
202 | Shortcuts
203 |
204 | When typing unit names, spaces are ignored, so they may be skipped.
205 | It is also generally ignored if a plural form of the unit name is
206 | typed. When a partially typed unit is highlighted in the list,
207 | hitting enter will complete the name. Also, hitting the tab key will
208 | complete the name and move to the next entry field.
209 |
210 | The "Recent Unit" button opens a menu of recently used units and
211 | unit combinations. The current unit combination is replaced with any
212 | selections from this menu.
213 |
214 | The number to be converted may be entered in either the "From" or
215 | "To" unit side. Standard or scientific notation may be used, or an
216 | expression including the normal math operators (+, -, *, /, **) and
217 | parenthesis may be entered.
218 |
219 | Unit Finder
220 |
221 | The unit finder can be used to filter units by type and/or search for
222 | units using a text string. It displays a separate unit list in a new
223 | window. The list will be updated based on the filter and search string
224 | entries.
225 |
226 | Buttons near the bottom of the finder window add the selected unit to
227 | the units in the main window. The "Replace" buttons replace an entire
228 | combined unit with the selection. The "Insert" button changes only the
229 | part of a combined unit that is active (at the cursor in the main
230 | window).
231 |
232 | Options
233 |
234 | The "Options..." button allows for changing several default
235 | settings. These settings are automatically stored so that ConvertAll
236 | will re-start with the settings last used.
237 |
238 | The first options control the display of numerical results,
239 | including the use of scientific notation and the number of decimal
240 | places. Be cautious about setting the number of decimal places to a
241 | low value, which can result in a significant loss of accuracy. Six
242 | places or higher is recommended (eight is the default).
243 |
244 | There is an option to set the number of recent units to be saved.
245 | Setting it to zero will disable the Recent Unit buttons.
246 |
247 | There is an option to hide the operator text option buttons (x, /,
248 | ^2, ^3, Clear Unit and Recent Unit). These can be hidden to save space
249 | if the keyboard will be used to enter the operators.
250 |
251 | Buttons are also included on the options dialog to control the colors
252 | of the text fields.
253 |
254 | Non-Linear Conversions
255 |
256 | The conversion of some units is non-linear. Examples of these
257 | include the Fahrenheit and Celsius temperature scales (due to an
258 | offset zero point) and the American Wire Gauge (logarithmic). The
259 | non-linear units are labeled as such in the comments column (located
260 | to the right of the "Type" column).
261 |
262 | These units can be converted only when they are not combined with
263 | other units or used with an exponential operator. Otherwise the
264 | conversion would not be meaningful.
265 |
266 | Command Line Usage
267 |
268 | Conversions may be done from the command line (Linux or DOS console)
269 | without invoking the graphical interface. Enter the command
270 | ("convertall" on Linux, "convertall_dos" from the Windows binary), the
271 | number, the from unit and the to unit (separated by spaces) to do the
272 | conversion. Unit names containing spaces should be surrounded by
273 | quotes. Or, to be prompted for each unit entry, use the "-i" option
274 | ("convertall -i" on Linux, "convertall_dos -i" from Windows).
275 |
276 | After the conversion is done, ConvertAll will prompt for a new number
277 | to do the same conversion. Or "n" can be entered to start a new
278 | conversion, "r" to reverse the conversion or "q" to quit.
279 |
280 | For a more detailed list of options, use the "-h" option ("convertall
281 | -h" on Linux, "convertall_dos -h" on Windows).
282 |
283 | Revision History
284 |
285 | The full revision history can be found in the English version of the
286 | ReadMe file.
287 |
288 | Questions, Comments, Criticisms?
289 |
290 | I can be contacted by email at: doug101 AT bellz DOT org
I
291 | welcome any feedback, including reports of any bugs you find. Also, you
292 | can periodically check back to www.bellz.org for any updates.
294 |
295 |
296 |
297 |
--------------------------------------------------------------------------------
/translations/working/convertall_it.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/working/convertall_it.qm
--------------------------------------------------------------------------------
/translations/working/qt_it.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/translations/working/qt_it.qm
--------------------------------------------------------------------------------
/uninstall.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | ****************************************************************************
5 | uninstall.py, Linux uninstall script for ConvertAll
6 |
7 | Copyright (C) 2013, 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 | """
15 |
16 | import sys
17 | import os.path
18 | import getopt
19 | import shutil
20 |
21 | prefixDir = '/usr/local'
22 | progName = 'convertall'
23 |
24 | def usage(exitCode=2):
25 | """Display usage info and exit.
26 |
27 | Arguments:
28 | exitCode -- the code to retuen when exiting.
29 | """
30 | global prefixDir
31 | print('Usage:')
32 | print(' python uninstall.py [-h] [-p dir]')
33 | print('where:')
34 | print(' -h display this help message')
35 | print(' -p dir install prefix [default: {0}]'.format(prefixDir))
36 | sys.exit(exitCode)
37 |
38 | def removeAll(path):
39 | """Remove path, whether it is a file or a directory,
40 | print status"""
41 | print(' Removing {0}...'.format(path))
42 | try:
43 | if os.path.isdir(path):
44 | shutil.rmtree(path)
45 | elif os.path.isfile(path):
46 | os.remove(path)
47 | else:
48 | print(' not found')
49 | return
50 | print(' done')
51 | except OSError as e:
52 | if str(e).find('Permission denied') >= 0:
53 | print('\nError - must be root to remove files')
54 | sys.exit(4)
55 | raise
56 |
57 |
58 | def main():
59 | """Main uninstaller function.
60 | """
61 | try:
62 | opts, args = getopt.getopt(sys.argv[1:], 'hp:')
63 | except getopt.GetoptError:
64 | usage(2)
65 | global prefixDir
66 | for opt, val in opts:
67 | if opt == '-h':
68 | usage(0)
69 | elif opt == '-p':
70 | prefixDir = val
71 | print('Removing files...')
72 | global progName
73 | removeAll(os.path.join(prefixDir, 'lib', progName))
74 | removeAll(os.path.join(prefixDir, 'share', 'doc', progName))
75 | removeAll(os.path.join(prefixDir, 'share', progName))
76 | removeAll(os.path.join(prefixDir, 'share', 'icons', progName))
77 | removeAll(os.path.join(prefixDir, 'share', 'icons', 'hicolor', 'scalable',
78 | 'apps', progName + '-icon.svg'))
79 | removeAll(os.path.join(prefixDir, 'share', 'applications',
80 | progName + '.desktop'))
81 | removeAll(os.path.join(prefixDir, 'bin', progName))
82 | print('Uninstall complete.')
83 |
84 |
85 | if __name__ == '__main__':
86 | main()
87 |
--------------------------------------------------------------------------------
/win/convertall-all.iss:
--------------------------------------------------------------------------------
1 | ; convertall-all.iss
2 |
3 | ; Inno Setup installer script for ConvertAll, an RPN calculator
4 | ; This will install for all users, admin rights are required.
5 |
6 | [Setup]
7 | AppName=ConvertAll
8 | AppVersion=0.8.0
9 | DefaultDirName={pf}\ConvertAll
10 | DefaultGroupName=ConvertAll
11 | DisableProgramGroupPage=yes
12 | OutputDir=.
13 | OutputBaseFilename=convertall-0.8.0-install-all
14 | PrivilegesRequired=poweruser
15 | SetupIconFile=convertall.ico
16 | Uninstallable=IsTaskSelected('adduninstall')
17 | UninstallDisplayIcon={app}\convertall.exe,0
18 |
19 | [Tasks]
20 | Name: "startmenu"; Description: "Add start menu shortcuts"
21 | Name: "deskicon"; Description: "Add a desktop shortcut"
22 | Name: "adduninstall"; Description: "Create an uninstaller"
23 | Name: "translate"; Description: "Include language translations"
24 | Name: "source"; Description: "Include source code"
25 | Name: "portable"; Description: "Use portable config file"; Flags: unchecked
26 |
27 | [InstallDelete]
28 | Type: files; Name: "{app}\library.zip"
29 | Type: files; Name: "{app}\python*.zip"
30 | Type: files; Name: "{app}\*.pyd"
31 | Type: files; Name: "{app}\*.dll"
32 | Type: filesandordirs; Name: "{app}\lib"
33 | Type: filesandordirs; Name: "{app}\imageformats"
34 | Type: filesandordirs; Name: "{app}\platforms"
35 |
36 | [Files]
37 | Source: "convertall.exe"; DestDir: "{app}"
38 | Source: "base_library.zip"; DestDir: "{app}"
39 | Source: "convertall.exe.manifest"; DestDir: "{app}"
40 | Source: "*.dll"; DestDir: "{app}"
41 | Source: "*.pyd"; DestDir: "{app}"
42 | Source: "PyQt5\*"; DestDir: "{app}\PyQt5"; Flags: recursesubdirs
43 | Source: "data\*.dat"; DestDir: "{app}\data"
44 | Source: "doc\*.html"; DestDir: "{app}\doc"
45 | Source: "doc\LICENSE"; DestDir: "{app}\doc"
46 | Source: "icons\*.png"; DestDir: "{app}\icons"
47 | Source: "translations\*.qm"; DestDir: "{app}\translations"; Tasks: "translate"
48 | Source: "source\*.py"; DestDir: "{app}\source"; Tasks: "source"
49 | Source: "source\convertall.pro"; DestDir: "{app}\source"; Tasks: "source"
50 | Source: "source\convertall.spec"; DestDir: "{app}\source"; Tasks: "source"
51 | Source: "convertall.ico"; DestDir: "{app}"; Tasks: "source"
52 | Source: "*.iss"; DestDir: "{app}"; Tasks: "source"
53 | Source: "convertall.ini"; DestDir: "{app}"; Tasks: "portable"
54 |
55 | [Icons]
56 | Name: "{commonstartmenu}\ConvertAll"; Filename: "{app}\convertall.exe"; \
57 | WorkingDir: "{app}"; Tasks: "startmenu"
58 | Name: "{group}\ConvertAll"; Filename: "{app}\convertall.exe"; \
59 | WorkingDir: "{app}"; Tasks: "startmenu"
60 | Name: "{group}\Uninstall"; Filename: "{uninstallexe}"; Tasks: "startmenu"
61 | Name: "{commondesktop}\ConvertAll"; Filename: "{app}\convertall.exe"; \
62 | WorkingDir: "{app}"; Tasks: "deskicon"
63 |
--------------------------------------------------------------------------------
/win/convertall-user.iss:
--------------------------------------------------------------------------------
1 | ; convertall-user.iss
2 |
3 | ; Inno Setup installer script for ConvertAll, an RPN calculator
4 | ; This will install for a single user, no admin rights are required.
5 |
6 | [Setup]
7 | AppName=ConvertAll
8 | AppVersion=0.8.0
9 | DefaultDirName={userappdata}\ConvertAll
10 | DefaultGroupName=ConvertAll
11 | DisableProgramGroupPage=yes
12 | OutputDir=.
13 | OutputBaseFilename=convertall-0.8.0-install-user
14 | PrivilegesRequired=lowest
15 | SetupIconFile=convertall.ico
16 | Uninstallable=IsTaskSelected('adduninstall')
17 | UninstallDisplayIcon={app}\convertall.exe,0
18 |
19 | [Tasks]
20 | Name: "startmenu"; Description: "Add start menu shortcuts"
21 | Name: "deskicon"; Description: "Add a desktop shortcut"
22 | Name: "adduninstall"; Description: "Create an uninstaller"
23 | Name: "translate"; Description: "Include language translations"
24 | Name: "source"; Description: "Include source code"
25 | Name: "portable"; Description: "Use portable config file"; Flags: unchecked
26 |
27 | [InstallDelete]
28 | Type: files; Name: "{app}\library.zip"
29 | Type: files; Name: "{app}\python*.zip"
30 | Type: files; Name: "{app}\*.pyd"
31 | Type: files; Name: "{app}\*.dll"
32 | Type: filesandordirs; Name: "{app}\lib"
33 | Type: filesandordirs; Name: "{app}\imageformats"
34 | Type: filesandordirs; Name: "{app}\platforms"
35 |
36 | [Files]
37 | Source: "convertall.exe"; DestDir: "{app}"
38 | Source: "base_library.zip"; DestDir: "{app}"
39 | Source: "convertall.exe.manifest"; DestDir: "{app}"
40 | Source: "*.dll"; DestDir: "{app}"
41 | Source: "*.pyd"; DestDir: "{app}"
42 | Source: "PyQt5\*"; DestDir: "{app}\PyQt5"; Flags: recursesubdirs
43 | Source: "data\*.dat"; DestDir: "{app}\data"
44 | Source: "doc\*.html"; DestDir: "{app}\doc"
45 | Source: "doc\LICENSE"; DestDir: "{app}\doc"
46 | Source: "icons\*.png"; DestDir: "{app}\icons"
47 | Source: "translations\*.qm"; DestDir: "{app}\translations"; Tasks: "translate"
48 | Source: "source\*.py"; DestDir: "{app}\source"; Tasks: "source"
49 | Source: "source\convertall.pro"; DestDir: "{app}\source"; Tasks: "source"
50 | Source: "source\convertall.spec"; DestDir: "{app}\source"; Tasks: "source"
51 | Source: "convertall.ico"; DestDir: "{app}"; Tasks: "source"
52 | Source: "*.iss"; DestDir: "{app}"; Tasks: "source"
53 | Source: "convertall.ini"; DestDir: "{app}"; Tasks: "portable"
54 |
55 | [Icons]
56 | Name: "{userstartmenu}\ConvertAll"; Filename: "{app}\convertall.exe"; \
57 | WorkingDir: "{app}"; Tasks: "startmenu"
58 | Name: "{group}\ConvertAll"; Filename: "{app}\convertall.exe"; \
59 | WorkingDir: "{app}"; Tasks: "startmenu"
60 | Name: "{group}\Uninstall"; Filename: "{uninstallexe}"; Tasks: "startmenu"
61 | Name: "{userdesktop}\ConvertAll"; Filename: "{app}\convertall.exe"; \
62 | WorkingDir: "{app}"; Tasks: "deskicon"
63 |
--------------------------------------------------------------------------------
/win/convertall.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/win/convertall.ico
--------------------------------------------------------------------------------
/win/convertall.ini:
--------------------------------------------------------------------------------
1 | # Options for the ConvertAll unit conversion program
2 | #
3 | # All options are set from within the program,
4 | # editing here is not recommended
5 | #
6 | ColorTheme system
7 | WindowColor
8 | WindowTextColor
9 | BaseColor
10 | TextColor
11 | HighlightColor
12 | HighlightedTextColor
13 | ButtonColor
14 | ButtonTextColor
15 | Text-DisabledColor
16 | ButtonText-DisabledColor
17 | GuiFont
18 | #
19 | DecimalPlaces 8
20 | Notation general
21 | ShowOpButtons yes
22 | ShowUnitButtons yes
23 | RecentUnits 8
24 | LoadLastUnit no
25 | ShowStartupTip yes
26 | RemenberDlgPos yes
27 | #
28 | MainDlgXSize 621
29 | MainDlgYSize 381
30 | MainDlgXPos 320
31 | MainDlgYPos 252
32 | MainDlgTopMargin 31
33 | MainDlgOtherMargin 1
34 | #
35 | RecentUnit1
36 | RecentUnit2
37 | RecentUnit3
38 | RecentUnit4
39 | RecentUnit5
40 | RecentUnit6
41 | RecentUnit7
42 | RecentUnit8
43 |
--------------------------------------------------------------------------------