source: src/mlx/gui/common.py

python3
Last change on this file was 1099:a2d5a4d1c6c1, checked in by István Váradi <ivaradi@…>, 9 months ago

Various minor bugfixes

File size: 18.0 KB
Line 
1
2from mlx.common import *
3
4import mlx.const as _const
5from mlx.i18n import xstr
6
7from mlx.util import secondaryInstallation
8
9import os
10import time
11import calendar
12
13#-----------------------------------------------------------------------------
14
15## @package mlx.gui.common
16#
17# Common definitions and utilities for the GUI
18
19#-----------------------------------------------------------------------------
20
21appIndicator = False
22
23import gi
24gi.require_version("Gdk", "3.0")
25from gi.repository import Gdk
26from gi.repository import GdkPixbuf
27gi.require_version("Gtk", "3.0")
28from gi.repository import Gtk
29try:
30 gi.require_version("AppIndicator3", "0.1")
31 from gi.repository import AppIndicator3
32 appIndicator = True
33except:
34 pass
35from gi.repository import Pango
36gi.require_version("PangoCairo", "1.0")
37from gi.repository import PangoCairo
38from gi.repository import GLib
39
40import codecs
41_utf8Decoder = codecs.getdecoder("utf-8")
42
43import cairo
44
45#------------------------------------------------------------------------------
46
47class FlightStatusHandler(object):
48 """Base class for objects that handle the flight status in some way."""
49 def __init__(self):
50 self._stage = None
51 self._rating = 100
52 self._noGoReason = None
53
54 def resetFlightStatus(self):
55 """Reset the flight status."""
56 self._stage = None
57 self._rating = 100
58 self._noGoReason = None
59 self._updateFlightStatus()
60
61 def setStage(self, stage):
62 """Set the stage of the flight."""
63 if stage!=self._stage:
64 self._stage = stage
65 self._updateFlightStatus()
66
67 def setRating(self, rating):
68 """Set the rating to the given value."""
69 if rating!=self._rating:
70 self._rating = rating
71 if self._noGoReason is None:
72 self._updateFlightStatus()
73
74 def setNoGo(self, reason):
75 """Set a No-Go condition with the given reason."""
76 if self._noGoReason is None:
77 self._noGoReason = reason
78 self._updateFlightStatus()
79
80#------------------------------------------------------------------------------
81
82class IntegerEntry(Gtk.Entry):
83 """An entry that allows only either an empty value, or an integer."""
84 def __init__(self, defaultValue = None):
85 """Construct the entry."""
86 Gtk.Entry.__init__(self)
87
88 self.set_alignment(1.0)
89
90 self._defaultValue = defaultValue
91 self._currentInteger = defaultValue
92 self._selfSetting = False
93 self._set_text()
94
95 self.connect("changed", self._handle_changed)
96
97 def get_int(self):
98 """Get the integer."""
99 return self._currentInteger
100
101 def reset(self):
102 """Reset the integer."""
103 self.set_int(None)
104
105 def set_int(self, value):
106 """Set the integer."""
107 if value!=self._currentInteger:
108 self._currentInteger = value
109 self.emit("integer-changed", self._currentInteger)
110 self._set_text()
111
112 def _handle_changed(self, widget):
113 """Handle the changed signal."""
114 if self._selfSetting:
115 return
116 text = self.get_text()
117 if text=="":
118 self.set_int(self._defaultValue)
119 else:
120 try:
121 self.set_int(int(text))
122 except:
123 self._set_text()
124
125 def _set_text(self):
126 """Set the text value from the current integer."""
127 self._selfSetting = True
128 self.set_text("" if self._currentInteger is None
129 else str(self._currentInteger))
130 self._selfSetting = False
131
132#------------------------------------------------------------------------------
133
134class TimeEntry(Gtk.Entry):
135 """Widget to display and edit a time value in HH:MM format."""
136 def __init__(self):
137 """Construct the entry"""
138 super(TimeEntry, self).__init__()
139 self.set_max_width_chars(5)
140
141 self.connect("insert-text", self._insertText)
142 self.connect("delete-text", self._deleteText)
143 self.connect("focus-out-event", self._focusOutEvent)
144
145 @property
146 def hour(self):
147 """Get the hour from the current text"""
148 text = self.get_text()
149 if not text or text==":":
150 return 0
151
152 words = text.split(":")
153 if len(words)==1:
154 return 0
155 elif len(words)>=2:
156 return 0 if len(words[0])==0 else int(words[0])
157 else:
158 return 0
159
160 @property
161 def minute(self):
162 """Get the hour from the current text"""
163 text = self.get_text()
164 if not text or text==":":
165 return 0
166
167 words = text.split(":")
168 if len(words)==1:
169 return 0 if len(words[0])==0 else int(words[0])
170 elif len(words)>=2:
171 return 0 if len(words[1])==0 else int(words[1])
172 else:
173 return 0
174
175 @property
176 def minutes(self):
177 """Get the time in minutes, i.e. hour*60+minute."""
178 return self.hour * 60 + self.minute
179
180 def setTimestamp(self, timestamp):
181 """Set the hour and minute from the given timestamp in UTC."""
182 tm = time.gmtime(timestamp)
183 self.set_text("%02d:%02d" % (tm.tm_hour, tm.tm_min))
184
185 def getTimestampFrom(self, timestamp):
186 """Get the timestamp by replacing the hour and minute from the given
187 timestamp with what is set in this widget."""
188 tm = time.gmtime(timestamp)
189 ts = calendar.timegm((tm.tm_year, tm.tm_mon, tm.tm_mday,
190 self.hour, self.minute, 0,
191 tm.tm_wday, tm.tm_yday, tm.tm_isdst))
192
193 if ts > (timestamp + (16*60*60)):
194 ts -= 24*60*60
195 elif (ts + 16*60*60) < timestamp:
196 ts += 24*60*60
197
198 return ts
199
200 def _focusOutEvent(self, widget, event):
201 """Reformat the text to match pattern HH:MM"""
202 text = "%02d:%02d" % (self.hour, self.minute)
203 if text!=self.get_text():
204 self.set_text(text)
205
206 def _insertText(self, entry, text, length, position):
207 """Called when some text is inserted into the entry."""
208 text=text[:length]
209 currentText = self.get_text()
210 position = self.get_position()
211 newText = currentText[:position] + text + currentText[position:]
212 self._checkText(newText, "insert-text")
213
214 def _deleteText(self, entry, start, end):
215 """Called when some text is erased from the entry."""
216 currentText = self.get_text()
217 newText = currentText[:start] + currentText[end:]
218 self._checkText(newText, "delete-text")
219
220 def _checkText(self, newText, signal):
221 """Check the given text.
222
223 If it is not suitable, stop the emission of the signal to prevent the
224 change from appearing."""
225 if not newText or newText==":":
226 return
227
228 words = newText.split(":")
229 if (len(words)==1 and
230 len(words[0])<=2 and (len(words[0])==0 or
231 (words[0].isdigit() and int(words[0])<60))) or \
232 (len(words)==2 and
233 len(words[0])<=2 and (len(words[0])==0 or
234 (words[0].isdigit() and int(words[0])<24)) and
235 len(words[1])<=2 and (len(words[1])==0 or
236 (words[1].isdigit() and int(words[1])<60))):
237 pass
238 else:
239 Gtk.gdk.display_get_default().beep()
240 self.stop_emission(signal)
241
242#------------------------------------------------------------------------------
243
244class CredentialsDialog(Gtk.Dialog):
245 """A dialog window to ask for a user name and a password."""
246 def __init__(self, gui, userName, password,
247 titleLabel, cancelButtonLabel, okButtonLabel,
248 userNameLabel, userNameTooltip,
249 passwordLabel, passwordTooltip,
250 infoText = None,
251 rememberPassword = None,
252 rememberLabel = None, rememberTooltip = None):
253 """Construct the dialog."""
254 super(CredentialsDialog, self).__init__(WINDOW_TITLE_BASE + " - " +
255 titleLabel,
256 gui.mainWindow,
257 Gtk.DialogFlags.MODAL)
258 self.add_button(cancelButtonLabel, Gtk.ResponseType.CANCEL)
259 self.add_button(okButtonLabel, Gtk.ResponseType.OK)
260
261 contentArea = self.get_content_area()
262
263 contentAlignment = Gtk.Alignment(xalign = 0.5, yalign = 0.5,
264 xscale = 0.0, yscale = 0.0)
265 contentAlignment.set_padding(padding_top = 4, padding_bottom = 16,
266 padding_left = 8, padding_right = 8)
267
268 contentArea.pack_start(contentAlignment, False, False, 0)
269
270 contentVBox = Gtk.VBox()
271 contentAlignment.add(contentVBox)
272
273 if infoText is not None:
274 label = Gtk.Label(infoText)
275 label.set_alignment(0.0, 0.0)
276
277 contentVBox.pack_start(label, False, False, 0)
278
279 tableAlignment = Gtk.Alignment(xalign = 0.5, yalign = 0.5,
280 xscale = 0.0, yscale = 0.0)
281 tableAlignment.set_padding(padding_top = 24, padding_bottom = 0,
282 padding_left = 0, padding_right = 0)
283
284 table = Gtk.Table(3, 2)
285 table.set_row_spacings(4)
286 table.set_col_spacings(16)
287 table.set_homogeneous(False)
288
289 tableAlignment.add(table)
290 contentVBox.pack_start(tableAlignment, True, True, 0)
291
292 label = Gtk.Label(userNameLabel)
293 label.set_use_underline(True)
294 label.set_alignment(0.0, 0.5)
295 table.attach(label, 0, 1, 0, 1)
296
297 self._userName = Gtk.Entry()
298 self._userName.set_width_chars(16)
299 # FIXME: enabled the OK button only when there is something in thr
300 # user name and password fields
301 #self._userName.connect("changed",
302 # lambda button: self._updateForwardButton())
303 self._userName.set_tooltip_text(userNameTooltip)
304 self._userName.set_text(userName)
305 table.attach(self._userName, 1, 2, 0, 1)
306 label.set_mnemonic_widget(self._userName)
307
308 label = Gtk.Label(passwordLabel)
309 label.set_use_underline(True)
310 label.set_alignment(0.0, 0.5)
311 table.attach(label, 0, 1, 1, 2)
312
313 self._password = Gtk.Entry()
314 self._password.set_visibility(False)
315 #self._password.connect("changed",
316 # lambda button: self._updateForwardButton())
317 self._password.set_tooltip_text(passwordTooltip)
318 self._password.set_text(password)
319 table.attach(self._password, 1, 2, 1, 2)
320 label.set_mnemonic_widget(self._password)
321
322 if rememberPassword is not None:
323 self._rememberButton = Gtk.CheckButton(rememberLabel)
324 self._rememberButton.set_use_underline(True)
325 self._rememberButton.set_tooltip_text(rememberTooltip)
326 self._rememberButton.set_active(rememberPassword)
327 table.attach(self._rememberButton, 1, 2, 2, 3, ypadding = 8)
328 else:
329 self._rememberButton = None
330
331 @property
332 def userName(self):
333 """Get the user name entered."""
334 return self._userName.get_text()
335
336 @property
337 def password(self):
338 """Get the password entered."""
339 return self._password.get_text()
340
341 @property
342 def rememberPassword(self):
343 """Get whether the password is to be remembered."""
344 return None if self._rememberButton is None \
345 else self._rememberButton.get_active()
346
347 def run(self):
348 """Run the dialog."""
349 self.show_all()
350
351 response = super(CredentialsDialog, self).run()
352
353 self.hide()
354
355 return response
356
357#------------------------------------------------------------------------------
358
359GObject.signal_new("integer-changed", IntegerEntry, GObject.SIGNAL_RUN_FIRST,
360 None, (object,))
361
362#------------------------------------------------------------------------------
363
364PROGRAM_NAME = "MAVA Logger X"
365
366WINDOW_TITLE_BASE = PROGRAM_NAME + " " + _const.VERSION
367if secondaryInstallation:
368 WINDOW_TITLE_BASE += " (" + xstr("secondary") + ")"
369
370#------------------------------------------------------------------------------
371
372# A mapping of aircraft types to their screen names
373aircraftNames = { _const.AIRCRAFT_B736 : xstr("aircraft_b736"),
374 _const.AIRCRAFT_B737 : xstr("aircraft_b737"),
375 _const.AIRCRAFT_B738 : xstr("aircraft_b738"),
376 _const.AIRCRAFT_B738C : xstr("aircraft_b738c"),
377 _const.AIRCRAFT_B732 : xstr("aircraft_b732"),
378 _const.AIRCRAFT_B733 : xstr("aircraft_b733"),
379 _const.AIRCRAFT_B734 : xstr("aircraft_b734"),
380 _const.AIRCRAFT_B735 : xstr("aircraft_b735"),
381 _const.AIRCRAFT_DH8D : xstr("aircraft_dh8d"),
382 _const.AIRCRAFT_B762 : xstr("aircraft_b762"),
383 _const.AIRCRAFT_B763 : xstr("aircraft_b763"),
384 _const.AIRCRAFT_CRJ2 : xstr("aircraft_crj2"),
385 _const.AIRCRAFT_F70 : xstr("aircraft_f70"),
386 _const.AIRCRAFT_DC3 : xstr("aircraft_dc3"),
387 _const.AIRCRAFT_T134 : xstr("aircraft_t134"),
388 _const.AIRCRAFT_T154 : xstr("aircraft_t154"),
389 _const.AIRCRAFT_YK40 : xstr("aircraft_yk40"),
390 _const.AIRCRAFT_B462 : xstr("aircraft_b462") }
391
392#------------------------------------------------------------------------------
393
394aircraftFamilyNames = {
395 _const.AIRCRAFT_FAMILY_B737NG: xstr("aircraft_family_b737ng"),
396
397 _const.AIRCRAFT_FAMILY_B737CL: xstr("aircraft_family_b737cl"),
398
399 _const.AIRCRAFT_FAMILY_DH8D: xstr("aircraft_family_dh8d"),
400
401 _const.AIRCRAFT_FAMILY_B767: xstr("aircraft_family_b767"),
402
403 _const.AIRCRAFT_FAMILY_CRJ2: xstr("aircraft_family_crj2"),
404
405 _const.AIRCRAFT_FAMILY_F70: xstr("aircraft_family_f70"),
406
407 _const.AIRCRAFT_FAMILY_DC3: xstr("aircraft_family_dc3"),
408
409 _const.AIRCRAFT_FAMILY_T134: xstr("aircraft_family_t134"),
410
411 _const.AIRCRAFT_FAMILY_T154: xstr("aircraft_family_t154"),
412
413 _const.AIRCRAFT_FAMILY_YK40: xstr("aircraft_family_yk40"),
414
415 _const.AIRCRAFT_FAMILY_B462: xstr("aircraft_family_b462")
416}
417
418#------------------------------------------------------------------------------
419
420def formatFlightLogLine(timeStr, line):
421 """Format the given flight log line."""
422 """Format the given line for flight logging."""
423 if timeStr is not None:
424 line = timeStr + ": " + line
425 return line + "\n"
426
427#------------------------------------------------------------------------------
428
429def addFaultTag(buffer):
430 """Add a tag named 'fault' to the given buffer."""
431 buffer.create_tag("fault", foreground="red", weight=Pango.Weight.BOLD)
432
433#------------------------------------------------------------------------------
434
435def appendTextBuffer(buffer, text, isFault = False):
436 """Append the given line at the end of the given text buffer.
437
438 If isFault is set, use the tag named 'fault'."""
439 insertTextBuffer(buffer, buffer.get_end_iter(), text, isFault)
440
441#------------------------------------------------------------------------------
442
443def insertTextBuffer(buffer, iter, text, isFault = False):
444 """Insert the given line into the given text buffer at the given iterator.
445
446 If isFault is set, use the tag named 'fault' else use the tag named
447 'normal'."""
448 line = iter.get_line()
449
450 buffer.insert(iter, text)
451
452 iter0 = buffer.get_iter_at_line(line)
453 iter1 = buffer.get_iter_at_line(line+1)
454 if isFault:
455 buffer.apply_tag_by_name("fault", iter0, iter1)
456 else:
457 buffer.remove_all_tags(iter0, iter1)
458
459#------------------------------------------------------------------------------
460
461def askYesNo(question, parent = None, title = WINDOW_TITLE_BASE):
462 """Ask a Yes/No question.
463
464 Return a boolean indicating the answer."""
465 dialog = Gtk.MessageDialog(parent = parent,
466 type = Gtk.MessageType.QUESTION,
467 message_format = question)
468
469 dialog.add_button(xstr("button_no"), Gtk.ResponseType.NO)
470 dialog.add_button(xstr("button_yes"), Gtk.ResponseType.YES)
471
472 dialog.set_title(title)
473 result = dialog.run()
474 dialog.hide()
475
476 return result==Gtk.ResponseType.YES
477
478#------------------------------------------------------------------------------
479
480def errorDialog(message, parent = None, secondary = None,
481 title = WINDOW_TITLE_BASE):
482 """Display an error dialog box with the given message."""
483 dialog = Gtk.MessageDialog(parent = parent,
484 type = Gtk.MessageType.ERROR,
485 message_format = message)
486 dialog.add_button(xstr("button_ok"), Gtk.ResponseType.OK)
487 dialog.set_title(title)
488 if secondary is not None:
489 dialog.format_secondary_markup(secondary)
490
491 dialog.run()
492 dialog.hide()
493
494#------------------------------------------------------------------------------
495
496def communicationErrorDialog(parent = None, title = WINDOW_TITLE_BASE):
497 """Display a communication error dialog."""
498 errorDialog(xstr("error_communication"), parent = parent,
499 secondary = xstr("error_communication_secondary"),
500 title = title)
501
502#------------------------------------------------------------------------------
503
504def createFlightTypeComboBox():
505 flightTypeModel = Gtk.ListStore(str, int)
506 for type in _const.flightTypes:
507 name = "flighttype_" + _const.flightType2string(type)
508 flightTypeModel.append([xstr(name), type])
509
510 flightType = Gtk.ComboBox(model = flightTypeModel)
511 renderer = Gtk.CellRendererText()
512 flightType.pack_start(renderer, True)
513 flightType.add_attribute(renderer, "text", 0)
514
515 return flightType
516
517#------------------------------------------------------------------------------
518
519def getTextViewText(textView):
520 """Get the text from the given text view."""
521 buffer = textView.get_buffer()
522 return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True)
523
524#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.