source: src/mlx/gui/common.py

python3
Last change on this file was 1002:5e741be6b47c, checked in by István Váradi <ivaradi@…>, 4 years ago

The printing of a briefing works with Gtk 3 (re #347)

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