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