|
| 1 | +from .. import software_api |
| 2 | +from cefpython3 import cefpython as cef |
| 3 | +import ctypes |
| 4 | + |
| 5 | +try: |
| 6 | + import tkinter as tk |
| 7 | +except ImportError: |
| 8 | + import Tkinter as tk |
| 9 | +import sys |
| 10 | +import os |
| 11 | +import platform |
| 12 | +import logging as _logging |
| 13 | +import requests |
| 14 | + |
| 15 | +app_icon = "Excel.png" |
| 16 | +software_name = "Microsoft Excel" |
| 17 | +software_dir = "MicrosoftExcel" |
| 18 | +is_GUI = True |
| 19 | +min_size = (800, 440) |
| 20 | +max_size = None |
| 21 | +default_size = (900, 640) |
| 22 | + |
| 23 | +# Fix for PyCharm hints warnings |
| 24 | +WindowUtils = cef.WindowUtils() |
| 25 | + |
| 26 | +# Platforms |
| 27 | +WINDOWS = (platform.system() == "Windows") |
| 28 | +LINUX = (platform.system() == "Linux") |
| 29 | +MAC = (platform.system() == "Darwin") |
| 30 | + |
| 31 | +# Globals |
| 32 | +logger = _logging.getLogger("tkinter_.py") |
| 33 | + |
| 34 | +# Constants |
| 35 | +# Tk 8.5 doesn't support png images |
| 36 | +IMAGE_EXT = ".png" if tk.TkVersion > 8.5 else ".gif" |
| 37 | + |
| 38 | +def test_connection(): |
| 39 | + url = "http://www.google.com" |
| 40 | + timeout = 5 |
| 41 | + try: |
| 42 | + request = requests.get(url, timeout=timeout) |
| 43 | + return True |
| 44 | + except (requests.ConnectionError, requests.Timeout): |
| 45 | + return False |
| 46 | + |
| 47 | +def on_app_launch(frame:tk.Frame, width:int=900, height:int=640): |
| 48 | + # Testing if connection |
| 49 | + if test_connection() is True: |
| 50 | + if "no_connection_title" in globals(): |
| 51 | + globals()["no_connection_title"].pack_forget() |
| 52 | + globals()["no_connection_title"].destroy() |
| 53 | + globals()["no_connection_subtitle"].pack_forget() |
| 54 | + globals()["no_connection_subtitle"].destroy() |
| 55 | + |
| 56 | + logger.setLevel(_logging.CRITICAL) |
| 57 | + stream_handler = _logging.StreamHandler() |
| 58 | + formatter = _logging.Formatter("[%(filename)s] %(message)s") |
| 59 | + stream_handler.setFormatter(formatter) |
| 60 | + logger.addHandler(stream_handler) |
| 61 | + logger.info("CEF Python {ver}".format(ver=cef.__version__)) |
| 62 | + logger.info("Python {ver} {arch}".format( |
| 63 | + ver=platform.python_version(), arch=platform.architecture()[0])) |
| 64 | + logger.info("Tk {ver}".format(ver=tk.Tcl().eval('info patchlevel'))) |
| 65 | + assert cef.__version__ >= "55.3", "CEF Python v55.3+ required to run this" |
| 66 | + sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error |
| 67 | + # Tk must be initialized before CEF otherwise fatal error (Issue #306) |
| 68 | + app = MainFrame(frame) |
| 69 | + settings = {} |
| 70 | + if MAC: |
| 71 | + settings["external_message_pump"] = True |
| 72 | + cef.Initialize(settings=settings) |
| 73 | + else: |
| 74 | + if not "no_connection_title" in globals(): |
| 75 | + globals()["no_connection_title"] = tk.Label( |
| 76 | + frame, |
| 77 | + text = "Oh No !", |
| 78 | + font = ("Impact", 22) |
| 79 | + ) |
| 80 | + globals()["no_connection_title"].pack() |
| 81 | + globals()["no_connection_subtitle"] = tk.Label( |
| 82 | + frame, |
| 83 | + text = "Sounds like no connection is available...", |
| 84 | + font = ("Impact", 14) |
| 85 | + ) |
| 86 | + globals()["no_connection_subtitle"].pack() |
| 87 | + # Testing again |
| 88 | + frame.after(5000, on_app_launch, frame, width, height) |
| 89 | + |
| 90 | +class MainFrame(tk.Frame): |
| 91 | + def __init__(self, frame): |
| 92 | + self.browser_frame = None |
| 93 | + self.frame = frame |
| 94 | + |
| 95 | + # Root |
| 96 | + tk.Grid.rowconfigure(frame, 0, weight=1) |
| 97 | + tk.Grid.columnconfigure(frame, 0, weight=1) |
| 98 | + |
| 99 | + # MainFrame |
| 100 | + tk.Frame.__init__(self, frame) |
| 101 | + self.master.bind("<Configure>", self.on_frame_configure) |
| 102 | + self.setup_icon() |
| 103 | + self.bind("<Configure>", self.on_configure) |
| 104 | + self.bind("<FocusIn>", self.on_focus_in) |
| 105 | + self.bind("<FocusOut>", self.on_focus_out) |
| 106 | + |
| 107 | + # BrowserFrame |
| 108 | + self.browser_frame = BrowserFrame(self) |
| 109 | + self.browser_frame.grid(row=1, column=0, |
| 110 | + sticky=(tk.N + tk.S + tk.E + tk.W)) |
| 111 | + tk.Grid.rowconfigure(self, 1, weight=1) |
| 112 | + tk.Grid.columnconfigure(self, 0, weight=1) |
| 113 | + |
| 114 | + # Pack MainFrame |
| 115 | + self.pack(fill=tk.BOTH, expand=tk.YES) |
| 116 | + |
| 117 | + def on_frame_configure(self, _): |
| 118 | + logger.debug("MainFrame.on_frame_configure") |
| 119 | + if self.browser_frame: |
| 120 | + self.browser_frame.on_frame_configure() |
| 121 | + |
| 122 | + def on_configure(self, event): |
| 123 | + logger.debug("MainFrame.on_configure") |
| 124 | + if self.browser_frame: |
| 125 | + width = event.width |
| 126 | + height = event.height |
| 127 | + self.browser_frame.on_mainframe_configure(width, height) |
| 128 | + |
| 129 | + def on_focus_in(self, _): |
| 130 | + logger.debug("MainFrame.on_focus_in") |
| 131 | + |
| 132 | + def on_focus_out(self, _): |
| 133 | + logger.debug("MainFrame.on_focus_out") |
| 134 | + |
| 135 | + def get_browser(self): |
| 136 | + if self.browser_frame: |
| 137 | + return self.browser_frame.browser |
| 138 | + return None |
| 139 | + |
| 140 | + def get_browser_frame(self): |
| 141 | + if self.browser_frame: |
| 142 | + return self.browser_frame |
| 143 | + return None |
| 144 | + |
| 145 | + def setup_icon(self): |
| 146 | + resources = os.path.join(os.path.dirname(__file__), "resources") |
| 147 | + icon_path = os.path.join(resources, "tkinter" + IMAGE_EXT) |
| 148 | + if os.path.exists(icon_path): |
| 149 | + self.icon = tk.PhotoImage(file=icon_path) |
| 150 | + # noinspection PyProtectedMember |
| 151 | + self.master.call("wm", "iconphoto", self.master._w, self.icon) |
| 152 | + |
| 153 | +class BrowserFrame(tk.Frame): |
| 154 | + |
| 155 | + def __init__(self, mainframe, navigation_bar=None): |
| 156 | + self.navigation_bar = navigation_bar |
| 157 | + self.closing = False |
| 158 | + self.browser = None |
| 159 | + tk.Frame.__init__(self, mainframe) |
| 160 | + self.mainframe = mainframe |
| 161 | + self.bind("<FocusIn>", self.on_focus_in) |
| 162 | + self.bind("<FocusOut>", self.on_focus_out) |
| 163 | + self.bind("<Configure>", self.on_configure) |
| 164 | + """For focus problems see Issue #255 and Issue #535. """ |
| 165 | + self.focus_set() |
| 166 | + |
| 167 | + def embed_browser(self): |
| 168 | + window_info = cef.WindowInfo() |
| 169 | + rect = [0, 0, self.winfo_width(), self.winfo_height()] |
| 170 | + window_info.SetAsChild(self.get_window_handle(), rect) |
| 171 | + self.browser = cef.CreateBrowserSync(window_info, |
| 172 | + url="https://www.office.com/launch/excel") |
| 173 | + assert self.browser |
| 174 | + self.browser.SetClientHandler(LifespanHandler(self)) |
| 175 | + self.browser.SetClientHandler(LoadHandler(self)) |
| 176 | + self.browser.SetClientHandler(FocusHandler(self)) |
| 177 | + self.message_loop_work() |
| 178 | + |
| 179 | + def get_window_handle(self): |
| 180 | + if MAC: |
| 181 | + # Do not use self.winfo_id() on Mac, because of these issues: |
| 182 | + # 1. Window id sometimes has an invalid negative value (Issue #308). |
| 183 | + # 2. Even with valid window id it crashes during the call to NSView.setAutoresizingMask: |
| 184 | + # https://github.com/cztomczak/cefpython/issues/309#issuecomment-661094466 |
| 185 | + # |
| 186 | + # To fix it using PyObjC package to obtain window handle. If you change structure of windows then you |
| 187 | + # need to do modifications here as well. |
| 188 | + # |
| 189 | + # There is still one issue with this solution. Sometimes there is more than one window, for example when application |
| 190 | + # didn't close cleanly last time Python displays an NSAlert window asking whether to Reopen that window. In such |
| 191 | + # case app will crash and you will see in console: |
| 192 | + # > Fatal Python error: PyEval_RestoreThread: NULL tstate |
| 193 | + # > zsh: abort python tkinter_.py |
| 194 | + # Error messages related to this: https://github.com/cztomczak/cefpython/issues/441 |
| 195 | + # |
| 196 | + # There is yet another issue that might be related as well: |
| 197 | + # https://github.com/cztomczak/cefpython/issues/583 |
| 198 | + |
| 199 | + # noinspection PyUnresolvedReferences |
| 200 | + from AppKit import NSApp |
| 201 | + # noinspection PyUnresolvedReferences |
| 202 | + import objc |
| 203 | + logger.info("winfo_id={}".format(self.winfo_id())) |
| 204 | + # noinspection PyUnresolvedReferences |
| 205 | + content_view = objc.pyobjc_id(NSApp.windows()[-1].contentView()) |
| 206 | + logger.info("content_view={}".format(content_view)) |
| 207 | + return content_view |
| 208 | + elif self.winfo_id() > 0: |
| 209 | + return self.winfo_id() |
| 210 | + else: |
| 211 | + raise Exception("Couldn't obtain window handle") |
| 212 | + |
| 213 | + def message_loop_work(self): |
| 214 | + cef.MessageLoopWork() |
| 215 | + self.after(10, self.message_loop_work) |
| 216 | + |
| 217 | + def on_configure(self, _): |
| 218 | + if not self.browser: |
| 219 | + self.embed_browser() |
| 220 | + |
| 221 | + def on_frame_configure(self): |
| 222 | + # Root <Configure> event will be called when top window is moved |
| 223 | + if self.browser: |
| 224 | + self.browser.NotifyMoveOrResizeStarted() |
| 225 | + |
| 226 | + def on_mainframe_configure(self, width, height): |
| 227 | + if self.browser: |
| 228 | + if WINDOWS: |
| 229 | + ctypes.windll.user32.SetWindowPos( |
| 230 | + self.browser.GetWindowHandle(), 0, |
| 231 | + 0, 0, width, height, 0x0002) |
| 232 | + elif LINUX: |
| 233 | + self.browser.SetBounds(0, 0, width, height) |
| 234 | + self.browser.NotifyMoveOrResizeStarted() |
| 235 | + |
| 236 | + def on_focus_in(self, _): |
| 237 | + logger.debug("BrowserFrame.on_focus_in") |
| 238 | + if self.browser: |
| 239 | + self.browser.SetFocus(True) |
| 240 | + |
| 241 | + def on_focus_out(self, _): |
| 242 | + logger.debug("BrowserFrame.on_focus_out") |
| 243 | + """For focus problems see Issue #255 and Issue #535. """ |
| 244 | + if LINUX and self.browser: |
| 245 | + self.browser.SetFocus(False) |
| 246 | + |
| 247 | + def on_frame_close(self): |
| 248 | + logger.info("BrowserFrame.on_frame_close") |
| 249 | + if self.browser: |
| 250 | + logger.debug("CloseBrowser") |
| 251 | + self.browser.CloseBrowser(True) |
| 252 | + self.clear_browser_references() |
| 253 | + cef.Shutdown() |
| 254 | + |
| 255 | + def clear_browser_references(self): |
| 256 | + # Clear browser references that you keep anywhere in your |
| 257 | + # code. All references must be cleared for CEF to shutdown cleanly. |
| 258 | + self.browser = None |
| 259 | + |
| 260 | +class LifespanHandler(object): |
| 261 | + |
| 262 | + def __init__(self, tkFrame): |
| 263 | + self.tkFrame = tkFrame |
| 264 | + |
| 265 | + def OnBeforeClose(self, browser, **_): |
| 266 | + logger.debug("LifespanHandler.OnBeforeClose") |
| 267 | + #self.tkFrame.quit() |
| 268 | + |
| 269 | +class LoadHandler(object): |
| 270 | + |
| 271 | + def __init__(self, browser_frame): |
| 272 | + self.browser_frame = browser_frame |
| 273 | + |
| 274 | + def OnLoadStart(self, browser, **_): |
| 275 | + pass |
| 276 | + |
| 277 | +class FocusHandler(object): |
| 278 | + """For focus problems see Issue #255 and Issue #535. """ |
| 279 | + |
| 280 | + def __init__(self, browser_frame): |
| 281 | + self.browser_frame = browser_frame |
| 282 | + |
| 283 | + def OnTakeFocus(self, next_component, **_): |
| 284 | + logger.debug("FocusHandler.OnTakeFocus, next={next}" |
| 285 | + .format(next=next_component)) |
| 286 | + |
| 287 | + def OnSetFocus(self, source, **_): |
| 288 | + logger.debug("FocusHandler.OnSetFocus, source={source}" |
| 289 | + .format(source=source)) |
| 290 | + if LINUX: |
| 291 | + return False |
| 292 | + else: |
| 293 | + return True |
| 294 | + |
| 295 | + def OnGotFocus(self, **_): |
| 296 | + logger.debug("FocusHandler.OnGotFocus") |
| 297 | + if LINUX: |
| 298 | + self.browser_frame.focus_set() |
0 commit comments