source: src/mlx/gui/callouts.py@ 266:b80521b41013

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

The approach callout editor works

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