source: src/mlx/gui/callouts.py@ 282:568be726e4bc

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

Fixed the way the altitude to edit is selected

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