source: src/mlx/gui/callouts.py@ 267:a4e5f3f529d7

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

If at least one row is selected, the lowermost one is used to get the starting value from

File size: 16.9 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 selection = self._fileList.get_selection()
348 (_model, paths) = selection.get_selected_rows()
349
350 if paths:
351 startIter = model.get_iter(max(paths))
352 else:
353 startIter = model.iter_nth_child(None, numEntries-1)
354
355 startValue = model.get_value(startIter, 0)
356
357 altitude = self._getNextValidUsualAltitude(startValue, descending)
358 if altitude is None:
359 altitude = self._getNextValidUsualAltitude(startValue,
360 not descending)
361
362 if altitude is None:
363 for altitude in range(0 if descending else 4999,
364 4999 if descending else 0,
365 1 if descending else -1):
366 if not self._hasAltitude(altitude): break
367
368 return altitude
369
370 def _getNewAltitudeFromFileName(self, baseName):
371 """Get a new altitude value from the given file name.
372
373 The name is traversed for numbers. If a number is less than
374 5000 and there is no such altitude yet in the table, it is
375 checked if it is divisible by 100 or 1000, and if so, it gets
376 a score of 2. If it is divisible by 10, the score will be 1,
377 otherwise 0. The first highest scoring number is returned, if
378 there are any at all, otherwise None."""
379 candidateAltitude = None
380 candidateScore = None
381
382 (baseName, _) = os.path.splitext(baseName)
383 numbers = ApproachCalloutsEditor.integerRE.findall(baseName)
384 for number in numbers:
385 value = int(number)
386 if value<5000 and not self._hasAltitude(value):
387 score = 2 if (value%100)==0 or (value%1000)==0 \
388 else 1 if (value%10)==0 else 0
389 if candidateAltitude is None or score>candidateScore:
390 candidateAltitude = value
391 candidateScore = score
392
393 return candidateAltitude
394
395 def _hasAltitude(self, altitude, ignorePath = None):
396 """Determine if the model already contains the given altitude
397 or not.
398
399 ignorePath is a path in the model to ignore."""
400 model = self._fileListModel
401 iter = model.get_iter_first()
402 while iter is not None:
403 path = model.get_path(iter)
404 if path!=ignorePath and altitude==model[path][0]:
405 return True
406 iter = model.iter_next(iter)
407
408 return False
409
410 def _getNextValidUsualAltitude(self, startValue, descending):
411 """Get the next valid usual altitude."""
412 value = startValue
413 while value is not None and self._hasAltitude(value):
414 value = \
415 ApproachCalloutsEditor._getNextUsualAltitude(value,
416 descending)
417
418 return value
419
420#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.