├── .gitignore ├── README.md ├── convertall.desktop ├── data ├── units.dat ├── units_ca.dat ├── units_de.dat ├── units_es.dat ├── units_fr.dat ├── units_ru.dat └── units_sv.dat ├── doc ├── INSTALL ├── LICENSE ├── README.html ├── README_de.html ├── README_es.html ├── README_fr.html └── README_ru.html ├── docs ├── README.html ├── convertall.png ├── convertall_sm.png ├── default.css ├── download.html ├── feature.html ├── index.html ├── install.html ├── logo.png ├── privacy.html ├── require.html ├── scrnsht.html └── use.html ├── icons ├── convertall-icon.png ├── convertall-icon.svg ├── convertall_lg.png ├── convertall_med.png ├── convertall_sm.png ├── helpback.png ├── helpforward.png ├── helphome.png ├── helpnext.png └── helpprevious.png ├── install.py ├── source ├── bases.py ├── cmdline.py ├── colorset.py ├── convertall.pro ├── convertall.py ├── convertall.spec ├── convertdlg.py ├── fontset.py ├── helpview.py ├── icondict.py ├── numedit.py ├── option.py ├── optiondefaults.py ├── optiondlg.py ├── recentunits.py ├── setup.py ├── unitatom.py ├── unitdata.py ├── unitedit.py ├── unitgroup.py └── unitlistview.py ├── translations ├── convertall_ca.qm ├── convertall_ca.ts ├── convertall_de.qm ├── convertall_de.ts ├── convertall_es.qm ├── convertall_es.ts ├── convertall_fr.qm ├── convertall_fr.ts ├── convertall_ru.qm ├── convertall_ru.ts ├── convertall_sv.qm ├── convertall_sv.ts ├── qt_ca.qm ├── qt_de.qm ├── qt_es.qm ├── qt_fr.qm ├── qt_ru.qm ├── qt_sv.qm └── working │ ├── README_it.html │ ├── README_xx.html │ ├── convertall_it.qm │ ├── convertall_it.ts │ ├── convertall_xx.ts │ ├── qt_it.qm │ └── units_xx.dat ├── uninstall.py └── win ├── convertall-all.iss ├── convertall-user.iss ├── convertall.ico └── convertall.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *.swp 4 | */__pycache__/ 5 | *.hgignore 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is ConvertAll? 2 | 3 | Why write another unit converter? There are plenty of them out there. Well, I 4 | couldn't find one that worked quite the way I wanted. 5 | 6 | With ConvertAll, you can combine the units any way you want. If you want to 7 | convert from inches per decade, that's fine. Or from meter-pounds. Or from 8 | cubic nautical miles. The units don't have to make sense to anyone else. 9 | 10 | # More Info 11 | 12 | See the [ConvertAll homepage](http://convertall.bellz.org) for more info. 13 | 14 | There is also an [online version](http://convertall-js.bellz.org), written in 15 | JavaScript (see the doug-101/ConvertAll-js repository for the source code). 16 | -------------------------------------------------------------------------------- /convertall.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Version=1.1 4 | Name=ConvertAll 5 | GenericName=Unit Converter 6 | Comment=a flexible unit converter 7 | Exec=convertall 8 | Icon=convertall-icon 9 | StartupNotify=true 10 | Terminal=false 11 | Categories=Math;Science;Education 12 | -------------------------------------------------------------------------------- /doc/INSTALL: -------------------------------------------------------------------------------- 1 | ConvertAll Installation Notes 2 | 3 | Extract the source files from the convertall tar file, then change to the 4 | 'ConvertAll' directory in a terminal. For a basic installation, simply 5 | execute the following command as root: 'python install.py' 6 | 7 | To see all install options, use: 'python install.py -h' 8 | 9 | To install ConvertAll with a different prefix (the default is 10 | '/usr/local'), use: 'python install.py -p /prefix/path' 11 | -------------------------------------------------------------------------------- /doc/README_fr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ConvertAll ReadMe 4 | 5 | 6 |
7 |

Fichier ReadMe de ConvertAll

8 |

un logiciel de conversion d'unités

9 | 10 |

écrit par Doug Bell
11 | Version 0.8.0

12 |
13 | 14 |

Contenus

15 | 16 | 37 | 38 |

Fondement

39 | 40 |

Pourquoi écrire un autre convertisseur d'unités? Eh bien, je n'en avais trouvé aucun qui était comme je le voulais.

41 | 42 |

Avec ConvertAll, vous pouvez combiner les unités comme vous le voulez. Si vous voulez convertir des pouces en kilomètre, ca fonctionne! Les unités n'ont pas à avoir de sens pour personne d'autres que vous.

43 | 44 |

