source: src/mlx/gui/callouts.py@ 300:f101bd18f39d

Last change on this file since 300:f101bd18f39d was 300:f101bd18f39d, checked in by István Váradi <ivaradi@…>, 12 years ago

Added the module comments for the GUI

File size: 20.2 KB
RevLine 
[264]1
2from common import *
3
4from mlx.i18n import xstr
5import mlx.const as const
6import mlx.config as config
7
8import os
[266]9import re
[264]10
11#------------------------------------------------------------------------------
12
[300]13## @package mlx.gui.callouts
14#
15# Editor dialog for approach callouts.
16#
17# The dialog consists of an aircraft type selector box at the top, and a table
18# with two buttons below it. The table contains the callout files with the
19# corresponding altitudes, and is sorted according to the altitude. When a new
20# file is added, the program finds out a new altitude for it. If the file's
21# name contains numbers that are not used as altitudes yet, the most suitable
22# of those numbers will be used. Otherwise a 'usual' altitude is searched for,
23# in the direction according to the sort order, and if that fails too, the
24# altitudes are tried one-by-one. See the
25# \ref ApproachCalloutsEditor._getNewAltitude function for more details.
26
27#------------------------------------------------------------------------------
28
[264]29class ApproachCalloutsEditor(gtk.Dialog):
30 """The dialog to edit the approach callouts."""
[266]31 integerRE = re.compile("[0-9]+")
32
33 # A list of "usual" altitudes for callouts
34 _usualAltitudes = [10, 20, 30, 40, 50, 100, 200, 300, 400, 500, 1000,
35 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000]
36
37 @staticmethod
38 def _getNextUsualAltitude(altitude, descending):
39 """Get the next altitude coming after the given one in the
40 given direction."""
41 if descending:
42 previous = None
43 for alt in ApproachCalloutsEditor._usualAltitudes:
44 if alt>=altitude: return previous
45 previous = alt
46 else:
47 for alt in ApproachCalloutsEditor._usualAltitudes:
48 if alt>altitude: return alt
49
50 return None
51
[264]52 def __init__(self, gui):
53 super(ApproachCalloutsEditor, self).__init__(WINDOW_TITLE_BASE + " - " +
54 xstr("callouts_title"),
55 gui.mainWindow,
56 DIALOG_MODAL)
57
58 self.add_button(xstr("button_cancel"), RESPONSETYPE_REJECT)
59 self.add_button(xstr("button_ok"), RESPONSETYPE_ACCEPT)
60
61 self._gui = gui
62 self._approachCallouts = {}
63 self._currentAircraftType = const.aircraftTypes[0]
[266]64 self._fileOpenDialog = None
[264]65
66 contentArea = self.get_content_area()
67
68 # FIXME: common code with the checklist editor
69 typeBox = gtk.HBox()
70
71 label = gtk.Label(xstr("callouts_aircraftType"))
72 label.set_use_underline(True)
73
74 typeBox.pack_start(label, False, False, 4)
75
76 self._aircraftTypeModel = gtk.ListStore(str, int)
77 for type in const.aircraftTypes:
78 name = aircraftNames[type] if type in aircraftNames \
79 else "Aircraft type #%d" % (type,)
80 self._aircraftTypeModel.append([name, type])
81 self._aircraftType = gtk.ComboBox(model = self._aircraftTypeModel)
82 renderer = gtk.CellRendererText()
83 self._aircraftType.pack_start(renderer, True)
84 self._aircraftType.add_attribute(renderer, "text", 0)
85 self._aircraftType.set_tooltip_text(xstr("callouts_aircraftType_tooltip"))
86 self._aircraftType.set_active(0)
87 self._aircraftType.connect("changed", self._aircraftTypeChanged)
88 label.set_mnemonic_widget(self._aircraftType)
89
90 typeBox.pack_start(self._aircraftType, True, True, 4)
91
92 typeBoxAlignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
93 xscale = 0.0, yscale = 0.0)
94 typeBoxAlignment.set_size_request(400, -1)
95 typeBoxAlignment.add(typeBox)
96
97 contentArea.pack_start(typeBoxAlignment, False, False, 12)
[266]98 # FIXME: common code until here, but note that some texts are different
[264]99
[268]100 contentBox = gtk.HBox()
[264]101
102 controlBox = gtk.VBox()
103 controlAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.0,
104 xscale = 0.0, yscale = 0.0)
105 controlAlignment.set_padding(padding_top = 0, padding_bottom = 0,
106 padding_left = 32, padding_right = 32)
107 controlAlignment.add(controlBox)
[268]108 contentBox.pack_start(controlAlignment, False, False, 0)
[264]109
110 self._addButton = gtk.Button(xstr("callouts_add"))
111 self._addButton.set_use_underline(True)
112 self._addButton.set_tooltip_text(xstr("callouts_add_tooltip"))
113 self._addButton.connect("clicked", self._addButtonClicked)
114 addAlignment = gtk.Alignment(xalign = 0.5, yalign = 0.0,
115 xscale = 0.0, yscale = 0.0)
[266]116 addAlignment.set_padding(padding_top = 24, padding_bottom = 0,
[264]117 padding_left = 0, padding_right = 0)
118 addAlignment.add(self._addButton)
119 controlBox.pack_start(addAlignment, False, False, 0)
120
121 self._removeButton = gtk.Button(xstr("callouts_remove"))
122 self._removeButton.set_use_underline(True)
123 self._removeButton.set_tooltip_text(xstr("callouts_remove_tooltip"))
124 self._removeButton.set_sensitive(False)
125 self._removeButton.connect("clicked", self._removeButtonClicked)
126
127 removeAlignment = gtk.Alignment(xalign = 0.5, yalign = 0.0,
128 xscale = 0.0, yscale = 0.0)
[266]129 removeAlignment.set_padding(padding_top = 24, padding_bottom = 0,
[264]130 padding_left = 0, padding_right = 0)
131 removeAlignment.add(self._removeButton)
132 controlBox.pack_start(removeAlignment, False, False, 0)
133
134 self._fileListModel = gtk.ListStore(int, str, str)
135 self._fileListModel.set_sort_column_id(0, SORT_DESCENDING)
[266]136
137 self._addingFile = False
138 self._fileListModel.connect("row-inserted", self._fileAdded)
[282]139 self._lastAddedAltitude = None
[266]140
[264]141 self._fileList = gtk.TreeView(model = self._fileListModel)
142
143 renderer = gtk.CellRendererSpin()
144 renderer.set_property("editable", True)
145
146 adjustment = gtk.Adjustment(0, 0, 5000, 10, 100)
147 renderer.set_property("adjustment", adjustment);
148 renderer.connect("edited", self._altitudeEdited)
149
150 column = gtk.TreeViewColumn(xstr("callouts_header_altitude"),
151 renderer, text = 0)
152 self._fileList.append_column(column)
153 column.set_expand(True)
154 column.set_clickable(True)
155 column.set_reorderable(False)
156 column.set_sort_indicator(True)
157 column.set_sort_column_id(0)
158 column.set_sort_order(SORT_DESCENDING)
159 column.set_expand(False)
160
161 column = gtk.TreeViewColumn(xstr("callouts_header_path"),
162 gtk.CellRendererText(), text = 1)
163 self._fileList.append_column(column)
164 column.set_expand(True)
165 column.set_clickable(False)
166 column.set_reorderable(False)
167 column.set_expand(True)
168
169 self._fileList.set_tooltip_column(2)
[268]170 self._fileList.set_size_request(300, -1)
[264]171 self._fileList.set_reorderable(False)
[280]172 self._fileList.connect("button-press-event",
173 self._fileListButtonPressed)
[264]174 selection = self._fileList.get_selection()
175 selection.set_mode(SELECTION_MULTIPLE)
176 selection.connect("changed", self._fileListSelectionChanged)
[280]177
178 self._buildFileListPopupMenu()
179
[268]180 scrolledWindow = gtk.ScrolledWindow()
181 scrolledWindow.add(self._fileList)
182 scrolledWindow.set_size_request(300, -1)
183 scrolledWindow.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
184 scrolledWindow.set_shadow_type(SHADOW_IN)
185
186 fileListAlignment = gtk.Alignment(xscale=1.0, yscale=1.0,
187 xalign=0.5, yalign=0.5)
188 fileListAlignment.set_padding(padding_top = 0, padding_bottom = 16,
189 padding_left = 0, padding_right = 8)
190 fileListAlignment.add(scrolledWindow)
191
192 contentBox.pack_start(fileListAlignment, False, False, 4)
[264]193
[268]194 contentArea.pack_start(contentBox, True, True, 4)
[264]195
[268]196 self.set_size_request(-1, 300)
[264]197
198 def run(self):
199 """Run the approach callouts editor dialog."""
200 self._approachCallouts = {}
201 self._displayCurrentApproachCallouts()
202 self.show_all()
203 response = super(ApproachCalloutsEditor, self).run()
204 self.hide()
205
206 if response==RESPONSETYPE_ACCEPT:
207 self._saveApproachCallouts()
208 config = self._gui.config
209 for (aircraftType, approachCallouts) in \
210 self._approachCallouts.iteritems():
211 config.setApproachCallouts(aircraftType, approachCallouts)
212 config.save()
213
214 def _aircraftTypeChanged(self, comboBox):
215 """Called when the aircraft's type has changed."""
216 self._saveApproachCallouts()
217 self._displayCurrentApproachCallouts()
218
219 def _addButtonClicked(self, button):
220 """Called when the Add button is clicked."""
[266]221 dialog = self._getFileOpenDialog()
[264]222
[266]223 dialog.show_all()
224 result = dialog.run()
225 dialog.hide()
226
227 if result==RESPONSETYPE_OK:
228 filePath = dialog.get_filename()
229 baseName = os.path.basename(filePath)
230 altitude = self._getNewAltitude(baseName)
231 self._addingFile = True
[282]232 self._lastAddedAltitude = altitude
[266]233 self._fileListModel.append([altitude, baseName, filePath])
234 self._addingFile = False
235
236 def _fileAdded(self, model, path, iter):
237 """Called when a file is added to the list of callouts.
238
239 Makes the treeview to edit the altitude in the given row."""
240 if self._addingFile:
[282]241 gobject.idle_add(self._selectFile)
[266]242 self._fileList.grab_focus()
243 self.grab_focus()
[282]244
245 def _selectFile(self):
246 """Select the file with the last added altitude."""
247 if self._lastAddedAltitude is None: return
248
249 model = self._fileListModel
250 iter = model.get_iter_first()
251 while iter is not None:
252 if model.get_value(iter, 0)==self._lastAddedAltitude: break
253 iter = model.iter_next(iter)
254 if iter is not None:
255 self._fileList.set_cursor(model.get_path(iter),
256 self._fileList.get_column(0), True)
257 self._lastAddedAltitude = None
[266]258
[264]259 def _removeButtonClicked(self, button):
260 """Called when the Remove button is clicked."""
[280]261 self._removeSelected()
262
263 def _removeSelected(self):
264 """Remove the selected files."""
[264]265 selection = self._fileList.get_selection()
266 (model, paths) = selection.get_selected_rows()
267
268 iters = [model.get_iter(path) for path in paths]
269
270 for i in iters:
271 if i is not None:
272 model.remove(i)
273
274 def _fileListSelectionChanged(self, selection):
275 """Called when the selection in the file list changes."""
276 anySelected = selection.count_selected_rows()>0
277 self._removeButton.set_sensitive(anySelected)
278
279 def _getAircraftType(self):
280 """Get the currently selected aircraft type."""
281 # FIXME: the same code as in the checklist editor
282 index = self._aircraftType.get_active()
283 return self._aircraftTypeModel[index][1]
284
285 def _altitudeEdited(self, widget, path, value):
286 """Called when an altitude is edited"""
287 newAltitude = int(value)
288
289 model = self._fileListModel
290 editedIter = model.get_iter_from_string(path)
291 editedPath = model.get_path(editedIter)
[272]292 otherPath = self._hasAltitude(newAltitude, ignorePath = editedPath)
293 if otherPath is not None:
[266]294 dialog = gtk.MessageDialog(parent = self,
295 type = MESSAGETYPE_QUESTION,
296 message_format =
297 xstr("callouts_altitude_clash"))
298 dialog.format_secondary_markup(xstr("callouts_altitude_clash_sec"))
299 dialog.add_button(xstr("button_no"), RESPONSETYPE_NO)
300 dialog.add_button(xstr("button_yes"), RESPONSETYPE_YES)
301 dialog.set_title(WINDOW_TITLE_BASE)
[264]302
[266]303 result = dialog.run()
304 dialog.hide()
[264]305
[283]306 if result!=RESPONSETYPE_YES:
[266]307 newAltitude = None
[264]308
309 if newAltitude is not None:
[266]310 model[editedPath][0] = newAltitude
[272]311 if otherPath is not None:
312 model.remove(model.get_iter(otherPath))
[264]313
314 def _saveApproachCallouts(self):
315 """Save the currently displayed list of approach callouts for the
316 previously displayed aircraft type."""
317 mapping = {}
318 model = self._fileListModel
319 iter = model.get_iter_first()
320 while iter is not None:
321 altitude = int(model.get(iter, 0)[0])
322 path = model.get(iter, 2)[0]
323 mapping[altitude] = path
324 iter = model.iter_next(iter)
325
326 self._approachCallouts[self._currentAircraftType] = \
327 config.ApproachCallouts(mapping)
328
329 def _displayCurrentApproachCallouts(self):
330 """Display the approach callouts for the currently selected aircraft
331 type."""
332 aircraftType = self._getAircraftType()
333 self._currentAircraftType = aircraftType
334 if aircraftType not in self._approachCallouts:
335 self._approachCallouts[aircraftType] = \
336 self._gui.config.getApproachCallouts(aircraftType).clone()
337 approachCallouts = self._approachCallouts[aircraftType]
338
339 self._fileListModel.clear()
340 for (altitude, path) in approachCallouts:
341 self._fileListModel.append([altitude, os.path.basename(path), path])
[266]342
343 def _getFileOpenDialog(self):
344 """Get the dialog to open a file.
345
346 If it does not exist yet, it will be created."""
347 if self._fileOpenDialog is None:
348 dialog = gtk.FileChooserDialog(title = WINDOW_TITLE_BASE + " - " +
349 xstr("callouts_open_title"),
350 action = FILE_CHOOSER_ACTION_OPEN,
351 buttons = (gtk.STOCK_CANCEL,
352 RESPONSETYPE_CANCEL,
353 gtk.STOCK_OK, RESPONSETYPE_OK),
354 parent = self)
355 dialog.set_modal(True)
356 dialog.set_do_overwrite_confirmation(True)
357
358 # FIXME: create the filters in one location and use them
359 # from there
360 filter = gtk.FileFilter()
361 filter.set_name(xstr("file_filter_audio"))
362 filter.add_pattern("*.wav")
363 filter.add_pattern("*.mp3")
364 dialog.add_filter(filter)
365
366 filter = gtk.FileFilter()
367 filter.set_name(xstr("file_filter_all"))
368 filter.add_pattern("*.*")
369 dialog.add_filter(filter)
370
371 self._fileOpenDialog = dialog
372
373 return self._fileOpenDialog
374
375 def _getNewAltitude(self, baseName):
376 """Get a new, unique altitude for the audio file with the given
377 base name.
378
379 First the given file name is searched for suitable
380 numbers. Otherwise the smallest altitude in the model is
381 considered, and, depending on the actual ordering of the
382 table, a suitable smaller or greater value is found. It is
383 ensured that the number is unique, unless all numbers are
384 taken.
385
386 If there is no entry in the table yet, 2500 is returned if the
387 table is sorted descending, 10 otherwise."""
388 altitude = self._getNewAltitudeFromFileName(baseName)
389 if altitude is not None: return altitude
390
391 descending = self._fileList.get_column(0).get_sort_order()==SORT_DESCENDING
392 model = self._fileListModel
393 numEntries = model.iter_n_children(None)
394 if numEntries==0:
395 return 2500 if descending else 10
396 else:
[267]397 selection = self._fileList.get_selection()
398 (_model, paths) = selection.get_selected_rows()
[266]399
[267]400 if paths:
401 startIter = model.get_iter(max(paths))
402 else:
403 startIter = model.iter_nth_child(None, numEntries-1)
404
405 startValue = model.get_value(startIter, 0)
406
407 altitude = self._getNextValidUsualAltitude(startValue, descending)
[266]408 if altitude is None:
[267]409 altitude = self._getNextValidUsualAltitude(startValue,
[266]410 not descending)
411
412 if altitude is None:
413 for altitude in range(0 if descending else 4999,
414 4999 if descending else 0,
415 1 if descending else -1):
416 if not self._hasAltitude(altitude): break
417
418 return altitude
419
420 def _getNewAltitudeFromFileName(self, baseName):
421 """Get a new altitude value from the given file name.
422
423 The name is traversed for numbers. If a number is less than
424 5000 and there is no such altitude yet in the table, it is
425 checked if it is divisible by 100 or 1000, and if so, it gets
426 a score of 2. If it is divisible by 10, the score will be 1,
427 otherwise 0. The first highest scoring number is returned, if
428 there are any at all, otherwise None."""
429 candidateAltitude = None
430 candidateScore = None
431
432 (baseName, _) = os.path.splitext(baseName)
433 numbers = ApproachCalloutsEditor.integerRE.findall(baseName)
434 for number in numbers:
435 value = int(number)
436 if value<5000 and not self._hasAltitude(value):
437 score = 2 if (value%100)==0 or (value%1000)==0 \
438 else 1 if (value%10)==0 else 0
439 if candidateAltitude is None or score>candidateScore:
440 candidateAltitude = value
441 candidateScore = score
442
443 return candidateAltitude
444
445 def _hasAltitude(self, altitude, ignorePath = None):
446 """Determine if the model already contains the given altitude
447 or not.
448
[272]449 ignorePath is a path in the model to ignore.
450
451 Returns the path of the element found, if any, or None, if the
452 altitude is not found."""
[266]453 model = self._fileListModel
454 iter = model.get_iter_first()
455 while iter is not None:
456 path = model.get_path(iter)
457 if path!=ignorePath and altitude==model[path][0]:
[272]458 return path
[266]459 iter = model.iter_next(iter)
460
[272]461 return None
[266]462
463 def _getNextValidUsualAltitude(self, startValue, descending):
464 """Get the next valid usual altitude."""
465 value = startValue
466 while value is not None and self._hasAltitude(value):
467 value = \
468 ApproachCalloutsEditor._getNextUsualAltitude(value,
469 descending)
470
471 return value
[264]472
[280]473 def _fileListButtonPressed(self, widget, event):
474 """Called when a mouse button is pressed on the file list."""
475 if event.type!=EVENT_BUTTON_PRESS or event.button!=3:
476 return
477
478 menu = self._fileListPopupMenu
479 if pygobject:
480 menu.popup(None, None, None, None, event.button, event.time)
481 else:
482 menu.popup(None, None, None, event.button, event.time)
483
484 def _buildFileListPopupMenu(self):
485 """Build the file list popup menu."""
486 menu = gtk.Menu()
487
488 menuItem = gtk.MenuItem()
489 menuItem.set_label(xstr("callouts_remove"))
490 menuItem.set_use_underline(True)
491 menuItem.connect("activate", self._popupRemove)
492 menuItem.show()
493 self._popupRemoveItem = menuItem
494
495 menu.append(menuItem)
496
497 self._fileListPopupMenu = menu
498
499 def _popupRemove(self, menuItem):
500 """Remove the currently selected menu items."""
501 self._removeSelected()
502
[264]503#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.