source: src/mlx/gui/callouts.py@ 280:4d2a277c703b

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

Added a popup menu to the callouts editor's file list

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