# Additional and wx specific layer of abstraction for the cefpython
# __author__ = "Greg Kacy "
#-------------------------------------------------------------------------------
from cefpython3 import cefpython
import os, sys, platform
import wx
import wx.lib.buttons as buttons
#-------------------------------------------------------------------------------
# CEF Python application settings
g_settings = None
def Debug(msg):
if g_settings and "debug" in g_settings and g_settings["debug"]:
print("[chromectrl.py] "+msg)
#-------------------------------------------------------------------------------
# Default timer interval when timer used to service CEF message loop
DEFAULT_TIMER_MILLIS = 10
# A global timer for CEF message loop processing.
g_messageLoopTimer = None
def CreateMessageLoopTimer(timerMillis):
# This function gets called multiple times for each ChromeWindow
# instance.
global g_messageLoopTimer
Debug("CreateMesageLoopTimer")
if g_messageLoopTimer:
return
g_messageLoopTimer = wx.Timer()
g_messageLoopTimer.Start(timerMillis)
Debug("g_messageLoopTimer.GetId() = "\
+str(g_messageLoopTimer.GetId()))
wx.EVT_TIMER(g_messageLoopTimer, g_messageLoopTimer.GetId(),\
MessageLoopTimer)
def MessageLoopTimer(event):
cefpython.MessageLoopWork()
def DestroyMessageLoopTimer():
global g_messageLoopTimer
Debug("DestroyMessageLoopTimer")
if g_messageLoopTimer:
g_messageLoopTimer.Stop()
g_messageLoopTimer = None
else:
# There was no browser created during session.
Debug("DestroyMessageLoopTimer: timer not started")
#-------------------------------------------------------------------------------
class NavigationBar(wx.Panel):
def __init__(self, parent, *args, **kwargs):
wx.Panel.__init__(self, parent, *args, **kwargs)
self.bitmapDir = os.path.join(os.path.dirname(
os.path.abspath(__file__)), "images")
self._InitComponents()
self._LayoutComponents()
self._InitEventHandlers()
def _InitComponents(self):
self.backBtn = buttons.GenBitmapButton(self, -1,
wx.Bitmap(os.path.join(self.bitmapDir, "back.png"),
wx.BITMAP_TYPE_PNG), style=wx.BORDER_NONE)
self.forwardBtn = buttons.GenBitmapButton(self, -1,
wx.Bitmap(os.path.join(self.bitmapDir, "forward.png"),
wx.BITMAP_TYPE_PNG), style=wx.BORDER_NONE)
self.reloadBtn = buttons.GenBitmapButton(self, -1,
wx.Bitmap(os.path.join(self.bitmapDir, "reload_page.png"),
wx.BITMAP_TYPE_PNG), style=wx.BORDER_NONE)
self.url = wx.TextCtrl(self, id=-1, style=0)
self.historyPopup = wx.Menu()
def _LayoutComponents(self):
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.backBtn, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|
wx.ALL, 0)
sizer.Add(self.forwardBtn, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|
wx.ALL, 0)
sizer.Add(self.reloadBtn, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|
wx.ALL, 0)
sizer.Add(self.url, 1, wx.EXPAND|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 12)
self.SetSizer(sizer)
self.Fit()
def _InitEventHandlers(self):
self.backBtn.Bind(wx.EVT_CONTEXT_MENU, self.OnButtonContext)
def __del__(self):
self.historyPopup.Destroy()
def GetBackButton(self):
return self.backBtn
def GetForwardButton(self):
return self.forwardBtn
def GetReloadButton(self):
return self.reloadBtn
def GetUrlCtrl(self):
return self.url
def InitHistoryPopup(self):
self.historyPopup = wx.Menu()
def AddToHistory(self, url):
self.historyPopup.Append(-1, url)
def OnButtonContext(self, event):
self.PopupMenu(self.historyPopup)
class ChromeWindow(wx.Window):
"""
Standalone CEF component. The class provides facilites for interacting
with wx message loop
"""
def __init__(self, parent, url="", useTimer=True,
timerMillis=DEFAULT_TIMER_MILLIS, browserSettings=None,
size=(-1, -1), *args, **kwargs):
wx.Window.__init__(self, parent, id=wx.ID_ANY, size=size,
*args, **kwargs)
# This timer is not used anymore, but creating it for backwards
# compatibility. In one of external projects ChromeWindow.timer.Stop()
# is being called during browser destruction.
self.timer = wx.Timer()
# On Linux absolute file urls need to start with "file://"
# otherwise a path of "/home/some" is converted to "http://home/some".
if platform.system() in ["Linux", "Darwin"]:
if url.startswith("/"):
url = "file://" + url
self.url = url
windowInfo = cefpython.WindowInfo()
if platform.system() == "Windows":
windowInfo.SetAsChild(self.GetHandle())
elif platform.system() == "Linux":
windowInfo.SetAsChild(self.GetGtkWidget())
elif platform.system() == "Darwin":
(width, height) = self.GetClientSizeTuple()
windowInfo.SetAsChild(self.GetHandle(),
[0, 0, width, height])
else:
raise Exception("Unsupported OS")
if not browserSettings:
browserSettings = {}
# Disable plugins:
# | browserSettings["plugins_disabled"] = True
self.browser = cefpython.CreateBrowserSync(windowInfo,
browserSettings=browserSettings, navigateUrl=url)
if platform.system() == "Windows":
self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus)
self.Bind(wx.EVT_SIZE, self.OnSize)
self._useTimer = useTimer
if useTimer:
CreateMessageLoopTimer(timerMillis)
else:
# Currently multiple EVT_IDLE events might be registered
# when creating multiple ChromeWindow instances. This will
# result in calling CEF message loop work multiple times
# simultaneously causing performance penalties and possibly
# some unwanted behavior (CEF Python Issue 129).
Debug("WARNING: Using EVT_IDLE for CEF message loop processing"\
" is not recommended")
self.Bind(wx.EVT_IDLE, self.OnIdle)
self.Bind(wx.EVT_CLOSE, self.OnClose)
def OnClose(self, event):
if not self._useTimer:
try:
self.Unbind(wx.EVT_IDLE)
except:
# Calling Unbind() may cause problems on Windows 8:
# https://groups.google.com/d/topic/cefpython/iXE7e1ekArI/discussion
# (it was causing problems in __del__, this might not
# be true anymore in OnClose, but still let's make sure)
pass
self.browser.ParentWindowWillClose()
def OnIdle(self, event):
"""Service CEF message loop when useTimer is False"""
cefpython.MessageLoopWork()
event.Skip()
def OnSetFocus(self, event):
"""OS_WIN only."""
cefpython.WindowUtils.OnSetFocus(self.GetHandle(), 0, 0, 0)
event.Skip()
def OnSize(self, event):
"""OS_WIN only. Handle the the size event"""
cefpython.WindowUtils.OnSize(self.GetHandle(), 0, 0, 0)
event.Skip()
def GetBrowser(self):
"""Returns the CEF's browser object"""
return self.browser
def LoadUrl(self, url, onLoadStart=None, onLoadEnd=None):
if onLoadStart or onLoadEnd:
self.GetBrowser().SetClientHandler(
CallbackClientHandler(onLoadStart, onLoadEnd))
browser = self.GetBrowser()
if cefpython.g_debug:
Debug("LoadUrl() self: %s" % self)
Debug("browser: %s" % browser)
Debug("browser id: %s" % browser.GetIdentifier())
Debug("mainframe: %s" % browser.GetMainFrame())
Debug("mainframe id: %s" % \
browser.GetMainFrame().GetIdentifier())
self.GetBrowser().GetMainFrame().LoadUrl(url)
#wx.CallLater(100, browser.ReloadIgnoreCache)
#wx.CallLater(200, browser.GetMainFrame().LoadUrl, url)
class ChromeCtrl(wx.Panel):
def __init__(self, parent, url="", useTimer=True,
timerMillis=DEFAULT_TIMER_MILLIS,
browserSettings=None, hasNavBar=True,
*args, **kwargs):
# You also have to set the wx.WANTS_CHARS style for
# all parent panels/controls, if it's deeply embedded.
wx.Panel.__init__(self, parent, style=wx.WANTS_CHARS, *args, **kwargs)
self.chromeWindow = ChromeWindow(self, url=str(url), useTimer=useTimer,
browserSettings=browserSettings)
sizer = wx.BoxSizer(wx.VERTICAL)
self.navigationBar = None
if hasNavBar:
self.navigationBar = self.CreateNavigationBar()
sizer.Add(self.navigationBar, 0, wx.EXPAND|wx.ALL, 0)
self._InitEventHandlers()
sizer.Add(self.chromeWindow, 1, wx.EXPAND, 0)
self.SetSizer(sizer)
self.Fit()
ch = DefaultClientHandler(self)
self.SetClientHandler(ch)
if self.navigationBar:
self.UpdateButtonsState()
def _InitEventHandlers(self):
self.navigationBar.backBtn.Bind(wx.EVT_BUTTON, self.OnLeft)
self.navigationBar.forwardBtn.Bind(wx.EVT_BUTTON, self.OnRight)
self.navigationBar.reloadBtn.Bind(wx.EVT_BUTTON, self.OnReload)
def GetNavigationBar(self):
return self.navigationBar
def SetNavigationBar(self, navigationBar):
sizer = self.GetSizer()
if self.navigationBar:
# remove previous one
sizer.Replace(self.navigationBar, navigationBar)
self.navigationBar.Hide()
del self.navigationBar
else:
sizer.Insert(0, navigationBar, 0, wx.EXPAND)
self.navigationBar = navigationBar
sizer.Fit(self)
def CreateNavigationBar(self):
np = NavigationBar(self)
return np
def SetClientHandler(self, handler):
self.chromeWindow.GetBrowser().SetClientHandler(handler)
def OnLeft(self, event):
if self.chromeWindow.GetBrowser().CanGoBack():
self.chromeWindow.GetBrowser().GoBack()
self.UpdateButtonsState()
self.chromeWindow.GetBrowser().SetFocus(True)
def OnRight(self, event):
if self.chromeWindow.GetBrowser().CanGoForward():
self.chromeWindow.GetBrowser().GoForward()
self.UpdateButtonsState()
self.chromeWindow.GetBrowser().SetFocus(True)
def OnReload(self, event):
self.chromeWindow.GetBrowser().Reload()
self.UpdateButtonsState()
self.chromeWindow.GetBrowser().SetFocus(True)
def UpdateButtonsState(self):
self.navigationBar.backBtn.Enable(
self.chromeWindow.GetBrowser().CanGoBack())
self.navigationBar.forwardBtn.Enable(
self.chromeWindow.GetBrowser().CanGoForward())
def OnLoadStart(self, browser, frame):
if self.navigationBar:
self.UpdateButtonsState()
self.navigationBar.GetUrlCtrl().SetValue(
browser.GetMainFrame().GetUrl())
self.navigationBar.AddToHistory(browser.GetMainFrame().GetUrl())
def OnLoadEnd(self, browser, frame, httpStatusCode):
if self.navigationBar:
# In CEF 3 the CanGoBack() and CanGoForward() methods
# sometimes do work, sometimes do not, when called from
# the OnLoadStart event. That's why we're calling it again
# here. This is still not perfect as OnLoadEnd() is not
# guaranteed to get called for all types of pages. See the
# cefpython documentation:
# https://code.google.com/p/cefpython/wiki/LoadHandler
# OnDomReady() would be perfect, but is still not implemented.
# Another option is to implement our own browser state
# using the OnLoadStart and OnLoadEnd callbacks.
self.UpdateButtonsState()
class DefaultClientHandler(object):
def __init__(self, parentCtrl):
self.parentCtrl = parentCtrl
def OnLoadStart(self, browser, frame):
self.parentCtrl.OnLoadStart(browser, frame)
def OnLoadEnd(self, browser, frame, httpStatusCode):
self.parentCtrl.OnLoadEnd(browser, frame, httpStatusCode)
def OnLoadError(self, browser, frame, errorCode, errorText, failedUrl):
# TODO
Debug("ERROR LOADING URL : %s" % failedUrl)
class CallbackClientHandler(object):
def __init__(self, onLoadStart=None, onLoadEnd=None):
self._onLoadStart = onLoadStart
self._onLoadEnd = onLoadEnd
def OnLoadStart(self, browser, frame):
if self._onLoadStart and frame.GetUrl() != "about:blank":
self._onLoadStart(browser, frame)
def OnLoadEnd(self, browser, frame, httpStatusCode):
if self._onLoadEnd and frame.GetUrl() != "about:blank":
self._onLoadEnd(browser, frame, httpStatusCode)
def OnLoadError(self, browser, frame, errorCode, errorText, failedUrl):
# TODO
Debug("ERROR LOADING URL : %s, %s" % (failedUrl, frame.GetUrl()))
#-------------------------------------------------------------------------------
def Initialize(settings=None, debug=False):
"""Initializes CEF, We should do it before initializing wx
If no settings passed a default is used
"""
switches = {}
global g_settings
if not settings:
settings = {}
if not "log_severity" in settings:
settings["log_severity"] = cefpython.LOGSEVERITY_INFO
if not "log_file" in settings:
settings["log_file"] = ""
if platform.system() == "Linux":
# On Linux we need to set locales and resources directories.
if not "locales_dir_path" in settings:
settings["locales_dir_path"] = \
cefpython.GetModuleDirectory() + "/locales"
if not "resources_dir_path" in settings:
settings["resources_dir_path"] = cefpython.GetModuleDirectory()
elif platform.system() == "Darwin":
# On Mac we need to set the resoures dir and the locale_pak switch
if not "resources_dir_path" in settings:
settings["resources_dir_path"] = (cefpython.GetModuleDirectory()
+ "/Resources")
locale_pak = (cefpython.GetModuleDirectory()
+ "/Resources/en.lproj/locale.pak")
if "locale_pak" in settings:
locale_pak = settings["locale_pak"]
del settings["locale_pak"]
switches["locale_pak"] = locale_pak
if not "browser_subprocess_path" in settings:
settings["browser_subprocess_path"] = \
"%s/%s" % (cefpython.GetModuleDirectory(), "subprocess")
# DEBUGGING options:
# ------------------
if debug:
settings["debug"] = True # cefpython messages in console and log_file
settings["log_severity"] = cefpython.LOGSEVERITY_VERBOSE
settings["log_file"] = "debug.log" # Set to "" to disable.
g_settings = settings
cefpython.Initialize(settings, switches)
def Shutdown():
"""Shuts down CEF, should be called by app exiting code"""
DestroyMessageLoopTimer()
cefpython.Shutdown()