source: src/mlx/gui/cef.py@ 1097:f2ac841cffec

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

The browser cache can be cleared from the menu (re #368)

File size: 14.8 KB
RevLine 
[919]1from .common import *
[648]2
[682]3from mlx.util import secondaryInstallation
4
5from cefpython3 import cefpython
6
[648]7import platform
8import json
[682]9import time
[648]10import os
11import re
[919]12import _thread
[682]13import threading
14import tempfile
15import traceback
[945]16import ctypes
[919]17import urllib.request, urllib.error, urllib.parse
[805]18from lxml import etree
[919]19from io import StringIO
[805]20import lxml.html
[1096]21import shutil
[648]22
23#------------------------------------------------------------------------------
24
25## @package mlx.gui.cef
26#
27# Some helper stuff related to the Chrome Embedded Framework
28
29#------------------------------------------------------------------------------
30
31# Indicate if we should quit
32_toQuit = False
33
[805]34# The SimBrief handler
35_simBriefHandler = None
[682]36
[1097]37# The cache directory
38cacheDir = os.path.join(GLib.get_user_cache_dir(), "mlxcef")
39
[682]40#------------------------------------------------------------------------------
41
[805]42SIMBRIEF_PROGRESS_SEARCHING_BROWSER = 1
43SIMBRIEF_PROGRESS_LOADING_FORM = 2
44SIMBRIEF_PROGRESS_FILLING_FORM = 3
45SIMBRIEF_PROGRESS_WAITING_LOGIN = 4
46SIMBRIEF_PROGRESS_LOGGING_IN = 5
47SIMBRIEF_PROGRESS_WAITING_RESULT = 6
[682]48
[805]49SIMBRIEF_PROGRESS_RETRIEVING_BRIEFING = 7
50SIMBRIEF_PROGRESS_DONE = 1000
[682]51
[805]52SIMBRIEF_RESULT_NONE = 0
53SIMBRIEF_RESULT_OK = 1
54SIMBRIEF_RESULT_ERROR_OTHER = 2
55SIMBRIEF_RESULT_ERROR_NO_FORM = 11
56SIMBRIEF_RESULT_ERROR_NO_POPUP = 12
57SIMBRIEF_RESULT_ERROR_LOGIN_FAILED = 13
[682]58
[648]59#------------------------------------------------------------------------------
60
[805]61class SimBriefHandler(object):
62 """An object to store the state of a SimBrief query."""
[1084]63 _formURLBase = MAVA_BASE_URL + "/simbrief_form.php"
64
65 _resultURLBase = MAVA_BASE_URL + "/simbrief_briefing.php"
66
67 @staticmethod
68 def getFormURL(plan):
69 """Get the form URL for the given plan."""
70 return SimBriefHandler._formURLBase + "?" + \
71 urllib.parse.urlencode(plan)
[692]72
[805]73 _querySettings = {
74 'navlog': True,
75 'etops': True,
76 'stepclimbs': True,
77 'tlr': True,
78 'notams': True,
79 'firnot': True,
80 'maps': 'Simple',
81 };
[682]82
83
[805]84 def __init__(self):
85 """Construct the handler."""
86 self._browser = None
87 self._plan = None
88 self._getCredentials = None
89 self._getCredentialsCount = 0
90 self._updateProgressFn = None
91 self._htmlFilePath = None
92 self._lastProgress = SIMBRIEF_PROGRESS_SEARCHING_BROWSER
93 self._timeoutID = None
[682]94
[805]95 def initialize(self):
[695]96 """Create and initialize the browser used for Simbrief."""
97 windowInfo = cefpython.WindowInfo()
98 windowInfo.SetAsOffscreen(int(0))
99
[805]100 self._browser = \
[1084]101 cefpython.CreateBrowserSync(windowInfo, browserSettings = {})
[805]102 self._browser.SetClientHandler(OffscreenRenderHandler())
103 self._browser.SetFocus(True)
104 self._browser.SetClientCallback("OnLoadEnd", self._onLoadEnd)
[1083]105 self._browser.SetClientCallback("OnLoadError", self._onLoadError)
106 self._browser.SetClientCallback("OnBeforeBrowse",
107 lambda browser, frame, request,
108 user_gesture, is_redirect: False)
[805]109
110 def call(self, plan, getCredentials, updateProgress, htmlFilePath):
[685]111 """Call SimBrief with the given plan."""
[995]112 self._timeoutID = GObject.timeout_add(120*1000, self._timedOut)
[805]113
114 self._plan = plan
115 self._getCredentials = getCredentials
116 self._getCredentialsCount = 0
117 self._updateProgressFn = updateProgress
118 self._htmlFilePath = htmlFilePath
119
[1084]120 self._browser.LoadUrl(SimBriefHandler.getFormURL(plan))
[805]121 self._updateProgress(SIMBRIEF_PROGRESS_LOADING_FORM,
122 SIMBRIEF_RESULT_NONE, None)
123
[1083]124 def finalize(self):
125 """Close the browser and release it."""
126 self._browser.CloseBrowser()
127 self._browser = None
128
[943]129 def _onLoadEnd(self, browser, frame, http_code):
[805]130 """Called when a page has been loaded in the SimBrief browser."""
131 url = frame.GetUrl()
[943]132 print("gui.cef.SimBriefHandler._onLoadEnd", http_code, url)
133 if http_code>=300:
[805]134 self._updateProgress(self._lastProgress,
135 SIMBRIEF_RESULT_ERROR_OTHER, None)
[1084]136 elif url.startswith(SimBriefHandler._formURLBase):
137 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_LOGIN,
[805]138 SIMBRIEF_RESULT_NONE, None)
[1084]139 elif url.startswith("https://www.simbrief.com/system/login.api.sso.php"):
140 js = "document.getElementsByClassName(\"login_option navigraph\")[0].click();"
[805]141 frame.ExecuteJavascript(js)
[1084]142 elif url.startswith("https://identity.api.navigraph.com/login?"):
[805]143 (user, password) = self._getCredentials(self._getCredentialsCount)
144 if user is None or password is None:
145 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_LOGIN,
146 SIMBRIEF_RESULT_ERROR_LOGIN_FAILED, None)
147 return
[682]148
[805]149 self._getCredentialsCount += 1
[1084]150
151 js = "form=document.getElementsByName(\"form\")[0];"
152 js +="form.username.value=\"" + user + "\";"
153 js +="form.password.value=\"" + password + "\";"
154 js +="form.submit();"
[805]155 frame.ExecuteJavascript(js)
[1084]156 elif url.startswith("https://www.simbrief.com/ofp/ofp.loader.api.php"):
[805]157 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_RESULT,
158 SIMBRIEF_RESULT_NONE, None)
[1084]159 elif url.startswith(SimBriefHandler._resultURLBase):
[805]160 self._updateProgress(SIMBRIEF_PROGRESS_RETRIEVING_BRIEFING,
[1084]161 SIMBRIEF_RESULT_OK, None)
[805]162
[1083]163 def _onLoadError(self, browser, frame, error_code, error_text_out,
164 failed_url):
165 """Called when loading of an URL fails."""
166 print("gui.cef.SimBriefHandler._onLoadError", browser, frame, error_code, error_text_out, failed_url)
[1095]167 if error_code==-3 and \
168 failed_url.startswith("https://identity.api.navigraph.com/connect/authorize"):
169 self._browser.LoadUrl(SimBriefHandler.getFormURL(self._plan))
170 self._updateProgress(SIMBRIEF_PROGRESS_LOADING_FORM,
171 SIMBRIEF_RESULT_NONE, None)
172 else:
173 self._updateProgress(self._lastProgress,
174 SIMBRIEF_RESULT_ERROR_OTHER, None)
[805]175
176 def _updateProgress(self, progress, results, flightInfo):
177 """Update the progress."""
178 self._lastProgress = progress
179 if results!=SIMBRIEF_RESULT_NONE:
[944]180 if self._timeoutID is not None:
[995]181 GObject.source_remove(self._timeoutID)
[805]182 self._plan = None
183
[944]184 if self._updateProgressFn is not None:
185 self._updateProgressFn(progress, results, flightInfo)
[805]186
187 def _timedOut(self):
188 """Called when the timeout occurs."""
189 if self._lastProgress==SIMBRIEF_PROGRESS_LOADING_FORM:
190 result = SIMBRIEF_RESULT_ERROR_NO_FORM
191 elif self._lastProgress==SIMBRIEF_PROGRESS_WAITING_LOGIN:
192 result = SIMBRIEF_RESULT_ERROR_NO_POPUP
193 else:
194 result = SIMBRIEF_RESULT_ERROR_OTHER
[685]195
[805]196 self._updateProgress(self._lastProgress, result, None)
197
198 return False
[685]199
[682]200#------------------------------------------------------------------------------
201
[805]202def initialize(initializedCallback):
[648]203 """Initialize the Chrome Embedded Framework."""
[805]204 global _toQuit, _simBriefHandler
[648]205 _toQuit = False
206
[995]207 GObject.threads_init()
[648]208
[805]209 _simBriefHandler = SimBriefHandler()
210 _initializeCEF([], initializedCallback)
[682]211
212#------------------------------------------------------------------------------
213
214def _initializeCEF(args, initializedCallback):
215 """Perform the actual initialization of CEF using the given arguments."""
[1091]216 if os.name=="nt":
217 _initializeCEF1(args, initializedCallback)
218 else:
219 GObject.timeout_add(100, _initializeCEF1, args, initializedCallback)
220
221#------------------------------------------------------------------------------
222
223def _initializeCEF1(args, initializedCallback):
224 """Perform the actual initialization of CEF using the given arguments."""
[919]225 print("Initializing CEF with args:", args)
[682]226
[648]227 settings = {
228 "debug": True, # cefpython debug messages in console and in log_file
229 "log_severity": cefpython.LOGSEVERITY_VERBOSE, # LOGSEVERITY_VERBOSE
230 "log_file": "", # Set to "" to disable
231 "release_dcheck_enabled": True, # Enable only when debugging
232 # This directories must be set on Linux
233 "locales_dir_path": os.path.join(cefpython.GetModuleDirectory(), "locales"),
234 "resources_dir_path": cefpython.GetModuleDirectory(),
235 "browser_subprocess_path": "%s/%s" % \
236 (cefpython.GetModuleDirectory(), "subprocess"),
[1083]237 "windowless_rendering_enabled": True,
[1097]238 "cache_path": cacheDir,
239 "persist_session_cookies": True,
240 "persist_user_preferences": True
[648]241 }
242
[682]243 switches={}
244 for arg in args:
245 if arg.startswith("--"):
246 if arg != "--enable-logging":
247 assignIndex = arg.find("=")
248 if assignIndex<0:
249 switches[arg[2:]] = ""
250 else:
251 switches[arg[2:assignIndex]] = arg[assignIndex+1:]
252 else:
[919]253 print("Unhandled switch", arg)
[682]254
255 cefpython.Initialize(settings, switches)
[648]256
[995]257 GObject.timeout_add(10, _handleTimeout)
[648]258
[919]259 print("Initialized, executing callback...")
[682]260 initializedCallback()
[1091]261 return False
[682]262
[648]263#------------------------------------------------------------------------------
264
265def getContainer():
266 """Get a container object suitable for running a browser instance
267 within."""
[1083]268 container = Gtk.DrawingArea()
269 container.set_property("can-focus", True)
270 container.connect("size-allocate", _handleSizeAllocate)
[648]271 container.show()
272
273 return container
274
275#------------------------------------------------------------------------------
276
277def startInContainer(container, url, browserSettings = {}):
278 """Start a browser instance in the given container with the given URL."""
279 if os.name=="nt":
[997]280 Gdk.threads_enter()
[945]281 ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
282 ctypes.pythonapi.PyCapsule_GetPointer.argtypes = \
283 [ctypes.py_object]
284 gpointer = ctypes.pythonapi.PyCapsule_GetPointer(
285 container.get_property("window").__gpointer__, None)
286 libgdk = ctypes.CDLL("libgdk-3-0.dll")
287 windowID = libgdk.gdk_win32_window_get_handle(gpointer)
288 container.windowID = windowID
[997]289 Gdk.threads_leave()
[1083]290 windowRect = [0, 0, 1, 1]
[648]291 else:
[924]292 container.set_visual(container.get_screen().lookup_visual(0x21))
293 windowID = container.get_window().get_xid()
[1083]294 windowRect = None
[648]295
296 windowInfo = cefpython.WindowInfo()
[805]297 if windowID is not None:
[1083]298 windowInfo.SetAsChild(windowID, windowRect = windowRect)
[648]299
[1083]300 browser = cefpython.CreateBrowserSync(windowInfo,
301 browserSettings = browserSettings,
302 navigateUrl = url)
303 container.browser = browser
304
305 return browser
[648]306
307#------------------------------------------------------------------------------
308
[685]309class OffscreenRenderHandler(object):
[943]310 def GetRootScreenRect(self, browser, rect_out):
[685]311 #print "GetRootScreenRect"
[1083]312 rect_out += [0, 0, 1920, 1080]
[685]313 return True
314
[943]315 def GetViewRect(self, browser, rect_out):
[685]316 #print "GetViewRect"
[1083]317 rect_out += [0, 40, 1920, 1040]
[685]318 return True
319
320 def GetScreenPoint(self, browser, viewX, viewY, screenCoordinates):
321 #print "GetScreenPoint", viewX, viewY
322 rect += [viewX, viewY]
323 return True
324
325 def GetScreenInfo(self, browser, screenInfo):
326 #print "GetScreenInfo"
327 pass
328
329 def OnPopupShow(self, browser, show):
330 #print "OnPopupShow", show
331 pass
332
333 def OnPopupSize(self, browser, rect):
334 #print "OnPopupSize", rect
335 pass
336
[943]337 def OnPaint(self, browser, element_type, dirty_rects, paint_buffer, width, height):
[685]338 #print "OnPaint", paintElementType, dirtyRects, buffer, width, height
339 pass
340
341 def OnCursorChange(self, browser, cursor):
342 #print "OnCursorChange", cursor
343 pass
344
345 def OnScrollOffsetChanged(self, browser):
346 #print "OnScrollOffsetChange"
347 pass
348
[1083]349 def OnBeforePopup(self, browser, frame, target_url, target_frame_name,
350 target_disposition, user_gesture,
351 popup_features, window_info_out, client,
352 browser_settings_out, no_javascript_access_out,
353 **kwargs):
[685]354 wInfo = cefpython.WindowInfo()
355 wInfo.SetAsOffscreen(int(0))
356
[1083]357 window_info_out.append(wInfo)
[685]358
359 return False
360
361#------------------------------------------------------------------------------
362
363def initializeSimBrief():
364 """Initialize the (hidden) browser window for SimBrief."""
[805]365 _simBriefHandler.initialize()
[685]366
367#------------------------------------------------------------------------------
368
369def callSimBrief(plan, getCredentials, updateProgress, htmlFilePath):
[805]370 """Call SimBrief with the given plan.
371
372 The callbacks will be called in the main GUI thread."""
373 _simBriefHandler.call(plan, getCredentials, updateProgress, htmlFilePath)
[685]374
375#------------------------------------------------------------------------------
376
[1097]377def finalizeSimBrief():
378 """Finallize the (hidden) browser window for SimBrief."""
379 if _simBriefHandler is not None:
380 _simBriefHandler.finalize()
381
382#------------------------------------------------------------------------------
383
[648]384def finalize():
385 """Finalize the Chrome Embedded Framework."""
[805]386 global _toQuit
387 _toQuit = True
[1083]388
[1097]389 if os.name!="nt":
390 cefpython.Shutdown()
391
392#------------------------------------------------------------------------------
[1083]393
[1097]394def clearCache():
395 """Clear the CEF cache directory."""
396 try:
397 shutil.rmtree(cacheDir)
398 print("gui.cef.clearCache: the cache directory is removed")
399 except Exception as e:
400 print("gui.cef.clearCache: cache directory removal failed:", e)
[648]401
402#------------------------------------------------------------------------------
403
404def _handleTimeout():
405 """Handle the timeout by running the CEF message loop."""
406 if _toQuit:
407 return False
408 else:
409 cefpython.MessageLoopWork()
410 return True
411
412#------------------------------------------------------------------------------
413
414def _handleSizeAllocate(widget, sizeAlloc):
[945]415 """Handle the size-allocate event on Windows."""
[1083]416 if os.name=="nt":
417 if widget is not None and hasattr(widget, "windowID"):
418 cefpython.WindowUtils.OnSize(widget.windowID, 0, 0, 0)
419 else:
420 if widget is not None and hasattr(widget, "browser") and \
421 widget.browser is not None:
422 widget.browser.SetBounds(sizeAlloc.x, sizeAlloc.y,
423 sizeAlloc.width, sizeAlloc.height)
Note: See TracBrowser for help on using the repository browser.