source: src/mlx/gui/callouts.py@ 278:f4860ca38620

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

Changed the way the altitude clashes are handled

File size: 17.8 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 contentBox = 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 contentBox.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_size_request(300, -1)
157 self._fileList.set_reorderable(False)
158 selection = self._fileList.get_selection()
159 selection.set_mode(SELECTION_MULTIPLE)
160 selection.connect("changed", self._fileListSelectionChanged)
161
162 scrolledWindow = gtk.ScrolledWindow()
163 scrolledWindow.add(self._fileList)
164 scrolledWindow.set_size_request(300, -1)
165 scrolledWindow.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
166 scrolledWindow.set_shadow_type(SHADOW_IN)
167
168 fileListAlignment = gtk.Alignment(xscale=1.0, yscale=1.0,
169 xalign=0.5, yalign=0.5)
170 fileListAlignment.set_padding(padding_top = 0, padding_bottom = 16,
171 padding_left = 0, padding_right = 8)
172 fileListAlignment.add(scrolledWindow)
173
174 contentBox.pack_start(fileListAlignment, False, False, 4)
175
176 contentArea.pack_start(contentBox, True, True, 4)
177
178 self.set_size_request(-1, 300)
179
180 def run(self):
181 """Run the approach callouts editor dialog."""
182 self._approachCallouts = {}
183 self._displayCurrentApproachCallouts()
184 self.show_all()
185 response = super(ApproachCalloutsEditor, self).run()
186 self.hide()
187
188 if response==RESPONSETYPE_ACCEPT:
189 self._saveApproachCallouts()
190 config = self._gui.config
191 for (aircraftType, approachCallouts) in \
192 self._approachCallouts.iteritems():
193 config.setApproachCallouts(aircraftType, approachCallouts)
194 config.save()
195
196 def _aircraftTypeChanged(self, comboBox):
197 """Called when the aircraft's type has changed."""
198 self._saveApproachCallouts()
199 self._displayCurrentApproachCallouts()
200
201 def _addButtonClicked(self, button):
202 """Called when the Add button is clicked."""
203 dialog = self._getFileOpenDialog()
204
205 dialog.show_all()
206 result = dialog.run()
207 dialog.hide()
208
209 if result==RESPONSETYPE_OK:
210 filePath = dialog.get_filename()
211 baseName = os.path.basename(filePath)
212 altitude = self._getNewAltitude(baseName)
213 self._addingFile = True
214 self._fileListModel.append([altitude, baseName, filePath])
215 self._addingFile = False
216
217 def _fileAdded(self, model, path, iter):
218 """Called when a file is added to the list of callouts.
219
220 Makes the treeview to edit the altitude in the given row."""
221 if self._addingFile:
222 gobject.idle_add(self._fileList.set_cursor,
223 model.get_path(iter),
224 self._fileList.get_column(0), True)
225 self._fileList.grab_focus()
226 self.grab_focus()
227
228 def _removeButtonClicked(self, button):
229 """Called when the Remove button is clicked."""
230 selection = self._fileList.get_selection()
231 (model, paths) = selection.get_selected_rows()
232
233 iters = [model.get_iter(path) for path in paths]
234
235 for i in iters:
236 if i is not None:
237 model.remove(i)
238
239 def _fileListSelectionChanged(self, selection):
240 """Called when the selection in the file list changes."""
241 anySelected = selection.count_selected_rows()>0
242 self._removeButton.set_sensitive(anySelected)
243
244 def _getAircraftType(self):
245 """Get the currently selected aircraft type."""
246 # FIXME: the same code as in the checklist editor
247 index = self._aircraftType.get_active()
248 return self._aircraftTypeModel[index][1]
249
250 def _altitudeEdited(self, widget, path, value):
251 """Called when an altitude is edited"""
252 newAltitude = int(value)
253
254 model = self._fileListModel
255 editedIter = model.get_iter_from_string(path)
256 editedPath = model.get_path(editedIter)
257 otherPath = self._hasAltitude(newAltitude, ignorePath = editedPath)
258 if otherPath is not None:
259 dialog = gtk.MessageDialog(parent = self,
260 type = MESSAGETYPE_QUESTION,
261 message_format =
262 xstr("callouts_altitude_clash"))
263 dialog.format_secondary_markup(xstr("callouts_altitude_clash_sec"))
264 dialog.add_button(xstr("button_no"), RESPONSETYPE_NO)
265 dialog.add_button(xstr("button_yes"), RESPONSETYPE_YES)
266 dialog.set_title(WINDOW_TITLE_BASE)
267
268 result = dialog.run()
269 dialog.hide()
270
271 if result==RESPONSETYPE_NO:
272 newAltitude = None
273
274 if newAltitude is not None:
275 model[editedPath][0] = newAltitude
276 if otherPath is not None:
277 model.remove(model.get_iter(otherPath))
278
279 def _saveApproachCallouts(self):
280 """Save the currently displayed list of approach callouts for the
281 previously displayed aircraft type."""
282 mapping = {}
283 model = self._fileListModel
284 iter = model.get_iter_first()
285 while iter is not None:
286 altitude = int(model.get(iter, 0)[0])
287 path = model.get(iter, 2)[0]
288 mapping[altitude] = path
289 iter = model.iter_next(iter)
290
291 self._approachCallouts[self._currentAircraftType] = \
292 config.ApproachCallouts(mapping)
293
294 def _displayCurrentApproachCallouts(self):
295 """Display the approach callouts for the currently selected aircraft
296 type."""
297 aircraftType = self._getAircraftType()
298 self._currentAircraftType = aircraftType
299 if aircraftType not in self._approachCallouts:
300 self._approachCallouts[aircraftType] = \
301 self._gui.config.getApproachCallouts(aircraftType).clone()
302 approachCallouts = self._approachCallouts[aircraftType]
303
304 self._fileListModel.clear()
305 for (altitude, path) in approachCallouts:
306 self._fileListModel.append([altitude, os.path.basename(path), path])
307
308 def _getFileOpenDialog(self):
309 """Get the dialog to open a file.
310
311 If it does not exist yet, it will be created."""
312 if self._fileOpenDialog is None:
313 dialog = gtk.FileChooserDialog(title = WINDOW_TITLE_BASE + " - " +
314 xstr("callouts_open_title"),
315 action = FILE_CHOOSER_ACTION_OPEN,
316 buttons = (gtk.STOCK_CANCEL,
317 RESPONSETYPE_CANCEL,
318 gtk.STOCK_OK, RESPONSETYPE_OK),
319 parent = self)
320 dialog.set_modal(True)
321 dialog.set_do_overwrite_confirmation(True)
322
323 # FIXME: create the filters in one location and use them
324 # from there
325 filter = gtk.FileFilter()
326 filter.set_name(xstr("file_filter_audio"))
327 filter.add_pattern("*.wav")
328 filter.add_pattern("*.mp3")
329 dialog.add_filter(filter)
330
331 filter = gtk.FileFilter()
332 filter.set_name(xstr("file_filter_all"))
333 filter.add_pattern("*.*")
334 dialog.add_filter(filter)
335
336 self._fileOpenDialog = dialog
337
338 return self._fileOpenDialog
339
340 def _getNewAltitude(self, baseName):
341 """Get a new, unique altitude for the audio file with the given
342 base name.
343
344 First the given file name is searched for suitable
345 numbers. Otherwise the smallest altitude in the model is
346 considered, and, depending on the actual ordering of the
347 table, a suitable smaller or greater value is found. It is
348 ensured that the number is unique, unless all numbers are
349 taken.
350
351 If there is no entry in the table yet, 2500 is returned if the
352 table is sorted descending, 10 otherwise."""
353 altitude = self._getNewAltitudeFromFileName(baseName)
354 if altitude is not None: return altitude
355
356 descending = self._fileList.get_column(0).get_sort_order()==SORT_DESCENDING
357 model = self._fileListModel
358 numEntries = model.iter_n_children(None)
359 if numEntries==0:
360 return 2500 if descending else 10
361 else:
362 selection = self._fileList.get_selection()
363 (_model, paths) = selection.get_selected_rows()
364
365 if paths:
366 startIter = model.get_iter(max(paths))
367 else:
368 startIter = model.iter_nth_child(None, numEntries-1)
369
370 startValue = model.get_value(startIter, 0)
371
372 altitude = self._getNextValidUsualAltitude(startValue, descending)
373 if altitude is None:
374 altitude = self._getNextValidUsualAltitude(startValue,
375 not descending)
376
377 if altitude is None:
378 for altitude in range(0 if descending else 4999,
379 4999 if descending else 0,
380 1 if descending else -1):
381 if not self._hasAltitude(altitude): break
382
383 return altitude
384
385 def _getNewAltitudeFromFileName(self, baseName):
386 """Get a new altitude value from the given file name.
387
388 The name is traversed for numbers. If a number is less than
389 5000 and there is no such altitude yet in the table, it is
390 checked if it is divisible by 100 or 1000, and if so, it gets
391 a score of 2. If it is divisible by 10, the score will be 1,
392 otherwise 0. The first highest scoring number is returned, if
393 there are any at all, otherwise None."""
394 candidateAltitude = None
395 candidateScore = None
396
397 (baseName, _) = os.path.splitext(baseName)
398 numbers = ApproachCalloutsEditor.integerRE.findall(baseName)
399 for number in numbers:
400 value = int(number)
401 if value<5000 and not self._hasAltitude(value):
402 score = 2 if (value%100)==0 or (value%1000)==0 \
403 else 1 if (value%10)==0 else 0
404 if candidateAltitude is None or score>candidateScore:
405 candidateAltitude = value
406 candidateScore = score
407
408 return candidateAltitude
409
410 def _hasAltitude(self, altitude, ignorePath = None):
411 """Determine if the model already contains the given altitude
412 or not.
413
414 ignorePath is a path in the model to ignore.
415
416 Returns the path of the element found, if any, or None, if the
417 altitude is not found."""
418 model = self._fileListModel
419 iter = model.get_iter_first()
420 while iter is not None:
421 path = model.get_path(iter)
422 if path!=ignorePath and altitude==model[path][0]:
423 return path
424 iter = model.iter_next(iter)
425
426 return None
427
428 def _getNextValidUsualAltitude(self, startValue, descending):
429 """Get the next valid usual altitude."""
430 value = startValue
431 while value is not None and self._hasAltitude(value):
432 value = \
433 ApproachCalloutsEditor._getNextUsualAltitude(value,
434 descending)
435
436 return value
437
438#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.