Comme je ne suis pas dans l'industrie du logiciel, je fais ce programme librement, je le distribue librement et j'autorise n'importe qui à le copier ou le modifier tant qu'il ne se retrouve pas dans un logiciel propriétaire. Si vous aimez ce logiciel, sentez-vous libre d'en parler. Aussi, donnez-moi des commentaires par courriel - mon adresse est doug101 AT bellz.org

45 | 46 |

Capacités

47 | 48 | 75 | 76 |

Dispositions légales

77 | 78 |

ConvertAll est un logiciel libre; vous pouvez le redistribuer et/ou le modifier sous les termes de la licence GNU GPL (GNU General Public License) comme publiée par la Free Software Foundation; la version 2 de cette license ou (comme vous le voulez) une version plus récente.

79 | 80 |

Ce programme est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE. Voyez le fichier LICENSE fourni avec ce programme pour plus d'information.

81 | 82 |

Minimum système

83 | 84 |

Linux

85 | 86 |

ConvertAll requiert les librairies suivantes : 87 |

95 | 96 |

Ces librairies sont relativement nouvelles. Des paquets n'existent peut-être pas pour votre distribution. Dans ce cas, une version antérieure de ConvertAll (0.3.2) est toujours disponible.

97 | 98 |

Windows

99 | 100 |

En utilisant les binaires de ConvertAll, n'importe quel PC utilsant Windows XP, Vista, Windows 7 ou Windows 8 devrait fonctionner.

101 | 102 |

Installation

103 | 104 |

Linux

105 | 106 |

Extraire les sources du fichier tar de convertall. Ensuite, allez dans le dossier ConvertAll. Pour une utilisation de base, tapez la commande suivante en mode super utilisateur (root) : python install.py

107 | 108 |

Pour les options, utilisez: python install.py -h

109 | 110 |

