source: src/mlx/gui/callouts.py@ 271:fa4c688d0fb4

Last change on this file since 271:fa4c688d0fb4 was 268:9a90e1d8c5a2, checked in by István Váradi <ivaradi@…>, 12 years ago

Added a scroll window around the file lists in the checklist and the approach callouts editors

File size: 17.5 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 if self._hasAltitude(newAltitude, ignorePath = editedPath):
258 dialog = gtk.MessageDialog(parent = self,
259 type = MESSAGETYPE_QUESTION,
260 message_format =
261 xstr("callouts_altitude_clash"))
262 dialog.format_secondary_markup(xstr("callouts_altitude_clash_sec"))
263 dialog.add_button(xstr("button_no"), RESPONSETYPE_NO)
264 dialog.add_button(xstr("button_yes"), RESPONSETYPE_YES)
265 dialog.set_title(WINDOW_TITLE_BASE)
266
267 result = dialog.run()
268 dialog.hide()
269
270 if result==RESPONSETYPE_NO:
271 newAltitude = None
272
273 if newAltitude is not None:
274 model[editedPath][0] = newAltitude
275
276 def _saveApproachCallouts(self):
277 """Save the currently displayed list of approach callouts for the
278 previously displayed aircraft type."""
279 mapping = {}
280 model = self._fileListModel
281 iter = model.get_iter_first()
282 while iter is not None:
283 altitude = int(model.get(iter, 0)[0])
284 path = model.get(iter, 2)[0]
285 mapping[altitude] = path
286 iter = model.iter_next(iter)
287
288 self._approachCallouts[self._currentAircraftType] = \
289 config.ApproachCallouts(mapping)
290
291 def _displayCurrentApproachCallouts(self):
292 """Display the approach callouts for the currently selected aircraft
293 type."""
294 aircraftType = self._getAircraftType()
295 self._currentAircraftType = aircraftType
296 if aircraftType not in self._approachCallouts:
297 self._approachCallouts[aircraftType] = \
298 self._gui.config.getApproachCallouts(aircraftType).clone()
299 approachCallouts = self._approachCallouts[aircraftType]
300
301 self._fileListModel.clear()
302 for (altitude, path) in approachCallouts:
303 self._fileListModel.append([altitude, os.path.basename(path), path])
304
305 def _getFileOpenDialog(self):
306 """Get the dialog to open a file.
307
308 If it does not exist yet, it will be created."""
309 if self._fileOpenDialog is None:
310 dialog = gtk.FileChooserDialog(title = WINDOW_TITLE_BASE + " - " +
311 xstr("callouts_open_title"),
312 action = FILE_CHOOSER_ACTION_OPEN,
313 buttons = (gtk.STOCK_CANCEL,
314 RESPONSETYPE_CANCEL,
315 gtk.STOCK_OK, RESPONSETYPE_OK),
316 parent = self)
317 dialog.set_modal(True)
318 dialog.set_do_overwrite_confirmation(True)
319
320 # FIXME: create the filters in one location and use them
321 # from there
322 filter = gtk.FileFilter()
323 filter.set_name(xstr("file_filter_audio"))
324 filter.add_pattern("*.wav")
325 filter.add_pattern("*.mp3")
326 dialog.add_filter(filter)
327
328 filter = gtk.FileFilter()
329 filter.set_name(xstr("file_filter_all"))
330 filter.add_pattern("*.*")
331 dialog.add_filter(filter)
332
333 self._fileOpenDialog = dialog
334
335 return self._fileOpenDialog
336
337 def _getNewAltitude(self, baseName):
338 """Get a new, unique altitude for the audio file with the given
339 base name.
340
341 First the given file name is searched for suitable
342 numbers. Otherwise the smallest altitude in the model is
343 considered, and, depending on the actual ordering of the
344 table, a suitable smaller or greater value is found. It is
345 ensured that the number is unique, unless all numbers are
346 taken.
347
348 If there is no entry in the table yet, 2500 is returned if the
349 table is sorted descending, 10 otherwise."""
350 altitude = self._getNewAltitudeFromFileName(baseName)
351 if altitude is not None: return altitude
352
353 descending = self._fileList.get_column(0).get_sort_order()==SORT_DESCENDING
354 model = self._fileListModel
355 numEntries = model.iter_n_children(None)
356 if numEntries==0:
357 return 2500 if descending else 10
358 else:
359 selection = self._fileList.get_selection()
360 (_model, paths) = selection.get_selected_rows()
361
362 if paths:
363 startIter = model.get_iter(max(paths))
364 else:
365 startIter = model.iter_nth_child(None, numEntries-1)
366
367 startValue = model.get_value(startIter, 0)
368
369 altitude = self._getNextValidUsualAltitude(startValue, descending)
370 if altitude is None:
371 altitude = self._getNextValidUsualAltitude(startValue,
372 not descending)
373
374 if altitude is None:
375 for altitude in range(0 if descending else 4999,
376 4999 if descending else 0,
377 1 if descending else -1):
378 if not self._hasAltitude(altitude): break
379
380 return altitude
381
382 def _getNewAltitudeFromFileName(self, baseName):
383 """Get a new altitude value from the given file name.
384
385 The name is traversed for numbers. If a number is less than
386 5000 and there is no such altitude yet in the table, it is
387 checked if it is divisible by 100 or 1000, and if so, it gets
388 a score of 2. If it is divisible by 10, the score will be 1,
389 otherwise 0. The first highest scoring number is returned, if
390 there are any at all, otherwise None."""
391 candidateAltitude = None
392 candidateScore = None
393
394 (baseName, _) = os.path.splitext(baseName)
395 numbers = ApproachCalloutsEditor.integerRE.findall(baseName)
396 for number in numbers:
397 value = int(number)
398 if value<5000 and not self._hasAltitude(value):
399 score = 2 if (value%100)==0 or (value%1000)==0 \
400 else 1 if (value%10)==0 else 0
401 if candidateAltitude is None or score>candidateScore:
402 candidateAltitude = value
403 candidateScore = score
404
405 return candidateAltitude
406
407 def _hasAltitude(self, altitude, ignorePath = None):
408 """Determine if the model already contains the given altitude
409 or not.
410
411 ignorePath is a path in the model to ignore."""
412 model = self._fileListModel
413 iter = model.get_iter_first()
414 while iter is not None:
415 path = model.get_path(iter)
416 if path!=ignorePath and altitude==model[path][0]:
417 return True
418 iter = model.iter_next(iter)
419
420 return False
421
422 def _getNextValidUsualAltitude(self, startValue, descending):
423 """Get the next valid usual altitude."""
424 value = startValue
425 while value is not None and self._hasAltitude(value):
426 value = \
427 ApproachCalloutsEditor._getNextUsualAltitude(value,
428 descending)
429
430 return value
431
432#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.