2b7be7552c8c4c333b417e40866f321993e37cf4
[gonvert] / src / gonvert_qt.py
1 #!/usr/bin/env python
2 # -*- coding: UTF8 -*-
3
4 from __future__ import with_statement
5
6 import sys
7 import os
8 import math
9 import simplejson
10 import logging
11
12 from PyQt4 import QtGui
13 from PyQt4 import QtCore
14
15 import constants
16 import maeqt
17 from util import misc as misc_utils
18 import unit_data
19
20
21 _moduleLogger = logging.getLogger("gonvert_glade")
22
23
24 IS_MAEMO = True
25
26
27 def change_menu_label(widgets, labelname, newtext):
28         item_label = widgets.get_widget(labelname).get_children()[0]
29         item_label.set_text(newtext)
30
31
32 def split_number(number):
33         try:
34                 fractional, integer = math.modf(number)
35         except TypeError:
36                 integerDisplay = number
37                 fractionalDisplay = ""
38         else:
39                 integerDisplay = str(integer)
40                 fractionalDisplay = str(fractional)
41                 if "e+" in integerDisplay:
42                         integerDisplay = number
43                         fractionalDisplay = ""
44                 elif "e-" in fractionalDisplay and 0.0 < integer:
45                         integerDisplay = number
46                         fractionalDisplay = ""
47                 elif "e-" in fractionalDisplay:
48                         integerDisplay = ""
49                         fractionalDisplay = number
50                 else:
51                         integerDisplay = integerDisplay.split(".", 1)[0] + "."
52                         fractionalDisplay = fractionalDisplay.rsplit(".", 1)[-1]
53
54         return integerDisplay, fractionalDisplay
55
56
57 class Gonvert(object):
58
59         _DATA_PATHS = [
60                 os.path.dirname(__file__),
61                 os.path.join(os.path.dirname(__file__), "../share"),
62                 os.path.join(os.path.dirname(__file__), "../data"),
63                 '/usr/share/gonvert',
64                 '/opt/gonvert/share',
65         ]
66
67         def __init__(self, app):
68                 self._dataPath = ""
69                 for dataPath in self._DATA_PATHS:
70                         appIconPath = os.path.join(dataPath, "pixmaps", "gonvert.png")
71                         if os.path.isfile(appIconPath):
72                                 self._dataPath = dataPath
73                                 break
74                 else:
75                         raise RuntimeError("UI Descriptor not found!")
76                 self._app = app
77                 self._appIconPath = appIconPath
78                 self._recent = []
79                 self._hiddenCategories = set()
80                 self._hiddenUnits = {}
81                 self._clipboard = QtGui.QApplication.clipboard()
82
83                 self._jumpWindow = None
84                 self._recentWindow = None
85                 self._mainWindow = None
86                 self._catWindow = None
87                 self._quickWindow = None
88
89                 self._on_jump_close = lambda obj = None: self._on_child_close("_jumpWindow", obj)
90                 self._on_recent_close = lambda obj = None: self._on_child_close("_recentWindow", obj)
91                 self._on_cat_close = lambda obj = None: self._on_child_close("_catWindow", obj)
92                 self._on_quick_close = lambda obj = None: self._on_child_close("_quickWindow", obj)
93
94                 self._condensedAction = QtGui.QAction(None)
95                 self._condensedAction.setText("Condensed View")
96                 self._condensedAction.setCheckable(True)
97                 self._condensedAction.triggered.connect(self._on_condensed_start)
98
99                 self._jumpAction = QtGui.QAction(None)
100                 self._jumpAction.setText("Quick Jump")
101                 self._jumpAction.setStatusTip("Search for a unit and jump straight to it")
102                 self._jumpAction.setToolTip("Search for a unit and jump straight to it")
103                 self._jumpAction.setShortcut(QtGui.QKeySequence("CTRL+j"))
104                 self._jumpAction.triggered.connect(self._on_jump_start)
105
106                 self._recentAction = QtGui.QAction(None)
107                 self._recentAction.setText("Recent Units")
108                 self._recentAction.setStatusTip("View the recent units")
109                 self._recentAction.setToolTip("View the recent units")
110                 self._recentAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
111                 self._recentAction.triggered.connect(self._on_recent_start)
112
113                 self._fullscreenAction = QtGui.QAction(None)
114                 self._fullscreenAction.setText("Fullscreen")
115                 self._fullscreenAction.setCheckable(True)
116                 self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
117                 self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
118
119                 self._showFavoritesAction = QtGui.QAction(None)
120                 self._showFavoritesAction.setCheckable(True)
121                 self._showFavoritesAction.setText("Favorites Only")
122
123                 self._sortActionGroup = QtGui.QActionGroup(None)
124                 self._sortByNameAction = QtGui.QAction(self._sortActionGroup)
125                 self._sortByNameAction.setText("Sort By Name")
126                 self._sortByNameAction.setStatusTip("Sort the units by name")
127                 self._sortByNameAction.setToolTip("Sort the units by name")
128                 self._sortByNameAction.setCheckable(True)
129                 self._sortByValueAction = QtGui.QAction(self._sortActionGroup)
130                 self._sortByValueAction.setText("Sort By Value")
131                 self._sortByValueAction.setStatusTip("Sort the units by value")
132                 self._sortByValueAction.setToolTip("Sort the units by value")
133                 self._sortByValueAction.setCheckable(True)
134                 self._sortByUnitAction = QtGui.QAction(self._sortActionGroup)
135                 self._sortByUnitAction.setText("Sort By Unit")
136                 self._sortByUnitAction.setStatusTip("Sort the units by unit")
137                 self._sortByUnitAction.setToolTip("Sort the units by unit")
138                 self._sortByUnitAction.setCheckable(True)
139
140                 self._sortByNameAction.setChecked(True)
141
142                 self._logAction = QtGui.QAction(None)
143                 self._logAction.setText("Log")
144                 self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
145                 self._logAction.triggered.connect(self._on_log)
146
147                 self._quitAction = QtGui.QAction(None)
148                 self._quitAction.setText("Quit")
149                 self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
150                 self._quitAction.triggered.connect(self._on_quit)
151
152                 self._app.lastWindowClosed.connect(self._on_app_quit)
153                 self.load_settings()
154
155                 self.request_category()
156                 if self._recent:
157                         self._mainWindow.select_category(self._recent[-1][0])
158
159         def request_category(self):
160                 if self._mainWindow is not None:
161                         self._mainWindow.hide()
162
163                 if self._condensedAction.isChecked():
164                         if self._quickWindow is None:
165                                 self._quickWindow = QuickConvert(None, self)
166                                 self._quickWindow.window.destroyed.connect(self._on_quick_close)
167                         else:
168                                 self._quickWindow.show()
169                         self._mainWindow = self._quickWindow
170                 else:
171                         if self._catWindow is None:
172                                 self._catWindow = CategoryWindow(None, self)
173                                 self._catWindow.window.destroyed.connect(self._on_cat_close)
174                         else:
175                                 self._catWindow.window.show()
176                         self._mainWindow = self._catWindow
177
178                 return self._mainWindow
179
180         def search_units(self):
181                 jumpWindow = QuickJump(None, self)
182                 jumpWindow.window.destroyed.connect(self._on_jump_close)
183                 self._fake_close_windows()
184                 self._jumpWindow = jumpWindow
185                 return self._jumpWindow
186
187         def show_recent(self):
188                 recentWindow = Recent(None, self)
189                 recentWindow.window.destroyed.connect(self._on_recent_close)
190                 self._fake_close_windows()
191                 self._recentWindow = recentWindow
192                 return self._recentWindow
193
194         def add_recent(self, categoryName, unitName):
195                 catUnit = categoryName, unitName
196                 try:
197                         self._recent.remove(catUnit)
198                 except ValueError:
199                         pass # ignore if its not already in the recent history
200                 assert catUnit not in self._recent
201                 self._recent.append(catUnit)
202
203         def get_recent_unit(self, categoryName, fromMostRecent = 0):
204                 recentUnitName = ""
205                 for catName, unitName in reversed(self._recent):
206                         if catName == categoryName:
207                                 recentUnitName = unitName
208                                 if fromMostRecent <= 0:
209                                         break
210                                 else:
211                                         fromMostRecent -= 1
212                 return recentUnitName
213
214         def get_recent(self):
215                 return reversed(self._recent)
216
217         @property
218         def hiddenCategories(self):
219                 return self._hiddenCategories
220
221         def get_hidden_units(self, categoryName):
222                 try:
223                         return self._hiddenUnits[categoryName]
224                 except KeyError:
225                         self._hiddenUnits[categoryName] = set()
226                         return self._hiddenUnits[categoryName]
227
228         def load_settings(self):
229                 try:
230                         with open(constants._user_settings_, "r") as settingsFile:
231                                 settings = simplejson.load(settingsFile)
232                 except IOError, e:
233                         _moduleLogger.info("No settings")
234                         settings = {}
235                 except ValueError:
236                         _moduleLogger.info("Settings were corrupt")
237                         settings = {}
238
239                 self._fullscreenAction.setChecked(settings.get("isFullScreen", False))
240
241                 recent = settings.get("recent", self._recent)
242                 for category, unit in recent:
243                         self.add_recent(category, unit)
244
245                 self._hiddenCategories = set(settings.get("hiddenCategories", set()))
246                 self._hiddenUnits = dict(
247                         (catName, set(units))
248                         for (catName, units) in settings.get("hiddenUnits", {}).iteritems()
249                 )
250
251                 self._showFavoritesAction.setChecked(settings.get("showFavorites", True))
252
253                 self._condensedAction.setChecked(settings.get("useQuick", self._condensedAction.isChecked()))
254
255         def save_settings(self):
256                 settings = {
257                         "isFullScreen": self._fullscreenAction.isChecked(),
258                         "recent": self._recent,
259                         "hiddenCategories": list(self._hiddenCategories),
260                         "hiddenUnits": dict(
261                                 (catName, list(units))
262                                 for (catName, units) in self._hiddenUnits.iteritems()
263                         ),
264                         "showFavorites": self._showFavoritesAction.isChecked(),
265                         "useQuick": self._condensedAction.isChecked(),
266                 }
267                 with open(constants._user_settings_, "w") as settingsFile:
268                         simplejson.dump(settings, settingsFile)
269
270         @property
271         def appIconPath(self):
272                 return self._appIconPath
273
274         @property
275         def jumpAction(self):
276                 return self._jumpAction
277
278         @property
279         def recentAction(self):
280                 return self._recentAction
281
282         @property
283         def fullscreenAction(self):
284                 return self._fullscreenAction
285
286         @property
287         def condensedAction(self):
288                 return self._condensedAction
289
290         @property
291         def sortByNameAction(self):
292                 return self._sortByNameAction
293
294         @property
295         def sortByValueAction(self):
296                 return self._sortByValueAction
297
298         @property
299         def sortByUnitAction(self):
300                 return self._sortByUnitAction
301
302         @property
303         def logAction(self):
304                 return self._logAction
305
306         @property
307         def quitAction(self):
308                 return self._quitAction
309
310         @property
311         def showFavoritesAction(self):
312                 return self._showFavoritesAction
313
314         def _walk_children(self):
315                 if self._catWindow is not None:
316                         yield self._catWindow
317                 if self._quickWindow is not None:
318                         yield self._quickWindow
319                 if self._jumpWindow is not None:
320                         yield self._jumpWindow
321                 if self._recentWindow is not None:
322                         yield self._recentWindow
323
324         def _fake_close_windows(self):
325                 if self._catWindow is not None:
326                         self._catWindow.hide()
327                 if self._quickWindow is not None:
328                         self._quickWindow.hide()
329                 if self._jumpWindow is not None:
330                         self._jumpWindow.window.destroyed.disconnect(self._on_jump_close)
331                         self._jumpWindow.close()
332                         self._jumpWindow = None
333                 if self._recentWindow is not None:
334                         self._recentWindow.window.destroyed.disconnect(self._on_recent_close)
335                         self._recentWindow.close()
336                         self._recentWindow = None
337
338         def _close_windows(self):
339                 if self._catWindow is not None:
340                         self._catWindow.window.destroyed.disconnect(self._on_cat_close)
341                         self._catWindow.close()
342                         self._catWindow = None
343                 if self._quickWindow is not None:
344                         self._quickWindow.window.destroyed.disconnect(self._on_quick_close)
345                         self._quickWindow.close()
346                         self._quickWindow = None
347                 if self._jumpWindow is not None:
348                         self._jumpWindow.window.destroyed.disconnect(self._on_jump_close)
349                         self._jumpWindow.close()
350                         self._jumpWindow = None
351                 if self._recentWindow is not None:
352                         self._recentWindow.window.destroyed.disconnect(self._on_recent_close)
353                         self._recentWindow.close()
354                         self._recentWindow = None
355
356         @misc_utils.log_exception(_moduleLogger)
357         def _on_app_quit(self, checked = False):
358                 self.save_settings()
359
360         @misc_utils.log_exception(_moduleLogger)
361         def _on_child_close(self, name, obj = None):
362                 if not hasattr(self, name):
363                         _moduleLogger.info("Something weird going on when we don't have a %s" % name)
364                         return
365                 setattr(self, name, None)
366
367         @misc_utils.log_exception(_moduleLogger)
368         def _on_toggle_fullscreen(self, checked = False):
369                 for window in self._walk_children():
370                         window.set_fullscreen(checked)
371
372         @misc_utils.log_exception(_moduleLogger)
373         def _on_condensed_start(self, checked = False):
374                 self.request_category()
375                 if self._recent:
376                         self._mainWindow.select_category(self._recent[-1][0])
377
378         @misc_utils.log_exception(_moduleLogger)
379         def _on_jump_start(self, checked = False):
380                 self.search_units()
381
382         @misc_utils.log_exception(_moduleLogger)
383         def _on_recent_start(self, checked = False):
384                 self.show_recent()
385
386         @misc_utils.log_exception(_moduleLogger)
387         def _on_log(self, checked = False):
388                 with open(constants._user_logpath_, "r") as f:
389                         logLines = f.xreadlines()
390                         log = "".join(logLines)
391                         self._clipboard.setText(log)
392
393         @misc_utils.log_exception(_moduleLogger)
394         def _on_quit(self, checked = False):
395                 self._close_windows()
396
397
398 class QuickJump(object):
399
400         MINIMAL_ENTRY = 3
401
402         def __init__(self, parent, app):
403                 self._app = app
404
405                 self._searchLabel = QtGui.QLabel("Search:")
406                 self._searchEntry = QtGui.QLineEdit("")
407                 self._searchEntry.textEdited.connect(self._on_search_edited)
408
409                 self._entryLayout = QtGui.QHBoxLayout()
410                 self._entryLayout.addWidget(self._searchLabel)
411                 self._entryLayout.addWidget(self._searchEntry)
412
413                 self._resultsBox = QtGui.QTreeWidget()
414                 self._resultsBox.setHeaderLabels(["Categories", "Units"])
415                 self._resultsBox.setHeaderHidden(True)
416                 if not IS_MAEMO:
417                         self._resultsBox.setAlternatingRowColors(True)
418                 self._resultsBox.itemClicked.connect(self._on_result_clicked)
419
420                 self._layout = QtGui.QVBoxLayout()
421                 self._layout.addLayout(self._entryLayout)
422                 self._layout.addWidget(self._resultsBox)
423
424                 centralWidget = QtGui.QWidget()
425                 centralWidget.setLayout(self._layout)
426
427                 self._window = QtGui.QMainWindow(parent)
428                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
429                 maeqt.set_autorient(self._window, True)
430                 maeqt.set_stackable(self._window, True)
431                 self._window.setWindowTitle("%s - Quick Jump" % constants.__pretty_app_name__)
432                 self._window.setWindowIcon(QtGui.QIcon(self._app.appIconPath))
433                 self._window.setCentralWidget(centralWidget)
434
435                 self._closeWindowAction = QtGui.QAction(None)
436                 self._closeWindowAction.setText("Close")
437                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
438                 self._closeWindowAction.triggered.connect(self._on_close_window)
439
440                 if IS_MAEMO:
441                         self._window.addAction(self._closeWindowAction)
442                         self._window.addAction(self._app.quitAction)
443                         self._window.addAction(self._app.fullscreenAction)
444                 else:
445                         fileMenu = self._window.menuBar().addMenu("&Units")
446                         fileMenu.addAction(self._closeWindowAction)
447                         fileMenu.addAction(self._app.quitAction)
448
449                         viewMenu = self._window.menuBar().addMenu("&View")
450                         viewMenu.addAction(self._app.fullscreenAction)
451
452                 self._window.addAction(self._app.logAction)
453
454                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
455                 self._window.show()
456
457         @property
458         def window(self):
459                 return self._window
460
461         def show(self):
462                 self._window.show()
463
464         def hide(self):
465                 self._window.hide()
466
467         def close(self):
468                 self._window.close()
469
470         def set_fullscreen(self, isFullscreen):
471                 if isFullscreen:
472                         self._window.showFullScreen()
473                 else:
474                         self._window.showNormal()
475
476         @misc_utils.log_exception(_moduleLogger)
477         def _on_close_window(self, checked = True):
478                 self.close()
479
480         @misc_utils.log_exception(_moduleLogger)
481         def _on_result_clicked(self, item, columnIndex):
482                 categoryName = unicode(item.text(0))
483                 unitName = unicode(item.text(1))
484                 catWindow = self._app.request_category()
485                 unitsWindow = catWindow.select_category(categoryName)
486                 unitsWindow.select_unit(unitName)
487                 self.close()
488
489         @misc_utils.log_exception(_moduleLogger)
490         def _on_search_edited(self, *args):
491                 userInput = self._searchEntry.text()
492                 if len(userInput) <  self.MINIMAL_ENTRY:
493                         return
494
495                 self._resultsBox.clear()
496                 lowerInput = str(userInput).lower()
497                 for catIndex, category in enumerate(unit_data.UNIT_CATEGORIES):
498                         units = unit_data.get_units(category)
499                         for unitIndex, unit in enumerate(units):
500                                 loweredUnit = unit.lower()
501                                 if lowerInput in loweredUnit:
502                                         twi = QtGui.QTreeWidgetItem(self._resultsBox)
503                                         twi.setText(0, category)
504                                         twi.setText(1, unit)
505
506
507 class Recent(object):
508
509         def __init__(self, parent, app):
510                 self._app = app
511
512                 self._resultsBox = QtGui.QTreeWidget()
513                 self._resultsBox.setHeaderLabels(["Categories", "Units"])
514                 self._resultsBox.setHeaderHidden(True)
515                 if not IS_MAEMO:
516                         self._resultsBox.setAlternatingRowColors(True)
517                 self._resultsBox.itemClicked.connect(self._on_result_clicked)
518
519                 self._layout = QtGui.QVBoxLayout()
520                 self._layout.addWidget(self._resultsBox)
521
522                 centralWidget = QtGui.QWidget()
523                 centralWidget.setLayout(self._layout)
524
525                 self._window = QtGui.QMainWindow(parent)
526                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
527                 maeqt.set_autorient(self._window, True)
528                 maeqt.set_stackable(self._window, True)
529                 self._window.setWindowTitle("%s - Recent" % constants.__pretty_app_name__)
530                 self._window.setWindowIcon(QtGui.QIcon(self._app.appIconPath))
531                 self._window.setCentralWidget(centralWidget)
532
533                 for cat, unit in self._app.get_recent():
534                         twi = QtGui.QTreeWidgetItem(self._resultsBox)
535                         twi.setText(0, cat)
536                         twi.setText(1, unit)
537
538                 self._closeWindowAction = QtGui.QAction(None)
539                 self._closeWindowAction.setText("Close")
540                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
541                 self._closeWindowAction.triggered.connect(self._on_close_window)
542
543                 if IS_MAEMO:
544                         self._window.addAction(self._closeWindowAction)
545                         self._window.addAction(self._app.quitAction)
546                         self._window.addAction(self._app.fullscreenAction)
547                 else:
548                         fileMenu = self._window.menuBar().addMenu("&Units")
549                         fileMenu.addAction(self._closeWindowAction)
550                         fileMenu.addAction(self._app.quitAction)
551
552                         viewMenu = self._window.menuBar().addMenu("&View")
553                         viewMenu.addAction(self._app.fullscreenAction)
554
555                 self._window.addAction(self._app.logAction)
556
557                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
558                 self._window.show()
559
560         @property
561         def window(self):
562                 return self._window
563
564         def show(self):
565                 self._window.show()
566
567         def hide(self):
568                 self._window.hide()
569
570         def close(self):
571                 self._window.close()
572
573         def set_fullscreen(self, isFullscreen):
574                 if isFullscreen:
575                         self._window.showFullScreen()
576                 else:
577                         self._window.showNormal()
578
579         @misc_utils.log_exception(_moduleLogger)
580         def _on_close_window(self, checked = True):
581                 self.close()
582
583         @misc_utils.log_exception(_moduleLogger)
584         def _on_result_clicked(self, item, columnIndex):
585                 categoryName = unicode(item.text(0))
586                 unitName = unicode(item.text(1))
587                 catWindow = self._app.request_category()
588                 unitsWindow = catWindow.select_category(categoryName)
589                 unitsWindow.select_unit(unitName)
590                 self.close()
591
592
593 class QuickConvert(object):
594
595         def __init__(self, parent, app):
596                 self._app = app
597                 self._categoryName = ""
598                 self._inputUnitName = ""
599                 self._outputUnitName = ""
600                 self._unitNames = []
601                 self._favoritesWindow = None
602
603                 self._inputUnitValue = QtGui.QLineEdit()
604                 self._inputUnitValue.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
605                 self._inputUnitValue.textEdited.connect(self._on_value_edited)
606                 self._inputUnitSymbol = QtGui.QLabel()
607
608                 self._outputUnitValue = QtGui.QLabel()
609                 self._outputUnitSymbol = QtGui.QLabel()
610
611                 self._conversionLayout = QtGui.QHBoxLayout()
612                 self._conversionLayout.addWidget(self._inputUnitValue)
613                 self._conversionLayout.addWidget(self._inputUnitSymbol)
614                 self._conversionLayout.addWidget(self._outputUnitValue)
615                 self._conversionLayout.addWidget(self._outputUnitSymbol)
616
617                 self._categoryView = QtGui.QTreeWidget()
618                 self._categoryView.setHeaderLabels(["Categories"])
619                 self._categoryView.setHeaderHidden(False)
620                 if not IS_MAEMO:
621                         self._categoryView.setAlternatingRowColors(True)
622                 self._categoryView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
623                 self._categoryView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
624                 for catName in unit_data.UNIT_CATEGORIES:
625                         twi = QtGui.QTreeWidgetItem(self._categoryView)
626                         twi.setText(0, catName)
627                 self._categorySelection = self._categoryView.selectionModel()
628                 self._categorySelection.selectionChanged.connect(self._on_category_selection_changed)
629
630                 self._inputView = QtGui.QTreeWidget()
631                 self._inputView.setHeaderLabels(["From", "Name"])
632                 self._inputView.setHeaderHidden(False)
633                 self._inputView.header().hideSection(1)
634                 if not IS_MAEMO:
635                         self._inputView.setAlternatingRowColors(True)
636                 self._inputView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
637                 self._inputView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
638                 self._inputSelection = self._inputView.selectionModel()
639                 self._inputSelection.selectionChanged.connect(self._on_input_selection_changed)
640
641                 self._outputView = QtGui.QTreeWidget()
642                 self._outputView.setHeaderLabels(["To", "Name"])
643                 self._outputView.setHeaderHidden(False)
644                 self._outputView.header().hideSection(1)
645                 if not IS_MAEMO:
646                         self._outputView.setAlternatingRowColors(True)
647                 self._outputView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
648                 self._outputView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
649                 self._outputWidgets = []
650                 self._outputSelection = self._outputView.selectionModel()
651                 self._outputSelection.selectionChanged.connect(self._on_output_selection_changed)
652
653                 self._selectionLayout = QtGui.QHBoxLayout()
654                 self._selectionLayout.addWidget(self._categoryView)
655                 self._selectionLayout.addWidget(self._inputView)
656                 self._selectionLayout.addWidget(self._outputView)
657
658                 self._layout = QtGui.QVBoxLayout()
659                 self._layout.addLayout(self._conversionLayout)
660                 self._layout.addLayout(self._selectionLayout)
661
662                 centralWidget = QtGui.QWidget()
663                 centralWidget.setLayout(self._layout)
664
665                 self._window = QtGui.QMainWindow(parent)
666                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
667                 maeqt.set_autorient(self._window, True)
668                 maeqt.set_stackable(self._window, True)
669                 self._window.setWindowTitle("%s - Quick Convert" % (constants.__pretty_app_name__, ))
670                 self._window.setWindowIcon(QtGui.QIcon(app.appIconPath))
671                 self._window.setCentralWidget(centralWidget)
672
673                 self._chooseCatFavoritesAction = QtGui.QAction(None)
674                 self._chooseCatFavoritesAction.setText("Select Categories")
675                 self._chooseCatFavoritesAction.triggered.connect(self._on_choose_category_favorites)
676
677                 self._chooseUnitFavoritesAction = QtGui.QAction(None)
678                 self._chooseUnitFavoritesAction.setText("Select Units")
679                 self._chooseUnitFavoritesAction.triggered.connect(self._on_choose_unit_favorites)
680                 self._chooseUnitFavoritesAction.setEnabled(False)
681
682                 self._app.showFavoritesAction.toggled.connect(self._on_show_favorites)
683
684                 self._closeWindowAction = QtGui.QAction(None)
685                 self._closeWindowAction.setText("Close Window")
686                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
687                 self._closeWindowAction.triggered.connect(self._on_close_window)
688
689                 if IS_MAEMO:
690                         self._window.addAction(self._closeWindowAction)
691                         self._window.addAction(self._app.quitAction)
692                         self._window.addAction(self._app.fullscreenAction)
693
694                         fileMenu = self._window.menuBar().addMenu("&Units")
695                         fileMenu.addAction(self._chooseCatFavoritesAction)
696                         fileMenu.addAction(self._chooseUnitFavoritesAction)
697
698                         viewMenu = self._window.menuBar().addMenu("&View")
699                         viewMenu.addAction(self._app.showFavoritesAction)
700                         viewMenu.addAction(self._app.condensedAction)
701                         viewMenu.addSeparator()
702                         viewMenu.addAction(self._app.jumpAction)
703                         viewMenu.addAction(self._app.recentAction)
704                 else:
705                         fileMenu = self._window.menuBar().addMenu("&Units")
706                         fileMenu.addAction(self._chooseCatFavoritesAction)
707                         fileMenu.addAction(self._chooseUnitFavoritesAction)
708                         fileMenu.addAction(self._closeWindowAction)
709                         fileMenu.addAction(self._app.quitAction)
710
711                         viewMenu = self._window.menuBar().addMenu("&View")
712                         viewMenu.addAction(self._app.showFavoritesAction)
713                         viewMenu.addAction(self._app.condensedAction)
714                         viewMenu.addSeparator()
715                         viewMenu.addAction(self._app.jumpAction)
716                         viewMenu.addAction(self._app.recentAction)
717                         viewMenu.addSeparator()
718                         viewMenu.addAction(self._app.fullscreenAction)
719
720                 self._window.addAction(self._app.logAction)
721
722                 self._update_favorites()
723                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
724                 self._window.show()
725
726         @property
727         def window(self):
728                 return self._window
729
730         def show(self):
731                 self._window.show()
732
733         def hide(self):
734                 self._window.hide()
735
736         def close(self):
737                 self._window.close()
738
739         def set_fullscreen(self, isFullscreen):
740                 if isFullscreen:
741                         self._window.showFullScreen()
742                 else:
743                         self._window.showNormal()
744
745         def select_category(self, categoryName):
746                 self._inputUnitName = ""
747                 self._outputUnitName = ""
748                 self._inputUnitValue.setText("")
749                 self._inputUnitSymbol.setText("")
750                 self._inputView.clear()
751                 self._outputUnitValue.setText("")
752                 self._outputUnitSymbol.setText("")
753                 self._outputView.clear()
754                 self._categoryName = categoryName
755                 self._chooseUnitFavoritesAction.setEnabled(True)
756
757                 unitData = unit_data.UNIT_DESCRIPTIONS[categoryName]
758                 self._unitNames = list(unit_data.get_units(categoryName))
759                 self._unitNames.sort()
760                 for key in self._unitNames:
761                         conversion, unit, description = unitData[key]
762                         if not unit:
763                                 unit = key
764
765                         twi = QtGui.QTreeWidgetItem(self._inputView)
766                         twi.setText(0, unit)
767                         twi.setText(1, key)
768
769                         twi = QtGui.QTreeWidgetItem(self._outputView)
770                         twi.setText(0, unit)
771                         twi.setText(1, key)
772
773                 i = unit_data.UNIT_CATEGORIES.index(categoryName)
774                 rootIndex = self._categoryView.rootIndex()
775                 currentIndex = self._categoryView.model().index(i, 0, rootIndex)
776                 self._categoryView.scrollTo(currentIndex)
777
778                 defaultInputUnitName = self._app.get_recent_unit(categoryName)
779                 if defaultInputUnitName:
780                         self.select_input(defaultInputUnitName)
781                         defaultOutputUnitName = self._app.get_recent_unit(categoryName, 1)
782                         assert defaultOutputUnitName
783                         self.select_output(defaultOutputUnitName)
784
785                 return self
786
787         def select_unit(self, name):
788                 self.select_input(name)
789
790         def select_input(self, name):
791                 self._app.add_recent(self._categoryName, name)
792                 self._inputUnitName = name
793
794                 unitData = unit_data.UNIT_DESCRIPTIONS[self._categoryName]
795                 conversion, unit, description = unitData[name]
796
797                 self._inputUnitSymbol.setText(unit if unit else name)
798
799                 i = self._unitNames.index(name)
800                 rootIndex = self._inputView.rootIndex()
801                 currentIndex = self._inputView.model().index(i, 0, rootIndex)
802                 self._inputView.scrollTo(currentIndex)
803
804                 if "" not in [self._categoryName, self._inputUnitName, self._outputUnitName]:
805                         self._update_conversion()
806
807         def select_output(self, name):
808                 # Add the output to recent but don't make things weird by making it the most recent
809                 self._app.add_recent(self._categoryName, name)
810                 self._app.add_recent(self._categoryName, self._inputUnitName)
811                 self._outputUnitName = name
812
813                 unitData = unit_data.UNIT_DESCRIPTIONS[self._categoryName]
814                 conversion, unit, description = unitData[name]
815
816                 self._outputUnitSymbol.setText(unit if unit else name)
817
818                 i = self._unitNames.index(name)
819                 rootIndex = self._outputView.rootIndex()
820                 currentIndex = self._outputView.model().index(i, 0, rootIndex)
821                 self._outputView.scrollTo(currentIndex)
822
823                 if "" not in [self._categoryName, self._inputUnitName, self._outputUnitName]:
824                         self._update_conversion()
825
826         def _sanitize_value(self, userEntry):
827                 if self._categoryName == "Computer Numbers":
828                         if userEntry == '':
829                                 value = '0'
830                         else:
831                                 value = userEntry
832                 else:
833                         if userEntry == '':
834                                 value = 0.0
835                         else:
836                                 value = float(userEntry)
837                 return value
838
839         def _update_conversion(self):
840                 assert self._categoryName
841                 assert self._inputUnitName
842                 assert self._outputUnitName
843
844                 userInput = str(self._inputUnitValue.text())
845                 value = self._sanitize_value(userInput)
846
847                 unitData = unit_data.UNIT_DESCRIPTIONS[self._categoryName]
848                 inputConversion, _, _ = unitData[self._inputUnitName]
849                 outputConversion, _, _ = unitData[self._outputUnitName]
850
851                 func, arg = inputConversion
852                 base = func.to_base(value, arg)
853
854                 func, arg = outputConversion
855                 newValue = func.from_base(base, arg)
856                 self._outputUnitValue.setText(str(newValue))
857
858         def _update_favorites(self):
859                 if self._app.showFavoritesAction.isChecked():
860                         assert self._categoryView.topLevelItemCount() == len(unit_data.UNIT_CATEGORIES)
861                         for i, catName in enumerate(unit_data.UNIT_CATEGORIES):
862                                 if catName in self._app.hiddenCategories:
863                                         self._categoryView.setRowHidden(i, self._categoryView.rootIndex(), True)
864                                 else:
865                                         self._categoryView.setRowHidden(i, self._categoryView.rootIndex(), False)
866
867                         for i, unitName in enumerate(self._unitNames):
868                                 if unitName in self._app.get_hidden_units(self._categoryName):
869                                         self._inputView.setRowHidden(i, self._inputView.rootIndex(), True)
870                                         self._outputView.setRowHidden(i, self._outputView.rootIndex(), True)
871                                 else:
872                                         self._inputView.setRowHidden(i, self._inputView.rootIndex(), False)
873                                         self._outputView.setRowHidden(i, self._outputView.rootIndex(), False)
874                 else:
875                         for i in xrange(self._categoryView.topLevelItemCount()):
876                                 self._categoryView.setRowHidden(i, self._categoryView.rootIndex(), False)
877
878                         for i in xrange(len(self._unitNames)):
879                                 self._inputView.setRowHidden(i, self._inputView.rootIndex(), False)
880                                 self._outputView.setRowHidden(i, self._outputView.rootIndex(), False)
881
882         @misc_utils.log_exception(_moduleLogger)
883         def _on_close_window(self, checked = True):
884                 self.close()
885
886         @misc_utils.log_exception(_moduleLogger)
887         def _on_show_favorites(self, checked = True):
888                 if checked:
889                         assert self._categoryView.topLevelItemCount() == len(unit_data.UNIT_CATEGORIES)
890                         for i, catName in enumerate(unit_data.UNIT_CATEGORIES):
891                                 if catName in self._app.hiddenCategories:
892                                         self._categoryView.setRowHidden(i, self._categoryView.rootIndex(), True)
893
894                         for i, unitName in enumerate(self._unitNames):
895                                 if unitName in self._app.get_hidden_units(self._categoryName):
896                                         self._inputView.setRowHidden(i, self._inputView.rootIndex(), True)
897                                         self._outputView.setRowHidden(i, self._outputView.rootIndex(), True)
898                 else:
899                         for i in xrange(self._categoryView.topLevelItemCount()):
900                                 self._categoryView.setRowHidden(i, self._categoryView.rootIndex(), False)
901
902                         for i in xrange(len(self._unitNames)):
903                                 self._inputView.setRowHidden(i, self._inputView.rootIndex(), False)
904                                 self._outputView.setRowHidden(i, self._outputView.rootIndex(), False)
905
906         @misc_utils.log_exception(_moduleLogger)
907         def _on_choose_category_favorites(self, obj = None):
908                 assert self._favoritesWindow is None
909                 self._favoritesWindow = FavoritesWindow(
910                         self._window,
911                         self._app,
912                         unit_data.UNIT_CATEGORIES,
913                         self._app.hiddenCategories
914                 )
915                 self._favoritesWindow.window.destroyed.connect(self._on_close_favorites)
916                 return self._favoritesWindow
917
918         @misc_utils.log_exception(_moduleLogger)
919         def _on_choose_unit_favorites(self, obj = None):
920                 assert self._favoritesWindow is None
921                 self._favoritesWindow = FavoritesWindow(
922                         self._window,
923                         self._app,
924                         unit_data.get_units(self._categoryName),
925                         self._app.get_hidden_units(self._categoryName)
926                 )
927                 self._favoritesWindow.window.destroyed.connect(self._on_close_favorites)
928                 return self._favoritesWindow
929
930         @misc_utils.log_exception(_moduleLogger)
931         def _on_close_favorites(self, obj = None):
932                 self._favoritesWindow = None
933                 self._update_favorites()
934
935         @misc_utils.log_exception(_moduleLogger)
936         def _on_value_edited(self, *args):
937                 self._update_conversion()
938
939         @misc_utils.log_exception(_moduleLogger)
940         def _on_category_selection_changed(self, selected, deselected):
941                 selectedNames = [
942                         str(item.text(0))
943                         for item in self._categoryView.selectedItems()
944                 ]
945                 assert len(selectedNames) == 1
946                 self.select_category(selectedNames[0])
947
948         @misc_utils.log_exception(_moduleLogger)
949         def _on_input_selection_changed(self, selected, deselected):
950                 selectedNames = [
951                         str(item.text(1))
952                         for item in self._inputView.selectedItems()
953                 ]
954                 if selectedNames:
955                         assert len(selectedNames) == 1
956                         name = selectedNames[0]
957                         self.select_input(name)
958                 else:
959                         pass
960
961         @misc_utils.log_exception(_moduleLogger)
962         def _on_output_selection_changed(self, selected, deselected):
963                 selectedNames = [
964                         str(item.text(1))
965                         for item in self._outputView.selectedItems()
966                 ]
967                 if selectedNames:
968                         assert len(selectedNames) == 1
969                         name = selectedNames[0]
970                         self.select_output(name)
971                 else:
972                         pass
973
974
975 class FavoritesWindow(object):
976
977         def __init__(self, parent, app, source, hidden):
978                 self._app = app
979                 self._source = list(source)
980                 self._hidden = hidden
981
982                 self._categories = QtGui.QTreeWidget()
983                 self._categories.setHeaderLabels(["Categories"])
984                 self._categories.setHeaderHidden(True)
985                 if not IS_MAEMO:
986                         self._categories.setAlternatingRowColors(True)
987                 self._categories.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
988                 self._categories.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
989                 self._childWidgets = []
990                 for catName in self._source:
991                         twi = QtGui.QTreeWidgetItem(self._categories)
992                         twi.setText(0, catName)
993                         self._childWidgets.append(twi)
994                         if catName not in self._hidden:
995                                 self._categories.setItemSelected(twi, True)
996                 self._selection = self._categories.selectionModel()
997                 self._selection.selectionChanged.connect(self._on_selection_changed)
998
999                 self._allButton = QtGui.QPushButton("All")
1000                 self._allButton.clicked.connect(self._on_select_all)
1001                 self._invertButton = QtGui.QPushButton("Invert")
1002                 self._invertButton.clicked.connect(self._on_invert_selection)
1003                 self._noneButton = QtGui.QPushButton("None")
1004                 self._noneButton.clicked.connect(self._on_select_none)
1005
1006                 self._buttonLayout = QtGui.QHBoxLayout()
1007                 self._buttonLayout.addWidget(self._allButton)
1008                 self._buttonLayout.addWidget(self._invertButton)
1009                 self._buttonLayout.addWidget(self._noneButton)
1010
1011                 self._layout = QtGui.QVBoxLayout()
1012                 self._layout.addWidget(self._categories)
1013                 self._layout.addLayout(self._buttonLayout)
1014
1015                 centralWidget = QtGui.QWidget()
1016                 centralWidget.setLayout(self._layout)
1017
1018                 self._window = QtGui.QMainWindow(parent)
1019                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1020                 maeqt.set_autorient(self._window, True)
1021                 maeqt.set_stackable(self._window, True)
1022                 self._window.setWindowTitle("%s - Favorites" % constants.__pretty_app_name__)
1023                 self._window.setWindowIcon(QtGui.QIcon(self._app.appIconPath))
1024                 self._window.setCentralWidget(centralWidget)
1025
1026                 self._closeWindowAction = QtGui.QAction(None)
1027                 self._closeWindowAction.setText("Close")
1028                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1029                 self._closeWindowAction.triggered.connect(self._on_close_window)
1030
1031                 if IS_MAEMO:
1032                         self._window.addAction(self._closeWindowAction)
1033                         self._window.addAction(self._app.quitAction)
1034                         self._window.addAction(self._app.fullscreenAction)
1035                 else:
1036                         fileMenu = self._window.menuBar().addMenu("&Units")
1037                         fileMenu.addAction(self._closeWindowAction)
1038                         fileMenu.addAction(self._app.quitAction)
1039
1040                         viewMenu = self._window.menuBar().addMenu("&View")
1041                         viewMenu.addAction(self._app.fullscreenAction)
1042
1043                 self._window.addAction(self._app.logAction)
1044
1045                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1046                 self._window.show()
1047
1048         @property
1049         def window(self):
1050                 return self._window
1051
1052         def show(self):
1053                 self._window.show()
1054
1055         def hide(self):
1056                 self._window.hide()
1057
1058         def close(self):
1059                 self._window.close()
1060
1061         def set_fullscreen(self, isFullscreen):
1062                 if isFullscreen:
1063                         self._window.showFullScreen()
1064                 else:
1065                         self._window.showNormal()
1066
1067         @misc_utils.log_exception(_moduleLogger)
1068         def _on_select_all(self, checked = False):
1069                 for child in self._childWidgets:
1070                         self._categories.setItemSelected(child, True)
1071
1072         @misc_utils.log_exception(_moduleLogger)
1073         def _on_invert_selection(self, checked = False):
1074                 for child in self._childWidgets:
1075                         isSelected = self._categories.isItemSelected(child)
1076                         self._categories.setItemSelected(child, not isSelected)
1077
1078         @misc_utils.log_exception(_moduleLogger)
1079         def _on_select_none(self, checked = False):
1080                 for child in self._childWidgets:
1081                         self._categories.setItemSelected(child, False)
1082
1083         @misc_utils.log_exception(_moduleLogger)
1084         def _on_selection_changed(self, selected, deselected):
1085                 self._hidden.clear()
1086                 selectedNames = set(
1087                         str(item.text(0))
1088                         for item in self._categories.selectedItems()
1089                 )
1090                 for name in self._source:
1091                         if name not in selectedNames:
1092                                 self._hidden.add(name)
1093
1094         @misc_utils.log_exception(_moduleLogger)
1095         def _on_close_window(self, checked = True):
1096                 self.close()
1097
1098
1099 class CategoryWindow(object):
1100
1101         def __init__(self, parent, app):
1102                 self._app = app
1103                 self._unitWindow = None
1104                 self._favoritesWindow = None
1105
1106                 self._categories = QtGui.QTreeWidget()
1107                 self._categories.setHeaderLabels(["Categories"])
1108                 self._categories.itemClicked.connect(self._on_category_clicked)
1109                 self._categories.setHeaderHidden(True)
1110                 self._categories.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1111                 self._categories.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
1112                 if not IS_MAEMO:
1113                         self._categories.setAlternatingRowColors(True)
1114                 for catName in unit_data.UNIT_CATEGORIES:
1115                         twi = QtGui.QTreeWidgetItem(self._categories)
1116                         twi.setText(0, catName)
1117
1118                 self._layout = QtGui.QVBoxLayout()
1119                 self._layout.addWidget(self._categories)
1120
1121                 centralWidget = QtGui.QWidget()
1122                 centralWidget.setLayout(self._layout)
1123
1124                 self._window = QtGui.QMainWindow(parent)
1125                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1126                 maeqt.set_autorient(self._window, True)
1127                 maeqt.set_stackable(self._window, True)
1128                 self._window.setWindowTitle("%s - Categories" % constants.__pretty_app_name__)
1129                 self._window.setWindowIcon(QtGui.QIcon(self._app.appIconPath))
1130                 self._window.setCentralWidget(centralWidget)
1131
1132                 self._chooseFavoritesAction = QtGui.QAction(None)
1133                 self._chooseFavoritesAction.setText("Select Favorites")
1134                 self._chooseFavoritesAction.setShortcut(QtGui.QKeySequence("CTRL+f"))
1135                 self._chooseFavoritesAction.triggered.connect(self._on_choose_favorites)
1136
1137                 self._app.showFavoritesAction.toggled.connect(self._on_show_favorites)
1138
1139                 self._closeWindowAction = QtGui.QAction(None)
1140                 self._closeWindowAction.setText("Close")
1141                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1142                 self._closeWindowAction.triggered.connect(self._on_close_window)
1143
1144                 if IS_MAEMO:
1145                         fileMenu = self._window.menuBar().addMenu("&Units")
1146                         fileMenu.addAction(self._chooseFavoritesAction)
1147
1148                         viewMenu = self._window.menuBar().addMenu("&View")
1149                         viewMenu.addAction(self._app.showFavoritesAction)
1150                         viewMenu.addAction(self._app.condensedAction)
1151                         viewMenu.addSeparator()
1152                         viewMenu.addAction(self._app.jumpAction)
1153                         viewMenu.addAction(self._app.recentAction)
1154
1155                         self._window.addAction(self._closeWindowAction)
1156                         self._window.addAction(self._app.quitAction)
1157                         self._window.addAction(self._app.fullscreenAction)
1158                 else:
1159                         fileMenu = self._window.menuBar().addMenu("&Units")
1160                         fileMenu.addAction(self._chooseFavoritesAction)
1161                         fileMenu.addAction(self._closeWindowAction)
1162                         fileMenu.addAction(self._app.quitAction)
1163
1164                         viewMenu = self._window.menuBar().addMenu("&View")
1165                         viewMenu.addAction(self._app.showFavoritesAction)
1166                         viewMenu.addAction(self._app.condensedAction)
1167                         viewMenu.addSeparator()
1168                         viewMenu.addAction(self._app.jumpAction)
1169                         viewMenu.addAction(self._app.recentAction)
1170                         viewMenu.addSeparator()
1171                         viewMenu.addAction(self._app.fullscreenAction)
1172
1173                 self._window.addAction(self._app.logAction)
1174
1175                 self._update_favorites()
1176                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1177                 self._window.show()
1178
1179         @property
1180         def window(self):
1181                 return self._window
1182
1183         def walk_children(self):
1184                 if self._unitWindow is not None:
1185                         yield self._unitWindow
1186                 if self._favoritesWindow is not None:
1187                         yield self._favoritesWindow
1188
1189         def show(self):
1190                 self._window.show()
1191                 for child in self.walk_children():
1192                         child.show()
1193
1194         def hide(self):
1195                 for child in self.walk_children():
1196                         child.hide()
1197                 self._window.hide()
1198
1199         def close(self):
1200                 for child in self.walk_children():
1201                         child.window.destroyed.disconnect(self._on_child_close)
1202                         child.close()
1203                 self._window.close()
1204
1205         def select_category(self, categoryName):
1206                 for child in self.walk_children():
1207                         child.window.destroyed.disconnect(self._on_child_close)
1208                         child.close()
1209                 self._unitWindow = UnitWindow(self._window, categoryName, self._app)
1210                 self._unitWindow.window.destroyed.connect(self._on_child_close)
1211
1212                 i = unit_data.UNIT_CATEGORIES.index(categoryName)
1213                 rootIndex = self._categories.rootIndex()
1214                 currentIndex = self._categories.model().index(i, 0, rootIndex)
1215                 self._categories.scrollTo(currentIndex)
1216                 return self._unitWindow
1217
1218         def set_fullscreen(self, isFullscreen):
1219                 if isFullscreen:
1220                         self._window.showFullScreen()
1221                 else:
1222                         self._window.showNormal()
1223                 for child in self.walk_children():
1224                         child.set_fullscreen(isFullscreen)
1225
1226         def _update_favorites(self):
1227                 if self._app.showFavoritesAction.isChecked():
1228                         assert self._categories.topLevelItemCount() == len(unit_data.UNIT_CATEGORIES)
1229                         for i, catName in enumerate(unit_data.UNIT_CATEGORIES):
1230                                 if catName in self._app.hiddenCategories:
1231                                         self._categories.setRowHidden(i, self._categories.rootIndex(), True)
1232                                 else:
1233                                         self._categories.setRowHidden(i, self._categories.rootIndex(), False)
1234                 else:
1235                         for i in xrange(self._categories.topLevelItemCount()):
1236                                 self._categories.setRowHidden(i, self._categories.rootIndex(), False)
1237
1238         @misc_utils.log_exception(_moduleLogger)
1239         def _on_show_favorites(self, checked = True):
1240                 if checked:
1241                         assert self._categories.topLevelItemCount() == len(unit_data.UNIT_CATEGORIES)
1242                         for i, catName in enumerate(unit_data.UNIT_CATEGORIES):
1243                                 if catName in self._app.hiddenCategories:
1244                                         self._categories.setRowHidden(i, self._categories.rootIndex(), True)
1245                 else:
1246                         for i in xrange(self._categories.topLevelItemCount()):
1247                                 self._categories.setRowHidden(i, self._categories.rootIndex(), False)
1248
1249         @misc_utils.log_exception(_moduleLogger)
1250         def _on_choose_favorites(self, obj = None):
1251                 assert self._favoritesWindow is None
1252                 self._favoritesWindow = FavoritesWindow(
1253                         self._window,
1254                         self._app,
1255                         unit_data.UNIT_CATEGORIES,
1256                         self._app.hiddenCategories
1257                 )
1258                 self._favoritesWindow.window.destroyed.connect(self._on_close_favorites)
1259                 return self._favoritesWindow
1260
1261         @misc_utils.log_exception(_moduleLogger)
1262         def _on_close_favorites(self, obj = None):
1263                 self._favoritesWindow = None
1264                 self._update_favorites()
1265
1266         @misc_utils.log_exception(_moduleLogger)
1267         def _on_child_close(self, obj = None):
1268                 self._unitWindow = None
1269
1270         @misc_utils.log_exception(_moduleLogger)
1271         def _on_close_window(self, checked = True):
1272                 self.close()
1273
1274         @misc_utils.log_exception(_moduleLogger)
1275         def _on_category_clicked(self, item, columnIndex):
1276                 categoryName = unicode(item.text(0))
1277                 self.select_category(categoryName)
1278
1279
1280 class UnitData(object):
1281
1282         HEADERS = ["Name", "Value", "", "Unit"]
1283         ALIGNMENT = [QtCore.Qt.AlignLeft, QtCore.Qt.AlignRight, QtCore.Qt.AlignLeft, QtCore.Qt.AlignLeft]
1284         NAME_COLUMN = 0
1285         VALUE_COLUMN_0 = 1
1286         VALUE_COLUMN_1 = 2
1287         UNIT_COLUMN = 3
1288
1289         def __init__(self, name, unit, description, conversion):
1290                 self._name = name
1291                 self._unit = unit
1292                 self._description = description
1293                 self._conversion = conversion
1294
1295                 self._value = 0.0
1296                 self._integerDisplay, self._fractionalDisplay = split_number(self._value)
1297
1298         @property
1299         def name(self):
1300                 return self._name
1301
1302         @property
1303         def value(self):
1304                 return self._value
1305
1306         def update_value(self, newValue):
1307                 self._value = newValue
1308                 self._integerDisplay, self._fractionalDisplay = split_number(newValue)
1309
1310         @property
1311         def unit(self):
1312                 return self._unit
1313
1314         @property
1315         def conversion(self):
1316                 return self._conversion
1317
1318         def data(self, column):
1319                 try:
1320                         return [self._name, self._integerDisplay, self._fractionalDisplay, self._unit][column]
1321                 except IndexError:
1322                         return None
1323
1324
1325 class UnitModel(QtCore.QAbstractItemModel):
1326
1327         def __init__(self, categoryName, parent=None):
1328                 super(UnitModel, self).__init__(parent)
1329                 self._categoryName = categoryName
1330                 self._unitData = unit_data.UNIT_DESCRIPTIONS[self._categoryName]
1331
1332                 self._children = []
1333                 for key in unit_data.get_units(self._categoryName):
1334                         conversion, unit, description = self._unitData[key]
1335                         self._children.append(UnitData(key, unit, description, conversion))
1336                 self._sortSettings = None
1337
1338         @misc_utils.log_exception(_moduleLogger)
1339         def columnCount(self, parent):
1340                 if parent.isValid():
1341                         return 0
1342                 else:
1343                         return len(UnitData.HEADERS)
1344
1345         @misc_utils.log_exception(_moduleLogger)
1346         def data(self, index, role):
1347                 if not index.isValid():
1348                         return None
1349                 elif role == QtCore.Qt.TextAlignmentRole:
1350                         return UnitData.ALIGNMENT[index.column()]
1351                 elif role != QtCore.Qt.DisplayRole:
1352                         return None
1353
1354                 item = index.internalPointer()
1355                 if isinstance(item, UnitData):
1356                         return item.data(index.column())
1357                 elif item is UnitData.HEADERS:
1358                         return item[index.column()]
1359
1360         @misc_utils.log_exception(_moduleLogger)
1361         def sort(self, column, order = QtCore.Qt.AscendingOrder):
1362                 self._sortSettings = column, order
1363                 isReverse = order == QtCore.Qt.AscendingOrder
1364                 if column == UnitData.NAME_COLUMN:
1365                         key_func = lambda item: item.name
1366                 elif column in [UnitData.VALUE_COLUMN_0, UnitData.VALUE_COLUMN_1]:
1367                         key_func = lambda item: item.value
1368                 elif column == UnitData.UNIT_COLUMN:
1369                         key_func = lambda item: item.unit
1370                 self._children.sort(key=key_func, reverse = isReverse)
1371
1372                 self._all_changed()
1373
1374         @misc_utils.log_exception(_moduleLogger)
1375         def flags(self, index):
1376                 if not index.isValid():
1377                         return QtCore.Qt.NoItemFlags
1378
1379                 return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
1380
1381         @misc_utils.log_exception(_moduleLogger)
1382         def headerData(self, section, orientation, role):
1383                 if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
1384                         return UnitData.HEADERS[section]
1385
1386                 return None
1387
1388         @misc_utils.log_exception(_moduleLogger)
1389         def index(self, row, column, parent):
1390                 if not self.hasIndex(row, column, parent):
1391                         return QtCore.QModelIndex()
1392
1393                 if parent.isValid():
1394                         return QtCore.QModelIndex()
1395
1396                 parentItem = UnitData.HEADERS
1397                 childItem = self._children[row]
1398                 if childItem:
1399                         return self.createIndex(row, column, childItem)
1400                 else:
1401                         return QtCore.QModelIndex()
1402
1403         @misc_utils.log_exception(_moduleLogger)
1404         def parent(self, index):
1405                 if not index.isValid():
1406                         return QtCore.QModelIndex()
1407
1408                 childItem = index.internalPointer()
1409                 if isinstance(childItem, UnitData):
1410                         return QtCore.QModelIndex()
1411                 elif childItem is UnitData.HEADERS:
1412                         return None
1413
1414         @misc_utils.log_exception(_moduleLogger)
1415         def rowCount(self, parent):
1416                 if 0 < parent.column():
1417                         return 0
1418
1419                 if not parent.isValid():
1420                         return len(self._children)
1421                 else:
1422                         return len(self._children)
1423
1424         def get_unit(self, index):
1425                 assert 0 <= index
1426                 return self._children[index]
1427
1428         def get_unit_names(self):
1429                 for child in self._children:
1430                         yield child.name
1431
1432         def index_unit(self, unitName):
1433                 for i, child in enumerate(self._children):
1434                         if child.name == unitName:
1435                                 return i
1436                 else:
1437                         raise RuntimeError("Unit not found")
1438
1439         def update_values(self, fromIndex, userInput):
1440                 value = self._sanitize_value(userInput)
1441                 func, arg = self._children[fromIndex].conversion
1442                 base = func.to_base(value, arg)
1443                 for i, child in enumerate(self._children):
1444                         if i == fromIndex:
1445                                 continue
1446                         func, arg = child.conversion
1447                         newValue = func.from_base(base, arg)
1448                         child.update_value(newValue)
1449
1450                 if (
1451                         self._sortSettings is not None and
1452                         self._sortSettings[0]  in [UnitData.VALUE_COLUMN_0, UnitData.VALUE_COLUMN_1]
1453                 ):
1454                         # Sort takes care of marking everything as changed
1455                         self.sort(*self._sortSettings)
1456                 else:
1457                         self._values_changed()
1458
1459         def __len__(self):
1460                 return len(self._children)
1461
1462         def _values_changed(self):
1463                 topLeft = self.createIndex(0, UnitData.VALUE_COLUMN_0, self._children[0])
1464                 bottomRight = self.createIndex(len(self._children)-1, UnitData.VALUE_COLUMN_1, self._children[-1])
1465                 self.dataChanged.emit(topLeft, bottomRight)
1466
1467         def _all_changed(self):
1468                 topLeft = self.createIndex(0, 0, self._children[0])
1469                 bottomRight = self.createIndex(len(self._children)-1, len(UnitData.HEADERS)-1, self._children[-1])
1470                 self.dataChanged.emit(topLeft, bottomRight)
1471
1472         def _sanitize_value(self, userEntry):
1473                 if self._categoryName == "Computer Numbers":
1474                         if userEntry == '':
1475                                 value = '0'
1476                         else:
1477                                 value = userEntry
1478                 else:
1479                         if userEntry == '':
1480                                 value = 0.0
1481                         else:
1482                                 value = float(userEntry)
1483                 return value
1484
1485
1486 class UnitWindow(object):
1487
1488         def __init__(self, parent, category, app):
1489                 self._app = app
1490                 self._categoryName = category
1491                 self._selectedIndex = 0
1492                 self._favoritesWindow = None
1493
1494                 self._selectedUnitName = QtGui.QLabel()
1495                 self._selectedUnitValue = QtGui.QLineEdit()
1496                 self._selectedUnitValue.textEdited.connect(self._on_value_edited)
1497                 maeqt.mark_numbers_preferred(self._selectedUnitValue)
1498                 self._selectedUnitSymbol = QtGui.QLabel()
1499
1500                 self._selectedUnitLayout = QtGui.QHBoxLayout()
1501                 self._selectedUnitLayout.addWidget(self._selectedUnitName)
1502                 self._selectedUnitLayout.addWidget(self._selectedUnitValue)
1503                 self._selectedUnitLayout.addWidget(self._selectedUnitSymbol)
1504
1505                 self._unitsModel = UnitModel(self._categoryName)
1506                 self._unitsView = QtGui.QTreeView()
1507                 self._unitsView.setModel(self._unitsModel)
1508                 self._unitsView.clicked.connect(self._on_unit_clicked)
1509                 self._unitsView.setUniformRowHeights(True)
1510                 self._unitsView.setSortingEnabled(True)
1511                 self._unitsView.setTextElideMode(QtCore.Qt.ElideNone)
1512                 self._unitsView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
1513                 self._unitsView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1514                 self._unitsView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
1515                 if not IS_MAEMO:
1516                         self._unitsView.setAlternatingRowColors(True)
1517                 if True:
1518                         self._unitsView.setHeaderHidden(True)
1519
1520                 viewHeader = self._unitsView.header()
1521                 viewHeader.setSortIndicatorShown(True)
1522                 viewHeader.setClickable(True)
1523
1524                 viewHeader.setResizeMode(UnitData.NAME_COLUMN, QtGui.QHeaderView.ResizeToContents)
1525                 viewHeader.setResizeMode(UnitData.VALUE_COLUMN_0, QtGui.QHeaderView.ResizeToContents)
1526                 viewHeader.setResizeMode(UnitData.VALUE_COLUMN_1, QtGui.QHeaderView.ResizeToContents)
1527                 viewHeader.setResizeMode(UnitData.UNIT_COLUMN, QtGui.QHeaderView.ResizeToContents)
1528                 viewHeader.setStretchLastSection(False)
1529
1530                 # Trying to make things faster by locking in the initial size of the immutable columns
1531                 nameSize = min(viewHeader.sectionSize(UnitData.NAME_COLUMN), 125)
1532                 viewHeader.setResizeMode(UnitData.NAME_COLUMN, QtGui.QHeaderView.Fixed)
1533                 viewHeader.resizeSection(UnitData.NAME_COLUMN, nameSize)
1534                 unitSize = min(viewHeader.sectionSize(UnitData.UNIT_COLUMN), 125)
1535                 viewHeader.setResizeMode(UnitData.UNIT_COLUMN, QtGui.QHeaderView.Fixed)
1536                 viewHeader.resizeSection(UnitData.UNIT_COLUMN, unitSize)
1537
1538                 self._layout = QtGui.QVBoxLayout()
1539                 self._layout.addLayout(self._selectedUnitLayout)
1540                 self._layout.addWidget(self._unitsView)
1541
1542                 centralWidget = QtGui.QWidget()
1543                 centralWidget.setLayout(self._layout)
1544
1545                 self._window = QtGui.QMainWindow(parent)
1546                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1547                 maeqt.set_autorient(self._window, True)
1548                 maeqt.set_stackable(self._window, True)
1549                 self._window.setWindowTitle("%s - %s" % (constants.__pretty_app_name__, category))
1550                 self._window.setWindowIcon(QtGui.QIcon(app.appIconPath))
1551                 self._window.setCentralWidget(centralWidget)
1552
1553                 defaultUnitName = self._app.get_recent_unit(self._categoryName)
1554                 if defaultUnitName:
1555                         self.select_unit(defaultUnitName)
1556                 else:
1557                         self._select_unit(0)
1558
1559                 if self._app.sortByNameAction.isChecked():
1560                         sortColumn = UnitData.NAME_COLUMN
1561                 elif self._app.sortByValueAction.isChecked():
1562                         sortColumn = UnitData.VALUE_COLUMN_0
1563                 elif self._app.sortByUnitAction.isChecked():
1564                         sortColumn = UnitData.UNIT_COLUMN
1565                 else:
1566                         raise RuntimeError("No sort column selected")
1567                 if sortColumn != 0:
1568                         # By default it sorts by he first column (name)
1569                         self._unitsModel.sort(sortColumn)
1570
1571                 self._chooseFavoritesAction = QtGui.QAction(None)
1572                 self._chooseFavoritesAction.setText("Select Favorites")
1573                 self._chooseFavoritesAction.setShortcut(QtGui.QKeySequence("CTRL+f"))
1574                 self._chooseFavoritesAction.triggered.connect(self._on_choose_favorites)
1575
1576                 self._app.showFavoritesAction.toggled.connect(self._on_show_favorites)
1577
1578                 self._previousUnitAction = QtGui.QAction(None)
1579                 self._previousUnitAction.setText("Previous Unit")
1580                 self._previousUnitAction.setShortcut(QtGui.QKeySequence("Up"))
1581                 self._previousUnitAction.triggered.connect(self._on_previous_unit)
1582
1583                 self._nextUnitAction = QtGui.QAction(None)
1584                 self._nextUnitAction.setText("Next Unit")
1585                 self._nextUnitAction.setShortcut(QtGui.QKeySequence("Down"))
1586                 self._nextUnitAction.triggered.connect(self._on_next_unit)
1587
1588                 self._closeWindowAction = QtGui.QAction(None)
1589                 self._closeWindowAction.setText("Close Window")
1590                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1591                 self._closeWindowAction.triggered.connect(self._on_close_window)
1592
1593                 if IS_MAEMO:
1594                         self._window.addAction(self._closeWindowAction)
1595                         self._window.addAction(self._app.quitAction)
1596                         self._window.addAction(self._app.fullscreenAction)
1597
1598                         fileMenu = self._window.menuBar().addMenu("&Units")
1599                         fileMenu.addAction(self._chooseFavoritesAction)
1600
1601                         viewMenu = self._window.menuBar().addMenu("&View")
1602                         viewMenu.addAction(self._app.showFavoritesAction)
1603                         viewMenu.addAction(self._app.condensedAction)
1604                         viewMenu.addSeparator()
1605                         viewMenu.addAction(self._app.sortByNameAction)
1606                         viewMenu.addAction(self._app.sortByValueAction)
1607                         viewMenu.addAction(self._app.sortByUnitAction)
1608                         viewMenu.addSeparator()
1609                         viewMenu.addAction(self._app.jumpAction)
1610                         viewMenu.addAction(self._app.recentAction)
1611                 else:
1612                         fileMenu = self._window.menuBar().addMenu("&Units")
1613                         fileMenu.addAction(self._chooseFavoritesAction)
1614                         fileMenu.addAction(self._closeWindowAction)
1615                         fileMenu.addAction(self._app.quitAction)
1616
1617                         viewMenu = self._window.menuBar().addMenu("&View")
1618                         viewMenu.addAction(self._app.showFavoritesAction)
1619                         viewMenu.addAction(self._app.condensedAction)
1620                         viewMenu.addSeparator()
1621                         viewMenu.addAction(self._app.sortByNameAction)
1622                         viewMenu.addAction(self._app.sortByValueAction)
1623                         viewMenu.addAction(self._app.sortByUnitAction)
1624                         viewMenu.addSeparator()
1625                         viewMenu.addAction(self._app.jumpAction)
1626                         viewMenu.addAction(self._app.recentAction)
1627                         viewMenu.addSeparator()
1628                         viewMenu.addAction(self._app.fullscreenAction)
1629
1630                 self._app.sortByNameAction.triggered.connect(self._on_sort_by_name)
1631                 self._app.sortByValueAction.triggered.connect(self._on_sort_by_value)
1632                 self._app.sortByUnitAction.triggered.connect(self._on_sort_by_unit)
1633
1634                 self._window.addAction(self._app.logAction)
1635                 self._window.addAction(self._nextUnitAction)
1636                 self._window.addAction(self._previousUnitAction)
1637                 self._window.addAction(self._chooseFavoritesAction)
1638
1639                 self._update_favorites()
1640                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1641                 self._window.show()
1642
1643         @property
1644         def window(self):
1645                 return self._window
1646
1647         def show(self):
1648                 for child in self.walk_children():
1649                         child.hide()
1650                 self._window.show()
1651
1652         def hide(self):
1653                 for child in self.walk_children():
1654                         child.hide()
1655                 self._window.hide()
1656
1657         def close(self):
1658                 for child in self.walk_children():
1659                         child.window.destroyed.disconnect(self._on_child_close)
1660                         child.close()
1661                 self._window.close()
1662
1663         def set_fullscreen(self, isFullscreen):
1664                 if isFullscreen:
1665                         self._window.showFullScreen()
1666                 else:
1667                         self._window.showNormal()
1668
1669         def select_unit(self, unitName):
1670                 index = self._unitsModel.index_unit(unitName)
1671                 self._select_unit(index)
1672
1673         def walk_children(self):
1674                 if self._favoritesWindow is not None:
1675                         yield self._favoritesWindow
1676
1677         def _update_favorites(self, force = False):
1678                 if self._app.showFavoritesAction.isChecked():
1679                         unitNames = list(self._unitsModel.get_unit_names())
1680                         for i, unitName in enumerate(unitNames):
1681                                 if unitName in self._app.get_hidden_units(self._categoryName):
1682                                         self._unitsView.setRowHidden(i, self._unitsView.rootIndex(), True)
1683                                 else:
1684                                         self._unitsView.setRowHidden(i, self._unitsView.rootIndex(), False)
1685                 else:
1686                         if force:
1687                                 for i in xrange(len(self._unitsModel)):
1688                                         self._unitsView.setRowHidden(i, self._unitsView.rootIndex(), False)
1689
1690         @misc_utils.log_exception(_moduleLogger)
1691         def _on_show_favorites(self, checked = True):
1692                 if checked:
1693                         unitNames = list(self._unitsModel.get_unit_names())
1694                         for i, unitName in enumerate(unitNames):
1695                                 if unitName in self._app.get_hidden_units(self._categoryName):
1696                                         self._unitsView.setRowHidden(i, self._unitsView.rootIndex(), True)
1697                 else:
1698                         for i in xrange(len(self._unitsModel)):
1699                                 self._unitsView.setRowHidden(i, self._unitsView.rootIndex(), False)
1700
1701         @misc_utils.log_exception(_moduleLogger)
1702         def _on_choose_favorites(self, obj = None):
1703                 assert self._favoritesWindow is None
1704                 self._favoritesWindow = FavoritesWindow(
1705                         self._window,
1706                         self._app,
1707                         unit_data.get_units(self._categoryName),
1708                         self._app.get_hidden_units(self._categoryName)
1709                 )
1710                 self._favoritesWindow.window.destroyed.connect(self._on_close_favorites)
1711                 return self._favoritesWindow
1712
1713         @misc_utils.log_exception(_moduleLogger)
1714         def _on_close_favorites(self, obj = None):
1715                 self._favoritesWindow = None
1716                 self._update_favorites(force=True)
1717
1718         @misc_utils.log_exception(_moduleLogger)
1719         def _on_previous_unit(self, checked = True):
1720                 self._select_unit(self._selectedIndex - 1)
1721
1722         @misc_utils.log_exception(_moduleLogger)
1723         def _on_next_unit(self, checked = True):
1724                 self._select_unit(self._selectedIndex + 1)
1725
1726         @misc_utils.log_exception(_moduleLogger)
1727         def _on_close_window(self, checked = True):
1728                 self.close()
1729
1730         @misc_utils.log_exception(_moduleLogger)
1731         def _on_sort_by_name(self, checked = False):
1732                 self._unitsModel.sort(UnitData.NAME_COLUMN, QtCore.Qt.DescendingOrder)
1733
1734         @misc_utils.log_exception(_moduleLogger)
1735         def _on_sort_by_value(self, checked = False):
1736                 self._unitsModel.sort(UnitData.VALUE_COLUMN_0)
1737
1738         @misc_utils.log_exception(_moduleLogger)
1739         def _on_sort_by_unit(self, checked = False):
1740                 self._unitsModel.sort(UnitData.UNIT_COLUMN, QtCore.Qt.DescendingOrder)
1741
1742         @misc_utils.log_exception(_moduleLogger)
1743         def _on_unit_clicked(self, index):
1744                 self._select_unit(index.row())
1745
1746         @misc_utils.log_exception(_moduleLogger)
1747         def _on_value_edited(self, *args):
1748                 userInput = self._selectedUnitValue.text()
1749                 self._unitsModel.update_values(self._selectedIndex, str(userInput))
1750                 self._update_favorites()
1751
1752         def _select_unit(self, index):
1753                 unit = self._unitsModel.get_unit(index)
1754                 self._selectedUnitName.setText(unit.name)
1755                 self._selectedUnitValue.setText(str(unit.value))
1756                 self._selectedUnitSymbol.setText(unit.unit)
1757
1758                 self._selectedIndex = index
1759                 qindex = self._unitsModel.createIndex(index, 0, self._unitsModel.get_unit(index))
1760                 self._unitsView.scrollTo(qindex)
1761                 self._app.add_recent(self._categoryName, self._unitsModel.get_unit(index).name)
1762
1763
1764 def run_gonvert():
1765         app = QtGui.QApplication([])
1766         handle = Gonvert(app)
1767         return app.exec_()
1768
1769
1770 if __name__ == "__main__":
1771         logging.basicConfig(level = logging.DEBUG)
1772         try:
1773                 os.makedirs(constants._data_path_)
1774         except OSError, e:
1775                 if e.errno != 17:
1776                         raise
1777
1778         val = run_gonvert()
1779         sys.exit(val)