source: src/mlx/gui/cef.py@ 948:483488585dd8

python3
Last change on this file since 948:483488585dd8 was 945:90f458a3bc2d, checked in by István Váradi <ivaradi@…>, 6 years ago

ACARS CEF works on Windows (re #347)

File size: 15.4 KB
Line 
1from .common import *
2
3from mlx.util import secondaryInstallation
4
5from cefpython3 import cefpython
6
7import platform
8import json
9import time
10import os
11import re
12import _thread
13import threading
14import tempfile
15import traceback
16import ctypes
17import urllib.request, urllib.error, urllib.parse
18from lxml import etree
19from io import StringIO
20import lxml.html
21
22#------------------------------------------------------------------------------
23
24## @package mlx.gui.cef
25#
26# Some helper stuff related to the Chrome Embedded Framework
27
28#------------------------------------------------------------------------------
29
30# Indicate if we should quit
31_toQuit = False
32
33# The SimBrief handler
34_simBriefHandler = None
35
36#------------------------------------------------------------------------------
37
38SIMBRIEF_PROGRESS_SEARCHING_BROWSER = 1
39SIMBRIEF_PROGRESS_LOADING_FORM = 2
40SIMBRIEF_PROGRESS_FILLING_FORM = 3
41SIMBRIEF_PROGRESS_WAITING_LOGIN = 4
42SIMBRIEF_PROGRESS_LOGGING_IN = 5
43SIMBRIEF_PROGRESS_WAITING_RESULT = 6
44
45SIMBRIEF_PROGRESS_RETRIEVING_BRIEFING = 7
46SIMBRIEF_PROGRESS_DONE = 1000
47
48SIMBRIEF_RESULT_NONE = 0
49SIMBRIEF_RESULT_OK = 1
50SIMBRIEF_RESULT_ERROR_OTHER = 2
51SIMBRIEF_RESULT_ERROR_NO_FORM = 11
52SIMBRIEF_RESULT_ERROR_NO_POPUP = 12
53SIMBRIEF_RESULT_ERROR_LOGIN_FAILED = 13
54
55#------------------------------------------------------------------------------
56
57class SimBriefHandler(object):
58 """An object to store the state of a SimBrief query."""
59 _formURL = "http://flare.privatedns.org/mava_simbrief/simbrief_form.html"
60
61 _querySettings = {
62 'navlog': True,
63 'etops': True,
64 'stepclimbs': True,
65 'tlr': True,
66 'notams': True,
67 'firnot': True,
68 'maps': 'Simple',
69 };
70
71
72 def __init__(self):
73 """Construct the handler."""
74 self._browser = None
75 self._plan = None
76 self._getCredentials = None
77 self._getCredentialsCount = 0
78 self._updateProgressFn = None
79 self._htmlFilePath = None
80 self._lastProgress = SIMBRIEF_PROGRESS_SEARCHING_BROWSER
81 self._timeoutID = None
82
83 def initialize(self):
84 """Create and initialize the browser used for Simbrief."""
85 windowInfo = cefpython.WindowInfo()
86 windowInfo.SetAsOffscreen(int(0))
87
88 self._browser = \
89 cefpython.CreateBrowserSync(windowInfo, browserSettings = {},
90 navigateUrl = SimBriefHandler._formURL)
91 self._browser.SetClientHandler(OffscreenRenderHandler())
92 self._browser.SetFocus(True)
93 self._browser.SetClientCallback("OnLoadEnd", self._onLoadEnd)
94
95 bindings = cefpython.JavascriptBindings(bindToFrames=True,
96 bindToPopups=True)
97 bindings.SetFunction("briefingData", self._briefingDataAvailable)
98 bindings.SetFunction("formFilled", self._formFilled)
99 self._browser.SetJavascriptBindings(bindings)
100
101 def call(self, plan, getCredentials, updateProgress, htmlFilePath):
102 """Call SimBrief with the given plan."""
103 self._timeoutID = gobject.timeout_add(120*1000, self._timedOut)
104
105 self._plan = plan
106 self._getCredentials = getCredentials
107 self._getCredentialsCount = 0
108 self._updateProgressFn = updateProgress
109 self._htmlFilePath = htmlFilePath
110
111 self._browser.LoadUrl(SimBriefHandler._formURL)
112 self._updateProgress(SIMBRIEF_PROGRESS_LOADING_FORM,
113 SIMBRIEF_RESULT_NONE, None)
114
115 def _onLoadEnd(self, browser, frame, http_code):
116 """Called when a page has been loaded in the SimBrief browser."""
117 url = frame.GetUrl()
118 print("gui.cef.SimBriefHandler._onLoadEnd", http_code, url)
119 if http_code>=300:
120 self._updateProgress(self._lastProgress,
121 SIMBRIEF_RESULT_ERROR_OTHER, None)
122 elif url.startswith("http://flare.privatedns.org/mava_simbrief/simbrief_form.html"):
123 if self._plan is None:
124 return
125
126 self._updateProgress(SIMBRIEF_PROGRESS_FILLING_FORM,
127 SIMBRIEF_RESULT_NONE, None)
128
129 js = "form=document.getElementById(\"sbapiform\");"
130 for (name, value) in self._plan.items():
131 js += "form." + name + ".value=\"" + value + "\";"
132 for (name, value) in SimBriefHandler._querySettings.items():
133 if isinstance(value, bool):
134 js += "form." + name + ".checked=" + \
135 ("true" if value else "false") + ";"
136 elif isinstance(value, str):
137 js += "form." + name + ".value=\"" + value + "\";"
138
139 js += "form.submitform.click();"
140 js += "window.formFilled();"
141
142 frame.ExecuteJavascript(js)
143 elif url.startswith("http://www.simbrief.com/system/login.api.php"):
144 (user, password) = self._getCredentials(self._getCredentialsCount)
145 if user is None or password is None:
146 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_LOGIN,
147 SIMBRIEF_RESULT_ERROR_LOGIN_FAILED, None)
148 return
149
150 self._getCredentialsCount += 1
151 js = "form=document.forms[0];"
152 js += "form.user.value=\"" + user + "\";"
153 js += "form.pass.value=\"" + password + "\";"
154 js += "form.submit();"
155 frame.ExecuteJavascript(js)
156 elif url.startswith("http://www.simbrief.com/ofp/ofp.loader.api.php"):
157 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_RESULT,
158 SIMBRIEF_RESULT_NONE, None)
159 elif url.startswith("http://flare.privatedns.org/mava_simbrief/simbrief_briefing.php"):
160 js = "form=document.getElementById(\"hiddenform\");"
161 js += "window.briefingData(form.hidden_is_briefing_available.value, form.hidden_link.value);";
162 frame.ExecuteJavascript(js)
163
164 def _formFilled(self):
165 """Called when the form has been filled and submitted."""
166 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_LOGIN,
167 SIMBRIEF_RESULT_NONE, None)
168
169 def _briefingDataAvailable(self, available, link):
170 """Called when the briefing data is available."""
171 if available:
172 link ="http://www.simbrief.com/ofp/flightplans/xml/" + link + ".xml"
173
174 self._updateProgress(SIMBRIEF_PROGRESS_RETRIEVING_BRIEFING,
175 SIMBRIEF_RESULT_NONE, None)
176
177 thread = threading.Thread(target = self._getResults, args = (link,))
178 _thread.daemon = True
179 _thread.start()
180 else:
181 self._updateProgress(SIMBRIEF_PROGRESS_RETRIEVING_BRIEFING,
182 SIMBRIEF_RESULT_ERROR_OTHER, None)
183
184 def _resultsAvailable(self, flightInfo):
185 """Called from the result retrieval thread when the result is
186 available.
187
188 It checks for the plan being not None, as we may time out."""
189 if self._plan is not None:
190 self._updateProgress(SIMBRIEF_PROGRESS_DONE,
191 SIMBRIEF_RESULT_OK, flightInfo)
192
193 def _updateProgress(self, progress, results, flightInfo):
194 """Update the progress."""
195 self._lastProgress = progress
196 if results!=SIMBRIEF_RESULT_NONE:
197 if self._timeoutID is not None:
198 gobject.source_remove(self._timeoutID)
199 self._plan = None
200
201 if self._updateProgressFn is not None:
202 self._updateProgressFn(progress, results, flightInfo)
203
204 def _timedOut(self):
205 """Called when the timeout occurs."""
206 if self._lastProgress==SIMBRIEF_PROGRESS_LOADING_FORM:
207 result = SIMBRIEF_RESULT_ERROR_NO_FORM
208 elif self._lastProgress==SIMBRIEF_PROGRESS_WAITING_LOGIN:
209 result = SIMBRIEF_RESULT_ERROR_NO_POPUP
210 else:
211 result = SIMBRIEF_RESULT_ERROR_OTHER
212
213 self._updateProgress(self._lastProgress, result, None)
214
215 return False
216
217 def _getResults(self, link):
218 """Get the result from the given link."""
219 availableInfo = {}
220 ## Holds analysis data to be used
221 flightInfo = {}
222
223 # Obtaining the xml
224 response = urllib.request.urlopen(link)
225 xmlContent = response.read()
226 # Processing xml
227 content = etree.iterparse(StringIO(xmlContent))
228
229 for (action, element) in content:
230 # Processing tags that occur multiple times
231 if element.tag == "weather":
232 weatherElementList = list(element)
233 for weatherElement in weatherElementList:
234 flightInfo[weatherElement.tag] = weatherElement.text
235 else:
236 availableInfo[element.tag] = element.text
237
238 # Processing plan_html
239 ## Obtaining chart links
240 imageLinks = []
241 for imageLinkElement in lxml.html.find_class(availableInfo["plan_html"],
242 "ofpmaplink"):
243 for imageLink in imageLinkElement.iterlinks():
244 if imageLink[1] == 'src':
245 imageLinks.append(imageLink[2])
246 flightInfo["image_links"] = imageLinks
247 print((sorted(availableInfo.keys())))
248 htmlFilePath = "simbrief_plan.html" if self._htmlFilePath is None \
249 else self._htmlFilePath
250 with open(htmlFilePath, 'w') as f:
251 f.write(availableInfo["plan_html"])
252
253 gobject.idle_add(self._resultsAvailable, flightInfo)
254
255#------------------------------------------------------------------------------
256
257def initialize(initializedCallback):
258 """Initialize the Chrome Embedded Framework."""
259 global _toQuit, _simBriefHandler
260 _toQuit = False
261
262 gobject.threads_init()
263
264 _simBriefHandler = SimBriefHandler()
265 _initializeCEF([], initializedCallback)
266
267#------------------------------------------------------------------------------
268
269def _initializeCEF(args, initializedCallback):
270 """Perform the actual initialization of CEF using the given arguments."""
271 print("Initializing CEF with args:", args)
272
273 settings = {
274 "debug": True, # cefpython debug messages in console and in log_file
275 "log_severity": cefpython.LOGSEVERITY_VERBOSE, # LOGSEVERITY_VERBOSE
276 "log_file": "", # Set to "" to disable
277 "release_dcheck_enabled": True, # Enable only when debugging
278 # This directories must be set on Linux
279 "locales_dir_path": os.path.join(cefpython.GetModuleDirectory(), "locales"),
280 "resources_dir_path": cefpython.GetModuleDirectory(),
281 "browser_subprocess_path": "%s/%s" % \
282 (cefpython.GetModuleDirectory(), "subprocess"),
283 }
284
285 switches={}
286 for arg in args:
287 if arg.startswith("--"):
288 if arg != "--enable-logging":
289 assignIndex = arg.find("=")
290 if assignIndex<0:
291 switches[arg[2:]] = ""
292 else:
293 switches[arg[2:assignIndex]] = arg[assignIndex+1:]
294 else:
295 print("Unhandled switch", arg)
296
297 cefpython.Initialize(settings, switches)
298
299 gobject.timeout_add(10, _handleTimeout)
300
301 print("Initialized, executing callback...")
302 initializedCallback()
303
304#------------------------------------------------------------------------------
305
306def getContainer():
307 """Get a container object suitable for running a browser instance
308 within."""
309 if os.name=="nt":
310 container = gtk.DrawingArea()
311 container.set_property("can-focus", True)
312 container.connect("size-allocate", _handleSizeAllocate)
313 else:
314 container = gtk.DrawingArea()
315
316 container.show()
317
318 return container
319
320#------------------------------------------------------------------------------
321
322def startInContainer(container, url, browserSettings = {}):
323 """Start a browser instance in the given container with the given URL."""
324 if os.name=="nt":
325 gdk.threads_enter()
326 ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
327 ctypes.pythonapi.PyCapsule_GetPointer.argtypes = \
328 [ctypes.py_object]
329 gpointer = ctypes.pythonapi.PyCapsule_GetPointer(
330 container.get_property("window").__gpointer__, None)
331 libgdk = ctypes.CDLL("libgdk-3-0.dll")
332 windowID = libgdk.gdk_win32_window_get_handle(gpointer)
333 container.windowID = windowID
334 gdk.threads_leave()
335 else:
336 container.set_visual(container.get_screen().lookup_visual(0x21))
337 windowID = container.get_window().get_xid()
338
339 windowInfo = cefpython.WindowInfo()
340 if windowID is not None:
341 windowInfo.SetAsChild(windowID)
342
343 return cefpython.CreateBrowserSync(windowInfo,
344 browserSettings = browserSettings,
345 navigateUrl = url)
346
347#------------------------------------------------------------------------------
348
349class OffscreenRenderHandler(object):
350 def GetRootScreenRect(self, browser, rect_out):
351 #print "GetRootScreenRect"
352 rect_out += [0, 0, 800, 600]
353 return True
354
355 def GetViewRect(self, browser, rect_out):
356 #print "GetViewRect"
357 rect_out += [0, 0, 800, 600]
358 return True
359
360 def GetScreenPoint(self, browser, viewX, viewY, screenCoordinates):
361 #print "GetScreenPoint", viewX, viewY
362 rect += [viewX, viewY]
363 return True
364
365 def GetScreenInfo(self, browser, screenInfo):
366 #print "GetScreenInfo"
367 pass
368
369 def OnPopupShow(self, browser, show):
370 #print "OnPopupShow", show
371 pass
372
373 def OnPopupSize(self, browser, rect):
374 #print "OnPopupSize", rect
375 pass
376
377 def OnPaint(self, browser, element_type, dirty_rects, paint_buffer, width, height):
378 #print "OnPaint", paintElementType, dirtyRects, buffer, width, height
379 pass
380
381 def OnCursorChange(self, browser, cursor):
382 #print "OnCursorChange", cursor
383 pass
384
385 def OnScrollOffsetChanged(self, browser):
386 #print "OnScrollOffsetChange"
387 pass
388
389 def OnBeforePopup(self, browser, frame, targetURL, targetFrameName,
390 popupFeatures, windowInfo, client, browserSettings,
391 noJavascriptAccess):
392 wInfo = cefpython.WindowInfo()
393 wInfo.SetAsOffscreen(int(0))
394
395 windowInfo.append(wInfo)
396
397 return False
398
399#------------------------------------------------------------------------------
400
401def initializeSimBrief():
402 """Initialize the (hidden) browser window for SimBrief."""
403 _simBriefHandler.initialize()
404
405#------------------------------------------------------------------------------
406
407def callSimBrief(plan, getCredentials, updateProgress, htmlFilePath):
408 """Call SimBrief with the given plan.
409
410 The callbacks will be called in the main GUI thread."""
411 _simBriefHandler.call(plan, getCredentials, updateProgress, htmlFilePath)
412
413#------------------------------------------------------------------------------
414
415def finalize():
416 """Finalize the Chrome Embedded Framework."""
417 global _toQuit
418 _toQuit = True
419 cefpython.Shutdown()
420
421#------------------------------------------------------------------------------
422
423def _handleTimeout():
424 """Handle the timeout by running the CEF message loop."""
425 if _toQuit:
426 return False
427 else:
428 cefpython.MessageLoopWork()
429 return True
430
431#------------------------------------------------------------------------------
432
433def _handleSizeAllocate(widget, sizeAlloc):
434 """Handle the size-allocate event on Windows."""
435 if widget is not None:
436 cefpython.WindowUtils.OnSize(widget.windowID, 0, 0, 0)
Note: See TracBrowser for help on using the repository browser.