Pour installer ConvertAll avec un prefix différent (par défaut c'est 111 | /usr/local), utilisez: python install.py -p 112 | /prefix/path

113 | 114 |

Windows

115 | 116 |

Simplement utiliser le fichier téléchargé (convertall-x.x.x-install-all.exe). Cela installera les fichiers nécessaires, fera les associations et créera les raccourcis.

117 | 118 |

Si vous souhaitez modifier le code source ou écrire vos propres programmes PyQt pour Windows, n'utilisez pas les procédures ci-haut. À la place, vous devez installer Python et les fichiers binaires pour PyQt. Ensuite, extraire le code source et les fichiers de données de la version Linux (fichier tar convertall) dans un dossier de votre choix et exécuter convertall.py

119 | 120 |

Utiliser ConvertAll

121 | 122 |

Bases

123 | 124 |

Simplement écrire le nom d'une unité dans le champ "de l'unité". Au fur que vous écrivez, une liste apparaîtra et vous pourrez sélectionner la bonne. Tapez l'abréviation complète, le nom complet ou sélectionné en un dans la liste et taper retour. Cliquer avec la souris fonctionne également tout comme les flèches haut et bas.

125 | 126 |

Refaire les mêmes étapes dans le champ "vers l'unité". Quand vous aurez terminé, si les unités sont compatibles, vous verrez un champ pour entrer des valeurs numériques sous les listes. Entrer un nombre dans un champ et vous verrez le résultat de la conversion dans l'autre champ.

127 | 128 |

Combiner les unités

129 | 130 |

La force de ConvertAll repose dans son habileté à combiner plusieurs unités. Écrivez simplement le nom de deux unités avec un "*" ou un "/" entre eux. Ainsi, cela permet les km/h ou les j/s. Le symbole "^" peut être utilisé pour des m?? "m^2". Même les exposants négatifs "sec^-1" sont possibles, mais le symbole de la division apparaîtra. ("m*sec^-2" deviendra "m/sec^2")

131 | 132 |

Dans ConvertAll, la multiplication passe avant la division. Donc "m/sec*h" veut dire "m/(sec*h)". Donc, "m/sec/h" revient au même que "m*h/sec". Faites donc attention aux priorités.

133 | 134 |

Les boutons sous les listes d'unités ('X', '/', '^2', '^3') placeront l'opérateur sur l'unité le plus près du curseur.

135 | 136 |

Aussi, cliquer sur une unité dans la liste remplace généralement l'unité la plus près du curseur.

137 | 138 |

Le bouton "Effacer l'unité" sous les opérateurs peu être utilisé pour nettoyer le champ et ainsi laissez de la place pour une nouvelle unité.

139 | 140 |

Raccouris

141 | 142 |

Quand vous tapez le nom d'une unité, les espaces seront ignorés. Ils peuvent donc être sautés. Le pluriel aussi est ignoré. Quand vous avez entré un nom partiel d'unité, si vous tapez retour, le nom s'écrira au complet. Si vous utilisez la touche TAB, le curseur ira au champ suivant en plus.

143 | 144 |

Le nombre devant être converti peut être entré dans le champ "De" ou "Vers" sans problème. La notation décimale et scientifique peuvent être utilisés tout comme les expressions usuelles en mathématique (+, -, *, /, **).

145 | 146 |

Trouver une unité

147 | 148 |

Le chercheur d'unité peut être utilisé pour filtrer les unités par type et/ou pour chercher des unités en utilisant une chaîne de caractères. Cela fera apparaître une liste séparée dans une nouvelle fenêtre. Cette liste sera ensuite mise à jour en fonction des filtres utilisés.

149 | 150 |

Les boutons près du bas de la fenêtre du chercheur ajoutent l'unité sélectionnée dans la fenêtre principale. Le bouton "Remplacer" remplace toutes les unités combinées par la sélection. Le bouton "Insertion" remplace seulement la partie active des unités.

151 | 152 |

Options

153 | 154 |

Le bouton "Options..." permet de changer les paramètres par défaut. Les nouveaux paramètres sont enregistrés automatiquement, donc à la prochaine utilisation de ConvertAll, vos paramètres modifiés seront encore présents.

155 | 156 |

Les premières options contrôlent l'affichage de résultats numériques, incluant l'utilisation de notation scientifique et le nombre de décimales. Faites attention si vous mettez un très petit nombre de décimal, car tout deviendrait moins précis. Six décimales ou plus sont recommandées (par défaut c'est huit).

157 | 158 |

Il y a une option pour cacher les boutons des opérateurs (x, /, 159 | ^2, ^3 and Clear Unit). Ils peuvent être cachés pour sauver de l'espace si le clavier est utilisé pour entrer ces opérateurs.

160 | 161 |

Des boutons sont aussi disponibles pour changer la couleur des champs de texte.

162 | 163 |

Conversion non linéaire

164 | 165 |

La conversion de quelques unités est non linéaire (non proportionnel). La conversion de la température des Fahrenheit et des Celsius est un exemple de conversion non linéaire. Les unités non linéaires sont identifiées dans les commentaires (à droite de la colonne "Type").

166 | 167 |

Ces unités ne peuvent être converties que lorsqu'ils sont utilisés seuls et sans exposants. Sinon la conversion ne sera pas juste.

168 | 169 |

Utilisation de la ligne de commande

170 | 171 |

Les conversions peuvent être faites à partir de la ligne de commande (dans un terminal Linux ou DOS). Entrer la commande ("convertall"), le nombre, l'unité à partir de et l'unité vers (le tout séparé par des espaces) pour faire une conversion. Les unités dont le nom comporte des espaces doit être entourées de guillemets. Encore, si vous voulez qu'on vous demande les entrées, utilisez "convertall -i" en ligne de commande.

172 | 173 |

Une fois la conversion terminée, ConvertAll vous demandera pour un nouveau nombre pour faire la même conversion. Vous pourrez soit utiliser "r" pour faire la conversion inverse ou "q" pour quitter.

174 | 175 |

Pour la liste complète des options, entrer "convertall -h" à l'invite.

176 | 177 |

Historique

178 | 179 |

23 mars 2010 - Version 0.4.90

180 | 181 | 192 | 193 |

L'historique complet du logiciel peut être trouvé dans la version anglaise du fichier README.

194 | 195 |

Questions, Commentaires, Critiques?

196 | 197 |

Vous pouvez me contacter par email à l'adresse : doug101 AT bellz.org
Tous les commentaires sont les bienvenus incluant les rapports de bogues. Aussi, vérifié périodiquement au www.bellz.org pour les mises à jour.

198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /docs/convertall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/docs/convertall.png -------------------------------------------------------------------------------- /docs/convertall_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/docs/convertall_sm.png -------------------------------------------------------------------------------- /docs/default.css: -------------------------------------------------------------------------------- 1 | /* bellz.org style sheet */ 2 | 3 | body { 4 | background: #033; 5 | color: white; 6 | } 7 | 8 | a { 9 | color: #0f9; 10 | text-decoration: none; 11 | } 12 | 13 | a:hover { 14 | text-decoration: underline; 15 | } 16 | 17 | hr { 18 | height: 4px; 19 | } 20 | 21 | h1 { 22 | text-align: center; 23 | border-top: thick double #ccc; 24 | padding: .3em; 25 | margin: 1em 0 0; 26 | clear: both; 27 | } 28 | 29 | h2 { 30 | border-top: thin solid #ccc; 31 | padding: .3em 0 0; 32 | margin: 1em 0 0; 33 | } 34 | 35 | ul { 36 | margin-left: 0; 37 | } 38 | 39 | li { 40 | list-style-type: square; 41 | padding: .2em 0; 42 | margin-left: 2em; 43 | } 44 | 45 | img { 46 | border: 0; 47 | } 48 | 49 | .interior { 50 | margin: 0 auto; 51 | width: 90%; 52 | } 53 | 54 | .title { 55 | margin: 0; 56 | padding: 4px 0; 57 | float: left; 58 | } 59 | 60 | .title h1 { 61 | font-size: 4em; 62 | margin: 0; 63 | padding: 0; 64 | } 65 | 66 | .toplinks { 67 | float: right; 68 | text-align: right; 69 | } 70 | 71 | .toplinks p { 72 | margin: 0; 73 | padding: 0; 74 | } 75 | 76 | .navigation { 77 | clear: both; 78 | line-height: 3em; 79 | margin: 0; 80 | padding: 0; 81 | } 82 | 83 | .navigation a { 84 | background: #366; 85 | margin-right: 3px; 86 | padding: .4em .4em; 87 | } 88 | 89 | .navigation a:hover { 90 | background: #099; 91 | text-decoration: none; 92 | } 93 | 94 | .current-nav { 95 | background: #366; 96 | margin-right: 3px; 97 | padding: .4em .4em 1em; 98 | } 99 | 100 | .content { 101 | background: #366; 102 | margin: 0 0 2em; 103 | padding: .5em 1.5em; 104 | zoom: 1; /* fixes IE layout bug */ 105 | /* height: 1%; /* fixes IE layout bug */ 106 | } 107 | 108 | .centered { 109 | text-align: center; 110 | } 111 | 112 | .smallimage { 113 | float: left; 114 | margin: 0 1em; 115 | } 116 | 117 | .clearer { 118 | clear: both; 119 | } 120 | -------------------------------------------------------------------------------- /docs/download.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ConvertAll - Downloads 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | ConvertAll 18 |
19 | 20 | 25 | 26 | 35 | 36 |
37 | 38 |

ConvertAll Downloads

39 | 40 |

Linux

41 | 42 |

On a Linux system that has the libraries noted on the requirements page, all that is needed 44 | is the source code (with an install script), included in 46 | convertall-0.8.0.tar.gz (276 KB).

47 | 48 |

But please read about the installation and use of ConvertAll. This information is 51 | available on this site and in the ReadMe file inside the above 53 | tar file.

54 | 55 |

Windows

56 | 57 |

Two windows installers are available. Use 59 | convertall-0.8.0-install-all.exe (16 MB) to 60 | install for all users (administrator permissions are required). 61 | Use 63 | convertall-0.8.0-install-user.exe (16 MB) to 64 | install for a single user (administrator rights are not 65 | required). The source code for the libraries used to build the 66 | Windows binaries is available upon request.

67 | 68 |

But please read about the installation and use of ConvertAll. This information is 71 | available on this site and in the ReadMe file included 73 | in the installer.

74 | 75 |
76 | 77 |
78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /docs/feature.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ConvertAll - Features 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | ConvertAll 18 |
19 | 20 | 25 | 26 | 35 | 36 |
37 | 38 |

ConvertAll Features

39 | 40 | 93 | 94 |
95 | 96 |
97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /docs/install.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ConvertAll - Installation 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | ConvertAll 18 |
19 | 20 | 25 | 26 | 35 | 36 |
37 | 38 |

ConvertAll Installation

39 | 40 |

Linux

41 | 42 |

Extract the source files from the convertall tar file, then 43 | change to the ConvertAll directory in a terminal. For 44 | a basic installation, simply execute the following command as 45 | root: python install.py

46 | 47 |

If your distribution defaults to Python 2.x, you may need to 48 | substitute python3 for python in these 49 | commands.

50 | 51 |

To see all install options, use: python install.py -h 52 | To install ConvertAll with a different prefix (the default 53 | is /usr/local), use: python install.py -p 54 | /prefix/path

55 | 56 |

Windows

57 | 58 |

To install for all users, execute the 59 | convertall-x.x.x-install-all.exe file. Administrator 60 | permissions are required.

61 | 62 |

To install for a single user (administrator rights are not 63 | required), execute the convertall-x.x.x-install-user.exe 64 | file.

65 | 66 |

For a portable install, execute the 67 | convertall-x.x.x-install-user.exe file, uncheck the 68 | shortcuts and uninstaller tasks, and check the portable config 69 | task.

70 | 71 |
72 | 73 |
74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/docs/logo.png -------------------------------------------------------------------------------- /docs/privacy.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ConvertAll - Privacy 11 | 12 | 13 | 14 | 15 |

Privacy Policy

16 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/require.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ConvertAll - Requirements 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | ConvertAll 18 |
19 | 20 | 25 | 26 | 35 | 36 |
37 | 38 |

Legal Issues

39 | 40 |

ConvertAll is free software; you can redistribute it and/or 41 | modify it under the terms of the GNU General Public License as 42 | published by the Free Software Foundation; either Version 2 of 43 | the License, or (at your option) any later version.

44 | 45 |

This program is distributed in the hope that it will be 46 | useful, but WITHOUT ANY WARRANTY. See the LICENSE 47 | file provided with this program for more information.

48 | 49 |

System Requirements

50 | 51 |

Linux

52 | 53 |

ConvertAll requires the following libraries: 54 |

63 | 64 |

Windows

65 | 66 |

Using the files provided in the binary distribution, 67 | ConvertAll should run on any computer running Windows XP, 68 | Vista, 7, 8 or 10.

69 | 70 |
71 | 72 |
73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/scrnsht.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ConvertAll - Screenshots 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | ConvertAll 18 |
19 | 20 | 25 | 26 | 35 | 36 |
37 | 38 |

ConvertAll Screenshot

39 | 40 |
41 |

42 | 43 |
44 | 45 |
46 | 47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/use.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ConvertAll - How to Use 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | ConvertAll 18 |
19 | 20 | 25 | 26 | 35 | 36 |
37 | 38 |

How to Use ConvertAll

39 | 40 |

Contents

41 | 42 | 51 | 52 |

Basics

53 | 54 |

Simply type a unit name in the "From Unit" edit window. As you 55 | type, the list below the window will be filtered to show only 56 | matching units. Matching unit names contain words starting with 57 | the words that are typed. Either type the complete unit 58 | name/abbreviation or hit the return key to use the unit 59 | highlighted (blue letters) in the list. Of course, clicking with 60 | the mouse on a unit in the list will also add the unit to the edit 61 | window. You may also use the up and down arrow keys to highlight 62 | nearby units from the list.

63 | 64 |

Repeat the unit selection in the "To Unit" edit window. When 65 | done, if the units are compatible, the numeric edit windows below 66 | the unit lists will activate. A number may be entered into either 67 | numeric window and the other window will display the conversion 68 | result.

69 | 70 |

Combining Units

71 | 72 |

The real strength of ConvertAll lies in its ability to combine 73 | multiple units. Simply type the unit names with an '*' or a '/' 74 | between them. This allows the entry of units such as "ft * lbf" 75 | or "mi / hr". The '^' symbol may be used for exponents, such as 76 | "ft^3" or "ft * lbm / sec^2". Negative exponents are allowed for 77 | units such as "sec^-1" (per second), but may switch the 78 | multiplication or division symbol ("ft * sec^-2" becomes "ft / 79 | sec^2").

80 | 81 |

Multiplication and division have the same precedence, so they 82 | are evaluated left-to-right. Parenthesis may also be used to 83 | group units in the denominator. So "m / sec / kg" can also be 84 | entered as "m / (sec * kg)". The version with parenthesis is 85 | probably less confusing.

86 | 87 |

The buttons below the unit text boxes can also be used to add 88 | operators to the active unit that is closest to the cursor. The 89 | Square and Cube buttons will add or replace exponents. The 90 | Multiply and Divide buttons will add "*" and "/" operators.

91 | 92 |

Similarly, clicking on a unit from the list generally replaces 93 | the unit nearest the cursor.

94 | 95 |

The "Clear Unit" button below the operator buttons may be used 96 | to empty the unit edit window to allow a new unit to be 97 | entered.

98 | 99 |

The "Filter List" button can be used to show only one type of 100 | unit in the list. Note that this doesn't show units that could be 101 | combined to form a type.

102 | 103 |

Non-Linear Conversions

104 | 105 |

The conversion of some units is non-linear. Examples of these 106 | include the Fahrenheit and Celsius temperature scales (due to an 107 | offset zero point) and the American Wire Gauge (logarithmic). The 108 | non-linear units are labeled as such in the comments column.

109 | 110 |

These units can be converted only when they are not combined 111 | with other units or used with an exponential operator. Otherwise 112 | the conversion would not be meaningful.

113 | 114 |

Shortcuts

115 | 116 |

When typing unit names, spaces are ignored, so they may be 117 | skipped. It is also generally ignored if a plural form of the 118 | unit name is typed. For squared and cubed units (positive 119 | exponents of 2 or 3) the "^" symbol does not need to be typed.

120 | 121 |

When a partially typed unit is highlighted in the list (blue 122 | lettering), hitting enter will complete the name. The up an ddown 123 | arrow keys can be used to highlight nearby units in the list prior 124 | to hitting enter. The unit closet to the cursor will be 125 | replaced.

126 | 127 |

The "Recent Unit" button opens a menu of recently used units 128 | and unit combinations. The current unit combination is replaced 129 | with any selections from this menu.

130 | 131 |

The tab key can be used to cycle between the "From" and "To" 132 | unit test boxes and the "From" and "To" number editors.

133 | 134 |

The number to be converted may be entered in either the "From" 135 | or "To" unit side. Standard or scientific notation may be used, 136 | or an expression including the normal math operators (+, -, *, /, 137 | **) and parenthesis may be entered.

138 | 139 |

Numbering System Conversions

140 | 141 |

The "Bases" button brings up a dialog that can convert between 142 | various numbering system bases. There are entry boxes for 143 | decimal, hexadecimal, octal and binary entries. A number can be 144 | entered into any of the boxes and equivalents will be shown in the 145 | others.

146 | 147 |

The "Fractions" button brings up a dialog that can list 148 | fractional equivalents for a decimal number. Type the number and 149 | press enter. The fractions are listed in order of increasing 150 | accuracy.

151 | 152 |

Options

153 | 154 |

The "Options..." button allows for changing several default 155 | settings. These settings are automatically stored so that 156 | ConvertAll will re-start with the settings last used.

157 | 158 |

The first options control the display of numerical results, 159 | including the use of scientific notation and the number of decimal 160 | places. Be cautious about setting the number of decimal places to 161 | a low value, which can result in a significant loss of accuracy. 162 | Six places or higher is recommended (eight is the default).

163 | 164 |

There is an option to set the number of recent units to be 165 | saved. Setting it to zero will disable the Recent Unit buttons. 166 | Another option will automatically load the most recent previous 167 | units at startup.

168 | 169 |

There are options to hide the operator text buttons (first 170 | row), and the unit buttons (second row). These can be hidden to 171 | save space if the keyboard will be used to enter the 172 | operators.

173 | 174 |

Buttons are also included on the options dialog to control the 175 | colors of the text fields.

176 | 177 |

Buttons are also included on the options dialog to control GUI 178 | colors and fonts. Colors can be selected by theme or 179 | individually. Larger fonts can be selected for use on 180 | high-resolution displays.

181 | 182 |

Command Line Usage

183 | 184 |

Conversions may be done from the command line (Linux or DOS 185 | console) without invoking the graphical interface. Enter the 186 | command ("convertall" on Linux, "convertall_dos" from the Windows 187 | binary), the number, the from unit and the to unit (separated by 188 | spaces) to do the conversion. Unit names containing spaces should 189 | be surrounded by quotes. Or, to be prompted for each unit entry, 190 | use the "-i" option ("convertall -i" on Linux, "convertall_dos -i" 191 | from Windows).

192 | 193 |

After the conversion is done, ConvertAll will prompt for a new 194 | number to do the same conversion. Or "n" can be entered to start 195 | a new conversion, "r" to reverse the conversion or "q" to 196 | quit.

197 | 198 |

For a more detailed list of options, use the "-h" option 199 | ("convertall -h" on Linux, "convertall_dos -h" on Windows).

200 | 201 |
202 | 203 |
204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /icons/convertall-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/convertall-icon.png -------------------------------------------------------------------------------- /icons/convertall-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 22 | 24 | 28 | 32 | 36 | 37 | 46 | 47 | 65 | 67 | 68 | 70 | image/svg+xml 71 | 73 | 74 | 75 | 76 | 81 | 84 | 86 | 94 | 98 | 99 | 103 | 104 | 107 | 110 | 123 | 128 | 129 | 145 | 146 | 149 | 152 | 165 | 170 | 171 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /icons/convertall_lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/convertall_lg.png -------------------------------------------------------------------------------- /icons/convertall_med.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/convertall_med.png -------------------------------------------------------------------------------- /icons/convertall_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/convertall_sm.png -------------------------------------------------------------------------------- /icons/helpback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/helpback.png -------------------------------------------------------------------------------- /icons/helpforward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/helpforward.png -------------------------------------------------------------------------------- /icons/helphome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/helphome.png -------------------------------------------------------------------------------- /icons/helpnext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/helpnext.png -------------------------------------------------------------------------------- /icons/helpprevious.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doug-101/ConvertAll-py/d6ca80317a9b5c83bc7da4df68687d3f14c6a35f/icons/helpprevious.png -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | **************************************************************************** 5 | install.py, Linux install script for ConvertAll 6 | 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 | 16 | import sys 17 | import os.path 18 | import getopt 19 | import shutil 20 | import compileall 21 | import py_compile 22 | import glob 23 | import re 24 | import subprocess 25 | 26 | prefixDir = '/usr/local' 27 | buildRoot = '/' 28 | progName = 'convertall' 29 | docDir = 'share/doc/{0}'.format(progName) 30 | iconDir = 'share/icons/{0}'.format(progName) 31 | 32 | def usage(exitCode=2): 33 | """Display usage info and exit. 34 | 35 | Arguments: 36 | exitCode -- the code to retuen when exiting. 37 | """ 38 | global prefixDir 39 | global buildRoot 40 | print('Usage:') 41 | print(' python install.py [-h] [-p dir] [-d dir] [-i dir] ' 42 | '[-b dir] [-s] [-x]') 43 | print('where:') 44 | print(' -h display this help message') 45 | print(' -p dir install prefix [default: {0}]'.format(prefixDir)) 46 | print(' -d dir documentaion dir [default: /{0}]' 47 | .format(docDir)) 48 | print(' -i dir icon dir [default: /{0}]'.format(iconDir)) 49 | print(' -b dir temporary build root for packagers [default: {0}]' 50 | .format(buildRoot)) 51 | print(' -s skip language translation files') 52 | print(' -x skip all dependency checks (risky)') 53 | sys.exit(exitCode) 54 | 55 | 56 | def cmpVersions(versionStr, reqdTuple): 57 | """Return True if point-sep values in versionStr are >= reqdTuple. 58 | 59 | Arguments: 60 | versionStr -- a string with point-separated version numbers 61 | reqdTuple -- a tuple of version integers for the minimum acceptable 62 | """ 63 | match = re.search(r'[0-9\.]+', versionStr) 64 | if not match: 65 | return False 66 | versionStr = match.group() 67 | versionList = [int(val) for val in versionStr.split('.') if val] 68 | reqdList = list(reqdTuple) 69 | while len(versionList) < len(reqdList): 70 | versionList.append(0) 71 | while len(reqdList) < len(versionList): 72 | reqdList.append(0) 73 | if versionList >= reqdList: 74 | return True 75 | return False 76 | 77 | def copyDir(srcDir, dstDir): 78 | """Copy all regular files from srcDir to dstDir. 79 | 80 | dstDir is created if necessary. 81 | Arguments: 82 | srcDir -- the source dir path 83 | dstDir -- the destination dir path 84 | """ 85 | try: 86 | if not os.path.isdir(dstDir): 87 | os.makedirs(dstDir) 88 | names = os.listdir(srcDir) 89 | for name in names: 90 | srcPath = os.path.join(srcDir, name) 91 | if os.path.isfile(srcPath): 92 | shutil.copy2(srcPath, os.path.join(dstDir, name)) 93 | except (IOError, OSError) as e: 94 | if str(e).find('Permission denied') >= 0: 95 | print('Error - must be root to install files') 96 | cleanSource() 97 | sys.exit(4) 98 | raise 99 | 100 | def createWrapper(execDir, execName): 101 | """Create a wrapper executable file for a python script in execDir. 102 | 103 | Arguments: 104 | execDir -- the path where the executable is placed 105 | execName -- the name for the executable file 106 | """ 107 | text = '#!/bin/sh\n\nexec {0} {1}/{2}.py "$@"'.format(sys.executable, 108 | execDir, 109 | execName) 110 | with open(execName, 'w') as f: 111 | f.write(text) 112 | os.chmod(execName, 0o755) 113 | 114 | def replaceLine(path, origLineStart, newLine): 115 | """Replaces lines with origLineStart with newLine and rewrites the file. 116 | 117 | Arguments: 118 | path -- the file to modify 119 | origLineStart -- the beginning of the line to be replaced 120 | newLine -- the replacement line 121 | """ 122 | with open(path, 'r') as f: 123 | lines = f.readlines() 124 | with open(path, 'w') as f: 125 | for line in lines: 126 | if line.startswith(origLineStart): 127 | f.write(newLine) 128 | else: 129 | f.write(line) 130 | 131 | def cleanSource(): 132 | """Remove any temporary files added to untarred dirs. 133 | """ 134 | for name in glob.glob(os.path.join('source', '*.py[co]')): 135 | os.remove(name) 136 | removeDir(os.path.join('source', '__pycache__')) 137 | global progName 138 | if os.path.isfile(progName): 139 | os.remove(progName) 140 | 141 | def removeDir(dir): 142 | """Remove dir and all files under it, ignore errors. 143 | 144 | Arguments: 145 | dir -- the directory to remove 146 | """ 147 | try: 148 | shutil.rmtree(dir, 1) 149 | except: # shouldn't be needed with ignore error param, but 150 | pass # some python versions have a bug 151 | 152 | def main(): 153 | """Main installer function. 154 | """ 155 | optLetters = 'hp:d:i:b:sx' 156 | try: 157 | opts, args = getopt.getopt(sys.argv[1:], optLetters) 158 | except getopt.GetoptError: 159 | usage(2) 160 | global prefixDir 161 | global docDir 162 | global iconDir 163 | global buildRoot 164 | global progName 165 | depCheck = True 166 | translated = True 167 | for opt, val in opts: 168 | if opt == '-h': 169 | usage(0) 170 | elif opt == '-p': 171 | prefixDir = os.path.abspath(val) 172 | elif opt == '-d': 173 | docDir = val 174 | elif opt == '-i': 175 | iconDir = val 176 | elif opt == '-b': 177 | buildRoot = val 178 | elif opt == '-s': 179 | translated = False 180 | elif opt == '-x': 181 | depCheck = False 182 | if not os.path.isfile('install.py'): 183 | print('Error - {0} files not found'.format(progName)) 184 | print('The directory containing "install.py" must be current') 185 | sys.exit(4) 186 | if (os.path.isdir('source') and 187 | not os.path.isfile('source/{0}.py'.format(progName))): 188 | print('Error - source files not found') 189 | print('Retry the extraction from the tar archive') 190 | sys.exit(4) 191 | if depCheck: 192 | print('Checking dependencies...') 193 | pyVersion = sys.version_info[:3] 194 | pyVersion = '.'.join([str(num) for num in pyVersion]) 195 | if cmpVersions(pyVersion, (3, 4)): 196 | print(' Python Version {0} -> OK'.format(pyVersion)) 197 | else: 198 | print(' Python Version {0} -> Sorry, 3.4 or higher is required' 199 | .format(pyVersion)) 200 | sys.exit(3) 201 | try: 202 | from PyQt5 import QtCore, QtWidgets 203 | except: 204 | print(' PyQt not found -> Sorry, PyQt 5.4 or higher is required' 205 | ' and must be built for Python 3') 206 | sys.exit(3) 207 | qtVersion = QtCore.qVersion() 208 | if cmpVersions(qtVersion, (5, 4)): 209 | print(' Qt Version {0} -> OK'.format(qtVersion)) 210 | else: 211 | print(' Qt Version {0} -> Sorry, 5.4 or higher is required' 212 | .format(qtVersion)) 213 | sys.exit(3) 214 | pyqtVersion = QtCore.PYQT_VERSION_STR 215 | if cmpVersions(pyqtVersion, (5, 4)): 216 | print(' PyQt Version {0} -> OK'.format(pyqtVersion)) 217 | else: 218 | print(' PyQt Version {0} -> Sorry, 5.4 or higher is required' 219 | .format(pyqtVersion)) 220 | sys.exit(3) 221 | 222 | pythonPrefixDir = os.path.join(prefixDir, 'share', progName) 223 | pythonBuildDir = os.path.join(buildRoot, pythonPrefixDir[1:]) 224 | 225 | if os.path.isdir('source'): 226 | print('Installing files...') 227 | print(' Copying python files to {0}'.format(pythonBuildDir)) 228 | removeDir(pythonBuildDir) # remove old? 229 | copyDir('source', pythonBuildDir) 230 | if os.path.isdir('translations') and translated: 231 | translationDir = os.path.join(pythonBuildDir, 'translations') 232 | print(' Copying translation files to {0}'.format(translationDir)) 233 | copyDir('translations', translationDir) 234 | if os.path.isdir('doc'): 235 | docPrefixDir = docDir.replace('/', '') 236 | if not os.path.isabs(docPrefixDir): 237 | docPrefixDir = os.path.join(prefixDir, docPrefixDir) 238 | docBuildDir = os.path.join(buildRoot, docPrefixDir[1:]) 239 | print(' Copying documentation files to {0}'.format(docBuildDir)) 240 | copyDir('doc', docBuildDir) 241 | if not translated: 242 | for name in glob.glob(os.path.join(docBuildDir, 243 | '*_[a-z][a-z].html')): 244 | os.remove(name) 245 | # update help file location in main python script 246 | replaceLine(os.path.join(pythonBuildDir, '{0}.py'.format(progName)), 247 | 'helpFilePath = None', 248 | 'helpFilePath = \'{0}\' # modified by install script\n' 249 | .format(docPrefixDir)) 250 | if os.path.isdir('data'): 251 | dataPrefixDir = os.path.join(prefixDir, 'share', progName, 'data') 252 | dataBuildDir = os.path.join(buildRoot, dataPrefixDir[1:]) 253 | print(' Copying data files to {0}'.format(dataBuildDir)) 254 | removeDir(dataBuildDir) # remove old? 255 | copyDir('data', dataBuildDir) 256 | if not translated: 257 | for name in glob.glob(os.path.join(dataBuildDir, 258 | '*_[a-z][a-z].dat')): 259 | os.remove(name) 260 | # update data file location in main python script 261 | replaceLine(os.path.join(pythonBuildDir, '{0}.py'.format(progName)), 262 | 'dataFilePath = None', 263 | 'dataFilePath = \'{0}\' # modified by install script\n' 264 | .format(dataPrefixDir)) 265 | if os.path.isdir('icons'): 266 | iconPrefixDir = iconDir.replace('/', '') 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 | --------------------------------------------------------------------------------